diff --git a/lib/select.js b/lib/select.js index 37999bb..e473982 100644 --- a/lib/select.js +++ b/lib/select.js @@ -160,7 +160,7 @@ function compileRule(rules, parent, values, ast) { if (rule.hasOwnProperty('nestingOperator')) { (function () { var immediate = rule.nestingOperator === '>'; - var nestedCompiled = compileRule(rules, rule, ast); + var nestedCompiled = compileRule(rules, rule, values, ast); fns.push(function (root, parent) { var method = immediate ? directParent : anyParent; diff --git a/package.json b/package.json index 82b5326..d9049dc 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "test": "karma start --single-run", "tdd": "karma start", - "build": "babel src --out-dir lib", + "build": "babel src --out-dir lib && cpy ./README.md ./lib", "release": "release" }, "repository": { @@ -31,6 +31,7 @@ "babel-core": "^5.8.25", "babel-loader": "^5.3.2", "chai": "^3.3.0", + "cpy": "^3.4.1", "karma": "^0.13.10", "karma-chai": "^0.1.0", "karma-chrome-launcher": "^0.2.0", @@ -52,6 +53,7 @@ "lodash": "^3.10.1" }, "release-script": { - "defaultDryRun": "false" + "defaultDryRun": "false", + "altPkgRootFolder": "lib" } } diff --git a/src/compiler.js b/src/compiler.js new file mode 100644 index 0000000..2bccb52 --- /dev/null +++ b/src/compiler.js @@ -0,0 +1,140 @@ +import React from 'react'; +import transform from 'lodash/object/transform'; +import has from 'lodash/object/has'; +import uid from 'lodash/utility/uniqueId'; +import { CssSelectorParser } from 'css-selector-parser'; + +const PREFIX = 'sub_____'; + +let parser = new CssSelectorParser(); + +parser.registerSelectorPseudos('has'); +parser.registerNestingOperators('>'); +parser.enableSubstitutes(); + +let prim = value => { + let typ = typeof value; + return value === null || ['string', 'number'].indexOf(typ) !== -1 +} + +export function parse(selector){ + let ast = typeof selector === 'string' + ? parser.parse(selector) + : selector; + + if (ast.rule){ + let rule = ast.rule; + return { rules: getRule(rule), ast } + } + else if (ast.selectors) { + return { + ast, + rules: ast.selectors.map(s => getRule(s.rule)), + multiple: true + } + } + + function getRule(rule){ + if (!rule) return [] + return getRule(rule.rule).concat(rule) + } +} + +export function create(options) { + const NESTING = Object.create(null); + const PSEUDOS = Object.create(null); + + let { traverse } = options; + + return { + compile, + compileRule, + registerNesting(name, fn){ + NESTING[name] = fn + }, + registerPseudo(name, fn){ + PSEUDOS[name] = fn + } + } + + function compile(selector, values = Object.create(null)){ + let { rules, ast, multiple } = parse(selector); + + if (!multiple) + return compileRule(rules, null, values, ast) + + return rules + .map(ruleSet => compileRule(ruleSet, null, values, ast)) + .reduce((current, next)=> { + return (root, parent) => current(root, parent) || next(root, parent) + }) + } + + function compileRule(rules, parent, values, ast){ + let fns = []; + let rule = rules.shift(); + + if (rule.tagName) + fns.push(getTagComparer(rule, values)) + + if (rule.attrs) + fns.push(getPropComparer(rule, values)) + + if (rule.classNames) + fns.push(({ props: { className } }) => { + return rule.classNames.every(clsName => + className && className.indexOf(clsName) !== -1) + }) + + if (rule.pseudos) { + fns = fns.concat( + rule.pseudos.map(pseudo => { + if (!PSEUDOS[pseudo.name]) + throw new Error('psuedo element: ' + psuedo.name + ' is not supported') + return PSEUDOS[pseudo.name](pseudo, values, options) + }) + ) + } + + if (rule.hasOwnProperty('nestingOperator') ){ + let operator = rule.nestingOperator || 'any' + let nestedCompiled = compileRule(rules, rule, values, ast); + + if (!NESTING[operator]) + throw new Error('nesting operator: ' + operator + ' is not supported') + + fns.push(NESTING[operator](nestedCompiled)) + } + + return fns.reduce((current, next = ()=> true)=> { + return (root, parent)=> next(root, parent) && current(root, parent) + }) + } +} + +function getTagComparer(rule, values) { + let isStr = t => typeof t === 'string' + let tagName = values[rule.tagName] || rule.tagName; + + if (rule.tagName === '*') + return ()=> true + + if (isStr(tagName)){ + tagName = tagName.toUpperCase(); + return root => isStr(root.type) && root.type.toUpperCase() === tagName; + } + + return root => root.type === tagName +} + +function getPropComparer(rule, values) { + return ({ props }) => rule.attrs.every(attr => { + if (!has(attr, 'value')) + return !!props[attr.name] + + if (!has(values, attr.value)) + return props[attr.name] == attr.value + + return props[attr.name] === values[attr.value] + }) +} diff --git a/src/select.js b/src/select.js index 9fa1049..0364e95 100644 --- a/src/select.js +++ b/src/select.js @@ -3,6 +3,7 @@ import transform from 'lodash/object/transform'; import has from 'lodash/object/has'; import uid from 'lodash/utility/uniqueId'; import { CssSelectorParser } from 'css-selector-parser'; +import { create as createCompiler, parse } from './compiler'; const PREFIX = 'sub_____'; @@ -17,17 +18,21 @@ let prim = value => { return value === null || ['string', 'number'].indexOf(typ) !== -1 } -const PSEUDOS = { - has(rule, valueMap) { - let compiled = compile(rule.value, valueMap) - return root => { - let matches = findAll(root, compiled) - return !!matches.length - } +let compiler = createCompiler({}) + +compiler.registerPseudo('has', function(rule, valueMap) { + let compiled = compiler.compile(rule.value, valueMap); + + return root => { + let matches = findAll(root, compiled) + return !!matches.length } -} +}) + +compiler.registerNesting('any', test => anyParent.bind(null, test)) + +compiler.registerNesting('>', test => directParent.bind(null, test)) -export let _parser = parser; export function selector(strings, ...values){ let valueMap = Object.create(null); @@ -56,88 +61,11 @@ export function match(selector, tree, includeSelf = true){ valueMap = selector.valueMap; selector = selector.selector } - let compiled = compile(selector, valueMap); - - let matches = findAll(tree, compiled, undefined, includeSelf) - return matches -} - -export function parse(selector){ - let ast = typeof selector === 'string' - ? parser.parse(selector) - : selector; - - if (ast.rule){ - let rule = ast.rule; - return { rules: getRule(rule), ast } - } - else if (ast.selectors) { - return { - ast, - rules: ast.selectors.map(s => getRule(s.rule)), - multiple: true - } - } - - function getRule(rule){ - if (!rule) return [] - return getRule(rule.rule).concat(rule) - } -} - -export function compile(selector, values = Object.create(null)){ - let { rules, ast, multiple } = parse(selector); - - if (!multiple) - return compileRule(rules, null, values, ast) - - return rules - .map(ruleSet => compileRule(ruleSet, null, values, ast)) - .reduce((current, next)=> { - return (root, parent) => current(root, parent) || next(root, parent) - }) -} - -export function compileRule(rules, parent, values, ast){ - let fns = []; - let rule = rules.shift(); - - if (rule.tagName) - fns.push(getTagComparer(rule, values)) - - if (rule.attrs) - fns.push(getPropComparer(rule, values)) - - if (rule.classNames) - fns.push(({ props: { className } }) => { - return rule.classNames.every(clsName => - className && className.indexOf(clsName) !== -1) - }) - - if (rule.pseudos) { - fns = fns.concat( - rule.pseudos.map(pseudo => { - if (!PSEUDOS[pseudo.name]) - throw new Error('psuedo element: ' + psuedo.name + ' is not supported') - return PSEUDOS[pseudo.name](pseudo) - }) - ) - } - if (rule.hasOwnProperty('nestingOperator') ){ - let immediate = rule.nestingOperator === '>' - let nestedCompiled = compileRule(rules, rule, values, ast); - - fns.push((root, parent) => { - let method = immediate ? directParent : anyParent - let result = method(root, nestedCompiled, parent) - return result - }) - } + let compiled = compiler.compile(selector, valueMap); + let matches = findAll(tree, compiled, undefined, includeSelf); - return fns.reduce((current, next = ()=> true)=> { - return (root, parent)=> next(root, parent) && current(root, parent) - }) + return matches } function findAll(root, test, getParent = ()=> ({ parent: null }), includeSelf){ @@ -168,34 +96,7 @@ function findAll(root, test, getParent = ()=> ({ parent: null }), includeSelf){ return found } -function getTagComparer(rule, values) { - let isStr = t => typeof t === 'string' - let tagName = values[rule.tagName] || rule.tagName; - - if (rule.tagName === '*') - return ()=> true - - if (isStr(tagName)){ - tagName = tagName.toUpperCase(); - return root => isStr(root.type) && root.type.toUpperCase() === tagName; - } - - return root => root.type === tagName -} - -function getPropComparer(rule, values) { - return ({ props }) => rule.attrs.every(attr => { - if (!has(attr, 'value')) - return !!props[attr.name] - - if (!has(values, attr.value)) - return props[attr.name] == attr.value - - return props[attr.name] === values[attr.value] - }) -} - -function anyParent(node, test, parentNode){ +function anyParent(test, node, parentNode){ var i = 0; do { i++; @@ -207,8 +108,9 @@ function anyParent(node, test, parentNode){ return !!node } -function directParent(node, test, parentNode) { +function directParent(test, node, parentNode) { node = parentNode().parent - - return !!(node && test(node, parentNode().getParent) ? node : null) + return !!(node && test(node, parentNode().getParent)) } + +export let { compile, compileRule } = compiler diff --git a/test.js b/test.js index efae5cb..e728af2 100644 --- a/test.js +++ b/test.js @@ -125,7 +125,7 @@ describe('Element Selecting', ()=> { let List = ()=>{}; let { selector, valueMap } = s`div ${List}.foo`; - ;(() => _parser.parse(selector)).should.not.throw() + ;(() => compile(selector)).should.not.throw() selector.match(/^div sub_____\d\.foo$/).should.be.ok }) @@ -168,7 +168,7 @@ describe('Element Selecting', ()=> { ).length.should.equal(1) }) - it('should match with prop value substitions', ()=>{ + it('should match with prop value substitutions', ()=>{ let date = new Date(); match(s`div[date=${date}].foo`,