Skip to content

Commit cc51df1

Browse files
committed
add :nth-of-type and :first-of-type
1 parent d0281df commit cc51df1

File tree

9 files changed

+194
-30
lines changed

9 files changed

+194
-30
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,11 @@ That's it!
6363
- [x] `:root`
6464
- [x] `:nth-child(2n+1)`
6565
- [x] `:nth-last-child(2n+1)`
66-
- [ ] `:nth-of-type(2n+1)`
66+
- [x] `:nth-of-type(2n+1)`
6767
- [ ] `:nth-last-of-type(2n+1)`
6868
- [x] `:first-child`
6969
- [x] `:last-child`
70-
- [ ] `:first-of-type`
70+
- [x] `:first-of-type`
7171
- [ ] `:last-of-type`
7272
- [x] `:only-child`
7373
- [ ] `:only-of-type`

lib/ast-walkers.js

Lines changed: 69 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,94 @@
11
'use strict';
22

3+
var TypeIndex = require('./type-index');
4+
35
var walkers = exports;
46

57

6-
walkers.topScan = function (node, nodeIndex, parent, iterator) {
8+
walkers.topScan = function (node, nodeIndex, parent, iterator, opts) {
9+
if (parent) {
10+
// We would like to avoid spinning an extra loop through the starting
11+
// node's siblings just to count its typeIndex.
12+
throw Error('topScan is supposed to be called from the root node');
13+
}
14+
715
iterator(node, nodeIndex, parent);
816
walkers.descendant.apply(this, arguments);
917
};
1018

1119

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-
});
20+
walkers.descendant = function (node, nodeIndex, parent, iterator, opts) {
21+
if (!node.children || !node.children.length) {
22+
return;
23+
}
24+
25+
if ((opts = opts || {}).typeIndex) {
26+
var typeIndex = TypeIndex();
1827
}
28+
29+
node.children.forEach(function (child, childIndex) {
30+
iterator(child, childIndex, node,
31+
opts.typeIndex ? { typeIndex: typeIndex(child) } : undefined);
32+
walkers.descendant(child, childIndex, node, iterator, opts);
33+
});
1934
};
2035

2136

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-
});
37+
walkers.child = function (node, nodeIndex, parent, iterator, opts) {
38+
if (!node.children || !node.children.length) {
39+
return;
40+
}
41+
42+
if ((opts = opts || {}).typeIndex) {
43+
var typeIndex = TypeIndex();
2744
}
45+
46+
node.children.forEach(function (child, childIndex) {
47+
iterator(child, childIndex, node,
48+
opts.typeIndex ? { typeIndex: typeIndex(child) } : undefined);
49+
});
2850
};
2951

3052

