var SPECIFICITY = (function() { var calculate, calculateSingle, compare; // Calculate the specificity for a selector by dividing it into simple selectors and counting them calculate = function(input) { var selectors, selector, i, len, results = []; // Separate input by commas selectors = input.split(','); for (i = 0, len = selectors.length; i < len; i += 1) { selector = selectors[i]; if (selector.length > 0) { results.push(calculateSingle(selector)); } } return results; }; /** * Calculates the specificity of CSS selectors * http://www.w3.org/TR/css3-selectors/#specificity * * Returns an object with the following properties: * - selector: the input * - specificity: e.g. 0,1,0,0 * - parts: array with details about each part of the selector that counts towards the specificity * - specificityArray: e.g. [0, 1, 0, 0] */ calculateSingle = function(input) { var selector = input, findMatch, typeCount = { 'a': 0, 'b': 0, 'c': 0 }, parts = [], // The following regular expressions assume that selectors matching the preceding regular expressions have been removed attributeRegex = /(\[[^\]]+\])/g, idRegex = /(#[^\s\+>~\.\[:]+)/g, classRegex = /(\.[^\s\+>~\.\[:]+)/g, pseudoElementRegex = /(::[^\s\+>~\.\[:]+|:first-line|:first-letter|:before|:after)/gi, // A regex for pseudo classes with brackets - :nth-child(), :nth-last-child(), :nth-of-type(), :nth-last-type(), :lang() pseudoClassWithBracketsRegex = /(:[\w-]+\([^\)]*\))/gi, // A regex for other pseudo classes, which don't have brackets pseudoClassRegex = /(:[^\s\+>~\.\[:]+)/g, elementRegex = /([^\s\+>~\.\[:]+)/g; // Find matches for a regular expression in a string and push their details to parts // Type is "a" for IDs, "b" for classes, attributes and pseudo-classes and "c" for elements and pseudo-elements findMatch = function(regex, type) { var matches, i, len, match, index, length; if (regex.test(selector)) { matches = selector.match(regex); for (i = 0, len = matches.length; i < len; i += 1) { typeCount[type] += 1; match = matches[i]; index = selector.indexOf(match); length = match.length; parts.push({ selector: input.substr(index, length), type: type, index: index, length: length }); // Replace this simple selector with whitespace so it won't be counted in further simple selectors selector = selector.replace(match, Array(length + 1).join(' ')); } } }; // Replace escaped characters with plain text, using the "A" character // https://www.w3.org/TR/CSS21/syndata.html#characters (function() { var replaceWithPlainText = function(regex) { var matches, i, len, match; if (regex.test(selector)) { matches = selector.match(regex); for (i = 0, len = matches.length; i < len; i += 1) { match = matches[i]; selector = selector.replace(match, Array(match.length + 1).join('A')); } } }, // Matches a backslash followed by six hexadecimal digits followed by an optional single whitespace character escapeHexadecimalRegex = /\\[0-9A-Fa-f]{6}\s?/g, // Matches a backslash followed by fewer than six hexadecimal digits followed by a mandatory single whitespace character escapeHexadecimalRegex2 = /\\[0-9A-Fa-f]{1,5}\s/g, // Matches a backslash followed by any character escapeSpecialCharacter = /\\./g; replaceWithPlainText(escapeHexadecimalRegex); replaceWithPlainText(escapeHexadecimalRegex2); replaceWithPlainText(escapeSpecialCharacter); }()); // Remove the negation psuedo-class (:not) but leave its argument because specificity is calculated on its argument (function() { var regex = /:not\(([^\)]*)\)/g; if (regex.test(selector)) { selector = selector.replace(regex, ' $1 '); } }()); // Remove anything after a left brace in case a user has pasted in a rule, not just a selector (function() { var regex = /{[^]*/gm, matches, i, len, match; if (regex.test(selector)) { matches = selector.match(regex); for (i = 0, len = matches.length; i < len; i += 1) { match = matches[i]; selector = selector.replace(match, Array(match.length + 1).join(' ')); } } }()); // Add attribute selectors to parts collection (type b) findMatch(attributeRegex, 'b'); // Add ID selectors to parts collection (type a) findMatch(idRegex, 'a'); // Add class selectors to parts collection (type b) findMatch(classRegex, 'b'); // Add pseudo-element selectors to parts collection (type c) findMatch(pseudoElementRegex, 'c'); // Add pseudo-class selectors to parts collection (type b) findMatch(pseudoClassWithBracketsRegex, 'b'); findMatch(pseudoClassRegex, 'b'); // Remove universal selector and separator characters selector = selector.replace(/[\*\s\+>~]/g, ' '); // Remove any stray dots or hashes which aren't attached to words // These may be present if the user is live-editing this selector selector = selector.replace(/[#\.]/g, ' '); // The only things left should be element selectors (type c) findMatch(elementRegex, 'c'); // Order the parts in the order they appear in the original selector // This is neater for external apps to deal with parts.sort(function(a, b) { return a.index - b.index; }); return { selector: input, specificity: '0,' + typeCount.a.toString() + ',' + typeCount.b.toString() + ',' + typeCount.c.toString(), specificityArray: [0, typeCount.a, typeCount.b, typeCount.c], parts: parts }; }; /** * Compares two CSS selectors for specificity * Alternatively you can replace one of the CSS selectors with a specificity array * * - it returns -1 if a has a lower specificity than b * - it returns 1 if a has a higher specificity than b * - it returns 0 if a has the same specificity than b */ compare = function(a, b) { var aSpecificity, bSpecificity, i; if (typeof a ==='string') { if (a.indexOf(',') !== -1) { throw 'Invalid CSS selector'; } else { aSpecificity = calculateSingle(a)['specificityArray']; } } else if (Array.isArray(a)) { if (a.filter(function(e) { return (typeof e === 'number'); }).length !== 4) { throw 'Invalid specificity array'; } else { aSpecificity = a; } } else { throw 'Invalid CSS selector or specificity array'; } if (typeof b ==='string') { if (b.indexOf(',') !== -1) { throw 'Invalid CSS selector'; } else { bSpecificity = calculateSingle(b)['specificityArray']; } } else if (Array.isArray(b)) { if (b.filter(function(e) { return (typeof e === 'number'); }).length !== 4) { throw 'Invalid specificity array'; } else { bSpecificity = b; } } else { throw 'Invalid CSS selector or specificity array'; } for (i = 0; i < 4; i += 1) { if (aSpecificity[i] < bSpecificity[i]) { return -1; } else if (aSpecificity[i] > bSpecificity[i]) { return 1; } } return 0; }; return { calculate: calculate, compare: compare }; }()); // Export for Node JS if (typeof exports !== 'undefined') { exports.calculate = SPECIFICITY.calculate; exports.compare = SPECIFICITY.compare; }