Skip to content

Commit

Permalink
[added] sibling combinators and psuedos
Browse files Browse the repository at this point in the history
  • Loading branch information
jquense committed Dec 18, 2015
1 parent 6b38f29 commit 007dbdb
Show file tree
Hide file tree
Showing 9 changed files with 262 additions and 81 deletions.
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,21 @@ matches[0] // { type: List, props }

### Supported

- classNames
- `div[propName="hi"]` or `div[boolProp]`
- `>`: `div > .foo`
- `:has()`: `div:has(a.foo)`
- classes: `.foo`
- attributes: `div[propName="hi"]` or `div[boolProp]`
- `>`: direct descendent `div > .foo`
- `+`: adjacent sibling selector
- `~`: general sibling selector
- `:has()`: parent selector `div:has(a.foo)`
- `:not()`: negation
- `:first-child`
- `:last-child`

### Not supported

- sibling selectors
- pseudo selectors (except for has)
- most pseudo selectors
- non string interpolations for anything other than "tag" or prop values


## API

### `match(selector, elementOrInstance) -> array<Element|Instance>`
Expand Down
7 changes: 6 additions & 1 deletion lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ exports.__esModule = true;
exports.anyParent = anyParent;
exports.directParent = directParent;
exports.legacySelector = legacySelector;

var isValidPlainElement = function isValidPlainElement(element) {
return typeof element === 'object' && element != null;
};

var isTextElement = function isTextElement(element) {
return typeof element === 'string';
return !isValidPlainElement(element) && element !== false;
};

exports.isTextElement = isTextElement;
Expand Down
66 changes: 66 additions & 0 deletions src/common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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))
}
2 changes: 1 addition & 1 deletion src/compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export function create(options = {}) {
return rules
.map(ruleSet => compileRule(ruleSet, null, values, ast))
.reduce((current, next)=> {
return (root, parent) => current(root, parent) || next(root, parent)
return (...args) => current(...args) || next(...args)
})
}

Expand Down
32 changes: 13 additions & 19 deletions src/element-selector.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,41 @@ 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
}
})

compiler.registerPseudo('dom', ()=> isDomElement)
compiler.registerPseudo('composite', ()=> isCompositeElement)

compiler.registerNesting('any', test => anyParent.bind(null, test))

compiler.registerNesting('>', test => directParent.bind(null, test))

export function match(selector, tree, includeSelf = true){
return findAll(tree, compiler.compile(selector), includeSelf)
}

export function findAll(root, test, includeSelf, getParent = ()=> ({ parent: null })) {
export function findAll(element, test, includeSelf, parent) {
let found = [], children = [];

if (root == null || root === false)
if (element == null || element === false)
return found;

if (React.isValidElement(root))
children = root.props.children
var node = createNode(element, parent);

if (includeSelf && test(root, getParent))
found.push(root);
if (includeSelf && test(element, node))
found.push(element);

if (React.Children.count(children) === 0)
return found

React.Children.forEach(children, child => {
let parent = ()=> ({ parent: root, getParent });
found = found.concat(findAll(child, test, true, parent))
})
if (React.isValidElement(element))
eachChild(element, child => {
found = found.concat(findAll(child, test, true, node))
})

return found
}
61 changes: 21 additions & 40 deletions src/instance-selector.js
Original file line number Diff line number Diff line change
@@ -1,70 +1,50 @@
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 } from './utils';


let isDOMComponent = inst => !!(inst && inst.nodeType === 1 && inst.tagName);

let isCompositeComponent = inst => !isDOMComponent(inst) || inst === null
|| typeof inst.render === 'function' && typeof inst.setState === 'function';

, isDomElement, isCompositeElement
, isReactInstance, isDOMComponent, isCompositeComponent } from './utils';

export let compiler = createCompiler()

common(compiler);