31-
walkers.adjacentSibling = function (node, nodeIndex, parent, iterator) {
32-
if (parent && ++nodeIndex < parent.children.length) {
33-
iterator(parent.children[nodeIndex], nodeIndex, parent);
53+
walkers.adjacentSibling = function (node, nodeIndex, parent, iterator, opts) {
54+
if (!parent) {
55+
return;
56+
}
57+
58+
if ((opts = opts || {}).typeIndex) {
59+
var typeIndex = TypeIndex();
60+
61+
// Prefill type indexes with preceding nodes.
62+
for (var prevIndex = 0; prevIndex <= nodeIndex; ++prevIndex) {
63+
typeIndex(parent.children[prevIndex]);
64+
}
65+
}
66+
67+
if (++nodeIndex < parent.children.length) {
68+
node = parent.children[nodeIndex];
69+
iterator(node, nodeIndex, parent,
70+
opts.typeIndex ? { typeIndex: typeIndex(node) } : undefined);
3471
}
3572
};
3673

3774

38-
walkers.generalSibling = function (node, nodeIndex, parent, iterator) {
39-
if (parent) {
40-
while (++nodeIndex < parent.children.length) {
41-
iterator(parent.children[nodeIndex], nodeIndex, parent);
75+
walkers.generalSibling = function (node, nodeIndex, parent, iterator, opts) {
76+
if (!parent) {
77+
return;
78+
}
79+
80+
if ((opts = opts || {}).typeIndex) {
81+
var typeIndex = TypeIndex();
82+
83+
// Prefill type indexes with preceding nodes.
84+
for (var prevIndex = 0; prevIndex <= nodeIndex; ++prevIndex) {
85+
typeIndex(parent.children[prevIndex]);
4286
}
4387
}
88+
89+
while (++nodeIndex < parent.children.length) {
90+
node = parent.children[nodeIndex];
91+
iterator(node, nodeIndex, parent,
92+
opts.typeIndex ? { typeIndex: typeIndex(node) } : undefined);
93+
}
4494
};

lib/match-node.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ module.exports = matchNode;
44

55

66
// Match node against a simple selector.
7-
function matchNode (rule, node, nodeIndex, parent) {
7+
function matchNode (rule, node, nodeIndex, parent, props) {
88
return matchType(rule, node) &&
99
matchAttrs(rule, node) &&
10-
matchPseudos(rule, node, nodeIndex, parent);
10+
matchPseudos(rule, node, nodeIndex, parent, props);
1111
}
1212

1313

@@ -58,7 +58,7 @@ function matchAttrs (rule, node) {
5858
}
5959

6060

61-
function matchPseudos (rule, node, nodeIndex, parent) {
61+
function matchPseudos (rule, node, nodeIndex, parent, props) {
6262
return !rule.pseudos || rule.pseudos.every(function (pseudo) {
6363
switch (pseudo.name) {
6464
case 'root':
@@ -70,20 +70,26 @@ function matchPseudos (rule, node, nodeIndex, parent) {
7070
case 'nth-last-child':
7171
return parent && pseudo.value(parent.children.length - 1 - nodeIndex);
7272

73+
case 'nth-of-type':
74+
return parent && pseudo.value(props.typeIndex);
75+
7376
case 'first-child':
7477
return parent && nodeIndex == 0;
7578

7679
case 'last-child':
7780
return parent && nodeIndex == parent.children.length - 1;
7881

82+
case 'first-of-type':
83+
return parent && props.typeIndex == 0;
84+
7985
case 'only-child':
8086
return parent && parent.children.length == 1;
8187

8288
case 'empty':
8389
return node.children && !node.children.length;
8490

8591
case 'not':
86-
return !matchNode(pseudo.value.rule, node, nodeIndex, parent);
92+
return !matchNode(pseudo.value.rule, node, nodeIndex, parent, props);
8793

8894
default:
8995
throw Error('Undefined pseudo-class: ' + pseudo.name);

lib/select.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@ select.rule = function (rule, ast) {
3636
'+': walkers.adjacentSibling,
3737
'~': walkers.generalSibling
3838
})[rule.nestingOperator](
39-
node, nodeIndex, parent, match.bind(null, rule)
39+
node, nodeIndex, parent, match.bind(null, rule), searchOpts(rule)
4040
);
4141
}
4242

43-
function match (rule, node, nodeIndex, parent) {
44-
if (matchNode(rule, node, nodeIndex, parent)) {
43+
function match (rule, node, nodeIndex, parent, props) {
44+
if (matchNode(rule, node, nodeIndex, parent, props)) {
4545
if (rule.rule) {
4646
search(rule.rule, node, nodeIndex, parent);
4747
}
@@ -51,3 +51,17 @@ select.rule = function (rule, ast) {
5151
}
5252
}
5353
};
54+
55+
56+
function searchOpts (rule) {
57+
var needsTypeIndex = rule.pseudos && rule.pseudos.some(function (pseudo) {
58+
return pseudo.name == 'nth-of-type' ||
59+
pseudo.name == 'nth-last-of-type' ||
60+
pseudo.name == 'first-of-type' ||
61+
pseudo.name == 'last-of-type';
62+
});
63+
64+
if (needsTypeIndex) {
65+
return { typeIndex: true };
66+
}
67+
}

lib/selector.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ function compileNthChecks (ast) {
3030
case 'rule':
3131
if (ast.pseudos) {
3232
ast.pseudos.forEach(function (pseudo) {
33-
if (pseudo.name == 'nth-child' || pseudo.name == 'nth-last-child') {
33+
if (pseudo.name == 'nth-child' ||
34+
pseudo.name == 'nth-last-child' ||
35+
pseudo.name == 'nth-of-type' ||
36+
pseudo.name == 'nth-last-of-type') {
3437
pseudo.value = nthCheck(pseudo.value);
3538
pseudo.valueType = 'function';
3639
}

lib/type-index.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use strict';
2+
3+
4+
module.exports = function TypeIndex () {
5+
var typeLists = Object.create(null);
6+
7+
return function (node) {
8+
var type = node.type;
9+
10+
if (!typeLists[type]) {
11+
typeLists[type] = [];
12+
}
13+
14+
return typeLists[type].push(node) - 1;
15+
};
16+
};

test/collector.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ var Collector = require('../lib/collector');
55
var test = require('tape');
66

77

8-
test('collector', function (t) {
8+
test('Collector', function (t) {
99
var collect = Collector();
1010
collect('foo');
1111
collect(['foo', 'bar', 'baz', 'bar']);

test/select.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,38 @@ test('structural pseudo-classes', function (t) {
176176
t.end();
177177
});
178178

179+
t.test(':nth-of-type', function (t) {
180+
t.deepEqual(select(ast, ':root > :nth-of-type(-2n+4)'), [
181+
path(ast, [1]),
182+
path(ast, [5]),
183+
path(ast, [6]),
184+
path(ast, [11]),
185+
path(ast, [12]),
186+
path(ast, [14])
187+
]);
188+
t.deepEqual(select(ast, ':root > :nth-of-type(odd)'), [
189+
path(ast, [0]),
190+
path(ast, [2]),
191+
path(ast, [3]),
192+
path(ast, [4]),
193+
path(ast, [7]),
194+
path(ast, [8]),
195+
path(ast, [9]),
196+
path(ast, [10]),
197+
path(ast, [13]),
198+
path(ast, [15]),
199+
path(ast, [16]),
200+
path(ast, [17]),
201+
path(ast, [18])
202+
]);
203+
t.deepEqual(select(ast, 'list ~ :nth-of-type(2)'), [
204+
path(ast, [5]),
205+
path(ast, [6]),
206+
path(ast, [14])
207+
]);
208+
t.end();
209+
});
210+
179211
t.test(':first-child', function (t) {
180212
t.deepEqual(select(ast, ':first-child'), select(ast, ':nth-child(1)'));
181213
t.deepEqual(select(ast, ':root:first-child'), []);
@@ -198,6 +230,27 @@ test('structural pseudo-classes', function (t) {
198230
t.end();
199231
});
200232

233+
t.test(':first-of-type', function (t) {
234+
t.deepEqual(select(ast, ':first-of-type'), select(ast, ':nth-of-type(1)'));
235+
t.deepEqual(select(ast, ':root:first-of-type'), []);
236+
t.deepEqual(select(ast, ':root > :first-of-type'), [
237+
path(ast, [0]),
238+
path(ast, [2]),
239+
path(ast, [3]),
240+
path(ast, [4]),
241+
path(ast, [9]),
242+
path(ast, [10]),
243+
path(ast, [13]),
244+
path(ast, [18])
245+
]);
246+
t.deepEqual(select(ast, 'code ~ :first-of-type'), [
247+
path(ast, [10]),
248+
path(ast, [13]),
249+
path(ast, [18])
250+
]);
251+
t.end();
252+
});
253+
201254
t.test(':only-child', function (t) {
202255
t.deepEqual(select(ast, ':only-child'),
203256
select(ast, ':first-child:last-child'));

test/type-index.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
'use strict';
2+
3+
var TypeIndex = require('../lib/type-index');
4+
5+
var test = require('tape');
6+
7+
8+
test('TypeIndex', function (t) {
9+
var typeIndex = TypeIndex();
10+
11+
t.equal(typeIndex({ type: 'foo' }), 0);
12+
t.equal(typeIndex({ type: 'bar' }), 0);
13+
t.equal(typeIndex({ type: 'foo' }), 1);
14+
t.equal(typeIndex({ type: 'foo' }), 2);
15+
t.equal(typeIndex({ type: 'bar' }), 1);
16+
t.equal(typeIndex({ type: 'baz' }), 0);
17+
18+
typeIndex = TypeIndex();
19+
t.equal(typeIndex({ type: 'foo' }), 0);
20+
21+
t.end();
22+
});

0 commit comments

Comments
 (0)