Adrian Rosca
Adrian Rosca

Reputation: 7392

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?

Upvotes: 6

Views: 3457

Answers (6)

KitKit
KitKit

Reputation: 9513

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; }

Upvotes: 0

Nicolas BADIA
Nicolas BADIA

Reputation: 5852

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, '');

Upvotes: 10

user663031
user663031

Reputation:

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.

Upvotes: 1

Anonymous0day
Anonymous0day

Reputation: 3042

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>

Upvotes: 0

CrowbarKZ
CrowbarKZ

Reputation: 1243

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

Upvotes: -1

mstruebing
mstruebing

Reputation: 1792

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.

Upvotes: 0

Related Questions