--- /dev/null
+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;
+}