6

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?

Adrian Rosca
  • 6,922
  • 9
  • 40
  • 57
  • You need to parse it! – Salman A Oct 20 '15 at 11:33
  • This discussion may be helpful in this case: http://stackoverflow.com/questions/8302437/how-to-parse-and-extract-css-selectors-as-strings – CmajSmith Oct 20 '15 at 11:35
  • You should look at the rework suite of tools (https://github.com/reworkcss/rework). It parses and transforms, and can work on strings. It's pretty well established that trying to parse languages with regexp is a poor idea. As you've found, you'll come up with some solution that you think works, and then right after that you'll find a case where your "solution" breaks. –  Oct 20 '15 at 13:02

6 Answers6

11

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, '');
Nicolas BADIA
  • 5,612
  • 7
  • 43
  • 46
  • Very elegant solution, used in 2 my projects, but be careful with @font-face. – Luca Trazzi Aug 27 '20 at 10:22
  • I did a simple workaround in the line where it sets isAt = true, by checking that nextChar is not 'f', like that ```if (char === '@' && nextChar !== 'f') isAt = true;``` – Luca Trazzi Aug 27 '20 at 10:26
1

You've posed as a question how to do what you think is the solution to your actual problem, which seems to be, how do I apply some CSS only to one section of a document?

Eventually, the solution will be <style scoped>. While we're waiting for that, you can try a polyfill such as https://github.com/PM5544/scoped-polyfill, http://thomaspark.co/2015/07/polyfill-for-scoped-css/, https://github.com/thingsinjars/jQuery-Scoped-CSS-plugin, or https://github.com/thomaspark/scoper.

If you have a string of CSS (which you got from where by the way?), then to use these polyfills, you can simply insert a <style scoped> element into your DOM under the .page element, containing the CSS you've got in the string.

All of these polyfills have the major advantage that they are based on CSS as correctly parsed by the browser, and will not break at the first sign of trouble, as the regexp solution you inadvisably accepted, and all other regexp solutinos, inevitably will.

0

If you would use sass you can simply write something like

.page {
  h1 {
    color: red;
  }

  h2 {
    color: green;
  }
}

This would be compiled to exactly what you want. But if this isn't an option, you have to write your own parser.

mstruebing
  • 1,674
  • 1
  • 16
  • 29
0

One solution, it to loop over CSS rules and update them on the fly.

/* debug purpose */
var j = JSON.stringify;
var el = document.getElementById('el');
var log = function( val ){el.innerHTML+='<div>'+val+'</div>'}

var doIt = function(){

// you have to identify your stylesheet to get it
var ss = document.styleSheets[0];
// we get all the rules from this stylesheets
var rules = ss.cssRules;

// we loop over all rules
for( var i = 0 ; i < rules.length; i++){
  var rule = rules[i];
  
  var selector = rule.selectorText;
  var def = rule.cssText.replace(selector , '');
  
  // we update the selector
  var selector2 = selector.replace(/([^,]+,?)/g , '.page $1 ' ); 

  // we modify the rules on the fly !
  var def2 = def.replace('red' , 'green');
  def2 = def2.replace('blue' , 'red');
  def2 = def2.replace('pink' , 'lime');
  
  log(i);
  log( 'def : ' + def);
  log( 'modified : ' + def2);
  log( 'selector : ' + selector);
  log( 'updated : ' + selector2);
  log('----------');
  ss.deleteRule(i);// we remove the old 
  ss.insertRule(selector2 + def2 , i);// we add the new 
  
};

  
  // we check the stylecheet !
  log( ' checking the results');
  for( var i = 0 ; i < rules.length; i++){
  var rule = rules[i];
  
  var curRule = rule.cssText;
  log( i + ':  curRule => ' + curRule);
 
};
  
};
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;
}
<div class='page'>
  <div id='rect'> RECT </div>
  <div class='info'> INFO </div>
<div>
<button onclick='doIt()'>update it</button>
<div id='el'><div>
Anonymous0day
  • 3,012
  • 1
  • 14
  • 16
0

In case you don't want to use Regex, and take advantage of postcss, you can install postcss-prefix-selector and do this:

import postcss from "postcss"
import prefixer from "postcss-prefix-selector"

const inputCssString = `ul { padding: 20px 0; flex: 1; } li { font-family:'Lato'; color: whitesmoke; line-height: 44px; }`
const out = postcss()
            .use(
                prefixer({
                    prefix: `.my-prefix`,
                })
            )
            .process(inputCssString).css

Output returned:

.my-prefix ul { padding: 20px 0; flex: 1; } .my-prefix li { font-family:'Lato'; color: whitesmoke; line-height: 44px; }
halfer
  • 19,824
  • 17
  • 99
  • 186
KitKit
  • 8,549
  • 12
  • 56
  • 82
-1

Take a look at regular expressions

for example something like this:

/([^]*?)({[^]*?}|,)/g

should capture all the selectors in your css string in first group and and rules in the second group. And then you do something like this to replace the matches

var str = "your css text here";
re = /([^]*?)({[^]*?}|,)/g;
var new_str = str.replace(re, ".prefix $1 $2"); //prefixed css
console.log(new_str); 

If you are not familiar with regular expressions I would recommend reading relevant chapters in Eloquent Javascript

CrowbarKZ
  • 1,203
  • 11
  • 18
  • 1
    This will fail on CSS such as `"html, body { foo: bar; content: '}'; } div { color: red }"`. It will also fail on cases such as `.\{boo\} { color: red; }`. Granted, these are contrived examples, but they still make the case that you can't reliably parse CSS with regexp. –  Oct 20 '15 at 13:09
  • For those cases I imagine you could modify a RegExp to account for escaped/quoted curly brackets – CrowbarKZ Oct 22 '15 at 07:33