Skip to content

jQuery CSS parser

Updated 2009-05-03

Every time I create or use a jQuery plugin, I realize that the assigning of behaviors to elements on the page is a design decision, not a programming one, and one that should be made by the guy in charge of the CSS, not the guy in charge of the javascript. Even when I'm the same guy, I want to wear each hat separately. But these presentational enhancements are written in javascript and applied in javascript. So my "presentational" code is in two different files:

style.css

.gallery a { border: 1px solid green }
style.js

$('.gallery a').lightbox({overlayBgColor: '#ddd'});

I could put the presentational javascript in the HTML with $.metadata but while that's fine for quick-and-dirty pages, it's evil in production since it completely violates the separation of data/structure/presentation/behavior.

As I and others have noted before, the application of these plugins belongs in the stylesheet, and I finally got it to work:

style.css

.gallery a {
  border: 1px solid green;
  -jquery-lightbox: {overlayBgColor: '#ddd'};
}

and a single call to start it off: $().parsecss($.parsecss.jquery) .

Download the code.

See the demo.

Use

$(selector).parsecss(callback) scans all <style> and <link type="text/css"> elements in $(selector) or its descendents, parses each one and passes an object (details below) to the callback function. It uses a callback because external stylesheets and @import statements need to use AJAX, asynchronously. The parser is smart enough to understand media attributes and statements.

The supplied callback is the $.parsecss.jquery function. That function finds properties that start with -jquery and applies them to the matching elements. Generally, you would want to parse all the stylesheets in the document, $(document), which in jQuery is abbreviated $(), so the simple call is: $().parsecss($.parsecss.jquery).

The CSS

Some jargon: in the stylesheet

foo {
  bar: quux;
  -bar-foo: quux-2;
}

foo > foo {
  bar: quux 2;
}

foo { bar: quux; -bar-foo: quux-2} is a statement, foo is a selector, {bar: quux; -bar-foo: quux-2} is its CSStext, -bar-foo: quux-2 is a declaration, -bar-foo is its property and quux-2 is its value.

$.parsecss.jquery

$.parsecss.jquery is the callback function that implements the custom-jQuery code. It looks for properties that start with -jquery. If the property is -jquery alone, then it executes the value as a javascript statement for each matched element, $(selector).each(Function(value)). For example:

span.important {
  -jquery: $(this).append('<span>I am important</span>')
}

executes

$('span.important').each(function() {$(this).append('<span>I am important!</span>')})

If the property is -jquery-show or -jquery-hide, then the normal show() and hide() are overridden for the matched elements to use the value as the arguments. It will recognize plugins and jQuery UI effects. Examples (see below how it parses the arguments):


a {
  -jquery-show: fast; /* $('a').show() does $('a').show('fast') */
  -jquery-show: fadeIn 2000 /* $('a').show() does $('a').fadeIn(2000) */
  -jquery-show: fold {size: 5} slow /* $('a').show() does $('a').show('fold', {size: 5}, 'slow') */
  -jquery-hide: explode fast /* $('a').hide() does $('a').hide('explode', {}, 'fast') It's smart enough to know that jQuery UI effects need an options object inserted */
}

Using hide() or show() with any arguments overrides the CSS-imposed functions.

Otherwise, if the property matches the regular expression /-jquery-(.*)/ then the (.*) is taken as a plugin name and the value is used for the arguments (see below how it parses the arguments):

.gallery a {
  -jquery-lightbox: {overlayBgColor: '#ddd'}
}

executes

$('.gallery a').lightbox({overlayBgColor: '#ddd'});

Valid CSS?

Does including the -jquery properties make the CSS invalid? Sort of. The easy answer is "Who cares?" The CSS syntax specifies that

