tidy-rules.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. var Spaces = require('../../options/format').Spaces;
  2. var Marker = require('../../tokenizer/marker');
  3. var formatPosition = require('../../utils/format-position');
  4. var CASE_ATTRIBUTE_PATTERN = /[\s"'][iI]\s*\]/;
  5. var CASE_RESTORE_PATTERN = /([\d\w])([iI])\]/g;
  6. var DOUBLE_QUOTE_CASE_PATTERN = /="([a-zA-Z][a-zA-Z\d\-_]+)"([iI])/g;
  7. var DOUBLE_QUOTE_PATTERN = /="([a-zA-Z][a-zA-Z\d\-_]+)"(\s|\])/g;
  8. var HTML_COMMENT_PATTERN = /^(?:(?:<!--|-->)\s*)+/;
  9. var SINGLE_QUOTE_CASE_PATTERN = /='([a-zA-Z][a-zA-Z\d\-_]+)'([iI])/g;
  10. var SINGLE_QUOTE_PATTERN = /='([a-zA-Z][a-zA-Z\d\-_]+)'(\s|\])/g;
  11. var RELATION_PATTERN = /[>+~]/;
  12. var WHITESPACE_PATTERN = /\s/;
  13. var ASTERISK_PLUS_HTML_HACK = '*+html ';
  14. var ASTERISK_FIRST_CHILD_PLUS_HTML_HACK = '*:first-child+html ';
  15. var LESS_THAN = '<';
  16. var PSEUDO_CLASSES_WITH_SELECTORS = [
  17. ':current',
  18. ':future',
  19. ':has',
  20. ':host',
  21. ':host-context',
  22. ':is',
  23. ':not',
  24. ':past',
  25. ':where'
  26. ];
  27. function hasInvalidCharacters(value) {
  28. var isEscaped;
  29. var isInvalid = false;
  30. var character;
  31. var isQuote = false;
  32. var i, l;
  33. for (i = 0, l = value.length; i < l; i++) {
  34. character = value[i];
  35. if (isEscaped) {
  36. // continue as always
  37. } else if (character == Marker.SINGLE_QUOTE || character == Marker.DOUBLE_QUOTE) {
  38. isQuote = !isQuote;
  39. } else if (!isQuote
  40. && (character == Marker.CLOSE_CURLY_BRACKET
  41. || character == Marker.EXCLAMATION
  42. || character == LESS_THAN
  43. || character == Marker.SEMICOLON)
  44. ) {
  45. isInvalid = true;
  46. break;
  47. } else if (!isQuote && i === 0 && RELATION_PATTERN.test(character)) {
  48. isInvalid = true;
  49. break;
  50. }
  51. isEscaped = character == Marker.BACK_SLASH;
  52. }
  53. return isInvalid;
  54. }
  55. function removeWhitespace(value, format) {
  56. var stripped = [];
  57. var character;
  58. var isNewLineNix;
  59. var isNewLineWin;
  60. var isEscaped;
  61. var wasEscaped;
  62. var isQuoted;
  63. var isSingleQuoted;
  64. var isDoubleQuoted;
  65. var isAttribute;
  66. var isRelation;
  67. var isWhitespace;
  68. var isSpaceAwarePseudoClass;
  69. var roundBracketLevel = 0;
  70. var wasComma = false;
  71. var wasRelation = false;
  72. var wasWhitespace = false;
  73. var withCaseAttribute = CASE_ATTRIBUTE_PATTERN.test(value);
  74. var spaceAroundRelation = format && format.spaces[Spaces.AroundSelectorRelation];
  75. var i, l;
  76. for (i = 0, l = value.length; i < l; i++) {
  77. character = value[i];
  78. isNewLineNix = character == Marker.NEW_LINE_NIX;
  79. isNewLineWin = character == Marker.NEW_LINE_NIX && value[i - 1] == Marker.CARRIAGE_RETURN;
  80. isQuoted = isSingleQuoted || isDoubleQuoted;
  81. isRelation = !isAttribute && !isEscaped && roundBracketLevel === 0 && RELATION_PATTERN.test(character);
  82. isWhitespace = WHITESPACE_PATTERN.test(character);
  83. isSpaceAwarePseudoClass = roundBracketLevel == 1 && character == Marker.CLOSE_ROUND_BRACKET
  84. ? false
  85. : isSpaceAwarePseudoClass
  86. || (roundBracketLevel === 0 && character == Marker.COLON && isPseudoClassWithSelectors(value, i));
  87. if (wasEscaped && isQuoted && isNewLineWin) {
  88. // swallow escaped new windows lines in comments
  89. stripped.pop();
  90. stripped.pop();
  91. } else if (isEscaped && isQuoted && isNewLineNix) {
  92. // swallow escaped new *nix lines in comments
  93. stripped.pop();
  94. } else if (isEscaped) {
  95. stripped.push(character);
  96. } else if (character == Marker.OPEN_SQUARE_BRACKET && !isQuoted) {
  97. stripped.push(character);
  98. isAttribute = true;
  99. } else if (character == Marker.CLOSE_SQUARE_BRACKET && !isQuoted) {
  100. stripped.push(character);
  101. isAttribute = false;
  102. } else if (character == Marker.OPEN_ROUND_BRACKET && !isQuoted) {
  103. stripped.push(character);
  104. roundBracketLevel++;
  105. } else if (character == Marker.CLOSE_ROUND_BRACKET && !isQuoted) {
  106. stripped.push(character);
  107. roundBracketLevel--;
  108. } else if (character == Marker.SINGLE_QUOTE && !isQuoted) {
  109. stripped.push(character);
  110. isSingleQuoted = true;
  111. } else if (character == Marker.DOUBLE_QUOTE && !isQuoted) {
  112. stripped.push(character);
  113. isDoubleQuoted = true;
  114. } else if (character == Marker.SINGLE_QUOTE && isQuoted) {
  115. stripped.push(character);
  116. isSingleQuoted = false;
  117. } else if (character == Marker.DOUBLE_QUOTE && isQuoted) {
  118. stripped.push(character);
  119. isDoubleQuoted = false;
  120. } else if (isWhitespace && wasRelation && !spaceAroundRelation) {
  121. continue;
  122. } else if (!isWhitespace && wasRelation && spaceAroundRelation) {
  123. stripped.push(Marker.SPACE);
  124. stripped.push(character);
  125. } else if (isWhitespace && !wasWhitespace && wasComma && roundBracketLevel > 0 && isSpaceAwarePseudoClass) {
  126. // skip space
  127. } else if (isWhitespace && !wasWhitespace && roundBracketLevel > 0 && isSpaceAwarePseudoClass) {
  128. stripped.push(character);
  129. } else if (isWhitespace && (isAttribute || roundBracketLevel > 0) && !isQuoted) {
  130. // skip space
  131. } else if (isWhitespace && wasWhitespace && !isQuoted) {
  132. // skip extra space
  133. } else if ((isNewLineWin || isNewLineNix) && (isAttribute || roundBracketLevel > 0) && isQuoted) {
  134. // skip newline
  135. } else if (isRelation && wasWhitespace && !spaceAroundRelation) {
  136. stripped.pop();
  137. stripped.push(character);
  138. } else if (isRelation && !wasWhitespace && spaceAroundRelation) {
  139. stripped.push(Marker.SPACE);
  140. stripped.push(character);
  141. } else if (isWhitespace) {
  142. stripped.push(Marker.SPACE);
  143. } else {
  144. stripped.push(character);
  145. }
  146. wasEscaped = isEscaped;
  147. isEscaped = character == Marker.BACK_SLASH;
  148. wasRelation = isRelation;
  149. wasWhitespace = isWhitespace;
  150. wasComma = character == Marker.COMMA;
  151. }
  152. return withCaseAttribute
  153. ? stripped.join('').replace(CASE_RESTORE_PATTERN, '$1 $2]')
  154. : stripped.join('');
  155. }
  156. function isPseudoClassWithSelectors(value, colonPosition) {
  157. var pseudoClass = value.substring(colonPosition, value.indexOf(Marker.OPEN_ROUND_BRACKET, colonPosition));
  158. return PSEUDO_CLASSES_WITH_SELECTORS.indexOf(pseudoClass) > -1;
  159. }
  160. function removeQuotes(value) {
  161. if (value.indexOf('\'') == -1 && value.indexOf('"') == -1) {
  162. return value;
  163. }
  164. return value
  165. .replace(SINGLE_QUOTE_CASE_PATTERN, '=$1 $2')
  166. .replace(SINGLE_QUOTE_PATTERN, '=$1$2')
  167. .replace(DOUBLE_QUOTE_CASE_PATTERN, '=$1 $2')
  168. .replace(DOUBLE_QUOTE_PATTERN, '=$1$2');
  169. }
  170. function replacePseudoClasses(value) {
  171. return value
  172. .replace('nth-child(1)', 'first-child')
  173. .replace('nth-of-type(1)', 'first-of-type')
  174. .replace('nth-of-type(even)', 'nth-of-type(2n)')
  175. .replace('nth-child(even)', 'nth-child(2n)')
  176. .replace('nth-of-type(2n+1)', 'nth-of-type(odd)')
  177. .replace('nth-child(2n+1)', 'nth-child(odd)')
  178. .replace('nth-last-child(1)', 'last-child')
  179. .replace('nth-last-of-type(1)', 'last-of-type')
  180. .replace('nth-last-of-type(even)', 'nth-last-of-type(2n)')
  181. .replace('nth-last-child(even)', 'nth-last-child(2n)')
  182. .replace('nth-last-of-type(2n+1)', 'nth-last-of-type(odd)')
  183. .replace('nth-last-child(2n+1)', 'nth-last-child(odd)');
  184. }
  185. function tidyRules(rules, removeUnsupported, adjacentSpace, format, warnings) {
  186. var list = [];
  187. var repeated = [];
  188. function removeHTMLComment(rule, match) {
  189. warnings.push('HTML comment \'' + match + '\' at ' + formatPosition(rule[2][0]) + '. Removing.');
  190. return '';
  191. }
  192. for (var i = 0, l = rules.length; i < l; i++) {
  193. var rule = rules[i];
  194. var reduced = rule[1];
  195. reduced = reduced.replace(HTML_COMMENT_PATTERN, removeHTMLComment.bind(null, rule));
  196. if (hasInvalidCharacters(reduced)) {
  197. warnings.push('Invalid selector \'' + rule[1] + '\' at ' + formatPosition(rule[2][0]) + '. Ignoring.');
  198. continue;
  199. }
  200. reduced = removeWhitespace(reduced, format);
  201. reduced = removeQuotes(reduced);
  202. if (adjacentSpace && reduced.indexOf('nav') > 0) {
  203. reduced = reduced.replace(/\+nav(\S|$)/, '+ nav$1');
  204. }
  205. if (removeUnsupported && reduced.indexOf(ASTERISK_PLUS_HTML_HACK) > -1) {
  206. continue;
  207. }
  208. if (removeUnsupported && reduced.indexOf(ASTERISK_FIRST_CHILD_PLUS_HTML_HACK) > -1) {
  209. continue;
  210. }
  211. if (reduced.indexOf('*') > -1) {
  212. reduced = reduced
  213. .replace(/\*([:#.[])/g, '$1')
  214. .replace(/^(:first-child)?\+html/, '*$1+html');
  215. }
  216. if (repeated.indexOf(reduced) > -1) {
  217. continue;
  218. }
  219. reduced = replacePseudoClasses(reduced);
  220. rule[1] = reduced;
  221. repeated.push(reduced);
  222. list.push(rule);
  223. }
  224. if (list.length == 1 && list[0][1].length === 0) {
  225. warnings.push('Empty selector \'' + list[0][1] + '\' at ' + formatPosition(list[0][2][0]) + '. Ignoring.');
  226. list = [];
  227. }
  228. return list;
  229. }
  230. module.exports = tidyRules;