Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: David Lund and Ryan Jeffords

Calculating CSS Selector Specificity Using ColdFusion

By Ben Nadel on
Tags: ColdFusion

Yesterday, I was working on merging some CSS (Cascading Style Sheets) rules into an HTML document, converting style sheets into inline "style" attributes. My default approach was to just inject the CSS rules in a top-down manner, which worked for the most part. But, when I was done, I realized that this methodology completely ignores the specificity of the CSS selectors. Each CSS selector has a specificity that is generally based on its use of IDs, classes, attributes, and elements. In order to take selector specificity into account, I needed to come up with a way to parse and measure CSS selectors.

NOTE: At the time of this writing, ColdFusion 10 was in public beta.

Going into this, I knew that CSS selectors worked according to a specificity; but, I didn't know exactly how that specificity was calculated. According to Smashing Magazine, and a number of other sites I read, selector specificity is determined by the following, in order of highest-to-lowest specificity:

  1. IDs.
  2. Classes, attributes, and pseudo-classes.
  3. Elements and pseudo-elements.

To calculate the actual specificity as a number, you add up each of the above criteria, concatenate the sums as strings, and then convert the string value into a number. For more information on how this is done, take a look at this Smashing Magazine article.

This morning, I tried to take this approach and build a ColdFusion function that takes a CSS selector and returns the numeric specificity value. I think what I came up with is pretty flexible; but, it doesn't know how to handle escaped character sequences. And, to be honest, I don't really know what the rules regarding escaped characters in CSS are - I tend to try and avoid them.