compiler.registerPseudo('has', function(compiledSelector) {
return (_, inst) => {
let matches = findAll(inst, compiledSelector)
return (_, node) => {
let matches = findAll(node.instance, compiledSelector)
return !!matches.length
}
})

compiler.registerPseudo('dom', ()=> isDomElement)
compiler.registerPseudo('composite', ()=> isCompositeElement)

compiler.registerNesting('any', test =>
(element, inst, parent) => anyParent(test, element, parent))

compiler.registerNesting('>', test =>
(element, inst, parent) => directParent(test, element, parent))


export function findAll(inst, test, includeSelf, getParent = ()=> ({ parent: null })) {
export function findAll(inst, test, includeSelf, parent) {
let found = [], publicInst;

if (!inst )
if (!inst)
return found;

if (inst.getPublicInstance)
publicInst = inst.getPublicInstance()

var element = inst._currentElement
, parent = ()=> ({ parent: element, getParent });
var node = createNode(inst, parent);

// ReactEmptyComponents (return null render <noscript/>) have null has their element
if (includeSelf && element !== null && test(element, inst, getParent))
if (includeSelf && node.element !== null && test(node.element, node))
found = found.concat(inst)

if (isDOMComponent(publicInst)) {
let renderedChildren = inst._renderedChildren || {}
, child = inst._currentElement.props.children;
eachChild(inst, child => {
let childNode = createNode(child, node);

if (typeof child === 'string' && test(child, null, parent) ) {
found = found.concat(child)
}
if (!isReactInstance(child) && test(childNode.element, childNode))
return found.push(child)

Object.keys(renderedChildren).forEach(key => {
found = found.concat(
findAll(renderedChildren[key], test, true, parent)
);
})
}
else if (isCompositeComponent(publicInst)) {
found = found.concat(
findAll(inst._renderedComponent, test, true, parent)
findAll(child, test, true, node)
);
}
})

return found;
}
Expand All @@ -83,4 +63,5 @@ export function match(selector, inst, includeSelf = true) {
return findAll(tree, compiler.compile(selector), includeSelf)
}


export let { compile, compileRule, selector } = compiler
81 changes: 81 additions & 0 deletions src/node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React from 'react';
import ReactInstanceMap from 'react/lib/ReactInstanceMap';
import isPlainObject from 'lodash/lang/isPlainObject';
import { create as createCompiler, parse } from './compiler';
import {
anyParent, directParent
, isDomElement, isCompositeElement
, isReactInstance, isDOMComponent, isCompositeComponent } from './utils';

export function eachChild(subject, fn) {
let inst, element, publicInst;

if (!subject) return;

if (React.isValidElement(subject))
return React.Children.forEach(subject.props.children, child => fn(child))

inst = subject
element = inst._currentElement

if (inst.getPublicInstance)
publicInst = inst.getPublicInstance()

if (isDOMComponent(publicInst)) {
let renderedChildren = inst._renderedChildren || {}
, child = element && element.props.children;

if (child != null && !isPlainObject(child) && !Array.isArray(child) && !isReactInstance(child))
fn(child)

Object.keys(renderedChildren).forEach(
key => fn(renderedChildren[key])
);
}
else if (isCompositeComponent(publicInst)) {
fn(inst._renderedComponent);
}
}


export function createNode(subject, lastWrapper) {
let element, inst;

if (React.isValidElement(subject))
element = subject, inst = null;

else if (isReactInstance(subject))
element = subject._currentElement, inst = subject;

return Object.defineProperties({}, {
element: { get: () => element, enumerable: true },
instance: { value: inst, enumerable: true },
parentNode: { value: lastWrapper, enumerable: true },
prevSibling: {
enumerable: true,
get(){
let children = lastWrapper ? lastWrapper.children : []
, idx = children.indexOf(inst || element) - 1

return idx < 0 ? null : createNode(children[idx], lastWrapper)
}
},
nextSibling: {
enumerable: true,
get(){
let children = lastWrapper ? lastWrapper.children : []
, idx = children.indexOf(inst || element) + 1

return idx >= children.length ? null : createNode(children[idx], lastWrapper)
}
},
children: {
enumerable: true,
get(){
let children = [];
eachChild(inst || element, child => children.push(child))
return children
}
}
})
}
23 changes: 11 additions & 12 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import has from 'lodash/object/has';
import React from 'react';

let isValidPlainElement = element => typeof element === 'object' && element != null;

Expand All @@ -10,20 +12,17 @@ export let isDomElement =
export let isCompositeElement =
element => !isTextElement(element) && typeof element.type === 'function'

export function anyParent(test, element, parentNode){
do {
var { getParent, parent } = parentNode();
element = parent
parentNode = getParent
} while(element && !test(element, test, getParent))

return !!element
}
export let isDOMComponent = inst => !!(inst && inst.nodeType === 1 && inst.tagName);

export let isCompositeComponent = inst => !isDOMComponent(inst) || inst === null
|| typeof inst.render === 'function' && typeof inst.setState === 'function';

export let isReactInstance = obj =>
obj != null &&
has(obj, '_currentElement') &&
has(obj, '_rootNodeID');

export function directParent(test, element, parentNode) {
element = parentNode().parent
return !!(element && test(element, parentNode().getParent))
}

export function legacySelector(...args){
let strings = []
Expand Down
Loading

0 comments on commit 007dbdb

Please sign in to comment.