Skip to content

Commit

Permalink
[changed] streamline psuedo api
Browse files Browse the repository at this point in the history
  • Loading branch information
jquense committed Sep 28, 2015
1 parent c810858 commit d0b4273
Show file tree
Hide file tree
Showing 6 changed files with 332 additions and 210 deletions.
53 changes: 53 additions & 0 deletions lib/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
bill
=======

A set of tools for matching React Elements against CSS selectors.

```js
import { match } from 'bill';

let matches = match('div li.foo'
<div>
<List>
<li className='foo'>John</li>
<li>Betty</li>
</List>
</div>
)

matches.length // 1
matches[0] // { type: 'li', props: { className: 'foo' } }
```

For selecting non string values, like custom Component types, we can use a [tagged template strings](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/template_strings)

```js
import { match, selector as s } from 'bill';

let min = 5;

let matches = match(s`div > ${List}, li[min=${min}]`
<div>
<List>
<li min={min}>John</li>
<li>Betty</li>
</List>
</div>
)

matches.length // 2
matches[0] // { type: List, props }
```

### Supported

- classNames
- `div[propName="hi"]` or `div[boolProp]`
- `>`: `div > .foo`
- `:has()`: `div:has(a.foo)`

### Not supported

- sibling selectors
- pseudo selectors (except for has)
- non string interpolations for anything other than "tag" or prop values
176 changes: 176 additions & 0 deletions lib/compiler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
'use strict';

exports.__esModule = true;
exports.parse = parse;
exports.create = create;

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }

var _react = require('react');

var _react2 = _interopRequireDefault(_react);

var _lodashObjectTransform = require('lodash/object/transform');

var _lodashObjectTransform2 = _interopRequireDefault(_lodashObjectTransform);

var _lodashObjectHas = require('lodash/object/has');

var _lodashObjectHas2 = _interopRequireDefault(_lodashObjectHas);

var _lodashUtilityUniqueId = require('lodash/utility/uniqueId');

var _lodashUtilityUniqueId2 = _interopRequireDefault(_lodashUtilityUniqueId);

var _cssSelectorParser = require('css-selector-parser');

var PREFIX = 'sub_____';

var parser = new _cssSelectorParser.CssSelectorParser();

parser.registerSelectorPseudos('has');
parser.registerNestingOperators('>');
parser.enableSubstitutes();

var prim = function prim(value) {
var typ = typeof value;
return value === null || ['string', 'number'].indexOf(typ) !== -1;
};

function parse(selector) {
var ast = typeof selector === 'string' ? parser.parse(selector) : selector;

if (ast.rule) {
var rule = ast.rule;
return { rules: getRule(rule), ast: ast };
} else if (ast.selectors) {
return {
ast: ast,
rules: ast.selectors.map(function (s) {
return getRule(s.rule);
}),
multiple: true
};
}

function getRule(rule) {
if (!rule) return [];
return getRule(rule.rule).concat(rule);
}
}

function create(options) {
var NESTING = Object.create(null);
var PSEUDOS = Object.create(null);

var traverse = options.traverse;

return {
compile: compile,
compileRule: compileRule,
registerNesting: function registerNesting(name, fn) {
NESTING[name] = fn;
},
registerPseudo: function registerPseudo(name, fn) {
PSEUDOS[name] = fn;
}
};

function compile(selector) {
var values = arguments.length <= 1 || arguments[1] === undefined ? Object.create(null) : arguments[1];

var _parse = parse(selector);

var rules = _parse.rules;
var ast = _parse.ast;
var multiple = _parse.multiple;

if (!multiple) return compileRule(rules, null, values, ast);

return rules.map(function (ruleSet) {
return compileRule(ruleSet, null, values, ast);
}).reduce(function (current, next) {
return function (root, parent) {
return current(root, parent) || next(root, parent);
};
});
}

function compileRule(rules, parent, values, ast) {
var fns = [];
var rule = rules.shift();

if (rule.tagName) fns.push(getTagComparer(rule, values));

if (rule.attrs) fns.push(getPropComparer(rule, values));

if (rule.classNames) fns.push(function (_ref) {
var className = _ref.props.className;

return rule.classNames.every(function (clsName) {
return className && className.indexOf(clsName) !== -1;
});
});

if (rule.pseudos) {
fns = fns.concat(rule.pseudos.map(function (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')) {
var operator = rule.nestingOperator || 'any';
var 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(function (current) {
var next = arguments.length <= 1 || arguments[1] === undefined ? function () {
return true;
} : arguments[1];

return function (root, parent) {
return next(root, parent) && current(root, parent);
};
});
}
}

function getTagComparer(rule, values) {
var isStr = function isStr(t) {
return typeof t === 'string';
};
var tagName = values[rule.tagName] || rule.tagName;

if (rule.tagName === '*') return function () {
return true;
};

if (isStr(tagName)) {
tagName = tagName.toUpperCase();
return function (root) {
return isStr(root.type) && root.type.toUpperCase() === tagName;
};
}

return function (root) {
return root.type === tagName;
};
}

function getPropComparer(rule, values) {
return function (_ref2) {
var props = _ref2.props;
return rule.attrs.every(function (attr) {
if (!_lodashObjectHas2['default'](attr, 'value')) return !!props[attr.name];

if (!_lodashObjectHas2['default'](values, attr.value)) return props[attr.name] == attr.value;

return props[attr.name] === values[attr.value];
});
};
}
28 changes: 28 additions & 0 deletions lib/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "bill",
"version": "1.0.5",
"description": "css selectors for React Elements",
"main": "index.js",
"repository": {
"type": "git",
"url": "https://github.com/jquense/bill"
},
"keywords": [
"react",
"test",
"query"
],
"author": "jquense",
"license": "MIT",
"bugs": {
"url": "https://github.com/jquense/bill/issues"
},
"homepage": "https://github.com/jquense/bill",
"peerDependencies": {
"react": ">=0.13.0 ||^0.14.0-alpha1"
},
"dependencies": {
"css-selector-parser": "^1.1.0",
"lodash": "^3.10.1"
}
}
Loading

0 comments on commit d0b4273

Please sign in to comment.