27

Update 2018: This question was asked long before PostCSS existed, and I would have probably used that.

I'd like to parse a blob of CSS into an AST so I can add prefixes to certain CSS directives.

Is there a CSS parser for JavaScript or Node that will do this?

I've searched NPM. The only useful result I've found is parser-lib, but it's stream-based and it looks like I'll need to write my own emitter for every CSS node.

Update: I also found JSCSSP, but it has no documentation...

a paid nerd
  • 30,702
  • 30
  • 134
  • 179

8 Answers8

12

Update: I previously mentioned JSCSSP, which is buggy seems to be abandoned. Obviously enough, the css module on NPM is the best:

css = require 'css'

input = '''
  body {
    font-family: sans-serif;
  }
  #thing.foo p.bar {
    font-weight: bold;
  }
'''

obj = css.parse input
sheet = obj.stylesheet

for rule in sheet.rules
  rule.selectors = ('#XXX ' + s for s in rule.selectors)

console.log css.stringify(obj)

Output:

#XXX body {
  font-family: sans-serif;
}
#XXX #thing.foo p.bar {
  font-weight: bold;
}
a paid nerd
  • 30,702
  • 30
  • 134
  • 179
  • 3
    BTW Just after you updated this (24-jul-2013), the JSCSSP project was updated with a load of fixes - http://www.glazman.org/JSCSSP/news.html – PandaWood May 16 '14 at 02:06
  • 1
    This is exactly what I've been looking to do - add a scope to the rules. – CTS_AE Feb 23 '18 at 20:34
  • 2
    I highly recommend against using the 'css' module at this point. It uses regexes and can't parse a lot of CSS, and development stopped several years ago. – erjiang May 06 '20 at 18:30
6

Here is our open source CSS parser css.js

Here is a simple parsing example :

<script type="text/javascript">
    var cssString = ' .someSelector { margin:40px 10px; padding:5px}';
    //initialize parser object
    var parser = new cssjs();
    //parse css string
    var parsed = parser.parseCSS(cssString);

    console.log(parsed);
</script>

To stringify parsed data structure into CSS string after editing

var newCSSString = parser.getCSSForEditor(parsed);

Main features of our CSS parser is :

  • It is lightweight.
  • It outputs easy to understand javascript object. No complex AST.
  • It is battle tested(and unit tested also) and constantly used in our products(JotForm Form Designer).
  • It supports media queries, keyframes and font-face rules.
  • It preserves comments while parsing.
Kemal Dağ
  • 2,743
  • 21
  • 27
  • The question was specifically about AST, so "No complex AST" does not help here – kca Feb 25 '22 at 13:33
  • The linked library has been archived and had plenty of omissions w.r.t. the CSS spec so shouldn't be relied on for general purpose: https://github.com/jotform/css.js/issues – EoghanM Jul 27 '23 at 09:22
5

Also worth mentioning is LESS. While it is primarily a (fantastic) extension to CSS, the LESS parser does give you access to the AST.

A pure CSS stylesheet is also a valid LESS stylesheet, so you can start with what you have now and ease in to LESS' extensions.

josh3736
  • 139,160
  • 33
  • 216
  • 263
  • 1
    Just an update for latecomers: it looks like the LESS project won't support direct access to the parser in the future: http://lesscss.org/usage/index.html#programmatic-usage. Of course, you can always just pick a current/past version and use that. – A. L. Flanagan Oct 12 '15 at 00:24
4

For what it's worth, JSDOM uses CSSOM.

Jed Schmidt
  • 2,907
  • 23
  • 23
  • This library seems to be a third the size of JSCSSP in terms of lines of code, probably due to everything it doesn't attempt to support. For HTML5 only projects, this is the way to go. – conartist6 Feb 27 '13 at 08:58
  • This is awesome, I was wondering if there was a direct object model instead of these crazy AST that all the other libraries have. – CTS_AE Feb 23 '18 at 20:33
  • One thing to note with the CSSOM is that if you parse the following shorthand: `background: var(--my-color)` it breaks it down into longhand properties, and throws away the variable. See https://bugs.chromium.org/p/chromium/issues/detail?id=1218159#c13 for browser devs pointing to omissions in the spec on this point. – EoghanM Jul 27 '23 at 09:25
3

No need use external css parser,we can use native css parser

   
var sheetRef=document.getElementsByTagName("style")[0];

console.log("----------------list of  rules--------------");
for (var i=0; i<sheetRef.sheet.cssRules.length; i++){


var sheet = sheetRef.sheet ? sheetRef.sheet : sheetRef.styleSheet;


if (sheet.cssRules.length > 0) {
//console.log(sheet.cssRules[i]);
  console.log(sheet.cssRules[i].selectorText);
  console.log(sheet.cssRules[i].cssText);

                }}
.red{

color:red;
}

To Insert Rule

var sheetRef=document.getElementsByTagName("style")[0];
var sheet = sheetRef.sheet ? sheetRef.sheet : sheetRef.styleSheet;
sheet.insertRule('.foo{color:red;}', 0);

To Remove Rule all browsers, except IE before version 9

var sheetRef=document.getElementsByTagName("style")[0];
var sheet = sheetRef.sheet ? sheetRef.sheet : sheetRef.styleSheet;
sheet.removeRule (0);

To Delete Rule all browsers, except IE before version 9

var sheetRef=document.getElementsByTagName("style")[0];
var sheet = sheetRef.sheet ? sheetRef.sheet : sheetRef.styleSheet;
sheet.deleteRule (0);

