specificity.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. // Calculate the specificity for a selector by dividing it into simple selectors and counting them
  2. var calculate = function(input) {
  3. var selectors,
  4. selector,
  5. i,
  6. len,
  7. results = [];
  8. // Separate input by commas
  9. selectors = input.split(',');
  10. for (i = 0, len = selectors.length; i < len; i += 1) {
  11. selector = selectors[i];
  12. if (selector.length > 0) {
  13. results.push(calculateSingle(selector));
  14. }
  15. }
  16. return results;
  17. };
  18. /**
  19. * Calculates the specificity of CSS selectors
  20. * http://www.w3.org/TR/css3-selectors/#specificity
  21. *
  22. * Returns an object with the following properties:
  23. * - selector: the input
  24. * - specificity: e.g. 0,1,0,0
  25. * - parts: array with details about each part of the selector that counts towards the specificity
  26. * - specificityArray: e.g. [0, 1, 0, 0]
  27. */
  28. var calculateSingle = function(input) {
  29. var selector = input,
  30. findMatch,
  31. typeCount = {
  32. 'a': 0,
  33. 'b': 0,
  34. 'c': 0
  35. },
  36. parts = [],
  37. // The following regular expressions assume that selectors matching the preceding regular expressions have been removed
  38. attributeRegex = /(\[[^\]]+\])/g,
  39. idRegex = /(#[^\#\s\+>~\.\[:\)]+)/g,
  40. classRegex = /(\.[^\s\+>~\.\[:\)]+)/g,
  41. pseudoElementRegex = /(::[^\s\+>~\.\[:]+|:first-line|:first-letter|:before|:after)/gi,
  42. // A regex for pseudo classes with brackets - :nth-child(), :nth-last-child(), :nth-of-type(), :nth-last-type(), :lang()
  43. // The negation psuedo class (:not) is filtered out because specificity is calculated on its argument
  44. // :global and :local are filtered out - they look like psuedo classes but are an identifier for CSS Modules
  45. pseudoClassWithBracketsRegex = /(:(?!not|global|local)[\w-]+\([^\)]*\))/gi,
  46. // A regex for other pseudo classes, which don't have brackets
  47. pseudoClassRegex = /(:(?!not|global|local)[^\s\+>~\.\[:]+)/g,
  48. elementRegex = /([^\s\+>~\.\[:]+)/g;
  49. // Find matches for a regular expression in a string and push their details to parts
  50. // Type is "a" for IDs, "b" for classes, attributes and pseudo-classes and "c" for elements and pseudo-elements
  51. findMatch = function(regex, type) {
  52. var matches, i, len, match, index, length;
  53. if (regex.test(selector)) {
  54. matches = selector.match(regex);
  55. for (i = 0, len = matches.length; i < len; i += 1) {
  56. typeCount[type] += 1;
  57. match = matches[i];
  58. index = selector.indexOf(match);
  59. length = match.length;
  60. parts.push({
  61. selector: input.substr(index, length),
  62. type: type,
  63. index: index,
  64. length: length
  65. });
  66. // Replace this simple selector with whitespace so it won't be counted in further simple selectors
  67. selector = selector.replace(match, Array(length + 1).join(' '));
  68. }
  69. }
  70. };
  71. // Replace escaped characters with plain text, using the "A" character
  72. // https://www.w3.org/TR/CSS21/syndata.html#characters
  73. (function() {
  74. var replaceWithPlainText = function(regex) {
  75. var matches, i, len, match;
  76. if (regex.test(selector)) {
  77. matches = selector.match(regex);
  78. for (i = 0, len = matches.length; i < len; i += 1) {
  79. match = matches[i];
  80. selector = selector.replace(match, Array(match.length + 1).join('A'));
  81. }
  82. }
  83. },
  84. // Matches a backslash followed by six hexadecimal digits followed by an optional single whitespace character
  85. escapeHexadecimalRegex = /\\[0-9A-Fa-f]{6}\s?/g,
  86. // Matches a backslash followed by fewer than six hexadecimal digits followed by a mandatory single whitespace character
  87. escapeHexadecimalRegex2 = /\\[0-9A-Fa-f]{1,5}\s/g,
  88. // Matches a backslash followed by any character
  89. escapeSpecialCharacter = /\\./g;
  90. replaceWithPlainText(escapeHexadecimalRegex);
  91. replaceWithPlainText(escapeHexadecimalRegex2);
  92. replaceWithPlainText(escapeSpecialCharacter);
  93. }());
  94. // Remove anything after a left brace in case a user has pasted in a rule, not just a selector
  95. (function() {
  96. var regex = /{[^]*/gm,
  97. matches, i, len, match;
  98. if (regex.test(selector)) {
  99. matches = selector.match(regex);
  100. for (i = 0, len = matches.length; i < len; i += 1) {
  101. match = matches[i];
  102. selector = selector.replace(match, Array(match.length + 1).join(' '));
  103. }
  104. }
  105. }());
  106. // Add attribute selectors to parts collection (type b)
  107. findMatch(attributeRegex, 'b');
  108. // Add ID selectors to parts collection (type a)
  109. findMatch(idRegex, 'a');
  110. // Add class selectors to parts collection (type b)
  111. findMatch(classRegex, 'b');
  112. // Add pseudo-element selectors to parts collection (type c)
  113. findMatch(pseudoElementRegex, 'c');
  114. // Add pseudo-class selectors to parts collection (type b)
  115. findMatch(pseudoClassWithBracketsRegex, 'b');
  116. findMatch(pseudoClassRegex, 'b');
  117. // Remove universal selector and separator characters
  118. selector = selector.replace(/[\*\s\+>~]/g, ' ');
  119. // Remove any stray dots or hashes which aren't attached to words
  120. // These may be present if the user is live-editing this selector
  121. selector = selector.replace(/[#\.]/g, ' ');
  122. // Remove the negation psuedo-class (:not) but leave its argument because specificity is calculated on its argument
  123. // Remove non-standard :local and :global CSS Module identifiers because they do not effect the specificity
  124. selector = selector.replace(/:not/g, ' ');
  125. selector = selector.replace(/:local/g, ' ');
  126. selector = selector.replace(/:global/g, ' ');
  127. selector = selector.replace(/[\(\)]/g, ' ');
  128. // The only things left should be element selectors (type c)
  129. findMatch(elementRegex, 'c');
  130. // Order the parts in the order they appear in the original selector
  131. // This is neater for external apps to deal with
  132. parts.sort(function(a, b) {
  133. return a.index - b.index;
  134. });
  135. return {
  136. selector: input,
  137. specificity: '0,' + typeCount.a.toString() + ',' + typeCount.b.toString() + ',' + typeCount.c.toString(),
  138. specificityArray: [0, typeCount.a, typeCount.b, typeCount.c],
  139. parts: parts
  140. };
  141. };
  142. /**
  143. * Compares two CSS selectors for specificity
  144. * Alternatively you can replace one of the CSS selectors with a specificity array
  145. *
  146. * - it returns -1 if a has a lower specificity than b
  147. * - it returns 1 if a has a higher specificity than b
  148. * - it returns 0 if a has the same specificity than b
  149. */
  150. var compare = function(a, b) {
  151. var aSpecificity,
  152. bSpecificity,
  153. i;
  154. if (typeof a ==='string') {
  155. if (a.indexOf(',') !== -1) {
  156. throw 'Invalid CSS selector';
  157. } else {
  158. aSpecificity = calculateSingle(a)['specificityArray'];
  159. }
  160. } else if (Array.isArray(a)) {
  161. if (a.filter(function(e) { return (typeof e === 'number'); }).length !== 4) {
  162. throw 'Invalid specificity array';
  163. } else {
  164. aSpecificity = a;
  165. }
  166. } else {
  167. throw 'Invalid CSS selector or specificity array';
  168. }
  169. if (typeof b ==='string') {
  170. if (b.indexOf(',') !== -1) {
  171. throw 'Invalid CSS selector';
  172. } else {
  173. bSpecificity = calculateSingle(b)['specificityArray'];
  174. }
  175. } else if (Array.isArray(b)) {
  176. if (b.filter(function(e) { return (typeof e === 'number'); }).length !== 4) {
  177. throw 'Invalid specificity array';
  178. } else {
  179. bSpecificity = b;
  180. }
  181. } else {
  182. throw 'Invalid CSS selector or specificity array';
  183. }
  184. for (i = 0; i < 4; i += 1) {
  185. if (aSpecificity[i] < bSpecificity[i]) {
  186. return -1;
  187. } else if (aSpecificity[i] > bSpecificity[i]) {
  188. return 1;
  189. }
  190. }
  191. return 0;
  192. };
  193. export {
  194. calculate,
  195. compare
  196. };