Skip to content

Commit 2d128bf

Browse files
committed
add :nth-last-of-type, :last-of-type, :only-of-type
Close #3.
1 parent 9513fc7 commit 2d128bf

File tree

6 files changed

+183
-56
lines changed

6 files changed

+183
-56
lines changed

lib/ast-walkers.js

Lines changed: 89 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ var walkers = exports;
1515
// if true, `props` will have an integer `typeIndex` field which
1616
// represents a node index among all its sibling of the same type
1717
//
18+
// - [typeCount]: boolean=false
19+
// if true, `props` will have an integer `typeCount` field which
20+
// is equal to number of siblings sharing the same type with this node
21+
//
1822

1923

2024
walkers.topScan = function (node, nodeIndex, parent, opts) {
@@ -24,27 +28,22 @@ walkers.topScan = function (node, nodeIndex, parent, opts) {
2428
throw Error('topScan is supposed to be called from the root node');
2529
}
2630

27-
if (!opts.typeIndex) {
31+
if (!opts.typeIndex && !opts.typeCount) {
2832
opts.iterator(node, nodeIndex, parent);
2933
}
3034
walkers.descendant.apply(this, arguments);
3135
};
3236

3337

3438
walkers.descendant = function (node, nodeIndex, parent, opts) {
35-
if (!node.children || !node.children.length) {
36-
return;
37-
}
39+
var iterator = opts.iterator;
3840

39-
if (opts.typeIndex) {
40-
var typeIndex = TypeIndex();
41-
}
41+
opts.iterator = function (node, nodeIndex, parent) {
42+
iterator.apply(this, arguments);
43+
walkers.child(node, nodeIndex, node, opts);
44+
};
4245

43-
node.children.forEach(function (child, childIndex) {
44-
opts.iterator(child, childIndex, node,
45-
opts.typeIndex ? { typeIndex: typeIndex(child) } : undefined);
46-
walkers.descendant(child, childIndex, node, opts);
47-
});
46+
return walkers.child(node, nodeIndex, parent, opts);
4847
};
4948

5049

@@ -53,14 +52,9 @@ walkers.child = function (node, nodeIndex, parent, opts) {
5352
return;
5453
}
5554

56-
if (opts.typeIndex) {
57-
var typeIndex = TypeIndex();
58-
}
59-
60-
node.children.forEach(function (child, childIndex) {
61-
opts.iterator(child, childIndex, node,
62-
opts.typeIndex ? { typeIndex: typeIndex(child) } : undefined);
63-
});
55+
walkIterator(node, opts)
56+
.each()
57+
.finally();
6458
};
6559

6660

@@ -69,20 +63,11 @@ walkers.adjacentSibling = function (node, nodeIndex, parent, opts) {
6963
return;
7064
}
7165

72-
if (opts.typeIndex) {
73-
var typeIndex = TypeIndex();
74-
75-
// Prefill type indexes with preceding nodes.
76-
for (var prevIndex = 0; prevIndex <= nodeIndex; ++prevIndex) {
77-
typeIndex(parent.children[prevIndex]);
78-
}
79-
}
80-
81-
if (++nodeIndex < parent.children.length) {
82-
node = parent.children[nodeIndex];
83-
opts.iterator(node, nodeIndex, parent,
84-
opts.typeIndex ? { typeIndex: typeIndex(node) } : undefined);
85-
}
66+
walkIterator(parent, opts)
67+
.prefillTypeIndex(0, ++nodeIndex)
68+
.each(nodeIndex, ++nodeIndex)
69+
.prefillTypeIndex(nodeIndex)
70+
.finally();
8671
};
8772

8873

@@ -91,18 +76,75 @@ walkers.generalSibling = function (node, nodeIndex, parent, opts) {
9176
return;
9277
}
9378

94-
if (opts.typeIndex) {
95-
var typeIndex = TypeIndex();
79+
walkIterator(parent, opts)
80+
.prefillTypeIndex(0, ++nodeIndex)
81+
.each(nodeIndex)
82+
.finally();
83+
};
9684

97-
// Prefill type indexes with preceding nodes.
98-
for (var prevIndex = 0; prevIndex <= nodeIndex; ++prevIndex) {
99-
typeIndex(parent.children[prevIndex]);
100-
}
101-
}
10285

