├── .gitignore ├── .travis.yml ├── package.json ├── README.md ├── index.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.12" 5 | - "4" 6 | - "5" 7 | - "6" 8 | - "io.js" 9 | - "node" 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vtree-select", 3 | "version": "2.1.0", 4 | "description": "Select virtual-dom nodes using css selectors", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node test.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:parshap/vtree-select.git" 12 | }, 13 | "keywords": [ 14 | "virtual-dom", 15 | "select", 16 | "css", 17 | "selector" 18 | ], 19 | "author": "Parsha Pourkhomami", 20 | "license": "Public Domain", 21 | "bugs": { 22 | "url": "https://github.com/parshap/vtree-select/issues" 23 | }, 24 | "homepage": "https://github.com/parshap/vtree-select", 25 | "dependencies": { 26 | "cssauron": "^1.2.0" 27 | }, 28 | "devDependencies": { 29 | "virtual-dom": "^2.1.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vtree-select 2 | 3 | [![build status](https://secure.travis-ci.org/parshap/vtree-select.svg?branch=master)](http://travis-ci.org/parshap/vtree-select) 4 | 5 | Select *[vtree][]* nodes (used by *[virtual-dom][]*) using css 6 | selectors. 7 | 8 | Selector matching is done using *[cssauron][]*. See the documentation 9 | for details on supported selectors. 10 | 11 | [virtual-dom]: https://www.npmjs.org/package/virtual-dom 12 | [vtree]: https://www.npmjs.org/package/vtree 13 | [cssauron]: https://www.npmjs.org/package/cssauron 14 | 15 | ## Usage 16 | 17 | ```js 18 | var select = require("vtree-select"); 19 | var h = require("virtual-hyperscript"); 20 | 21 | var vtree = h("div", [ 22 | h("span", "hello"), 23 | h("strong", "world"), 24 | ]); 25 | 26 | select("div strong")(vtree) 27 | // -> world (VNode object) 28 | 29 | select("div strong").matches(vtree) 30 | // -> false 31 | 32 | select("div").matches(vtree) 33 | // -> true 34 | ``` 35 | 36 | ## API 37 | 38 | ### `require("vtree-select")(selector [, options]) -> match` 39 | 40 | Create a match function that takes a vtree and returns an array of nodes 41 | that match the selector. 42 | 43 | `options.caseSensitiveTag` enables case sensitivity for tag names. This 44 | is useful for XML, which has case-sensitive tag names. HTML tag names 45 | are not case sensitive. 46 | 47 | ### `match(vtree) -> array` 48 | 49 | Returns an array of matched nodes, or `null` if no nodes match. 50 | 51 | Unlike css and `querySelectorAll`, *text nodes* are matched and 52 | returned. 53 | 54 | Selectors are **case-sensitive**. 55 | 56 | ### `match.matches(vtree) -> boolean` 57 | 58 | Returns `true` if the vtree matches the selector, or `false` otherwise. 59 | 60 | 61 | ## Installation 62 | 63 | ``` 64 | npm install vtree-select 65 | ``` 66 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // @TODO text content 4 | 5 | var language = require("cssauron")({ 6 | tag: "tagName", 7 | class: function (node) { 8 | if (node.className !== undefined) { 9 | return node.className; 10 | } 11 | // html-to-vdom puts the 'class' in attributes. 12 | if (node.properties && node.properties.attributes) { 13 | return node.properties.attributes.class; 14 | } 15 | return undefined; 16 | }, 17 | "id" :"id", 18 | children: "children", 19 | parent: "parent", 20 | contents: function(node) { 21 | return node.contents || ""; 22 | }, 23 | attr: function(node, attr) { 24 | if (node.properties) { 25 | var attrs = node.properties.attributes; 26 | if(attrs && attrs[attr]){ 27 | return attrs[attr]; 28 | } 29 | return node.properties[attr]; 30 | } 31 | }, 32 | }); 33 | 34 | module.exports = function(sel, options) { 35 | options = options || {}; 36 | var selector = language(sel); 37 | function match(vtree) { 38 | var node = mapTree(vtree, null, options) || {}; 39 | var matched = []; 40 | 41 | // Traverse each node in the tree and see if it matches our selector 42 | traverse(node, function(node) { 43 | var result = selector(node); 44 | if (result) { 45 | if ( ! Array.isArray(result)) { 46 | result = [result]; 47 | } 48 | matched.push.apply(matched, result); 49 | } 50 | }); 51 | 52 | var results = mapResult(matched); 53 | if (results.length === 0) { 54 | return null; 55 | } 56 | return results; 57 | } 58 | match.matches = function(vtree) { 59 | var node = mapTree(vtree, null, options); 60 | return !!selector(node); 61 | }; 62 | return match; 63 | }; 64 | 65 | function traverse(vtree, fn) { 66 | fn(vtree); 67 | if (vtree.children) { 68 | vtree.children.forEach(function(vtree) { 69 | traverse(vtree, fn); 70 | }); 71 | } 72 | } 73 | 74 | function mapResult(result) { 75 | return result 76 | .filter(function(node) { 77 | return !! node.vtree; 78 | }) 79 | .map(function(node){ 80 | return node.vtree; 81 | }); 82 | } 83 | 84 | function getNormalizeCaseFn(caseSensitive) { 85 | return caseSensitive ? 86 | function noop(str) { 87 | return str; 88 | } : 89 | function toLowerCase(str) { 90 | return str.toLowerCase(); 91 | }; 92 | } 93 | 94 | // Map a virtual-dom node tree into a data structure that cssauron can use to 95 | // traverse. 96 | function mapTree(vtree, parent, options) { 97 | var normalizeTagCase = getNormalizeCaseFn(options.caseSensitiveTag); 98 | 99 | // VText represents text nodes 100 | // See https://github.com/Matt-Esch/virtual-dom/blob/master/docs/vtext.md 101 | if (vtree.type === "VirtualText") { 102 | return { 103 | contents: vtree.text, 104 | parent: parent, 105 | vtree: vtree, 106 | }; 107 | } 108 | 109 | if (vtree.tagName != null) { 110 | var node = {}; 111 | node.parent = parent; 112 | node.vtree = vtree; 113 | node.tagName = normalizeTagCase(vtree.tagName); 114 | if (vtree.properties) { 115 | node.properties = vtree.properties; 116 | if (typeof vtree.properties.className === "string") { 117 | node.className = vtree.properties.className; 118 | } 119 | if (typeof vtree.properties.id === "string") { 120 | node.id = vtree.properties.id; 121 | } 122 | } 123 | if (vtree.children && typeof vtree.children.map === "function") { 124 | node.children = vtree.children.map(function(child) { 125 | return mapTree(child, node, options); 126 | }).filter(Boolean); 127 | } 128 | return node; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var h = require("virtual-dom/h"); 4 | var select = require("./"); 5 | 6 | var span1 = h("span.span1", "hello world"); 7 | var span2 = h("span.span2", "hello world2"); 8 | var li = h("li", "item"); 9 | var ul = h("ul", [li]); 10 | var props = h("div", { label : 'label' }); 11 | var attrs = h("div", { attributes : {'custom-attr' : 'custom'} }); 12 | var tree = h("div#tree", [span1, span2, ul]); 13 | 14 | var assert = require("assert"); 15 | 16 | // Select root element 17 | assert.deepEqual(select("div")(tree), [tree]); 18 | assert.deepEqual(select("#tree")(tree), [tree]); 19 | assert.deepEqual(select("[id]")(tree), [tree]); 20 | assert.deepEqual(select("[id=tree]")(tree), [tree]); 21 | assert.deepEqual(select("div[id=tree]")(tree), [tree]); 22 | 23 | // properties/attributes 24 | assert.deepEqual(select('[label]')(props),[props]); 25 | assert.deepEqual(select('[custom-attr]')(attrs),[attrs]); 26 | 27 | // Select children 28 | assert.deepEqual(select("span")(tree), [span1, span2]); 29 | assert.deepEqual(select("span.span1")(tree), [span1]); 30 | assert.deepEqual(select("span.span2")(tree), [span2]); 31 | 32 | // 3rd tier children 33 | assert.deepEqual(select("div ul li")(tree), [li]); 34 | 35 | // * operator 36 | assert.deepEqual(select("div ul > li")(tree), [li]); 37 | assert.deepEqual(select("div * > li")(tree), [li]); 38 | assert.deepEqual(select("*:root")(tree), [tree]); 39 | 40 | // :contains() pseudo 41 | assert.deepEqual(select("!* > :contains('hello')")(tree), [span1, span2]); 42 | 43 | // matches(vtree) 44 | assert.strictEqual(select("*:root#tree").matches(tree), true); 45 | assert.strictEqual(select("span.span1").matches(tree.children[0]), true); 46 | assert.strictEqual(select("zz").matches(tree), false); 47 | assert.strictEqual(select(":not(span)").matches(tree), true); 48 | assert.strictEqual(select(":not(div)").matches(tree), false); 49 | 50 | // Widget 51 | // Use widget from virtual-dom example 52 | // See https://github.com/Matt-Esch/virtual-dom/blob/master/docs/widget.md 53 | var TestWidget = function() {}; 54 | TestWidget.prototype.type = "Widget"; 55 | TestWidget.prototype.init = function() {}; 56 | TestWidget.prototype.update = function() {}; 57 | TestWidget.prototype.destroy = function() {}; 58 | var widget = new TestWidget(); 59 | var divWithWidget = h("div", [widget]); 60 | assert.strictEqual(select("div").matches(divWithWidget), true); 61 | assert.strictEqual(select("div *").matches(divWithWidget), false); 62 | assert.strictEqual(select("*").matches(widget), false); 63 | assert.deepEqual(select("div")(divWithWidget), [divWithWidget]); 64 | assert.strictEqual(select("div *")(divWithWidget), null); 65 | assert.deepEqual(select("*")(divWithWidget), [divWithWidget]); 66 | assert.strictEqual(select("*")(widget), null); 67 | 68 | // Thunk 69 | // Use widget from virtual-dom example 70 | // See https://github.com/Matt-Esch/virtual-dom/blob/master/docs/thunk.md 71 | var ExampleThunk = function () {}; 72 | ExampleThunk.prototype.type = "Thunk"; 73 | ExampleThunk.prototype.render = function() {}; 74 | var thunk = new ExampleThunk(); 75 | var divWithThunk = h("div", [thunk]); 76 | assert.strictEqual(select("div").matches(divWithThunk), true); 77 | assert.strictEqual(select("div *").matches(divWithThunk), false); 78 | assert.strictEqual(select("*").matches(thunk), false); 79 | assert.deepEqual(select("div")(divWithThunk), [divWithThunk]); 80 | assert.strictEqual(select("div *")(divWithThunk), null); 81 | assert.deepEqual(select("*")(divWithThunk), [divWithThunk]); 82 | assert.strictEqual(select("*")(thunk), null); 83 | 84 | // Comments created by vdom-virtualze 85 | // See https://github.com/parshap/vtree-select/pull/7 86 | // See https://github.com/marcelklehr/vdom-virtualize/commit/c98fb6f20489df6d84305885cbb5c9ca7383e5d5 87 | var vdomVirtualizeComment = { 88 | type: "Widget", 89 | text: "text", 90 | }; 91 | var divWithComment = h("div", [vdomVirtualizeComment]); 92 | assert.strictEqual(select("div").matches(divWithComment), true); 93 | assert.strictEqual(select("div *").matches(divWithComment), false); 94 | assert.strictEqual(select("*").matches(vdomVirtualizeComment), false); 95 | assert.deepEqual(select("div")(divWithComment), [divWithComment]); 96 | assert.strictEqual(select("div *")(divWithComment), null); 97 | assert.deepEqual(select("*")(divWithComment), [divWithComment]); 98 | assert.strictEqual(select("*")(vdomVirtualizeComment), null); 99 | 100 | // Class selector also works if class name in propreties 101 | var spanWithClassInAttributes = h("span", { attributes: { class: "cls" } }, "hello world") 102 | var treeWithClassInAttributes = h("div", [spanWithClassInAttributes]); 103 | assert.deepEqual( 104 | select(".cls")(treeWithClassInAttributes), 105 | [spanWithClassInAttributes]); 106 | 107 | 108 | console.log("TAP version 13"); 109 | console.log("ok"); 110 | --------------------------------------------------------------------------------