/* compiles a selector to an executable function */ module.exports = compile; var parse = require("css-what"); var BaseFuncs = require("boolbase"); var sortRules = require("./sort.js"); var procedure = require("./procedure.json"); var Rules = require("./general.js"); var Pseudos = require("./pseudos.js"); var trueFunc = BaseFuncs.trueFunc; var falseFunc = BaseFuncs.falseFunc; var filters = Pseudos.filters; function compile(selector, options, context){ var next = compileUnsafe(selector, options, context); return wrap(next, options); } function wrap(next, options){ var adapter = options.adapter; return function base(elem){ return adapter.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" }; var FLEXIBLE_DESCENDANT_TOKEN = { type: "_flexibleDescendant" }; var SCOPE_TOKEN = { type: "pseudo", name: "scope" }; var PLACEHOLDER_ELEMENT = {}; //CSS 4 Spec (Draft): 3.3.1. Absolutizing a Scope-relative Selector //http://www.w3.org/TR/selectors4/#absolutizing function absolutize(token, options, context){ var adapter = options.adapter; //TODO better check if context is document var hasContext = !!context && !!context.length && context.every(function(e){ return e === PLACEHOLDER_ELEMENT || !!adapter.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, options, context); var shouldTestNextSiblings = false; var query = token .map(function(rules){ if(rules[0] && rules[1] && rules[0].name === "scope"){ var ruleType = rules[1].type; if(isArrayContext && ruleType === "descendant"){ rules[1] = FLEXIBLE_DESCENDANT_TOKEN; } else if(ruleType === "adjacent" || ruleType === "sibling"){ shouldTestNextSiblings = true; } } return compileRules(rules, options, context); }) .reduce(reduceRules, falseFunc); query.shouldTestNextSiblings = shouldTestNextSiblings; return query; } function isTraversal(t){ return procedure[t.type] < 0; } function compileRules(rules, options, context){ return rules.reduce(function(func, rule){ if(func === falseFunc) return func; if(!(rule.type in Rules)){ throw new Error( "Rule type " + rule.type + " is not supported by css-select" ); } return Rules[rule.type](func, rule, options, context); }, (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); }; } function containsTraversal(t){ return t.some(isTraversal); } //:not, :has and :matches have to compile selectors //doing this in lib/pseudos.js would lead to circular dependencies, //so we add them here filters.not = function(next, token, options, context){ var opts = { xmlMode: !!(options && options.xmlMode), strict: !!(options && options.strict), adapter: options.adapter }; if(opts.strict){ if(token.length > 1 || token.some(containsTraversal)){ throw new Error( "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 not(elem){ return !func(elem) && next(elem); }; }; filters.has = function(next, token, options){ var adapter = options.adapter; var opts = { xmlMode: !!(options && options.xmlMode), strict: !!(options && options.strict), adapter: adapter }; //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 hasChild(elem){ return adapter.getChildren(elem).some(adapter.isTag) && next(elem); }; } func = wrap(func, options); if(context){ return function has(elem){ return ( next(elem) && ((context[0] = elem), adapter.existsOne(func, adapter.getChildren(elem))) ); }; } return function has(elem){ return next(elem) && adapter.existsOne(func, adapter.getChildren(elem)); }; }; filters.matches = function(next, token, options, context){ var opts = { xmlMode: !!(options && options.xmlMode), strict: !!(options && options.strict), rootFunc: next, adapter: options.adapter }; return compileToken(token, opts, context); }; compile.compileToken = compileToken; compile.compileUnsafe = compileUnsafe; compile.Pseudos = Pseudos;