is-mergeable.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. var Marker = require('../../tokenizer/marker');
  2. var split = require('../../utils/split');
  3. var DEEP_SELECTOR_PATTERN = /\/deep\//;
  4. var DOUBLE_COLON_PATTERN = /^::/;
  5. var VENDOR_PREFIXED_PATTERN = /:(-moz-|-ms-|-o-|-webkit-)/;
  6. var NOT_PSEUDO = ':not';
  7. var PSEUDO_CLASSES_WITH_ARGUMENTS = [
  8. ':dir',
  9. ':lang',
  10. ':not',
  11. ':nth-child',
  12. ':nth-last-child',
  13. ':nth-last-of-type',
  14. ':nth-of-type'
  15. ];
  16. var RELATION_PATTERN = /[>+~]/;
  17. var UNMIXABLE_PSEUDO_CLASSES = [
  18. ':after',
  19. ':before',
  20. ':first-letter',
  21. ':first-line',
  22. ':lang'
  23. ];
  24. var UNMIXABLE_PSEUDO_ELEMENTS = [
  25. '::after',
  26. '::before',
  27. '::first-letter',
  28. '::first-line'
  29. ];
  30. var Level = {
  31. DOUBLE_QUOTE: 'double-quote',
  32. SINGLE_QUOTE: 'single-quote',
  33. ROOT: 'root'
  34. };
  35. function isMergeable(selector, mergeablePseudoClasses, mergeablePseudoElements, multiplePseudoMerging) {
  36. var singleSelectors = split(selector, Marker.COMMA);
  37. var singleSelector;
  38. var i, l;
  39. for (i = 0, l = singleSelectors.length; i < l; i++) {
  40. singleSelector = singleSelectors[i];
  41. if (singleSelector.length === 0
  42. || isDeepSelector(singleSelector)
  43. || isVendorPrefixed(singleSelector)
  44. || (singleSelector.indexOf(Marker.COLON) > -1
  45. && !areMergeable(
  46. singleSelector,
  47. extractPseudoFrom(singleSelector),
  48. mergeablePseudoClasses,
  49. mergeablePseudoElements,
  50. multiplePseudoMerging
  51. ))) {
  52. return false;
  53. }
  54. }
  55. return true;
  56. }
  57. function isDeepSelector(selector) {
  58. return DEEP_SELECTOR_PATTERN.test(selector);
  59. }
  60. function isVendorPrefixed(selector) {
  61. return VENDOR_PREFIXED_PATTERN.test(selector);
  62. }
  63. function extractPseudoFrom(selector) {
  64. var list = [];
  65. var character;
  66. var buffer = [];
  67. var level = Level.ROOT;
  68. var roundBracketLevel = 0;
  69. var isQuoted;
  70. var isEscaped;
  71. var isPseudo = false;
  72. var isRelation;
  73. var wasColon = false;
  74. var index;
  75. var len;
  76. for (index = 0, len = selector.length; index < len; index++) {
  77. character = selector[index];
  78. isRelation = !isEscaped && RELATION_PATTERN.test(character);
  79. isQuoted = level == Level.DOUBLE_QUOTE || level == Level.SINGLE_QUOTE;
  80. if (isEscaped) {
  81. buffer.push(character);
  82. } else if (character == Marker.DOUBLE_QUOTE && level == Level.ROOT) {
  83. buffer.push(character);
  84. level = Level.DOUBLE_QUOTE;
  85. } else if (character == Marker.DOUBLE_QUOTE && level == Level.DOUBLE_QUOTE) {
  86. buffer.push(character);
  87. level = Level.ROOT;
  88. } else if (character == Marker.SINGLE_QUOTE && level == Level.ROOT) {
  89. buffer.push(character);
  90. level = Level.SINGLE_QUOTE;
  91. } else if (character == Marker.SINGLE_QUOTE && level == Level.SINGLE_QUOTE) {
  92. buffer.push(character);
  93. level = Level.ROOT;
  94. } else if (isQuoted) {
  95. buffer.push(character);
  96. } else if (character == Marker.OPEN_ROUND_BRACKET) {
  97. buffer.push(character);
  98. roundBracketLevel++;
  99. } else if (character == Marker.CLOSE_ROUND_BRACKET && roundBracketLevel == 1 && isPseudo) {
  100. buffer.push(character);
  101. list.push(buffer.join(''));
  102. roundBracketLevel--;
  103. buffer = [];
  104. isPseudo = false;
  105. } else if (character == Marker.CLOSE_ROUND_BRACKET) {
  106. buffer.push(character);
  107. roundBracketLevel--;
  108. } else if (character == Marker.COLON && roundBracketLevel === 0 && isPseudo && !wasColon) {
  109. list.push(buffer.join(''));
  110. buffer = [];
  111. buffer.push(character);
  112. } else if (character == Marker.COLON && roundBracketLevel === 0 && !wasColon) {
  113. buffer = [];
  114. buffer.push(character);
  115. isPseudo = true;
  116. } else if (character == Marker.SPACE && roundBracketLevel === 0 && isPseudo) {
  117. list.push(buffer.join(''));
  118. buffer = [];
  119. isPseudo = false;
  120. } else if (isRelation && roundBracketLevel === 0 && isPseudo) {
  121. list.push(buffer.join(''));
  122. buffer = [];
  123. isPseudo = false;
  124. } else {
  125. buffer.push(character);
  126. }
  127. isEscaped = character == Marker.BACK_SLASH;
  128. wasColon = character == Marker.COLON;
  129. }
  130. if (buffer.length > 0 && isPseudo) {
  131. list.push(buffer.join(''));
  132. }
  133. return list;
  134. }
  135. function areMergeable(selector, matches, mergeablePseudoClasses, mergeablePseudoElements, multiplePseudoMerging) {
  136. return areAllowed(matches, mergeablePseudoClasses, mergeablePseudoElements)
  137. && needArguments(matches)
  138. && (matches.length < 2 || !someIncorrectlyChained(selector, matches))
  139. && (matches.length < 2 || multiplePseudoMerging && allMixable(matches));
  140. }
  141. function areAllowed(matches, mergeablePseudoClasses, mergeablePseudoElements) {
  142. var match;
  143. var name;
  144. var i, l;
  145. for (i = 0, l = matches.length; i < l; i++) {
  146. match = matches[i];
  147. name = match.indexOf(Marker.OPEN_ROUND_BRACKET) > -1
  148. ? match.substring(0, match.indexOf(Marker.OPEN_ROUND_BRACKET))
  149. : match;
  150. if (mergeablePseudoClasses.indexOf(name) === -1 && mergeablePseudoElements.indexOf(name) === -1) {
  151. return false;
  152. }
  153. }
  154. return true;
  155. }
  156. function needArguments(matches) {
  157. var match;
  158. var name;
  159. var bracketOpensAt;
  160. var hasArguments;
  161. var i, l;
  162. for (i = 0, l = matches.length; i < l; i++) {
  163. match = matches[i];
  164. bracketOpensAt = match.indexOf(Marker.OPEN_ROUND_BRACKET);
  165. hasArguments = bracketOpensAt > -1;
  166. name = hasArguments
  167. ? match.substring(0, bracketOpensAt)
  168. : match;
  169. if (hasArguments && PSEUDO_CLASSES_WITH_ARGUMENTS.indexOf(name) == -1) {
  170. return false;
  171. }
  172. if (!hasArguments && PSEUDO_CLASSES_WITH_ARGUMENTS.indexOf(name) > -1) {
  173. return false;
  174. }
  175. }
  176. return true;
  177. }
  178. function someIncorrectlyChained(selector, matches) {
  179. var positionInSelector = 0;
  180. var match;
  181. var matchAt;
  182. var nextMatch;
  183. var nextMatchAt;
  184. var name;
  185. var nextName;
  186. var areChained;
  187. var i, l;
  188. for (i = 0, l = matches.length; i < l; i++) {
  189. match = matches[i];
  190. nextMatch = matches[i + 1];
  191. if (!nextMatch) {
  192. break;
  193. }
  194. matchAt = selector.indexOf(match, positionInSelector);
  195. nextMatchAt = selector.indexOf(match, matchAt + 1);
  196. positionInSelector = nextMatchAt;
  197. areChained = matchAt + match.length == nextMatchAt;
  198. if (areChained) {
  199. name = match.indexOf(Marker.OPEN_ROUND_BRACKET) > -1
  200. ? match.substring(0, match.indexOf(Marker.OPEN_ROUND_BRACKET))
  201. : match;
  202. nextName = nextMatch.indexOf(Marker.OPEN_ROUND_BRACKET) > -1
  203. ? nextMatch.substring(0, nextMatch.indexOf(Marker.OPEN_ROUND_BRACKET))
  204. : nextMatch;
  205. if (name != NOT_PSEUDO || nextName != NOT_PSEUDO) {
  206. return true;
  207. }
  208. }
  209. }
  210. return false;
  211. }
  212. function allMixable(matches) {
  213. var unmixableMatches = 0;
  214. var match;
  215. var i, l;
  216. for (i = 0, l = matches.length; i < l; i++) {
  217. match = matches[i];
  218. if (isPseudoElement(match)) {
  219. unmixableMatches += UNMIXABLE_PSEUDO_ELEMENTS.indexOf(match) > -1 ? 1 : 0;
  220. } else {
  221. unmixableMatches += UNMIXABLE_PSEUDO_CLASSES.indexOf(match) > -1 ? 1 : 0;
  222. }
  223. if (unmixableMatches > 1) {
  224. return false;
  225. }
  226. }
  227. return true;
  228. }
  229. function isPseudoElement(pseudo) {
  230. return DOUBLE_COLON_PATTERN.test(pseudo);
  231. }
  232. module.exports = isMergeable;