Skip to content

Commit 6e3d9f8

Browse files
committed
split AST walkers from collectors from selector processing
1 parent afa3458 commit 6e3d9f8

File tree

6 files changed

+198
-132
lines changed

6 files changed

+198
-132
lines changed

lib/ast-walkers.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
'use strict';
2+
3+
var walkers = exports;
4+
5+
6+
walkers.topScan = function (node, nodeIndex, parent, iterator) {
7+
iterator(node, nodeIndex, parent);
8+
walkers.descendant.apply(this, arguments);
9+
};
10+
11+
12+
walkers.descendant = function (node, nodeIndex, parent, iterator) {
13+
if (node.children) {
14+
node.children.forEach(function (child, childIndex) {
15+
iterator(child, childIndex, node);
16+
walkers.descendant(child, childIndex, node, iterator);
17+
});
18+
}
19+
};
20+
21+
22+
walkers.child = function (node, nodeIndex, parent, iterator) {
23+
if (node.children) {
24+
node.children.forEach(function (child, childIndex) {
25+
iterator(child, childIndex, node);
26+
});
27+
}
28+
};
29+
30+
31+
walkers.adjacentSibling = function (node, nodeIndex, parent, iterator) {
32+
if (parent && ++nodeIndex < parent.children.length) {
33+
iterator(parent.children[nodeIndex], nodeIndex, parent);
34+
}
35+
};
36+
37+
38+
walkers.generalSibling = function (node, nodeIndex, parent, iterator) {
39+
if (parent) {
40+
while (++nodeIndex < parent.children.length) {
41+
iterator(parent.children[nodeIndex], nodeIndex, parent);
42+
}
43+
}
44+
};

lib/collector.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
'use strict';
2+
3+
4+
// @example
5+
// var collect = Collector();
6+
// collect('foo');
7+
// collect(['foo', 'bar', 'baz']);
8+
// collect.result
9+
// //=> ['foo', 'bar', 'baz']
10+
//
11+
module.exports = function Collector () {
12+
var result = [];
13+
14+
// Append elements to array, filtering out duplicates.
15+
function collect (source) {
16+
if (Array.isArray(source)) {
17+
source.forEach(collectOne);
18+
}
19+
else {
20+
collectOne(source);
21+
}
22+
23+
function collectOne (element) {
24+
if (result.indexOf(element) < 0) {
25+
result.push(element);
26+
}
27+
}
28+
}
29+
30+
collect.result = result;
31+
return collect;
32+
};

lib/match-node.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
'use strict';
2+
3+
module.exports = matchNode;
4+
5+
6+
// Match node against a simple selector.
7+
function matchNode (rule, node, parent) {
8+
return matchType(rule, node) &&
9+
matchAttrs(rule, node) &&
10+
matchPseudos(rule, node, parent);
11+
}
12+
13+
14+
function matchType (rule, node) {
15+
return !rule.tagName || rule.tagName == '*' || rule.tagName == node.type;
16+
}
17+
18+
19+
function matchAttrs (rule, node) {
20+
return !rule.attrs || rule.attrs.every(function (attr) {
21+
switch (attr.operator) {
22+
case undefined:
23+
return attr.name in node;
24+
25+
case '=':
26+
// First, check for special values.
27+
switch (attr.value) {
28+
case 'null':
29+
if (attr.name in node && node[attr.name] == null) return true;
30+
break;
31+
32+
case 'true':
33+
if (node[attr.name] === true) return true;
34+
break;
35+
36+
case 'false':
37+
if (node[attr.name] === false) return true;
38+
break;
39+
}
40+
return node[attr.name] == attr.value;
41+
42+
case '^=':
43+
return typeof node[attr.name] == 'string' &&
44+
node[attr.name].slice(0, attr.value.length) == attr.value;
45+
46+
case '*=':
47+
return typeof node[attr.name] == 'string' &&
48+
node[attr.name].indexOf(attr.value) >= 0;
49+
50+
case '$=':
51+
return typeof node[attr.name] == 'string' &&
52+
node[attr.name].slice(-attr.value.length) == attr.value;
53+
54+
default:
55+
throw Error('Undefined attribute operator: ' + attr.operator);
56+
}
57+
});
58+
}
59+
60+
61+
function matchPseudos (rule, node, parent) {
62+
return !rule.pseudos || rule.pseudos.every(function (pseudo) {
63+
switch (pseudo.name) {
64+
case 'root':
65+
return parent == null;
66+
67+
case 'not':
68+
return !matchNode(pseudo.value.rule, node, parent);
69+
70+
default:
71+
throw Error('Undefined pseudo-class: ' + pseudo.name);
72+
}
73+
});
74+
}

lib/select.js

Lines changed: 33 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,151 +1,53 @@
11
'use strict';
22

3+
var walkers = require('./ast-walkers'),
4+
matchNode = require('./match-node'),
5+
Collector = require('./collector');
6+
37
var select = exports;
48

59

