38

I'm trying to implement parsing of CSS in JavaScript so that:

a {
  color: red;
}

is parsed into the object:

{
  'a' {
    'color': 'red'
  }
}

First off, is there a JavaScript / jQuery library I can use?

My implementation is pretty basic, so I'm sure it is not fool-proof by any means. For example, it works fine for basic CSS, but for a property of the type:

background: url(data:image/png;base64, ....);

It fails because I am using split(';') to separate property:value pairs. Here, ; occurs in the value, so it splits at that point too.

Is there an alternate way to do this?

Here is the code:

parseCSS: function(css) {
    var rules = {};
    css = this.removeComments(css);
    var blocks = css.split('}');
    blocks.pop();
    var len = blocks.length;
    for (var i = 0; i < len; i++)
    {
        var pair = blocks[i].split('{');
        rules[$.trim(pair[0])] = this.parseCSSBlock(pair[1]);
    }
    return rules;
},

parseCSSBlock: function(css) { 
    var rule = {};
    var declarations = css.split(';');
    declarations.pop();
    var len = declarations.length;
    for (var i = 0; i < len; i++)
    {
        var loc = declarations[i].indexOf(':');
        var property = $.trim(declarations[i].substring(0, loc));
        var value = $.trim(declarations[i].substring(loc + 1));

        if (property != "" && value != "")
            rule[property] = value;
    }
    return rule;
},