User agents must handle unexpected tokens encountered while parsing a declaration by reading until the end of the declaration, while observing the rules for matching pairs of (), [], {}, "", and ''
That means that the extensions described here will never cause errors in interpreting the rest of the CSS. Also, the specification says
Keywords and property names beginning with -' or '_' are reserved for vendor-specific extensions.
So, -jquery-show, while not valid, is "expected" in real-life CSS, just like -moz-border-radius.

But, some people want CSS that passes the validator. I adopted a solution similar to Microsoft's conditional comments. Text surrounded by /*@ and */ will be parsed by the parser despite being ignored by the browser.

span.important {
      font-weight: bold;
/*@   -jquery-after: '<span>!!!</span>'; */ /* "hide" the jQuery CSS in the comments */
}

$.livequery

What makes the combination of CSS and javascript so powerful is that CSS is "live": changes in the DOM are immediately rendered on the page (this is why Microsoft, when they invented it, called it DHTML—Dynamic HTML). If the stylesheet says .important { font-weight: bold; color: red } then running $('#x').addClass('important') changes the text to red and bold. In addition, $('#x').removeClass('important') changes the text back. The browser keeps track of all the elements and the selector in the CSS, and when an element starts to match the CSS is applied, and when it no longer matches, the browser knows how to "undo" the styling.

jQuery plugins can't do this. The javascript is run at $(document).ready, applied if the elements match at that moment, and that's it. This makes it much less useful, especially for AJAX-y sites that are constantly changing their elements.

Enter Brandon Aaron's $.livequery plugin. It keeps track of the selectors used and overrides all the plugins that modify the DOM, and re-applies the javascript if need be. It also allows you to specify an "unmatched" function that will be run when the element no longer matches. Obviously, you or the plugin programmer has to provide the "undo" ability; there's no way for $.livequery to know how to do that.

If $.livequery is available, $.parsecss can use it. Separate the two parts (match code/unmatch code) with a ! (the exclamation point implies "not"):

.important {
  -jquery-css: font-weight bold ! font-weight normal ;
}
/* yes, in real life you'd do font-weight: bold;  and not rely on javascript */

You can leave either side of the ! blank:

.gallery a {
  -jquery-lightbox: {overlayBgColor: '#ddd'} ! ; /* the '!' means we'll use livequery */
  -jquery-unbind: ! click; /* remove the click handler when the element no longer matches
}

Some notes on $.livequery:

  • If $.livequery is not available, the value after the ! is ignored and the code is applied only when $.parsecss is called.
  • $.livequery checks every selector on every modification of the DOM. It can make complicated pages and script very slow. Be aware of the tradeoffs of using it.
  • jQuery 1.3 includes a $.live plugin. This is not the same as $.livequery! It uses event delegation to keep event handlers on new elements, but does not re-apply javascript to new elements. You have to use $.livequery for that.

The parser

The parser turns CSS into a javascript object in the obvious way:


div div:first {
  font-weight: bold;
  -jquery: each(function() {alert(this.tagName);})
}

div > span {
  color: red;
}

is passed to the callback as:


{
  'div div:first' : {
    'font-weight' : 'bold',
    '-jquery': 'each(function() {alert(this.tagName);})'
  },
  'div > span' : {
    'color': 'red'
  }
}

The callback is called separately for each <style> and <link> element, and for every @rule that includes CSS text (@import and @media). There is no guarantee of any particular order in calling the callback. The parser (and $.parsecss.jquery)does not attempt to obey the priority rules of the CSS cascade; it will execute all the relevant code.

The parser is very simple; it uses regular expressions and String.remove to pull out comments, strings, brace-delimited blocks and @-rules, then parsing the grammar by using String.split on } to get each statement, splitting on { to get the selector and cssText, splitting the cssText on ; to get each declaration and on : to get the properties and values. It's not a sophisticated finite-state automaton but it serves.

$.parsecss.parseArguments

CSS values use space-delimited arguments, border: 1px solid black

. To use this in javascript, we use apply which requires an array: ['1px', 'solid', 'black']. The function $.parsecss.parseArguments({String}) returns that array. It tries to be sophisticated about arrays, objects and functions, using eval to see if the item is interpretable as is; if not, it is made a string. This means that global variables (this, window) or expressions will cause unexpected results (put them in quotes to be sure):


a b c                                       =>  ['a', 'b', 'c']
1 2 c                                       =>  [1, 2, 'c']
[1,2] {a: 'b c'}                          =>  [[1,2], {a: 'b c'} ]
{a: b, c: d}                              =>  ["{a: b, c: d}"] // inside arrays, objects and functions, you have to use real javascript syntax
{a: 'b', c: 'd'}                            =>  [{a:"b", c:"d"}] // the automagic quoting is only at the top level
1+2                                         =>  [3]
'1+2'                                        =>  ['1+2']
10 fast function() {$(this).hide}   =>  [10, "fast", (function () {$(this).hide;})]

$.parsecss.isValidSelector

$.parsecss.isValidSelector(selector {String}) is a function I wrote for Any Kent's JSS, which is sort of the opposite of $.parsecss.jquery: it goes through the stylesheets and finds valid CSS selectors that the browser cannot handle, and uses jQuery to implement them. Eric Meyer recently had a similar idea. $.parsecss.isValidSelector takes a string and returns true if the browser recognizes it as a CSS selector. Thus in IE6, $.parsecss.isValidSelector('div p') returns true and $.parsecss.isValidSelector('div > p') returns false. In Firefox 3, $.parsecss.isValidSelector('div > p') returns true and $.parsecss.isValidSelector('div:visible') returns false.

A simple (though very inefficient) version of JSS would be:

$().parsecss(function(css){
  for (var selector in css){
    if (! $.parsecss.isValidSelector(selector)) $(selector).css(css[selector]);
  }
});

It works by creating a new stylesheet and creating a rule with that selector and seeing if the browser recognized it.

$.parsecss.mediumApplies

$.parsecss.mediumApplies(medium {String}) is similar to $.parsecss.isValidSelector, but it returns true if the argument is a valid media attribute on a <link> element or @media rule. Thus, for a regular computer, $.parsecss.mediumApplies('screen') returns true, $.parsecss.mediumApplies('all') returns true, $.parsecss.mediumApplies('print') returns false, and $.parsecss.mediumApplies('') returns true.

The second argument to $.fn.parsecss

parsecss accepts a second argument, parseAttributes {Boolean}. If it is true, the parser will attempt to parse the style attribute of each element and pass it to the callback.

This is problematic on many levels. The first is how to identify the relevant element. If the element is <div id="test" style="-jquery-show: fast">a div</div> we want the object {'#test' : {'-jquery-show': 'fast'} }. If the element has an id attribute, then it's easy. Otherwise, it counts the tags in the page and uses :eq(): thus if <div style="-jquery-show: fast">a div</div> is the fifth div element, the object generated includes {'div:eq(4)' : {'-jquery-show': 'fast'} }.

The real problem is that there is no way to get the actual style attributes for elements in the DOM. The source code is parsed and interpreted by the browser and any declarations that the browser does not understand are discarded. There's no way to get the style="-jquery-show: fast" using any of the tools available. What I've done is used AJAX to get the source code back again: $.get(location.pathname+location.search, 'text', function(html){/*code that scans the html for style attributes*/). This assumes that getting the file again will get the same code (which might not be true if the real page was retrieved with a POST request) and that the DOM has not been changed. Otherwise 'div:eq(4)' will not refer to the correct element. You can obviate this problem by using id's which should not change. Still, the approach is problematic and you might want to use $.metadata to add information to the HTML itself.

If anyone knows how to get the actual text of the "style" attribute, please let me know!