6-
select.selectors = function (selector, ast) {
7-
var result = [];
8-
selector.selectors.forEach(function (selector) {
9-
append(result, select.ruleSet(selector, ast));
10+
select.selectors = function (selectors, ast) {
11+
var collect = Collector();
12+
selectors.selectors.forEach(function (ruleSet) {
13+
collect(select.ruleSet(ruleSet, ast));
1014
});
11-
return result;
15+
return collect.result;
1216
};
1317

1418

15-
select.ruleSet = function (selector, ast) {
16-
return select.rule(selector.rule, ast);
19+
select.ruleSet = function (ruleSet, ast) {
20+
return select.rule(ruleSet.rule, ast);
1721
};
1822

1923

20-
select.rule = function (selector, ast, parentNode) {
21-
var result = [];
22-
23-
switch (selector.nestingOperator) {
24-
case null:
25-
case undefined:
26-
case '>':
27-
walk(ast, parentNode);
28-
break;
29-
30-
case '+':
31-
if (ast.children && ast.children.length) {
32-
walk(ast.children[0], ast);
33-
}
34-
break;
35-
36-
case '~':
37-
(ast.children || []).forEach(function (node) {
38-
walk(node, ast);
39-
});
40-
break;
41-
42-
default:
43-
throw Error('Undefined nesting operator: ' + selector.nestingOperator);
24+
select.rule = function (rule, ast) {
25+
var collect = Collector();
26+
search(rule, ast, 0, null);
27+
return collect.result;
28+
29+
function search (rule, node, nodeIndex, parent) {
30+
({
31+
// `undefined` is the operator on the top rule selector.
32+
undefined: walkers.topScan,
33+
// `null` stands for the descendant combinator.
34+
null: walkers.descendant,
35+
'>': walkers.child,
36+
'+': walkers.adjacentSibling,
37+
'~': walkers.generalSibling
38+
})[rule.nestingOperator](
39+
node, nodeIndex, parent, match.bind(null, rule)
40+
);
4441
}
4542

46-
return result;
47-
48-
function walk (node, parent) {
49-
if (matches(selector, node, parent == null)) {
50-
if (!selector.rule) {
51-
append(result, [node]);
52-
}
53-
else if (!selector.rule.nestingOperator ||
54-
selector.rule.nestingOperator == '>') {
55-
if (node.children) {
56-
node.children.forEach(function (childNode) {
57-
append(result, select.rule(selector.rule, childNode, node));
58-
});
59-
}
43+
function match (rule, node, nodeIndex, parent) {
44+
if (matchNode(rule, node, parent)) {
45+
if (rule.rule) {
46+
search(rule.rule, node, nodeIndex, parent);
6047
}
6148
else {
62-
if (parent) {
63-
append(result, select.rule(selector.rule, {
64-
children: parent.children.slice(parent.children.indexOf(node) + 1)
65-
}, parent));
66-
}
49+
collect(node);
6750
}
6851
}
69-
70-
if (!selector.nestingOperator && node.children) {
71-
node.children.forEach(function (child) {
72-
walk(child, node);
73-
});
74-
}
7552
}
7653
};
77-
78-
79-
// True if node matches head of selector rule.
80-
function matches (rule, node, isRoot) {
81-
var match = true;
82-
83-
// Match type.
84-
match = match && (!rule.tagName || rule.tagName == '*' ||
85-
rule.tagName == node.type);
86-
87-
// Match attributes.
88-
match = match && (rule.attrs || []).every(function (attr) {
89-
switch (attr.operator) {
90-
case undefined:
91-
return attr.name in node;
92-
93-
case '=':
94-
// First, check for special values.
95-
switch (attr.value) {
96-
case 'null':
97-
if (attr.name in node && node[attr.name] == null) return true;
98-
break;
99-
100-
case 'true':
101-
if (node[attr.name] === true) return true;
102-
break;
103-
104-
case 'false':
105-
if (node[attr.name] === false) return true;
106-
break;
107-
}
108-
return node[attr.name] == attr.value;
109-
110-
case '^=':
111-
return typeof node[attr.name] == 'string' &&
112-
node[attr.name].slice(0, attr.value.length) == attr.value;
113-
114-
case '*=':
115-
return typeof node[attr.name] == 'string' &&
116-
node[attr.name].indexOf(attr.value) >= 0;
117-
118-
case '$=':
119-
return typeof node[attr.name] == 'string' &&
120-
node[attr.name].slice(-attr.value.length) == attr.value;
121-
122-
default:
123-
throw Error('Undefined attribute operator: ' + attr.operator);
124-
}
125-
});
126-
127-
// Match pseudo classes.
128-
match = match && (rule.pseudos || []).every(function (pseudo) {
129-
switch (pseudo.name) {
130-
case 'root':
131-
return isRoot;
132-
133-
case 'not':
134-
return !matches(pseudo.value.rule, node);
135-
136-
default:
137-
throw Error('Undefined pseudo-class: ' + pseudo.name);
138-
}
139-
});
140-
141-
return match;
142-
}
143-
144-
145-
function append (array, elements) {
146-
elements.forEach(function (el) {
147-
if (array.indexOf(el) < 0) {
148-
array.push(el);
149-
}
150-
});
151-
}

test/collector.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict';
2+
3+
var Collector = require('../lib/collector');
4+
5+
var test = require('tape');
6+
7+
8+
test('collector', function (t) {
9+
var collect = Collector();
10+
collect('foo');
11+
collect(['foo', 'bar', 'baz', 'bar']);
12+
collect('foo');
13+
t.deepEqual(collect.result, ['foo', 'bar', 'baz']);
14+
t.end();
15+
});

test/test.js renamed to test/select.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ test('edge cases', function (t) {
1515

1616

1717
test('type selector', function (t) {
18-
t.equal(select(ast, 'root').length, 1);
1918
t.deepEqual(select(ast, 'root'), [ast]);
2019
t.equal(select(ast, 'text').length, 39);
2120
t.equal(select(ast, 'text')[1], ast.children[1].children[0]);

0 commit comments

Comments
 (0)