removeComments: function(css) {
    return css.replace(/\/\*(\r|\n|.)*\*\//g,"");
}

Thanks!

Gumbo
  • 643,351
  • 109
  • 780
  • 844
ankit
  • 1,273
  • 3
  • 13
  • 12
  • 1
    Why do you want to do that? what are you trying to achieve. Maybe there's another (simpler) way of solving your problem – Pablo Fernandez Jul 24 '10 at 19:19
  • @Pablo I've tried hard to think of alternate ways by which I might be able to avoid the need to parse CSS, but unfortunately, I do need to store the rules in some data structure. My project actually works quite well with this, since it mostly involves basic CSS rules (major use case). However, it will be nice to be fool-proof since there is a use case where it may need to parse any CSS. – ankit Jul 24 '10 at 19:59

5 Answers5

95

You can easily use the Browser's own CSSOM to parse CSS:

var rulesForCssText = function (styleContent) {
    var doc = document.implementation.createHTMLDocument(""),
        styleElement = document.createElement("style");

   styleElement.textContent = styleContent;
    // the style will only be parsed once it is added to a document
    doc.body.appendChild(styleElement);

    return styleElement.sheet.cssRules;
};

For each rule returned you can look at the properties in rule.style. See http://jsfiddle.net/v2JsZ/ for an example.

cburgmer
  • 2,150
  • 1
  • 24
  • 18
  • 7
    You rock! It works like a charm! :) I suppose it is the best choice for implementation in the browser. You don't need any library and it is faster then any library, I think. – Konstantin Smolyanin Sep 10 '13 at 22:11
  • I've updated your jsfiddle with an example which only parses the css: http://jsfiddle.net/v2JsZ/85/ – Peter van Kekem Aug 01 '14 at 11:45
  • How come if my styles have a background image this code returns an empty path: `url()`? – Moss Nov 14 '14 at 02:45
  • @Moss Chrome exposes a bug here https://code.google.com/p/chromium/issues/detail?id=161644 – cburgmer Nov 29 '14 at 10:22
  • 2
    Any shortcomings to using this approach? If this works, I wonder what's the point of using any JS library for such a non trivial task – Don Box Jun 09 '16 at 11:04
  • @donBox: The stylesheet is required to be part of your document. This could be a pretty substantial drawback depending on what you are doing. – kamelkev Jun 13 '16 at 23:25
  • @kamelkev I was referring only for client side. Is there any drawback on client side? – Don Box Jun 16 '16 at 15:28
  • 3
    @DonBox: Well, as I said: In order to use the browser CSSOM the stylesheet must be part of the existing document. This means the end user will see the styles as part of the page they are viewing. This is less than ideal for certain circumstances. I.E. you cannot use this technique to handle an abstract stylesheet that is not directly related to what the current user is viewing. – kamelkev Jun 17 '16 at 16:22
  • @kamelkev sorry for the old reply, but using the fiddle in question, I managed to do something that works pretty well - I put the CSS in a div (in my use case, there's an editor that will contain it), and called in that div's content (while also making the div invisible if you so wish). It works fine - [jsfiddle](http://jsfiddle.net/muffinjello/ckx521e2/1/). – Adam Jul 05 '16 at 19:26
  • 1
    there is a shortcomming, browser-specific rules are lost – useless Oct 02 '16 at 17:11
  • @kamelkev @Adam If you're adding a style element to your current HTML document, you can disable the element so it does not affect the displayed styles. First append an empty style element, then disable it `styleElement.disabled = true;`, and then set its text content. – jkdev Oct 23 '19 at 22:25
  • 1
    @kamelkev, the style element is not inserted in the document the user is viewing. It's inserted in a different document. – GetFree Nov 13 '19 at 15:12
29

There is a CSS parser written in Javascript called JSCSSP

Matthew Manela
  • 16,572
  • 3
  • 64
  • 66
  • 3
    I did take a look at it earlier, but didn't want to use it as it is so **heavy**. It does a lot of stuff I don't need to do – ankit Jul 24 '10 at 19:54
  • 10
    @ankit: Then what are you asking for? If you want to parse *correctly* (meaning you can handle any arbitrary CSS), then you're going to end up with a "heavy" library. Otherwise, you can stick with your lightweight implementation knowing that it's easy to break. – josh3736 Jul 24 '10 at 20:07
  • @josh3736 Looks like your comment was the push I required. I was worried about performance issues, but turns out it works pretty well! – ankit Jul 24 '10 at 21:33
  • I couldn't find a way to parse back from a JSON object to CSS using JSCSSP. JSCSSP parses the CSS to JSON great, and the structure it uses is just what we need, but we also need a way to parse back the JSON object to CSS. Any ideas / suggestions ? – Norman Jun 05 '14 at 17:39
  • do not use that parser, I tested with compressed source of "Bootstrap v3.3.2", and parser only returned 25 rules. – useless Oct 02 '16 at 17:03
15

To write the most fool-proof parser, follow the exact rules for tokenization and CSS grammar as defined in the spec. Note that you don't have to implement the spec by the ink. You can start with small parts and CSS that you will most likely encounter, and then expand from there. Even better, skip the entire process altogether and go with @Matthew's solution unless this is a learning exercise.

There are various lexical scanners and parser generators available for JavaScript. The entire grammar is available on w3's website. Why do the re-work when you can simply use that and the parser generators to generate the parser in JavaScript.

  1. Jison
  2. Peg.js
  3. Cruiser.Parse
  4. McLexer
  5. JS/CC

The production rules for CSS are given below.

stylesheet
  : [ CHARSET_SYM STRING ';' ]?
    [S|CDO|CDC]* [ import [ CDO S* | CDC S* ]* ]*
    [ [ ruleset | media | page ] [ CDO S* | CDC S* ]* ]*
  ;
import
  : IMPORT_SYM S*
    [STRING|URI] S* media_list? ';' S*
  ;
media
  : MEDIA_SYM S* media_list LBRACE S* ruleset* '}' S*
  ;
media_list
  : medium [ COMMA S* medium]*
  ;
medium
  : IDENT S*
  ;
page
  : PAGE_SYM S* pseudo_page?
    '{' S* declaration? [ ';' S* declaration? ]* '}' S*
  ;
pseudo_page
  : ':' IDENT S*
  ;
operator
  : '/' S* | ',' S*
  ;
combinator
  : '+' S*
  | '>' S*
  ;
unary_operator
  : '-' | '+'
  ;
property
  : IDENT S*
  ;
ruleset
  : selector [ ',' S* selector ]*
    '{' S* declaration? [ ';' S* declaration? ]* '}' S*
  ;
selector
  : simple_selector [ combinator selector | S+ [ combinator? selector ]? ]?
  ;
simple_selector
  : element_name [ HASH | class | attrib | pseudo ]*
  | [ HASH | class | attrib | pseudo ]+
  ;
class
  : '.' IDENT
  ;
element_name
  : IDENT | '*'
  ;
attrib
  : '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S*
    [ IDENT | STRING ] S* ]? ']'
  ;
pseudo
  : ':' [ IDENT | FUNCTION S* [IDENT S*]? ')' ]
  ;
declaration
  : property ':' S* expr prio?
  ;
prio
  : IMPORTANT_SYM S*
  ;
expr
  : term [ operator? term ]*
  ;
term
  : unary_operator?
    [ NUMBER S* | PERCENTAGE S* | LENGTH S* | EMS S* | EXS S* | ANGLE S* |
      TIME S* | FREQ S* ]
  | STRING S* | IDENT S* | URI S* | hexcolor | function
  ;
function
  : FUNCTION S* expr ')' S*
  ;
/*
 * There is a constraint on the color that it must
 * have either 3 or 6 hex-digits (i.e., [0-9a-fA-F])
 * after the "#"; e.g., "#000" is OK, but "#abcd" is not.
 */
hexcolor
  : HASH S*
  ;
Anurag
  • 140,337
  • 36
  • 221
  • 257
  • These production rules are outdated for CSS3. I don't see '~' for example – huyz Aug 26 '11 at 11:05
  • 1
    You can look at CoffeeScript for a nice well documented example: http://jashkenas.github.com/coffee-script/documentation/docs/grammar.html – Brandon Sep 08 '11 at 18:08
  • In case anyone comes across this, as mentioned by @huyz, not only will this answer be outdated (at no fault of the OP), but at this stage, CSS parsing is (unfortunately) way more complex than probably anyone really realizes. – Matthew Dean Sep 09 '19 at 20:04
2

Simple example, not tested but should work, I'm using similar in my project.

var div = jQuery('<div/>');
div[0].style = 'position:absolute;left:5px;top:10px;'; //Css to parse

div.css('left'); // => '5px'
div.css('top'); // => '10px'
div[0].style; // => Object containing all css
0

Heres a parser from Tab Atkins that follows the spec strictly

https://github.com/tabatkins/parse-css

theres a pretty good video about how this parser works compared to others

Amia
  • 96
  • 6