1 var SPECIFICITY = (function() {
6 // Calculate the specificity for a selector by dividing it into simple selectors and counting them
7 calculate = function(input) {
14 // Separate input by commas
15 selectors = input.split(',');
17 for (i = 0, len = selectors.length; i < len; i += 1) {
18 selector = selectors[i];
19 if (selector.length > 0) {
20 results.push(calculateSingle(selector));
28 * Calculates the specificity of CSS selectors
29 * http://www.w3.org/TR/css3-selectors/#specificity
31 * Returns an object with the following properties:
32 * - selector: the input
33 * - specificity: e.g. 0,1,0,0
34 * - parts: array with details about each part of the selector that counts towards the specificity
35 * - specificityArray: e.g. [0, 1, 0, 0]
37 calculateSingle = function(input) {
46 // The following regular expressions assume that selectors matching the preceding regular expressions have been removed
47 attributeRegex = /(\[[^\]]+\])/g,
48 idRegex = /(#[^\#\s\+>~\.\[:]+)/g,
49 classRegex = /(\.[^\s\+>~\.\[:]+)/g,
50 pseudoElementRegex = /(::[^\s\+>~\.\[:]+|:first-line|:first-letter|:before|:after)/gi,
51 // A regex for pseudo classes with brackets - :nth-child(), :nth-last-child(), :nth-of-type(), :nth-last-type(), :lang()
52 pseudoClassWithBracketsRegex = /(:[\w-]+\([^\)]*\))/gi,
53 // A regex for other pseudo classes, which don't have brackets
54 pseudoClassRegex = /(:[^\s\+>~\.\[:]+)/g,
55 elementRegex = /([^\s\+>~\.\[:]+)/g;
57 // Find matches for a regular expression in a string and push their details to parts
58 // Type is "a" for IDs, "b" for classes, attributes and pseudo-classes and "c" for elements and pseudo-elements
59 findMatch = function(regex, type) {
60 var matches, i, len, match, index, length;
61 if (regex.test(selector)) {
62 matches = selector.match(regex);
63 for (i = 0, len = matches.length; i < len; i += 1) {
66 index = selector.indexOf(match);
67 length = match.length;
69 selector: input.substr(index, length),
74 // Replace this simple selector with whitespace so it won't be counted in further simple selectors
75 selector = selector.replace(match, Array(length + 1).join(' '));
80 // Replace escaped characters with plain text, using the "A" character
81 // https://www.w3.org/TR/CSS21/syndata.html#characters
83 var replaceWithPlainText = function(regex) {
84 var matches, i, len, match;
85 if (regex.test(selector)) {
86 matches = selector.match(regex);
87 for (i = 0, len = matches.length; i < len; i += 1) {
89 selector = selector.replace(match, Array(match.length + 1).join('A'));
93 // Matches a backslash followed by six hexadecimal digits followed by an optional single whitespace character
94 escapeHexadecimalRegex = /\\[0-9A-Fa-f]{6}\s?/g,
95 // Matches a backslash followed by fewer than six hexadecimal digits followed by a mandatory single whitespace character
96 escapeHexadecimalRegex2 = /\\[0-9A-Fa-f]{1,5}\s/g,
97 // Matches a backslash followed by any character
98 escapeSpecialCharacter = /\\./g;
100 replaceWithPlainText(escapeHexadecimalRegex);
101 replaceWithPlainText(escapeHexadecimalRegex2);
102 replaceWithPlainText(escapeSpecialCharacter);
105 // Remove the negation psuedo-class (:not) but leave its argument because specificity is calculated on its argument
107 var regex = /:not\(([^\)]*)\)/g;
108 if (regex.test(selector)) {
109 selector = selector.replace(regex, ' $1 ');
113 // Remove anything after a left brace in case a user has pasted in a rule, not just a selector
115 var regex = /{[^]*/gm,
116 matches, i, len, match;
117 if (regex.test(selector)) {
118 matches = selector.match(regex);
119 for (i = 0, len = matches.length; i < len; i += 1) {
121 selector = selector.replace(match, Array(match.length + 1).join(' '));
126 // Add attribute selectors to parts collection (type b)
127 findMatch(attributeRegex, 'b');
129 // Add ID selectors to parts collection (type a)
130 findMatch(idRegex, 'a');
132 // Add class selectors to parts collection (type b)
133 findMatch(classRegex, 'b');
135 // Add pseudo-element selectors to parts collection (type c)
136 findMatch(pseudoElementRegex, 'c');
138 // Add pseudo-class selectors to parts collection (type b)
139 findMatch(pseudoClassWithBracketsRegex, 'b');
140 findMatch(pseudoClassRegex, 'b');
142 // Remove universal selector and separator characters
143 selector = selector.replace(/[\*\s\+>~]/g, ' ');
145 // Remove any stray dots or hashes which aren't attached to words
146 // These may be present if the user is live-editing this selector
147 selector = selector.replace(/[#\.]/g, ' ');
149 // The only things left should be element selectors (type c)
150 findMatch(elementRegex, 'c');
152 // Order the parts in the order they appear in the original selector
153 // This is neater for external apps to deal with
154 parts.sort(function(a, b) {
155 return a.index - b.index;
160 specificity: '0,' + typeCount.a.toString() + ',' + typeCount.b.toString() + ',' + typeCount.c.toString(),
161 specificityArray: [0, typeCount.a, typeCount.b, typeCount.c],
167 * Compares two CSS selectors for specificity
168 * Alternatively you can replace one of the CSS selectors with a specificity array
170 * - it returns -1 if a has a lower specificity than b
171 * - it returns 1 if a has a higher specificity than b
172 * - it returns 0 if a has the same specificity than b
174 compare = function(a, b) {
179 if (typeof a ==='string') {
180 if (a.indexOf(',') !== -1) {
181 throw 'Invalid CSS selector';
183 aSpecificity = calculateSingle(a)['specificityArray'];
185 } else if (Array.isArray(a)) {
186 if (a.filter(function(e) { return (typeof e === 'number'); }).length !== 4) {
187 throw 'Invalid specificity array';
192 throw 'Invalid CSS selector or specificity array';
195 if (typeof b ==='string') {
196 if (b.indexOf(',') !== -1) {
197 throw 'Invalid CSS selector';
199 bSpecificity = calculateSingle(b)['specificityArray'];
201 } else if (Array.isArray(b)) {
202 if (b.filter(function(e) { return (typeof e === 'number'); }).length !== 4) {
203 throw 'Invalid specificity array';
208 throw 'Invalid CSS selector or specificity array';
211 for (i = 0; i < 4; i += 1) {
212 if (aSpecificity[i] < bSpecificity[i]) {
214 } else if (aSpecificity[i] > bSpecificity[i]) {
223 calculate: calculate,
228 // Export for Node JS
229 if (typeof exports !== 'undefined') {
230 exports.calculate = SPECIFICITY.calculate;
231 exports.compare = SPECIFICITY.compare;