The following code starts with my ColdFusion UDF - calculateSelectorSpecificity() - and then executes some tests on known CSS selectors:

  • <cfscript>
  •  
  •  
  • // I roughly calculate the numeric specificity of a CSS selector.
  • // CAUTION: This algorithm doesn't know how to take into account
  • // character escape sequences.
  • function calculateSelectorSpecificity( String selector ){
  •  
  • // Before we start parsing the selector, we're gonna try to
  • // strip out characters that will making pattern matching more
  • // difficult.
  •  
  • // Strip out wild-card matches - these don't contribute to
  • // a selector specificity.
  • selector = replace( selector, "*", "", "all" );
  •  
  • // Strip out any quoted values - these will only be in the
  • // attribute selectors (and don't contribute to our
  • // specificity calculation).
  • selector = reReplace( selector, """[^""]*""", "", "all" );
  • selector = reReplace( selector, "'[^']*'", "", "all" );
  •  
  • // Now that we've stripped out the quoted values, let's strip
  • // out any content within the attribute selectors.
  • selector = reReplace( selector, "\[[^\]]*\]", "[]", "all" );
  •  
  • // Strip out any special child and descendant selectors as
  • // these don't really contribute to specificity.
  • selector = reReplace( selector, "[>+~]+", " ", "all" );
  •  
  • // Strip out any "function calls"; these will be for complex
  • // selectors like :not() and :eq(). We're gonna do this in a
  • // loop so that we can simplify the replace and handle nested
  • // groups of parenthesis.
  • while (find( "(", selector )){
  •  
  • // Strip out the smallest parenthesis.
  • selector = reReplace( selector, "\([^)]*\)", "", "all" );
  •  
  • }
  •  
  • // Now that we've stripped off any parenthesis, our pseudo-
  • // elements and pseudo-classes should all be in a uniform.
  • // However, pseudo-elements and pseudo-classes actually have
  • // different specifity than each other. To make things simple,
  • // let's convert pseudo-classes (which have high specificity)
  • // into mock classes.
  • selector = reReplace(
  • selector,
  • ":(first-child|last-child|link|visited|hover|active|focus|lang)",
  • ".pseudo",
  • "all"
  • );
  •  
  • // Now that we've removed the pseudo-classes, the only
  • // constructs that start with ":" should be the pseudo-
  • // elements. Let's replace these with mock elements. Notice
  • // that we are injecting a space before the element name.
  • selector = reReplace( selector, ":[\w-]+", " pseudo", "all" );
  •  
  • // Now that we've cleaned up the selector, we can count the
  • // number of key elements within the selector.
  •  
  • // Count the number of ID selectors. These are the selectors
  • // with the highest specificity.
  • var idCount = arrayLen(
  • reMatch( "##[\w-]+", selector )
  • );
  •  
  • // Count the number of classes, attributes, and pseudo-
  • // classes. Remember, we converted our pseudo-classes to be
  • // mock classes (.pseudo).
  • var classCount = arrayLen(
  • reMatch( "\.[\w_-]+|\[\]", selector )
  • );
  •  
  • // Count the number of elements and pseudo-elements. Remember,
  • // we converted our pseudo-selements to be mock elements
  • // (pseudo).
  • var elementCount = arrayLen(
  • reMatch( "(^|\s)[\w_-]+", selector )
  • );
  •  
  • // Now that we have our count of the various parts of the
  • // selector, we can calculate the specificity by concatenating
  • // the parts (as strings), and then converting to a number -
  • // the number will be the specificity of the selector.
  • return(
  • fix( idCount & classCount & elementCount )
  • );
  •  
  • }
  •  
  •  
  • // ------------------------------------------------------ //
  • // ------------------------------------------------------ //
  • // ------------------------------------------------------ //
  • // ------------------------------------------------------ //
  •  
  •  
  • // Let's create a collection of selectors with a known specificity
  • // so we can test our calculations. These are taking from the
  • // Smashing Magazine site:
  • // http://coding.smashingmagazine.com/2007/07/27/
  • // -> css-specificity-things-you-should-know/
  • testCases = [
  • {
  • selector: "*",
  • known: 0
  • },
  • {
  • selector: "li",
  • known: 1
  • },
  • {
  • selector: "li:first-line",
  • known: 2
  • },
  • {
  • selector: "ul li",
  • known: 2
  • },
  • {
  • selector: "ul ol+li",
  • known: 3
  • },
  • {
  • selector: "h1 + *[rel=up]",
  • known: 11
  • },
  • {
  • selector: "ul ol li.red",
  • known: 13
  • },
  • {
  • selector: "li.red.level",
  • known: 21
  • },
  • {
  • selector: "div p",
  • known: 2
  • },
  • {
  • selector: ".sith",
  • known: 10
  • },
  • {
  • selector: "div p.sith",
  • known: 12
  • },
  • {
  • selector: "##sith",
  • known: 100
  • },
  • {
  • selector: "body ##darkside .sith p",
  • known: 112
  • }
  • ];
  •  
  • // Add a few more test cases. I am making these ones up to test
  • // some of the more advanced complex CSS cases.
  • arrayAppend(
  • testCases,
  • [
  • {
  • selector: "p:has( a[href] )",
  • known: 2
  • },
  • {
  • selector: "body##top:lang(fr-ca) div.alert",
  • known: 122
  • }
  • ],
  • true
  • );
  •  
  •  
  • // Let's calculate the specificity.
  • for (testCase in testCases){
  •  
  • // Get and set the calculated specificity.
  • testCase.calcualted = calculateSelectorSpecificity(
  • testCase.selector
  • );
  •  
  • }
  •  
  • // Now, output the results of the known vs. claculated values.
  • for (testCase in testCases){
  •  
  • writeOutput( testCase.selector & "<br />" );
  • writeOutput( "-- Known: " & testCase.known & "<br />" );
  • writeOutput( "-- Calculated: " & testCase.calcualted & "<br />" );
  • writeOutput( "<br />" );
  •  
  • }
  •  
  •  
  • </cfscript>

In order to make the specificity calculation easier, I start off by stripping out and converting a lot of the selector content into constructs that will simplify the Regular Expression pattern matching. When we run the above code, we get the following page output:

*
-- Known: 0
-- Calculated: 0

li
-- Known: 1
-- Calculated: 1

li:first-line
-- Known: 2
-- Calculated: 2

ul li
-- Known: 2
-- Calculated: 2

ul ol+li
-- Known: 3
-- Calculated: 3

h1 + *[rel=up]
-- Known: 11
-- Calculated: 11

ul ol li.red
-- Known: 13
-- Calculated: 13

li.red.level
-- Known: 21
-- Calculated: 21

div p
-- Known: 2
-- Calculated: 2

.sith
-- Known: 10
-- Calculated: 10

div p.sith
-- Known: 12
-- Calculated: 12

#sith
-- Known: 100
-- Calculated: 100

body #darkside .sith p
-- Known: 112
-- Calculated: 112

p:has( a[href] )
-- Known: 2
-- Calculated: 2

body#top:lang(fr-ca) div.alert
-- Known: 122
-- Calculated: 122

As you can see, my ColdFusion UDF is able to replicate the known specificity of our test-case CSS selectors.