103-
while (++nodeIndex < parent.children.length) {
104-
node = parent.children[nodeIndex];
105-
opts.iterator(node, nodeIndex, parent,
106-
opts.typeIndex ? { typeIndex: typeIndex(node) } : undefined);
107-
}
108-
};
86+
// Handles typeIndex and typeCount properties for every walker.
87+
function walkIterator (parent, opts) {
88+
var hasTypeIndex = opts.typeIndex || opts.typeCount;
89+
var typeIndex = hasTypeIndex ? TypeIndex() : Function.prototype;
90+
var nodeThunks = [];
91+
92+
var rangeDefaults = function (iter) {
93+
return function (start, end) {
94+
if (start == null || start < 0) {
95+
start = 0;
96+
}
97+
if (end == null || end > parent.children.length) {
98+
end = parent.children.length;
99+
}
100+
return iter.call(this, start, end);
101+
};
102+
};
103+
104+
return {
105+
prefillTypeIndex: rangeDefaults(function (start, end) {
106+
if (hasTypeIndex) {
107+
for (var nodeIndex = start; nodeIndex < end; ++nodeIndex) {
108+
typeIndex(parent.children[nodeIndex]);
109+
}
110+
}
111+
return this;
112+
}),
113+
114+
each: rangeDefaults(function each (start, end) {
115+
if (start >= end) {
116+
return this;
117+
}
118+
119+
var nodeIndex = start;
120+
var node = parent.children[nodeIndex];
121+
var props = {};
122+
var nodeTypeIndex = typeIndex(node);
123+
124+
if (opts.typeIndex) {
125+
props.typeIndex = nodeTypeIndex;
126+
}
127+
128+
if (opts.typeCount) {
129+
nodeThunks.push(function () {
130+
props.typeCount = typeIndex.count(node);
131+
pushNode();
132+
});
133+
}
134+
else {
135+
pushNode();
136+
}
137+
138+
return each.call(this, start + 1, end);
139+
140+
function pushNode () {
141+
opts.iterator(node, nodeIndex, parent, props);
142+
}
143+
}),
144+
145+
finally: function () {
146+
nodeThunks.forEach(Function.call.bind(Function.call));
147+
return this;
148+
}
149+
};
150+
}