To add Media

  function AddScreenMedia () {
            var styleTag = document.getElementsByTagName("style")[0];

                // the style sheet in the style tag
            var sheet = styleTag.sheet ? styleTag.sheet : styleTag.styleSheet;

            if (sheet.cssRules) {   // all browsers, except IE before version 9
                var rule = sheet.cssRules[0];
                var mediaList = rule.media;

                alert ("The media types before adding the screen media type: " + mediaList.mediaText);
                mediaList.appendMedium ("screen");
                alert ("The media types after adding the screen media type: " + mediaList.mediaText);
            }
            else {  // Internet Explorer before version 9
                    // note: the rules collection does not contain the at-rules
                alert ("Your browser does not support this example!");
            }
        }
  @media print {
            body {
                font-size: 13px;
                color: #FF0000;
            }
          }
some text
<button onclick="AddScreenMedia ();">Add screen media</button>

To get rules

 
var sheetRef=document.getElementsByTagName("style")[0];

console.log("----------------list of  rules--------------");
for (var i=0; i<sheetRef.sheet.cssRules.length; i++){


var sheet = sheetRef.sheet ? sheetRef.sheet : sheetRef.styleSheet;


if (sheet.cssRules.length > 0) {
//console.log(sheet.cssRules[i]);
  console.log(sheet.cssRules[i].selectorText);
  console.log(sheet.cssRules[i].cssText);
  
  console.log(sheet.cssRules[i].style.color)
  console.log(sheet.cssRules[i].style.background)
    console.log(sheet.cssRules[i].style)

                }}
.red{

color:red;
background:orange;
}
<h1>red</h1>
Balaji
  • 9,657
  • 5
  • 47
  • 47
2

Edit

I ended up using this library which was light enough for my implementation (provided in Kemal Dağ's answer). Other options were too heavy for the client-side implementation I was after.

https://github.com/jotform/css.js

Original Content

a paid nerd's original answer worked great until I hit media queries.

I had to add some recursion and this is what I ended up with.

Forgive me for the TypeScript.

TypeScript Implementation

private scopeCSS(css: string): CSS.Stylesheet {
  let ast: CSS.Stylesheet = CSS.parse(css);
  let stylesheet: CSS.StyleRules|undefined = ast.stylesheet;
  if (stylesheet) {
    let rules: Array<CSS.Rule|CSS.Media> = stylesheet.rules;
    let prefix = `[data-id='sticky-container-${this.parent.id}']`;

    // Append our container scope to rules
    // Recursive rule appender
    let ruleAppend = (rules: Array<CSS.Rule|CSS.Media>) => {
      rules.forEach(rule => {
        let cssRule = <CSS.Rule>rule;
        let mediaRule = <CSS.Media>rule;
        if (cssRule.selectors !== undefined) {
          cssRule.selectors = cssRule.selectors.map(selector => `${prefix} ${selector}`);
        }
        if (mediaRule.rules !== undefined) {
          ruleAppend(mediaRule.rules);
        }
      });
    };

    ruleAppend(rules);
  }
  return ast;
}

Babel'ized Vanilla JS Implementation

function scopeCSS(css, prefix) {
  var ast = CSS.parse(css);
  var stylesheet = ast.stylesheet;
  if (stylesheet) {
    var rules = stylesheet.rules;
    // Append our container scope to rules
    // Recursive rule appender
    var ruleAppend = function(rules) {
      rules.forEach(function(rule) {
        if (rule.selectors !== undefined) {
          rule.selectors = rule.selectors.map(function(selector) {
            return prefix + " " + selector;
          });
        }
        if (rule.rules !== undefined) {
          ruleAppend(rule.rules);
        }
      });
    };
    ruleAppend(rules);
  }
  return ast;
}
CTS_AE
  • 12,987
  • 8
  • 62
  • 63
  • Warning: jotform's css.js can't parse all CSS (it's built with regexes). It may mangle or lose some of your input. – erjiang May 08 '20 at 17:44
1

http://www.glazman.org/JSCSSP/

http://jsfiddle.net/cYEgT/

sheet is sort of like an AST.

Cameron Martin
  • 5,952
  • 2
  • 40
  • 53
0

Very simple, one function.

function parseCSStxt(cssXTX){
    var p2=[], p1=cssXTX.split("}");
    p1.forEach(element => {
        var rtmp=element.split("{") ;
        if( rtmp.length>1 && Array.isArray(rtmp) ){
            var s=rtmp[0].split(",");
            var v=rtmp[1].split(";");
            const notNil = (i) => !(typeof i === 'undefined' || i === null || i=='');
            s = s.filter(notNil);
            v = v.filter(notNil);
            p2.push( {s,v} );
        }        
    });

    console.log(p2);
    }
    parseCSStxt( ".cls-1,.cls-5{fill:none;}.cls-1,.cls-2,.cls-5,.cls-6{stroke:#000;}.cls-1{stroke-linecap:round;stroke-linejoin:round;}.cls-1,.cls-2,.cls-6{stroke-width:4px;}.cls-2{fill:#ffad17;}.cls-2,.cls-5,.cls-6{stroke-miterlimit:10;}.cls-3{fill:#d86212;}.cls-4{fill:#87270e;}.cls-5{stroke-width:2px;}.cls-6{fill:#006d31;}" );
Muisca
  • 69
  • 1
  • 5
  • `parseCSStxt("@keyframes foo {0% {color: orange} 100% {color:black}}")` is valid CSS that isn't parsed meaningfully by this code. – AKX Dec 02 '22 at 15:18
  • Yep, no parser keyframes, mediaquerys,.... – Muisca Dec 03 '22 at 02:16
  • Also you can't just split on those characters, as CSS values can contain strings e.g. `background-image: url('svg+xml;123{you get the idea}');` – EoghanM Jul 27 '23 at 09:27