is-mergeable.js 6.8 KB

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