Skip to content

jQuery CSS parser

Updated 2011-02-28; minor bug fix. Thanks, brian!

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: $(document).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), so the simple call is: $(document).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:

$(document).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!

{ 47 } Comments


  1. Fatal error: Uncaught Error: Call to undefined function ereg() in /home/public/blog/wp-content/themes/barthelme/functions.php:178 Stack trace: #0 /home/public/blog/wp-content/themes/barthelme/comments.php(34): barthelme_commenter_link() #1 /home/public/blog/wp-includes/comment-template.php(1469): require('/home/public/bl...') #2 /home/public/blog/wp-content/themes/barthelme/single.php(44): comments_template() #3 /home/public/blog/wp-includes/template-loader.php(74): include('/home/public/bl...') #4 /home/public/blog/wp-blog-header.php(19): require_once('/home/public/bl...') #5 /home/public/blog/index.php(17): require('/home/public/bl...') #6 {main} thrown in /home/public/blog/wp-content/themes/barthelme/functions.php on line 178