lib/match-node.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ function matchPseudos (rule, node, nodeIndex, parent, props) {
7373
case 'nth-of-type':
7474
return parent && pseudo.value(props.typeIndex);
7575

76+
case 'nth-last-of-type':
77+
return parent && pseudo.value(props.typeCount - 1 - props.typeIndex);
78+
7679
case 'first-child':
7780
return parent && nodeIndex == 0;
7881

@@ -82,9 +85,15 @@ function matchPseudos (rule, node, nodeIndex, parent, props) {
8285
case 'first-of-type':
8386
return parent && props.typeIndex == 0;
8487

88+
case 'last-of-type':
89+
return parent && props.typeIndex == props.typeCount - 1;
90+
8591
case 'only-child':
8692
return parent && parent.children.length == 1;
8793

94+
case 'only-of-type':
95+
return parent && props.typeCount == 1;
96+
8897
case 'empty':
8998
return node.children && !node.children.length;
9099

lib/select.js

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,18 @@ select.rule = function (rule, ast) {
5757

5858

5959
function searchOpts (opts, rule) {
60-
if (rule.pseudos && rule.pseudos.some(function (pseudo) {
61-
return (pseudo.name == 'nth-of-type' ||
62-
pseudo.name == 'nth-last-of-type' ||
63-
pseudo.name == 'first-of-type' ||
64-
pseudo.name == 'last-of-type');
65-
})) {
66-
opts.typeIndex = true;
67-
}
60+
rule.pseudos && rule.pseudos.forEach(function (pseudo) {
61+
switch (pseudo.name) {
62+
case 'nth-last-of-type':
63+
case 'last-of-type':
64+
case 'only-of-type':
65+
opts.typeCount = true;
66+
67+
case 'nth-of-type':
68+
case 'first-of-type':
69+
opts.typeIndex = true;
70+
}
71+
});
6872

6973
return opts;
7074
}

lib/type-index.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
module.exports = function TypeIndex () {
55
var typeLists = Object.create(null);
66

7-
return function (node) {
7+
var index = function (node) {
88
var type = node.type;
99

1010
if (!typeLists[type]) {
@@ -13,4 +13,11 @@ module.exports = function TypeIndex () {
1313

1414
return typeLists[type].push(node) - 1;
1515
};
16+
17+
index.count = function (node) {
18+
var typeList = typeLists[node.type];
19+
return typeList ? typeList.length : 0;
20+
};
21+
22+
return index;
1623
};

test/select.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,31 @@ test('structural pseudo-classes', function (t) {
208208
t.end();
209209
});
210210

211+
t.test(':nth-last-of-type', function (t) {
212+
t.deepEqual(select(ast, ':root > :nth-last-of-type(n+3)'), [
213+
path(ast, [0]),
214+
path(ast, [1]),
215+
path(ast, [3]),
216+
path(ast, [5]),
217+
path(ast, [7]),
218+
path(ast, [8]),
219+
path(ast, [13])
220+
]);
221+
t.deepEqual(select(ast, ':root > :nth-last-of-type(even)'), [
222+
path(ast, [1]),
223+
path(ast, [4]),
224+
path(ast, [5]),
225+
path(ast, [11]),
226+
path(ast, [12]),
227+
path(ast, [14])
228+
]);
229+
t.deepEqual(select(ast, 'list + :nth-last-of-type(n+3)'), [
230+
path(ast, [5]),
231+
path(ast, [7])
232+
]);
233+
t.end();
234+
});
235+
211236
t.test(':first-child', function (t) {
212237
t.deepEqual(select(ast, ':first-child'), select(ast, ':nth-child(1)'));
213238
t.deepEqual(select(ast, ':root:first-child'), []);
@@ -251,6 +276,29 @@ test('structural pseudo-classes', function (t) {
251276
t.end();
252277
});
253278

279+
t.test(':last-of-type', function (t) {
280+
t.deepEqual(select(ast, ':last-of-type'),
281+
select(ast, ':nth-last-of-type(1)'));
282+
t.deepEqual(select(ast, ':root:last-of-type'), []);
283+
t.deepEqual(select(ast, ':root > :last-of-type'), [
284+
path(ast, [2]),
285+
path(ast, [6]),
286+
path(ast, [9]),
287+
path(ast, [10]),
288+
path(ast, [15]),
289+
path(ast, [16]),
290+
path(ast, [17]),
291+
path(ast, [18])
292+
]);
293+
t.deepEqual(select(ast, 'table ~ :last-of-type'), [
294+
path(ast, [15]),
295+
path(ast, [16]),
296+
path(ast, [17]),
297+
path(ast, [18])
298+
]);
299+
t.end();
300+
});
301+
254302
t.test(':only-child', function (t) {
255303
t.deepEqual(select(ast, ':only-child'),
256304
select(ast, ':first-child:last-child'));
@@ -262,6 +310,18 @@ test('structural pseudo-classes', function (t) {
262310
t.end();
263311
});
264312

313+
t.test(':only-of-type', function (t) {
314+
t.deepEqual(select(ast, ':only-of-type'),
315+
select(ast, ':first-of-type:last-of-type'));
316+
t.deepEqual(select(ast, ':root > :only-of-type'), [
317+
path(ast, [2]),
318+
path(ast, [9]),
319+
path(ast, [10]),
320+
path(ast, [18])
321+
]);
322+
t.end();
323+
});
324+
265325
t.test(':empty', function (t) {
266326
t.deepEqual(select(ast, ':root:empty'), []);
267327
t.deepEqual(select(ast, 'text:empty'), []);

test/type-index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,20 @@ var test = require('tape');
88
test('TypeIndex', function (t) {
99
var typeIndex = TypeIndex();
1010

11+
t.equal(typeIndex.count({ type: 'foo' }), 0);
1112
t.equal(typeIndex({ type: 'foo' }), 0);
13+
t.equal(typeIndex.count({ type: 'foo' }), 1);
1214
t.equal(typeIndex({ type: 'bar' }), 0);
1315
t.equal(typeIndex({ type: 'foo' }), 1);
1416
t.equal(typeIndex({ type: 'foo' }), 2);
1517
t.equal(typeIndex({ type: 'bar' }), 1);
1618
t.equal(typeIndex({ type: 'baz' }), 0);
19+
t.equal(typeIndex.count({ type: 'foo' }), 3);
1720

1821
typeIndex = TypeIndex();
22+
t.equal(typeIndex.count({ type: 'foo' }), 0);
1923
t.equal(typeIndex({ type: 'foo' }), 0);
24+
t.equal(typeIndex.count({ type: 'foo' }), 1);
2025

2126
t.end();
2227
});

0 commit comments

Comments
 (0)