Search code examples
javascriptcsscss-parsing

Add prefix to css rules with javascript


I have a string with the following css that I need to process with javascript

h1
{
    color: red;
}

.info
{
    border: 1px dotted blue;
    padding: 10px;
}

#rect
{
    background: pink;
}

h2,
h3,
h4
{
    font-weight: bold;
}

table td:last td
{
    background: gainsboro;
}

How can I add the prefix .page to each rule so the css doesn't break?

I want this result

.page h1
{
    color: red;
}

.page .info
{
    border: 1px dotted blue;
    padding: 10px;
}

...

Right now, I solve it with looking for the indentation, but the code fails on this case

h1
{
color: red;
}

Which then ends up with

.page h1
{
.page color: red;
}

I could also look for rows that only has brackets, but then this case would fail

h1 { color: red; }

I don't want to build my own css parser and the ones I found mostly handles css applied to elements in the DOM and not strings with css. Is there a good parser or can this be achieved otherwise?


Solution

  • Here is a function to prefix all selectors with a class name. This function properly handle the @ rules:

    var prefixCssSelectors = function(rules, className) {
      var classLen = className.length,
        char, nextChar, isAt, isIn;
    
      // makes sure the className will not concatenate the selector
      className += ' ';
    
      // removes comments
      rules = rules.replace( /\/\*(?:(?!\*\/)[\s\S])*\*\/|[\r\n\t]+/g, '' );
    
      // makes sure nextChar will not target a space
      rules = rules.replace( /}(\s*)@/g, '}@' );
      rules = rules.replace( /}(\s*)}/g, '}}' );
    
      for (var i = 0; i < rules.length-2; i++) {
        char = rules[i];
        nextChar = rules[i+1];
    
        if (char === '@' && nextChar !== 'f') isAt = true;
        if (!isAt && char === '{') isIn = true;
        if (isIn && char === '}') isIn = false;
    
        if (
          !isIn &&
          nextChar !== '@' &&
          nextChar !== '}' &&
          (
            char === '}' ||
            char === ',' ||
            ((char === '{' || char === ';') && isAt)
          )
        ) {
          rules = rules.slice(0, i+1) + className + rules.slice(i+1);
          i += classLen;
          isAt = false;
        }
      };
    
      // prefix the first select if it is not `@media` and if it is not yet prefixed
      if (rules.indexOf(className) !== 0 && rules.indexOf('@') !== 0) rules = className+rules;
    
      return rules;
    }
    
    
    // Some examples:
    console.log(prefixCssSelectors('div { width: 100%; }', '.page'));
    
    console.log(prefixCssSelectors('@charset "utf-8"; div { width: 100%; }', '.page'));
    
    console.log(prefixCssSelectors('@media only screen { div { width: 100%; } p { size: 1.2rem; } } @media only print { p { size: 1.2rem; } } div { height: 100%; font-family: "Arial", Times; }', '.page'));
    
    console.log(prefixCssSelectors('@font-face { font-family: "Open Sans"; src: url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2"); } div { width: 100%; }', '.page'));

    If you only want to have the class name for the html and body selectors, add this:

    rules = rules.replace(/( html| body)/g, '');