/* compiles a selector to an executable function */ module.exports = compileFactory; var parse = require("css-what"), BaseFuncs = require("boolbase"), sortRules = require("./sort.js"), procedure = require("./procedure.json"), rulesFactory = require("./general.js"), pseudosFactory = require("./pseudos.js"), trueFunc = BaseFuncs.trueFunc, falseFunc = BaseFuncs.falseFunc; function compileFactory(adapter){ var Pseudos = pseudosFactory(adapter), filters = Pseudos.filters, Rules = rulesFactory(adapter, Pseudos); function compile(selector, options, context){ var next = compileUnsafe(selector, options, context); return wrap(next); } function wrap(next){ 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"}, FLEXIBLE_DESCENDANT_TOKEN = {type: "_flexibleDescendant"}, SCOPE_TOKEN = {type: "pseudo", name: "scope"}, PLACEHOLDER_ELEMENT = {}; //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 || !!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, 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; 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) }; 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(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 adapter.getChildren(elem).some(adapter.isTag) && next(elem); }; } func = wrap(func); 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 }; return compileToken(token, opts, context); }; compile.compileToken = compileToken; compile.compileUnsafe = compileUnsafe; compile.Pseudos = Pseudos; return compile; }