Knowing the CSS selector specificity is one thing - applying it is another. Now, when I go to merge a Style Sheet into an HTML DOM (Document Object Model), I'll have to defer the rendering of the style attribute until I have collected all of the selectors that apply to a given element. Then, I'll have to sort the selectors based on specificity and document-position and then apply them in order. This should be an interesting next step!




Reader Comments

@Ben,

Don'tcha just hate it when you add a class to something, and it doesn't work, and you end up having to add that stupid "!important" to styles in the rule?

Well, I do.

The problem with the specificity rules are that we don't compose selectors for specificity. We compose them to hit the elements we want to hit. Then the CSS renderer says: Screw you. This rule applies to this element alright, but this other CSS rule, that was applied for a totally different reason, gets to invalidate your request, simply because of the syntax you had to use to hit all the elements you wanted to hit.

Seems pretty stupid, doesn't it?

I wish CSS rules could set their own specificity values, overriding the values calculated from their selectors:

  • specificity: 110;

Oh well, back in the early early days, I once proposed (on Usenet comp.lang.html or some such) to create a new HTML element "if" that you could use to choose markup based on conditions such as screen resolution. I was told, shut up, that's not how HTML works, all markup should be presentational. The powers that be said no, but that's what opened the market for ColdFusion's "cfif", right?

Reply to this Comment

P.S.: Thanks for this post. I already knew how specificity was calculated, but I hate it so much, I never bother explaining it to anyone. :-) You've done the fledgling CSS coders a great service.

Might be worthwhile to comment rules with your calculated values:

  • ul ol li.red /* specificity 13 */
  • {
  • arf: bowwow;
  • }

If you converted calculateSelectorSpecificity to JavaScript, you could write a nested loop and do it to every rule on a page: Start with document.styleSheets (all browsers). Loop through that array. Within each sheet, there's a property called rules (MSIE) or cssRules (everyone else). Loop through that array. For every rule, there's a property called selectorText (all browsers). Then you could print them all out to the console along with the specificity value you calculated:

  • console.log(r.selectorText + " /* specificity "
  • + calculateSelectorSpecificity(r.selectorText)
  • + " */");

Then you could just copy and paste from the console log to the css file.

Just a thought.

Reply to this Comment

@Brian,

Ha ha, what can't be explained with Sharks?

@Randall,

Thanks man! That put a smile on my face :D

@WebManWalking,

Constructing "non-trivial" CSS is one of those things that *feels* like it should be easy; but when I go to make it happen, it's always way harder than I expect. I want to believe that there's a way to build modular CSS where the rules make sense and can all live in parallel without messing with each other. But then, I go to do it... and it's not so good :(

It makes me think of something I heard the other day. I'm sure I will mangle it, but it was something like:

"Programming is all about coming up with ever better answers to the question: where I put this code."

... one day, it will be easy :D

Reply to this Comment

I might be an exception, but I kinda like the way css specificity works. In most cases it works perfectly naturally and you don't think about it and only in those few cases where you *do* encounter problems with it you will think about it and thus the overall impression of the system might be negative, however as far as I am concerned the system is quite ingenious (better at the very least than by order mentioned or anything along those lines) and in most cases you can work around any trouble you encounter with it by adding an unnecessary parent id to the selector (which is possible in 99/100 cases).

And btw, first of all *why* would you want to put all styles inline and second of all, isn't it far easier in that case to copy your styles from firebug as all the overridden styles will be striked through.

Reply to this Comment

@Tom,

When I first started digging into this little project, I did some reading up on CSSParser. It seemed cool, but I saw some people saying that it ignored specificity and will also completely halt if it doesn't recognize any of the CSS properties being used. As such, I wanted to try and fall back to something that was more based on string-parsing rather than actual CSS parsing and validation.

I didn't actually try using it though; this was just some hasty reading.

As the HTML part of this project, I was going to use jSoup which I was recently told about. It has a great API for searching and traversing (much like jQuery).

I'll have to take a further look at CSSParser, though, it sounds like you've had quite a bit of success with it.

@David,

To me, CSS works really well ... until I realize that I made a bad choice about defining some base class. Like putting a line-height on a generic P tag or something rather than on a content container. I've been getting better at not doing things like that; but... :)

That said, CSS is definitely a million percent better than inline styles! This is just for a fun little side project.

Reply to this Comment

Post A Comment

?
You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.