| /* |
| compiles a selector to an executable function |
| */ |
| |
| module.exports = compile; |
| module.exports.compileUnsafe = compileUnsafe; |
| module.exports.compileToken = compileToken; |
| |
| var parse = require("css-what"), |
| DomUtils = require("domutils"), |
| isTag = DomUtils.isTag, |
| Rules = require("./general.js"), |
| sortRules = require("./sort.js"), |
| BaseFuncs = require("boolbase"), |
| trueFunc = BaseFuncs.trueFunc, |
| falseFunc = BaseFuncs.falseFunc, |
| procedure = require("./procedure.json"); |
| |
| function compile(selector, options, context){ |
| var next = compileUnsafe(selector, options, context); |
| return wrap(next); |
| } |
| |
| function wrap(next){ |
| return function base(elem){ |
| return isTag(elem) && next(elem); |
| }; |
| } |
| |
| function compileUnsafe(selector, options, context){ |
| var token = parse(selector, options); |
| return compileToken(token, options, context); |
| } |
| |
| function includesScopePseudo(t){ |
| return t.type === "pseudo" && ( |
| t.name === "scope" || ( |
| Array.isArray(t.data) && |
| t.data.some(function(data){ |
| return data.some(includesScopePseudo); |
| }) |
| ) |
| ); |
| } |
| |
| var DESCENDANT_TOKEN = {type: "descendant"}, |
| SCOPE_TOKEN = {type: "pseudo", name: "scope"}, |
| PLACEHOLDER_ELEMENT = {}, |
| getParent = DomUtils.getParent; |
| |
| //CSS 4 Spec (Draft): 3.3.1. Absolutizing a Scope-relative Selector |
| //http://www.w3.org/TR/selectors4/#absolutizing |
| function absolutize(token, context){ |
| //TODO better check if context is document |
| var hasContext = !!context && !!context.length && context.every(function(e){ |
| return e === PLACEHOLDER_ELEMENT || !!getParent(e); |
| }); |
| |
| |
| token.forEach(function(t){ |
| if(t.length > 0 && isTraversal(t[0]) && t[0].type !== "descendant"){ |
| //don't return in else branch |
| } else if(hasContext && !includesScopePseudo(t)){ |
| t.unshift(DESCENDANT_TOKEN); |
| } else { |
| return; |
| } |
| |
| t.unshift(SCOPE_TOKEN); |
| }); |
| } |
| |
| function compileToken(token, options, context){ |
| token = token.filter(function(t){ return t.length > 0; }); |
| |
| token.forEach(sortRules); |
| |
| var isArrayContext = Array.isArray(context); |
| |
| context = (options && options.context) || context; |
| |
| if(context && !isArrayContext) context = [context]; |
| |
| absolutize(token, context); |
| |
| return token |
| .map(function(rules){ return compileRules(rules, options, context, isArrayContext); }) |
| .reduce(reduceRules, falseFunc); |
| } |
| |
| function isTraversal(t){ |
| return procedure[t.type] < 0; |
| } |
| |
| function compileRules(rules, options, context, isArrayContext){ |
| var acceptSelf = (isArrayContext && rules[0].name === "scope" && rules[1].type === "descendant"); |
| return rules.reduce(function(func, rule, index){ |
| if(func === falseFunc) return func; |
| return Rules[rule.type](func, rule, options, context, acceptSelf && index === 1); |
| }, options && options.rootFunc || trueFunc); |
| } |
| |
| function reduceRules(a, b){ |
| if(b === falseFunc || a === trueFunc){ |
| return a; |
| } |
| if(a === falseFunc || b === trueFunc){ |
| return b; |
| } |
| |
| return function combine(elem){ |
| return a(elem) || b(elem); |
| }; |
| } |
| |
| //:not, :has and :matches have to compile selectors |
| //doing this in lib/pseudos.js would lead to circular dependencies, |
| //so we add them here |
| |
| var Pseudos = require("./pseudos.js"), |
| filters = Pseudos.filters, |
| existsOne = DomUtils.existsOne, |
| isTag = DomUtils.isTag, |
| getChildren = DomUtils.getChildren; |
| |
| |
| function containsTraversal(t){ |
| return t.some(isTraversal); |
| } |
| |
| filters.not = function(next, token, options, context){ |
| var opts = { |
| xmlMode: !!(options && options.xmlMode), |
| strict: !!(options && options.strict) |
| }; |
| |
| if(opts.strict){ |
| if(token.length > 1 || token.some(containsTraversal)){ |
| throw new SyntaxError("complex selectors in :not aren't allowed in strict mode"); |
| } |
| } |
| |
| var func = compileToken(token, opts, context); |
| |
| if(func === falseFunc) return next; |
| if(func === trueFunc) return falseFunc; |
| |
| return function(elem){ |
| return !func(elem) && next(elem); |
| }; |
| }; |
| |
| filters.has = function(next, token, options){ |
| var opts = { |
| xmlMode: !!(options && options.xmlMode), |
| strict: !!(options && options.strict) |
| }; |
| |
| //FIXME: Uses an array as a pointer to the current element (side effects) |
| var context = token.some(containsTraversal) ? [PLACEHOLDER_ELEMENT] : null; |
| |
| var func = compileToken(token, opts, context); |
| |
| if(func === falseFunc) return falseFunc; |
| if(func === trueFunc) return function(elem){ |
| return getChildren(elem).some(isTag) && next(elem); |
| }; |
| |
| func = wrap(func); |
| |
| if(context){ |
| return function has(elem){ |
| return next(elem) && ( |
| (context[0] = elem), existsOne(func, getChildren(elem)) |
| ); |
| }; |
| } |
| |
| return function has(elem){ |
| return next(elem) && existsOne(func, getChildren(elem)); |
| }; |
| }; |
| |
| filters.matches = function(next, token, options, context){ |
| var opts = { |
| xmlMode: !!(options && options.xmlMode), |
| strict: !!(options && options.strict), |
| rootFunc: next |
| }; |
| |
| return compileToken(token, opts, context); |
| }; |