compile.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. /*
  2. compiles a selector to an executable function
  3. */
  4. module.exports = compile;
  5. var parse = require("css-what").parse;
  6. var BaseFuncs = require("boolbase");
  7. var sortRules = require("./sort.js");
  8. var procedure = require("./procedure.json");
  9. var Rules = require("./general.js");
  10. var Pseudos = require("./pseudos.js");
  11. var trueFunc = BaseFuncs.trueFunc;
  12. var falseFunc = BaseFuncs.falseFunc;
  13. var filters = Pseudos.filters;
  14. function compile(selector, options, context) {
  15. var next = compileUnsafe(selector, options, context);
  16. return wrap(next, options);
  17. }
  18. function wrap(next, options) {
  19. var adapter = options.adapter;
  20. return function base(elem) {
  21. return adapter.isTag(elem) && next(elem);
  22. };
  23. }
  24. function compileUnsafe(selector, options, context) {
  25. var token = parse(selector, options);
  26. return compileToken(token, options, context);
  27. }
  28. function includesScopePseudo(t) {
  29. return (
  30. t.type === "pseudo" &&
  31. (t.name === "scope" ||
  32. (Array.isArray(t.data) &&
  33. t.data.some(function(data) {
  34. return data.some(includesScopePseudo);
  35. })))
  36. );
  37. }
  38. var DESCENDANT_TOKEN = { type: "descendant" };
  39. var FLEXIBLE_DESCENDANT_TOKEN = { type: "_flexibleDescendant" };
  40. var SCOPE_TOKEN = { type: "pseudo", name: "scope" };
  41. var PLACEHOLDER_ELEMENT = {};
  42. //CSS 4 Spec (Draft): 3.3.1. Absolutizing a Scope-relative Selector
  43. //http://www.w3.org/TR/selectors4/#absolutizing
  44. function absolutize(token, options, context) {
  45. var adapter = options.adapter;
  46. //TODO better check if context is document
  47. var hasContext =
  48. !!context &&
  49. !!context.length &&
  50. context.every(function(e) {
  51. return e === PLACEHOLDER_ELEMENT || !!adapter.getParent(e);
  52. });
  53. token.forEach(function(t) {
  54. if (t.length > 0 && isTraversal(t[0]) && t[0].type !== "descendant") {
  55. //don't return in else branch
  56. } else if (hasContext && !(Array.isArray(t) ? t.some(includesScopePseudo) : includesScopePseudo(t))) {
  57. t.unshift(DESCENDANT_TOKEN);
  58. } else {
  59. return;
  60. }
  61. t.unshift(SCOPE_TOKEN);
  62. });
  63. }
  64. function compileToken(token, options, context) {
  65. token = token.filter(function(t) {
  66. return t.length > 0;
  67. });
  68. token.forEach(sortRules);
  69. var isArrayContext = Array.isArray(context);
  70. context = (options && options.context) || context;
  71. if (context && !isArrayContext) context = [context];
  72. absolutize(token, options, context);
  73. var shouldTestNextSiblings = false;
  74. var query = token
  75. .map(function(rules) {
  76. if (rules[0] && rules[1] && rules[0].name === "scope") {
  77. var ruleType = rules[1].type;
  78. if (isArrayContext && ruleType === "descendant") {
  79. rules[1] = FLEXIBLE_DESCENDANT_TOKEN;
  80. } else if (ruleType === "adjacent" || ruleType === "sibling") {
  81. shouldTestNextSiblings = true;
  82. }
  83. }
  84. return compileRules(rules, options, context);
  85. })
  86. .reduce(reduceRules, falseFunc);
  87. query.shouldTestNextSiblings = shouldTestNextSiblings;
  88. return query;
  89. }
  90. function isTraversal(t) {
  91. return procedure[t.type] < 0;
  92. }
  93. function compileRules(rules, options, context) {
  94. return rules.reduce(function(func, rule) {
  95. if (func === falseFunc) return func;
  96. if (!(rule.type in Rules)) {
  97. throw new Error("Rule type " + rule.type + " is not supported by css-select");
  98. }
  99. return Rules[rule.type](func, rule, options, context);
  100. }, (options && options.rootFunc) || trueFunc);
  101. }
  102. function reduceRules(a, b) {
  103. if (b === falseFunc || a === trueFunc) {
  104. return a;
  105. }
  106. if (a === falseFunc || b === trueFunc) {
  107. return b;
  108. }
  109. return function combine(elem) {
  110. return a(elem) || b(elem);
  111. };
  112. }
  113. function containsTraversal(t) {
  114. return t.some(isTraversal);
  115. }
  116. //:not, :has and :matches have to compile selectors
  117. //doing this in lib/pseudos.js would lead to circular dependencies,
  118. //so we add them here
  119. filters.not = function(next, token, options, context) {
  120. var opts = {
  121. xmlMode: !!(options && options.xmlMode),
  122. strict: !!(options && options.strict),
  123. adapter: options.adapter
  124. };
  125. if (opts.strict) {
  126. if (token.length > 1 || token.some(containsTraversal)) {
  127. throw new Error("complex selectors in :not aren't allowed in strict mode");
  128. }
  129. }
  130. var func = compileToken(token, opts, context);
  131. if (func === falseFunc) return next;
  132. if (func === trueFunc) return falseFunc;
  133. return function not(elem) {
  134. return !func(elem) && next(elem);
  135. };
  136. };
  137. filters.has = function(next, token, options) {
  138. var adapter = options.adapter;
  139. var opts = {
  140. xmlMode: !!(options && options.xmlMode),
  141. strict: !!(options && options.strict),
  142. adapter: adapter
  143. };
  144. //FIXME: Uses an array as a pointer to the current element (side effects)
  145. var context = token.some(containsTraversal) ? [PLACEHOLDER_ELEMENT] : null;
  146. var func = compileToken(token, opts, context);
  147. if (func === falseFunc) return falseFunc;
  148. if (func === trueFunc) {
  149. return function hasChild(elem) {
  150. return adapter.getChildren(elem).some(adapter.isTag) && next(elem);
  151. };
  152. }
  153. func = wrap(func, options);
  154. if (context) {
  155. return function has(elem) {
  156. return next(elem) && ((context[0] = elem), adapter.existsOne(func, adapter.getChildren(elem)));
  157. };
  158. }
  159. return function has(elem) {
  160. return next(elem) && adapter.existsOne(func, adapter.getChildren(elem));
  161. };
  162. };
  163. filters.matches = function(next, token, options, context) {
  164. var opts = {
  165. xmlMode: !!(options && options.xmlMode),
  166. strict: !!(options && options.strict),
  167. rootFunc: next,
  168. adapter: options.adapter
  169. };
  170. return compileToken(token, opts, context);
  171. };
  172. compile.compileToken = compileToken;
  173. compile.compileUnsafe = compileUnsafe;
  174. compile.Pseudos = Pseudos;