Skip to content

Commit

Permalink
[changed] refactor internals to allow customizable selection
Browse files Browse the repository at this point in the history
  • Loading branch information
jquense committed Sep 28, 2015
1 parent 189fe36 commit c672b2b
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 125 deletions.
2 changes: 1 addition & 1 deletion lib/select.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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",
Expand All @@ -52,6 +53,7 @@
"lodash": "^3.10.1"
},
"release-script": {
"defaultDryRun": "false"
"defaultDryRun": "false",
"altPkgRootFolder": "lib"
}
}
140 changes: 140 additions & 0 deletions src/compiler.js
Original file line number Diff line number Diff line change
@@ -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]
})
}
142 changes: 22 additions & 120 deletions src/select.js
Original file line number Diff line number Diff line change
Expand Up @@ -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_____';

Expand All @@ -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);
Expand Down Expand Up @@ -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){
Expand Down Expand Up @@ -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++;
Expand All @@ -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
4 changes: 2 additions & 2 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
Expand Down Expand Up @@ -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`,
Expand Down

0 comments on commit c672b2b

Please sign in to comment.