diff --git a/src/common.js b/src/common.js
deleted file mode 100644
index 9224297..0000000
--- a/src/common.js
+++ /dev/null
@@ -1,66 +0,0 @@
-import {
- isDomElement, isCompositeElement
- , isReactInstance, isDOMComponent, isCompositeComponent } from './utils';
-
-function anyParent(test, element, node){
- do {
- node = node.parentNode
- } while(node && !test(node.element, node))
-
- return !!node
-}
-
-function directParent(test, _, node) {
- node = node.parentNode
- return !!(node && test(node.element, node))
-}
-
-function anySibling(test, _, node) {
- do {
- node = node.prevSibling
- } while(node && !test(node.element, node))
-
- return !!node
-}
-
-function directSibling(test, _, node) {
- node = node.prevSibling
- return !!(node && test(node.element, node))
-}
-
-export default function(compiler) {
- compiler.registerPseudo('dom', ()=> isDomElement)
- compiler.registerPseudo('composite', ()=> isCompositeElement)
-
- compiler.registerPseudo('not', function(compiledSelector) {
- return (element, node) => {
- let matches = compiledSelector(element, node)
- return !matches
- }
- })
-
- compiler.registerPseudo('first-child', () =>
- (element, node) => {
- let parent = node.parentNode;
- return parent && parent.children.indexOf(node.instance || element) === 0
- })
-
- compiler.registerPseudo('last-child', () =>
- (element, node) => {
- let parent = node.parentNode;
- let children = parent && parent.children
- return parent && children.indexOf(node.instance || element) === (children.length - 1)
- })
-
- compiler.registerNesting('any', test =>
- (element, node) => anyParent(test, element, node))
-
- compiler.registerNesting('>', test =>
- (element, node) => directParent(test, element, node))
-
- compiler.registerNesting('~', test =>
- (element, node) => anySibling(test, element, node))
-
- compiler.registerNesting('+', test =>
- (element, node) => directSibling(test, element, node))
-}
diff --git a/src/compiler.js b/src/compiler.js
index 8b395b2..4f2e8e6 100644
--- a/src/compiler.js
+++ b/src/compiler.js
@@ -2,12 +2,15 @@ import React from 'react';
import transform from 'lodash/object/transform';
import has from 'lodash/object/has';
import uid from 'lodash/utility/uniqueId';
+import { createNode, NODE_TYPES } from './node';
import { isTextElement, legacySelector } from './utils';
import { CssSelectorParser } from 'css-selector-parser';
import fnName from 'fn-name';
let parser = new CssSelectorParser();
-let name = type => type.displayName || fnName(type) || ''
+
+let name = type => typeof type === 'string'
+ ? type : type.displayName || fnName(type) || ''
let prim = value => {
let typ = typeof value;
@@ -15,7 +18,7 @@ let prim = value => {
}
function failText(fn){
- return (...args) => isTextElement(args[0]) ? false : fn(...args)
+ return (...args) => args[0].nodeType === NODE_TYPES.TEXT ? false : fn(...args)
}
export function parse(selector){
@@ -88,9 +91,7 @@ export function create(options = {}) {
let rule = rules.shift();
if (rule.tagName)
- fns.push(
- failText(getTagComparer(rule, values))
- )
+ fns.push(getTagComparer(rule, values))
if (rule.attrs)
fns.push(
@@ -99,7 +100,7 @@ export function create(options = {}) {
if (rule.classNames)
fns.push(
- failText(({ props }) => {
+ failText(({ element: { props } }) => {
let className = props && props.className
return rule.classNames.every(clsName =>
className && className.indexOf(clsName) !== -1)
@@ -131,9 +132,11 @@ export function create(options = {}) {
fns.push(NESTING[operator](nestedCompiled))
}
- return fns.reduce((current, next = ()=> true)=> {
+ let compiledRule = fns.reduce((current, next = ()=> true)=> {
return (...args)=> current(...args) && next(...args)
})
+
+ return (element, ...args)=> compiledRule(createNode(element), ...args)
}
function selector(strings, ...values){
@@ -161,24 +164,27 @@ export function create(options = {}) {
}
function getTagComparer(rule, values) {
- let isStr = t => typeof t === 'string'
- let tagName = values[rule.tagName] || rule.tagName;
+ let tagName = values[rule.tagName] || rule.tagName
+ , test;
if (rule.tagName === '*')
- return ()=> true
+ test = ()=> true
+
+ else {
+ if (typeof tagName !== 'string')
+ test = root => root.element.type === tagName
+ else {
+ test = root => name(root.element.type).toUpperCase() === tagName.toUpperCase();
+ }
- if (isStr(tagName)){
- tagName = tagName.toUpperCase();
- return root => isStr(root.type)
- ? root.type.toUpperCase() === tagName
- : name(root.type).toUpperCase() === tagName;
+ test = failText(test)
}
- return root => root.type === tagName
+ return test
}
function getPropComparer(rule, values) {
- return ({ props }) => rule.attrs.every(attr => {
+ return ({ element: { props } }) => rule.attrs.every(attr => {
if (!has(attr, 'value'))
return !!props[attr.name]
diff --git a/src/element-selector.js b/src/element-selector.js
deleted file mode 100644
index 006ed87..0000000
--- a/src/element-selector.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import React from 'react';
-import transform from 'lodash/object/transform';
-import has from 'lodash/object/has';
-import { create as createCompiler, parse } from './compiler';
-import common from './common';
-import { createNode, eachChild } from './node';
-import { anyParent, directParent, isDomElement, isCompositeElement } from './utils';
-
-export let compiler = createCompiler()
-
-common(compiler);
-
-compiler.registerPseudo('has', function(compiledSelector) {
- return root => {
- let matches = findAll(root, compiledSelector)
- return !!matches.length
- }
-})
-
-export function match(selector, tree, includeSelf = true){
- return findAll(tree, compiler.compile(selector), includeSelf)
-}
-
-export function findAll(element, test, includeSelf, parent) {
- let found = [], children = [];
-
- if (element == null || element === false)
- return found;
-
- var node = createNode(element, parent);
-
- if (includeSelf && test(element, node))
- found.push(element);
-
-
- if (React.isValidElement(element))
- eachChild(element, child => {
- found = found.concat(findAll(child, test, true, node))
- })
-
- return found
-}
diff --git a/src/index.js b/src/index.js
index ef3ca32..078b651 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,15 +1,25 @@
import { isValidElement } from 'react';
-import * as elements from './element-selector';
-import * as instance from './instance-selector';
+import { NODE_TYPE } from './node';
+import { findAll, createNode } from './node';
+import { create as createCompiler, parse } from './compiler';
+import selectors from './selectors';
-function match(selector, element){
- if (isValidElement(element))
- return elements.match(selector, element)
+let compiler = createCompiler()
- return instance.match(selector, element)
+selectors(compiler);
+
+function match(selector, tree, includeSelf = true) {
+ return createNode(tree).findAll(compiler.compile(selector), includeSelf)
+}
+
+function matchKind(selector, element, includeSelf) {
+ return match(selector, element, includeSelf)
+ .map(node => node.instance || node.element)
}
module.exports = {
match,
- selector: elements.compiler.selector
+ matchKind,
+ findAll,
+ isNode: el => el && el.$$typeof === NODE_TYPE,
}
diff --git a/src/instance-selector.js b/src/instance-selector.js
deleted file mode 100644
index edc4fee..0000000
--- a/src/instance-selector.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import React from 'react';
-import ReactInstanceMap from 'react/lib/ReactInstanceMap';
-import isPlainObject from 'lodash/lang/isPlainObject';
-import { create as createCompiler, parse } from './compiler';
-import common from './common';
-import { createNode, eachChild } from './node';
-import {
- anyParent, directParent
- , isDomElement, isCompositeElement
- , isReactInstance, isDOMComponent, isCompositeComponent } from './utils';
-
-export let compiler = createCompiler()
-
-common(compiler);
-
-compiler.registerPseudo('has', function(compiledSelector) {
- return (_, node) => {
- let matches = findAll(node.instance, compiledSelector)
- return !!matches.length
- }
-})
-
-export function findAll(inst, test, includeSelf, parent) {
- let found = [], publicInst;
-
- if (!inst)
- return found;
-
- if (inst.getPublicInstance)
- publicInst = inst.getPublicInstance()
-
- var node = createNode(inst, parent);
-
- // ReactEmptyComponents (return null render ) have null has their element
- if (includeSelf && node.element !== null && test(node.element, node))
- found = found.concat(inst)
-
- eachChild(inst, child => {
- let childNode = createNode(child, node);
-
- if (!isReactInstance(child) && test(childNode.element, childNode))
- return found.push(child)
-
- found = found.concat(
- findAll(child, test, true, node)
- );
- })
-
- return found;
-}
-
-/**
- * The matcher actually works on internal instances, not public ones
- * since DOM and stateless components don't have helpful public instances
- */
-export function match(selector, inst, includeSelf = true) {
- let tree = inst.getPublicInstance
- ? inst //already a private instance
- : inst._reactInternalComponent //is a DOM node
- ? inst._reactInternalComponent
- : ReactInstanceMap.get(inst)
-
- return findAll(tree, compiler.compile(selector), includeSelf)
-}
-
-
-export let { compile, compileRule, selector } = compiler
diff --git a/src/node.js b/src/node.js
index 734f2c3..ed31926 100644
--- a/src/node.js
+++ b/src/node.js
@@ -1,19 +1,71 @@
import React from 'react';
+import { findDOMNode } from 'react-dom';
import ReactInstanceMap from 'react/lib/ReactInstanceMap';
import isPlainObject from 'lodash/lang/isPlainObject';
+import findIndex from 'lodash/array/findIndex';
import { create as createCompiler, parse } from './compiler';
import {
- anyParent, directParent
- , isDomElement, isCompositeElement, isTextElement
- , isReactInstance, isDOMComponent, isCompositeComponent } from './utils';
+ isDomElement, isCompositeElement
+ , isTextElement, isReactInstance
+ , isDOMComponent, isCompositeComponent, getAllPropertyNames} from './utils';
+
+export const NODE_TYPE = (typeof Symbol === 'function' && Symbol.for && Symbol.for('bill.node')) || 0xeac7;
+
+export const NODE_TYPES = {
+ COMPOSITE: 1,
+ DOM: 2,
+ TEXT: 3
+}
+
+function indexOfNode(arr, instOrElement) {
+ return findIndex(arr, (node, i) => {
+ return node.privateInstance === instOrElement || node.element === instOrElement
+ })
+}
+
+function instanceFromNativeNode(subject) {
+ if (subject._reactInternalComponent)
+ return subject._reactInternalComponent
+
+ // TODO: react 0.15.0 is going to break this
+ // need to use ReactDOMComponentTree.getInstanceFromNode
+}
+
+function normalizeSubject(subject) {
+ return subject && !subject.getPublicInstance
+ ? instanceFromNativeNode(subject) || ReactInstanceMap.get(subject) || subject
+ : subject
+}
+
+export function findAll(subject, test, includeSelf) {
+ if (!subject)
+ return [];
+
+ let found = [];
+ let node = createNode(subject);
+
+ // ReactEmptyComponents (return null; render ) have null has their element
+ // so don't assume it's there.
+ if (includeSelf && node.element != null && test(node))
+ found = found.concat(node)
+
+ return node.children.reduce(
+ (found, child) => found.concat(findAll(child, test, true)), found)
+}
export function eachChild(subject, fn) {
let inst, element, publicInst;
if (!subject) return;
+ subject = normalizeSubject(subject);
+
if (React.isValidElement(subject))
- return React.Children.forEach(subject.props.children, child => fn(child))
+ return React.Children.forEach(subject.props.children,
+ child => child != null && fn(child))
+
+ if (!isReactInstance(subject))
+ return
inst = subject
element = inst._currentElement
@@ -35,49 +87,97 @@ export function eachChild(subject, fn) {
else if (child != null && isTextElement(child))
fn(child)
}
- else if (isCompositeComponent(publicInst) && inst._renderedComponent != null) {
+ else if (
+ React.isValidElement(element) &&
+ typeof element.type === 'function' &&
+ inst._renderedComponent != null
+ ) {
fn(inst._renderedComponent);
}
}
export function createNode(subject, lastWrapper) {
- let element, inst;
+ let node, element, inst;
+
+ if (subject != null && subject.$$typeof === NODE_TYPE)
+ return subject;
+
+ subject = normalizeSubject(subject);
if (isReactInstance(subject))
element = subject._currentElement, inst = subject;
else
element = subject, inst = null;
- return Object.defineProperties({}, {
- element: { get: () => element, enumerable: true },
- instance: { value: inst, enumerable: true },
+ let children, type;
+
+ if (element) {
+ if (isCompositeElement(element))
+ type = NODE_TYPES.COMPOSITE;
+ else if (isDomElement(element))
+ type = NODE_TYPES.DOM;
+ else if (isTextElement(element))
+ type = NODE_TYPES.TEXT;
+ }
+
+ node = Object.defineProperties({}, {
+ $$typeof: { value: NODE_TYPE },
+ nodeType: { value: type },
+ element: { value: element, enumerable: true },
+ privateInstance: { value: inst, enumerable: true },
+ instance: {
+ enumerable: true,
+ get() {
+ let publicInst;
+ if (!inst) return
+ if (inst.getPublicInstance) {
+ publicInst = inst.getPublicInstance()
+ if (publicInst === null)
+ publicInst = inst._instance
+ }
+ else if (isTextElement(element))
+ publicInst = element
+
+ return publicInst
+ }
+ },
parentNode: { value: lastWrapper, enumerable: true },
+ findAll: {
+ enumerable: true,
+ value: (test, includeSelf) => findAll(node, test, includeSelf)
+ },
prevSibling: {
enumerable: true,
get(){
let children = lastWrapper ? lastWrapper.children : []
- , idx = children.indexOf(inst || element) - 1
+ , idx = indexOfNode(children, inst || element) - 1
- return idx < 0 ? null : createNode(children[idx], lastWrapper)
+ return idx < 0 ? null : children[idx]
}
},
nextSibling: {
enumerable: true,
get(){
let children = lastWrapper ? lastWrapper.children : []
- , idx = children.indexOf(inst || element) + 1
+ , idx = indexOfNode(children, inst || element) + 1
- return idx >= children.length ? null : createNode(children[idx], lastWrapper)
+ return idx >= children.length ? null : children[idx]
}
},
children: {
enumerable: true,
get(){
- let children = [];
- eachChild(inst || element, child => children.push(child))
+ if (!children) {
+ children = []
+ eachChild(inst || element,
+ child => children.push(createNode(child, node)))
+ }
+
return children
}
}
})
+
+ return node
}
diff --git a/src/selectors.js b/src/selectors.js
new file mode 100644
index 0000000..2e99de3
--- /dev/null
+++ b/src/selectors.js
@@ -0,0 +1,57 @@
+import { isReactInstance, isDOMComponent, isCompositeComponent } from './utils';
+import { NODE_TYPES } from './node';
+
+export default function(compiler) {
+
+ Object.keys(NODE_TYPES).forEach(type => {
+ compiler.registerPseudo(type.toLowerCase(),
+ ()=> node => node.nodeType === NODE_TYPES[type])
+ })
+
+ compiler.registerPseudo('has', test => node => {
+ let matches = node.findAll(test)
+ return !!matches.length
+ })
+
+ compiler.registerPseudo('not', test => node => {
+ let matches = test(node)
+ return !matches
+ })
+
+ compiler.registerPseudo('first-child', () => node => {
+ let parent = node.parentNode;
+ return parent && parent.children.indexOf(node) === 0
+ })
+
+ compiler.registerPseudo('last-child', () => node => {
+ let parent = node.parentNode;
+ let children = parent && parent.children
+ return parent && children.indexOf(node) === (children.length - 1)
+ })
+
+ compiler.registerNesting('any', test => node => {
+ do {
+ node = node.parentNode
+ } while(node && !test(node))
+
+ return !!node
+ })
+
+ compiler.registerNesting('>', test => node => {
+ node = node.parentNode
+ return !!(node && test(node))
+ })
+
+ compiler.registerNesting('~', test => node => {
+ do {
+ node = node.prevSibling
+ } while(node && !test(node))
+
+ return !!node
+ })
+
+ compiler.registerNesting('+', test => node => {
+ node = node.prevSibling
+ return !!(node && test(node))
+ })
+}
diff --git a/src/utils.js b/src/utils.js
index 7d6f42f..2d59263 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -41,3 +41,13 @@ export function legacySelector(...args){
return [strings, values]
}
+
+export function getAllPropertyNames(obj) {
+ let proto = Object.getPrototypeOf(obj)
+ , names = Object.getOwnPropertyNames(obj)
+
+ if (proto)
+ names.concat(getAllPropertyNames(proto))
+
+ return names
+}
diff --git a/test/compiler.js b/test/compiler.js
index dad635d..d1a030a 100644
--- a/test/compiler.js
+++ b/test/compiler.js
@@ -41,7 +41,7 @@ describe('create compiler', ()=> {
})
it('should handle non element values', ()=> {
- registerPseudo('text', ()=> element => isTextElement(element))
+ registerPseudo('text', ()=> node => isTextElement(node.element))
let result = compile(':text')
result({}).should.equal(true)
@@ -55,7 +55,14 @@ describe('create compiler', ()=> {
result(null).should.equal(false)
})
- it('should match props', ()=>{
+ it('universal selector * should include text nodes', ()=> {
+ let result = compile('*')
+
+ result('hello').should.equal(true)
+ result(500).should.equal(true)
+ })
+
+ it('should match props', ()=> {
let result = compile('[foo="5"]')
result({