{ 12 } Comments

  1. Mattias Hising | January 16, 2009 at 8:37 am | Permalink

    I love this approach to specify the visual/design aspects of javascripts in css. Good work.

  2. Danny | January 16, 2009 at 11:02 am | Permalink

    Thanks. I like your idea of hosting it on github and making it formally open source.

  3. Thomas Jaggi | February 12, 2009 at 7:21 am | Permalink

    Great work! Even if I don’t really understand what’s going on. ;)

    Is it possible getting the styles of a specific class?

    In the jQuery Google Group you have posted something like “var style = $.parsecss()['.myclassname']; “
    Using this with the current version I get an error (“str has no properties”).

    Using the script version on http://bililite.com/blog/blogfiles/cssparser/cssparser.jquery.js this works in Firefox but not in IE.

    Thanks for your help.

  4. Thomas Jaggi | February 12, 2009 at 8:03 am | Permalink

    Whoups, my fault: I tried animating a z-index which killed IE.
    But still: Is this also possible with the new version (just getting the styles of a specific class)? I understand this is not the main purpose but anyway…

  5. Danny | February 12, 2009 at 8:34 pm | Permalink

    @Thomas Jaggi:
    The version I mentioned on the jQuery Google group actually returned a parsed object; it did not use a callback. It used synchronous Ajax, which is always a bad idea. The current version is better.
    Parsing the CSS is probably not the best way of getting the styles of a specific class. You can go through the stylesheets (see this discussion, but realize the code is untested: http://groups.google.com/group/jquery-en/browse_thread/thread/a95f419f185f63dd/03df7ac2f2c82bfe), using something like this:

    function classStyle (className){
      var re = new RegExp('(^|,)\\s*\\.'+className+'\\s*(,|$)');
      var style = "";
      $.each (document.styleSheets, function(){
        $.each (this.cssRules || this.rules, function() {
          if (re.test(this.selectorText)) style += this.style.cssText + ';';
        });
      });
      return style;
    }
    

    or see the jQuery UI code for animating classes for a clever trick: create an element, record the styles, then apply the class name and see what changed.
    -Danny

  6. Thomas Jaggi | February 13, 2009 at 5:20 pm | Permalink

    Thanks a lot for your help, now I’m using the function above (with some changes to get it working in IE).

  7. James Vivian | April 2, 2009 at 3:05 pm | Permalink

    Am I missing something, or has the linked code been updated beyond this post? It seems like calls to $.cssparser should be $.parsecss instead.

  8. Danny | April 2, 2009 at 7:32 pm | Permalink

    @James Vivian:
    You’re absolutely right! I can’t imagine how I missed that. I’m changing everything to parsecss now.
    Thanks for catching my idiocy.
    -Danny

  9. MJ | May 1, 2009 at 4:01 pm | Permalink

    Could you help clarify which js file is the most recent. The Download code link doesn’t go the correct file and is named differently than what’s in the git repository. However, both files are labeled in the comments as version 1.0 with differences between the two. Thanks for the help in advance.

  10. Danny | May 3, 2009 at 7:04 am | Permalink

    @MJ:
    You are right. That’s the problem with changing the name of the file (which I did in response to James Vivian above). I’ve updated the link to the source code, made it version 1.1 (there was a minor bug fix) and removed the github link entirely, since it takes too much time to maintain it.
    –Danny

  11. ft | July 31, 2009 at 2:00 pm | Permalink

    Thanks for sharing! Is it theoretically possible to modify your code that it searches for say “border-radius” and apply according actions for ie?

  12. Danny | July 31, 2009 at 4:02 pm | Permalink

    @ft:
    Sure. Just use

    $().parsecss(function(css){
    	for (var selector in css){
    		for (var property in css[selector]){
    			if (property == 'border-radius'){
    				// fix it appropriately; css[selector][property] is the value
    			}
    		}
    	}
    });

    You still need to apply this only for IE (with conditional comments, $.browser or something. Up to you)
    –Danny

Post a Comment

Your email is never published nor shared. Required fields are marked *