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