{"id":251,"date":"2009-01-16T00:21:06","date_gmt":"2009-01-16T06:21:06","guid":{"rendered":"http:\/\/bililite.nfshost.com\/blog\/?p=251"},"modified":"2012-01-16T10:43:24","modified_gmt":"2012-01-16T16:43:24","slug":"jquery-css-parser","status":"publish","type":"post","link":"https:\/\/bililite.com\/blog\/2009\/01\/16\/jquery-css-parser\/","title":{"rendered":"jQuery CSS parser"},"content":{"rendered":"<p>Updated 2011-02-28; minor bug fix. Thanks, <a href=\"#comment-3416\">brian<\/a>!<\/p>\r\n<p>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:<\/p>\r\n<dl>\r\n<dt>style.css<\/dt><dd><pre><code class=\"language-css\">\r\n.gallery a { border: 1px solid green }\r\n<\/code><\/pre><\/dd>\r\n<dt>style.js<\/dt><dd><pre><code class=\"language-javascript\">\r\n$('.gallery a').<a href=\"http:\/\/leandrovieira.com\/projects\/jquery\/lightbox\/\">lightbox<\/a>({overlayBgColor: '#ddd'});\r\n<\/code><\/pre><\/dd>\r\n<\/dl>\r\n<p>I could put the presentational javascript in the HTML with <a href=\"http:\/\/docs.jquery.com\/Plugins\/Metadata\" class=\"language-javascript\">$.metadata<\/a> but while that's fine for quick-and-dirty pages, it's <a href=\"\/blog\/2008\/08\/29\/making-metadata-extensible\/\">evil<\/a> in production since it completely violates the separation of data\/structure\/presentation\/behavior.<\/p>\r\n<p>As <a href=\"\/blog\/2007\/10\/19\/css-parser-in-javascript\/\">I<\/a> and <a href=\"http:\/\/frontendbook.com\/javascript-css-parser-for-custom-properties\/\">others<\/a> have noted before, the application of these plugins belongs in the stylesheet, and I finally got it to work:<\/p>\r\n<dl>\r\n<dt>style.css<\/dt><dd><pre><code class=\"language-css\">\r\n.gallery a {\r\n  border: 1px solid green;\r\n  -jquery-lightbox: {overlayBgColor: '#ddd'};\r\n}\r\n<\/code><\/pre><\/dd>\r\n<\/dl>\r\n<p>and a single call to start it off: <code class=\"language-javascript\">$(document).parsecss($.parsecss.jquery)<\/code> .<\/p>\r\n<p><a href=\"\/inc\/jquery.parsecss.js\">Download the code<\/a>.<\/p>\r\n<p><a href=\"\/blog\/blogfiles\/cssparser\/cssparsertest.php\">See the demo<\/a>.<\/p>\r\n<!--more-->\r\n<h4>Use<\/h4>\r\n<p><code class=\"language-javascript\">$(selector).parsecss(callback)<\/code> scans all <code class=\"language-html\">&lt;style&gt;<\/code> and <code>&lt;link type=\"text\/css\"&gt;<\/code> elements in <code class=\"language-javascript\">$(selector)<\/code> or its descendents, parses each one and passes an object (details <a href=\"#parserdetails\">below<\/a>) to the callback function. It uses a callback because external stylesheets and <code class=\"language-css\">@import<\/code> statements need to use AJAX, asynchronously. The parser is smart enough to understand <code>media<\/code> attributes and statements.<\/p>\r\n<p>The supplied callback is the <code class=\"language-javascript\">$.parsecss.jquery<\/code> function. That function finds properties that start with <code class=\"language-css\">-jquery<\/code> and applies them to the matching elements. Generally, you would want to parse all the stylesheets in the document, <code class=\"language-javascript\">$(document)<\/code>, so the simple call is: <code class=\"language-javascript\">$(document).parsecss($.parsecss.jquery)<\/code>.<\/p>\r\n<h4>The CSS<\/h4>\r\n<p>Some jargon: in the stylesheet<p>\r\n<pre><code class=\"language-css\">foo {\r\n  bar: quux;\r\n  -bar-foo: quux-2;\r\n}\r\n\r\nfoo > foo {\r\n  bar: quux 2;\r\n}\r\n<\/code><\/pre>\r\n<p><code class=\"language-css\">foo { bar: quux; -bar-foo: quux-2}<\/code> is a statement, <code class=\"language-css\">foo<\/code> is a selector, <code class=\"language-css\">{bar: quux; -bar-foo: quux-2}<\/code> is its CSStext, <code class=\"language-css\">-bar-foo: quux-2<\/code> is a declaration, <code class=\"language-css\">-bar-foo<\/code> is its property and <code class=\"language-css\">quux-2<\/code> is its value.<\/p>\r\n<h4><code>$.parsecss.jquery<\/code><\/h4>\r\n<p><code class=\"language-javascript\">$.parsecss.jquery<\/code> is the callback function that implements the custom-jQuery code. It looks for properties that start with <code>-jquery<\/code>. If the property is <code class=\"language-css\">-jquery<\/code> alone, then it executes the value as a javascript statement for each matched element, <code class=\"language-javascript\">$(selector).each(Function(value))<\/code>. For example:<\/p>\r\n<pre><code class=\"language-css\">span.important {\r\n  -jquery: $(this).append('&lt;span&gt;I am important&lt;\/span&gt;')\r\n}<\/code><\/pre>\r\n<p>executes<\/p>\r\n<pre><code class=\"language-javascript\">$('span.important').each(function() {$(this).append('&lt;span&gt;I am important!&lt;\/span&gt;')})<\/code><\/pre>\r\n<p>If the property is <code class=\"language-css\">-jquery-show<\/code> or <code class=\"language-css\">-jquery-hide<\/code>, then the normal show() and hide() are overridden for the matched elements to use the value as the arguments. It will recognize plugins and <a href=\"http:\/\/docs.jquery.com\/UI\/Effects\/effect\">jQuery UI effects<\/a>. Examples (<a href=\"#parsearguments\">see below<\/a> how it parses the arguments):<\/p>\r\n<pre><code>\r\na {\r\n  -jquery-show: fast; \/* <code class=\"language-javascript\">$('a').show()<\/code> does <code class=\"language-javascript\">$('a').show('fast')<\/code> *\/\r\n  -jquery-show: fadeIn 2000 \/* <code class=\"language-javascript\">$('a').show()<\/code> does <code class=\"language-javascript\">$('a').fadeIn(2000)<\/code> *\/\r\n  -jquery-show: fold {size: 5} slow \/* <code class=\"language-javascript\">$('a').show()<\/code> does <code class=\"language-javascript\">$('a').show('fold', {size: 5}, 'slow')<\/code> *\/\r\n  -jquery-hide: explode fast \/* <code class=\"language-javascript\">$('a').hide()<\/code> does <code class=\"language-javascript\">$('a').hide('explode', {}, 'fast')<\/code> It's smart enough to know that jQuery UI effects need an options object inserted *\/\r\n}<\/code><\/pre>\r\n<p>Using <code>hide()<\/code> or <code>show()<\/code> with any arguments overrides the CSS-imposed functions.<\/p>\r\n<p>Otherwise, if the property matches the regular expression <code class=\"language-javascript\">\/-jquery-(.*)\/<\/code> then the (.*) is taken as a plugin name and the value is used for the arguments (<a href=\"#parsearguments\">see below<\/a> how it parses the arguments):<\/p>\r\n<pre><code class=\"language-css\">.gallery a {\r\n  -jquery-lightbox: {overlayBgColor: '#ddd'}\r\n}<\/code><\/pre>\r\n<p>executes<\/p>\r\n<pre><code class=\"language-javascript\">$('.gallery a').lightbox({overlayBgColor: '#ddd'});<\/code><\/pre>\r\n<h4>Valid CSS?<\/h4>\r\n<p>Does including the <code>-jquery<\/code> properties make the CSS invalid? Sort of. The easy answer is \"Who cares?\" \r\nThe CSS syntax <a href=\"http:\/\/www.w3.org\/TR\/CSS21\/syndata.html#parsing-errors\">specifies<\/a> that <blockquote>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 ''<\/blockquote>\r\nThat means that the extensions described here will never cause errors in interpreting the rest of the CSS. Also, the specification says\r\n<blockquote>Keywords and property names beginning with -' or '_' are reserved for vendor-specific extensions.<\/blockquote>\r\nSo, <code>-jquery-show<\/code>, while not valid, is \"expected\" in real-life CSS, just like <code>-moz-border-radius<\/code>.<\/p>\r\n<p>But, some people want CSS that passes the <a href=\"http:\/\/jigsaw.w3.org\/css-validator\/\">validator<\/a>. I adopted a solution similar to <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/121hztk3(VS.71).aspx\">Microsoft's conditional comments<\/a>. Text surrounded by <code>\/*@<\/code> and <code>*\/<\/code> will be parsed by the parser despite being ignored by the browser.<\/p>\r\n<pre><code class=\"language-css\">span.important {\r\n      font-weight: bold;\r\n\/*@   -jquery-after: '&lt;span&gt;!!!&lt;\/span&gt;'; *\/ \/* \"hide\" the jQuery CSS in the comments *\/\r\n}<\/code><\/pre>\r\n<h4><code>$.livequery<\/code><\/h4>\r\n<p>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 <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/ms533050.aspx\">DHTML<\/a>&mdash;Dynamic HTML). If the stylesheet says <code class=\"language-css\">.important { font-weight: bold; color: red }<\/code> then running <code class=\"language-javascript\">$('#x').addClass('important')<\/code> changes the text to red and bold. In addition, <code class=\"language-javascript\">$('#x').removeClass('important')<\/code> 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.<\/p>\r\n<p>jQuery plugins can't do this. The javascript is run at <code class=\"language-javascript\">$(document).ready<\/code>, 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.<\/p>\r\n<p>Enter Brandon Aaron's <a href=\"http:\/\/plugins.jquery.com\/project\/livequery\"><code class=\"language-javascript\">$.livequery<\/code> plugin<\/a>. 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 <a href=\"http:\/\/docs.jquery.com\/Plugins\/livequery\/livequery#matchedFnunmatchedFn\">an \"unmatched\" function<\/a> 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 <code class=\"language-javascript\">$.livequery<\/code> to know how to do that.<\/p>\r\n<p>If <code class=\"language-javascript\">$.livequery<\/code> is available, <code class=\"language-javascript\">$.parsecss<\/code> can use it. Separate the two parts (match code\/unmatch code) with a <code class=\"language-javascript\">!<\/code> (the exclamation point implies \"not\"):<\/p>\r\n<pre><code class=\"language-javascript\">.important {\r\n  -jquery-css: font-weight bold ! font-weight normal ;\r\n}\r\n\/* yes, in real life you'd do font-weight: bold;  and not rely on javascript *\/\r\n<\/code><\/pre>\r\n<p>You can leave either side of the <code class=\"language-javascript\">!<\/code> blank:<\/p>\r\n<pre><code class=\"language-javascript\">.gallery a {\r\n  -jquery-lightbox: {overlayBgColor: '#ddd'} ! ; \/* the '!' means we'll use livequery *\/\r\n  -jquery-unbind: ! click; \/* remove the click handler when the element no longer matches\r\n}\r\n<\/code><\/pre>\r\n<p>Some notes on <code class=\"language-javascript\">$.livequery<\/code>:<\/p>\r\n<ul>\r\n<li>If <code class=\"language-javascript\">$.livequery<\/code> is not available, the value after the <code class=\"language-javascript\">!<\/code> is ignored and the code is applied only when <code class=\"language-javascript\">$.parsecss<\/code> is called.<\/li>\r\n<li><code class=\"language-javascript\">$.livequery<\/code> checks every selector on every modification of the DOM. It can make <a href=\"http:\/\/news.ycombinator.com\/item?id=433631\">complicated pages and script very slow<\/a>. Be aware of the tradeoffs of using it.<\/li>\r\n<li>jQuery 1.3 includes a <code class=\"language-javascript\">$.live<\/code> plugin. This is not the same as <code class=\"language-javascript\">$.livequery<\/code>! It uses event delegation to keep event handlers on new elements, but does not re-apply javascript to new elements. You have to use <code class=\"language-javascript\">$.livequery<\/code> for that.<\/li>\r\n<\/ul>\r\n<h4 id=\"parserdetails\">The parser<\/h4>\r\n<p>The parser turns CSS into a javascript object in the obvious way:<\/p>\r\n<pre><code class=\"language-css\">\r\ndiv div:first {\r\n  font-weight: bold;\r\n  -jquery: each(function() {alert(this.tagName);})\r\n}\r\n\r\ndiv > span {\r\n  color: red;\r\n}\r\n<\/code><\/pre>\r\n<p>is passed to the callback as:<\/p>\r\n<pre><code class=\"language-javascript\">\r\n{\r\n  'div div:first' : {\r\n    'font-weight' : 'bold',\r\n    '-jquery': 'each(function() {alert(this.tagName);})'\r\n  },\r\n  'div > span' : {\r\n    'color': 'red'\r\n  }\r\n}\r\n<\/code><\/pre>\r\n<p>The callback is called separately for each &lt;style&gt; and &lt;link&gt; 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 <code>$.parsecss.jquery<\/code>)does not attempt to obey the <a href=\"http:\/\/www.w3.org\/TR\/CSS21\/cascade.html#cascade\">priority rules of the CSS cascade<\/a>; it will execute all the relevant code.<\/p> \r\n<p>The parser is very simple; it uses regular expressions and <code>String.remove<\/code> to pull out comments, strings, brace-delimited blocks and @-rules, then parsing the grammar by using <code>String.split<\/code> on <code>}<\/code> to get each statement, splitting on <code>{<\/code> to get the selector and cssText, splitting the cssText on <code>;<\/code> to get each declaration and on <code>:<\/code> to get the properties and values. It's not a sophisticated <a href=\"http:\/\/www.amazon.com\/Compilers-Principles-Techniques-Tools-2nd\/dp\/0321486811\">finite-state automaton<\/a> but it serves.<\/p>\r\n<h4 id=\"parsearguments\"><code>$.parsecss.parseArguments<\/code><\/h4>\r\n<p>CSS values use space-delimited arguments, <code>border: 1px solid black<\/code><\/p>. To use this in javascript, we use <code>apply<\/code> which requires an array: <code>['1px', 'solid', 'black']<\/code>. The function <code>$.parsecss.parseArguments({String})<\/code> 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):<\/p>\r\n<pre><code>\r\na b c                                       =>  ['a', 'b', 'c']\r\n1 2 c                                       =>  [1, 2, 'c']\r\n[1,2] {a: 'b c'}                          =>  [[1,2], {a: 'b c'} ]\r\n{a: b, c: d}                              =>  [\"{a: b, c: d}\"] \/\/ inside arrays, objects and functions, you have to use real javascript syntax\r\n{a: 'b', c: 'd'}                            =>  [{a:\"b\", c:\"d\"}] \/\/ the automagic quoting is only at the top level\r\n1+2                                         =>  [3]\r\n'1+2'                                        =>  ['1+2']\r\n10 fast function() {$(this).hide}   =>  [10, \"fast\", (function () {$(this).hide;})]\r\n<\/code><\/pre>\r\n<p>\r\n<h4><code>$.parsecss.isValidSelector<\/code><\/h4>\r\n<p><code class=\"language-javascript\">$.parsecss.isValidSelector(selector {String})<\/code> is a function I wrote for Any Kent's <a href=\"http:\/\/github.com\/andykent\/jss\/tree\/master\">JSS<\/a>, which is sort of the opposite of <code class=\"language-javascript\">$.parsecss.jquery<\/code>: 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 <a href=\"http:\/\/meyerweb.com\/eric\/thoughts\/2008\/10\/22\/javascript-will-save-us-all\/\">similar idea<\/a>. <code class=\"language-javascript\">$.parsecss.isValidSelector<\/code> takes a string and returns <code class=\"language-javascript\">true<\/code> if the browser recognizes it as a CSS selector. Thus in IE6, <code class=\"language-javascript\">$.parsecss.isValidSelector('div p')<\/code> returns <code class=\"language-javascript\">true<\/code> and <code class=\"language-javascript\">$.parsecss.isValidSelector('div > p')<\/code> returns <code class=\"language-javascript\">false<\/code>. In Firefox 3, <code class=\"language-javascript\">$.parsecss.isValidSelector('div > p')<\/code> returns <code class=\"language-javascript\">true<\/code> and <code class=\"language-javascript\">$.parsecss.isValidSelector('div:visible')<\/code> returns <code class=\"language-javascript\">false<\/code>.<\/p>\r\n<p>A simple (though very inefficient) version of <a href=\"http:\/\/github.com\/andykent\/jss\/tree\/master\">JSS<\/a> would be:<\/p>\r\n<pre><code class=\"language-javascript\">$(document).parsecss(function(css){\r\n  for (var selector in css){\r\n    if (! $.parsecss.isValidSelector(selector)) $(selector).css(css[selector]);\r\n  }\r\n});<\/code><\/pre>\r\n<p>It works by creating a new stylesheet and creating a rule with that selector and seeing if the browser recognized it.<\/p>\r\n<h4><code>$.parsecss.mediumApplies<\/code><\/h4>\r\n<p><code class=\"language-javascript\">$.parsecss.mediumApplies(medium {String})<\/code> is similar to <code class=\"language-javascript\">$.parsecss.isValidSelector<\/code>, but it returns true if the argument is a valid media attribute on a<code> &lt;link&gt;<\/code> element or <code>@media<\/code> rule. Thus, for a regular computer, <code class=\"language-javascript\">$.parsecss.mediumApplies('screen')<\/code> returns <code class=\"language-javascript\">true<\/code>, <code class=\"language-javascript\">$.parsecss.mediumApplies('all')<\/code> returns <code class=\"language-javascript\">true<\/code>, <code class=\"language-javascript\">$.parsecss.mediumApplies('print')<\/code> returns <code class=\"language-javascript\">false<\/code>, and <code class=\"language-javascript\">$.parsecss.mediumApplies('')<\/code> returns <code class=\"language-javascript\">true<\/code>.<\/p>\r\n<h4>The second argument to <code>$.fn.parsecss<\/code><\/h4>\r\n<p><code>parsecss<\/code> accepts a second argument, <code>parseAttributes {Boolean}<\/code>. If it is true, the parser will attempt to parse the <code>style<\/code> attribute of each element and pass it to the callback.<\/p>\r\n<p>This is problematic on many levels. The first is how to identify the relevant element. If the element is <code class=\"language-html\">&lt;div id=\"test\" style=\"-jquery-show: fast\"&gt;a div&lt;\/div&gt;<\/code> we want the object <code class=\"language-javascript\">{'#test' : {'-jquery-show': 'fast'} }<\/code>. If the element has an id attribute, then it's easy. Otherwise, it counts the tags in the page and uses <code>:eq()<\/code>: thus if <code class=\"language-html\">&lt;div style=\"-jquery-show: fast\"&gt;a div&lt;\/div&gt;<\/code> is the fifth <code>div<\/code> element, the object generated includes <code class=\"language-javascript\">{'div:eq(4)' : {'-jquery-show': 'fast'} }<\/code>.<\/p>\r\n<p>The real problem is that there is no way to get the actual <code>style<\/code> 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: <code class=\"language-javascript\">$.get(location.pathname+location.search, 'text', function(html){\/*code that scans the html for style attributes*\/)<\/code>. 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 <code>$.metadata<\/code> to add information to the HTML itself.<\/p>\r\n<p>If anyone knows how to get the actual text of the \"<code>style<\/code>\" attribute, please let me know!<\/p>","protected":false},"excerpt":{"rendered":"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 [&hellip;]","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5,20],"tags":[],"_links":{"self":[{"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/posts\/251"}],"collection":[{"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/comments?post=251"}],"version-history":[{"count":44,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/posts\/251\/revisions"}],"predecessor-version":[{"id":2216,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/posts\/251\/revisions\/2216"}],"wp:attachment":[{"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/media?parent=251"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/categories?post=251"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/bililite.com\/blog\/wp-json\/wp\/v2\/tags?post=251"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}