Closes GH-14.
Showing 5 of 9 files from the diff.
Newly tracked file
lib/walk.js
created.
lib/pseudo.js
changed.
lib/parse.js
changed.
lib/name.js
changed.
Other files ignored by Codecov
@@ -0,0 +1,290 @@
Loading
1 | + | /** |
|
2 | + | * @typedef {import('./types.js').Node} Node |
|
3 | + | * @typedef {import('./types.js').Parent} Parent |
|
4 | + | * @typedef {import('./types.js').RuleSet} RuleSet |
|
5 | + | * @typedef {import('./types.js').SelectState} SelectState |
|
6 | + | * @typedef {import('./types.js').Selectors} Selectors |
|
7 | + | * |
|
8 | + | * @typedef Nest |
|
9 | + | * Rule sets by nesting. |
|
10 | + | * @property {Array<RuleSet> | undefined} descendant |
|
11 | + | * `a b` |
|
12 | + | * @property {Array<RuleSet> | undefined} directChild |
|
13 | + | * `a > b` |
|
14 | + | * @property {Array<RuleSet> | undefined} adjacentSibling |
|
15 | + | * `a + b` |
|
16 | + | * @property {Array<RuleSet> | undefined} generalSibling |
|
17 | + | * `a ~ b` |
|
18 | + | * |
|
19 | + | * @typedef Counts |
|
20 | + | * Info on nodes in a parent. |
|
21 | + | * @property {number} count |
|
22 | + | * Number of nodes. |
|
23 | + | * @property {Map<string, number>} types |
|
24 | + | * Number of nodes by type. |
|
25 | + | */ |
|
26 | + | ||
27 | + | import {test} from './test.js' |
|
28 | + | import {parent} from './util.js' |
|
29 | + | ||
30 | + | /** @type {Array<never>} */ |
|
31 | + | const empty = [] |
|
32 | + | ||
33 | + | /** |
|
34 | + | * Turn a query into a uniform object. |
|
35 | + | * |
|
36 | + | * @param {Selectors | RuleSet | null} query |
|
37 | + | * @returns {Selectors} |
|
38 | + | */ |
|
39 | + | export function queryToSelectors(query) { |
|
40 | + | if (query === null) { |
|
41 | + | return {type: 'selectors', selectors: []} |
|
42 | + | } |
|
43 | + | ||
44 | + | if (query.type === 'ruleSet') { |
|
45 | + | return {type: 'selectors', selectors: [query]} |
|
46 | + | } |
|
47 | + | ||
48 | + | return query |
|
49 | + | } |
|
50 | + | ||
51 | + | /** |
|
52 | + | * Walk a tree. |
|
53 | + | * |
|
54 | + | * @param {SelectState} state |
|
55 | + | * @param {Node | undefined} tree |
|
56 | + | */ |
|
57 | + | export function walk(state, tree) { |
|
58 | + | if (tree) { |
|
59 | + | one(state, [], tree, undefined, undefined) |
|
60 | + | } |
|
61 | + | } |
|
62 | + | ||
63 | + | /** |
|
64 | + | * Check a node. |
|
65 | + | * |
|
66 | + | * @param {SelectState} state |
|
67 | + | * @param {Array<RuleSet>} currentRules |
|
68 | + | * @param {Node} node |
|
69 | + | * @param {number | undefined} index |
|
70 | + | * @param {Parent | undefined} parentNode |
|
71 | + | * @returns {Nest} |
|
72 | + | */ |
|
73 | + | function one(state, currentRules, node, index, parentNode) { |
|
74 | + | /** @type {Nest} */ |
|
75 | + | let nestResult = { |
|
76 | + | directChild: undefined, |
|
77 | + | descendant: undefined, |
|
78 | + | adjacentSibling: undefined, |
|
79 | + | generalSibling: undefined |
|
80 | + | } |
|
81 | + | ||
82 | + | nestResult = applySelectors( |
|
83 | + | state, |
|
84 | + | // Try the root rules for this node too. |
|
85 | + | combine(currentRules, state.rootQuery.selectors), |
|
86 | + | node, |
|
87 | + | index, |
|
88 | + | parentNode |
|
89 | + | ) |
|
90 | + | ||
91 | + | // If this is a parent, and we want to delve into them, and we haven’t found |
|
92 | + | // our single result yet. |
|
93 | + | if (parent(node) && !state.shallow && !(state.one && state.found)) { |
|
94 | + | all(state, nestResult, node) |
|
95 | + | } |
|
96 | + | ||
97 | + | return nestResult |
|
98 | + | } |
|
99 | + | ||
100 | + | /** |
|
101 | + | * Check a node. |
|
102 | + | * |
|
103 | + | * @param {SelectState} state |
|
104 | + | * @param {Nest} nest |
|
105 | + | * @param {Parent} node |
|
106 | + | * @returns {void} |
|
107 | + | */ |
|
108 | + | function all(state, nest, node) { |
|
109 | + | const fromParent = combine(nest.descendant, nest.directChild) |
|
110 | + | /** @type {Array<RuleSet> | undefined} */ |
|
111 | + | let fromSibling |
|
112 | + | let index = -1 |
|
113 | + | /** |
|
114 | + | * Total counts. |
|
115 | + | * @type {Counts} |
|
116 | + | */ |
|
117 | + | const total = {count: 0, types: new Map()} |
|
118 | + | /** |
|
119 | + | * Counts of previous siblings. |
|
120 | + | * @type {Counts} |
|
121 | + | */ |
|
122 | + | const before = {count: 0, types: new Map()} |
|
123 | + | ||
124 | + | while (++index < node.children.length) { |
|
125 | + | count(total, node.children[index]) |
|
126 | + | } |
|
127 | + | ||
128 | + | index = -1 |
|
129 | + | ||
130 | + | while (++index < node.children.length) { |
|
131 | + | const child = node.children[index] |
|
132 | + | // Uppercase to prevent prototype polution, injecting `constructor` or so. |
|
133 | + | const name = child.type.toUpperCase() |
|
134 | + | // Before counting further nodes: |
|
135 | + | state.nodeIndex = before.count |
|
136 | + | state.typeIndex = before.types.get(name) || 0 |
|
137 | + | // After counting all nodes. |
|
138 | + | state.nodeCount = total.count |
|
139 | + | state.typeCount = total.types.get(name) |
|
140 | + | ||
141 | + | // Only apply if this is a parent. |
|
142 | + | const forSibling = combine(fromParent, fromSibling) |
|
143 | + | const nest = one(state, forSibling, node.children[index], index, node) |
|
144 | + | fromSibling = combine(nest.generalSibling, nest.adjacentSibling) |
|
145 | + | ||
146 | + | // We found one thing, and one is enough. |
|
147 | + | if (state.one && state.found) { |
|
148 | + | break |
|
149 | + | } |
|
150 | + | ||
151 | + | count(before, node.children[index]) |
|
152 | + | } |
|
153 | + | } |
|
154 | + | ||
155 | + | /** |
|
156 | + | * Apply selectors to a node. |
|
157 | + | * |
|
158 | + | * @param {SelectState} state |
|
159 | + | * Current state. |
|
160 | + | * @param {Array<RuleSet>} rules |
|
161 | + | * Rules to apply. |
|
162 | + | * @param {Node} node |
|
163 | + | * Node to apply rules to. |
|
164 | + | * @param {number | undefined} index |
|
165 | + | * Index of node in parent. |
|
166 | + | * @param {Parent | undefined} parent |
|
167 | + | * Parent of node. |
|
168 | + | * @returns {Nest} |
|
169 | + | * Further rules. |
|
170 | + | */ |
|
171 | + | function applySelectors(state, rules, node, index, parent) { |
|
172 | + | /** @type {Nest} */ |
|
173 | + | const nestResult = { |
|
174 | + | directChild: undefined, |
|
175 | + | descendant: undefined, |
|
176 | + | adjacentSibling: undefined, |
|
177 | + | generalSibling: undefined |
|
178 | + | } |
|
179 | + | let selectorIndex = -1 |
|
180 | + | ||
181 | + | while (++selectorIndex < rules.length) { |
|
182 | + | const ruleSet = rules[selectorIndex] |
|
183 | + | ||
184 | + | // We found one thing, and one is enough. |
|
185 | + | if (state.one && state.found) { |
|
186 | + | break |
|
187 | + | } |
|
188 | + | ||
189 | + | // When shallow, we don’t allow nested rules. |
|
190 | + | // Idea: we could allow a stack of parents? |
|
191 | + | // Might get quite complex though. |
|
192 | + | if (state.shallow && ruleSet.rule.rule) { |
|
193 | + | throw new Error('Expected selector without nesting') |
|
194 | + | } |
|
195 | + | ||
196 | + | // If this rule matches: |
|
197 | + | if (test(ruleSet.rule, node, index, parent, state)) { |
|
198 | + | const nest = ruleSet.rule.rule |
|
199 | + | ||
200 | + | // Are there more? |
|
201 | + | if (nest) { |
|
202 | + | /** @type {RuleSet} */ |
|
203 | + | const rule = {type: 'ruleSet', rule: nest} |
|
204 | + | /** @type {keyof Nest} */ |
|
205 | + | const label = |
|
206 | + | nest.nestingOperator === '+' |
|
207 | + | ? 'adjacentSibling' |
|
208 | + | : nest.nestingOperator === '~' |
|
209 | + | ? 'generalSibling' |
|
210 | + | : nest.nestingOperator === '>' |
|
211 | + | ? 'directChild' |
|
212 | + | : 'descendant' |
|
213 | + | add(nestResult, label, rule) |
|
214 | + | } else { |
|
215 | + | // We have a match! |
|
216 | + | state.found = true |
|
217 | + | ||
218 | + | if (!state.results.includes(node)) { |
|
219 | + | state.results.push(node) |
|
220 | + | } |
|
221 | + | } |
|
222 | + | } |
|
223 | + | ||
224 | + | // Descendant. |
|
225 | + | if (ruleSet.rule.nestingOperator === null) { |
|
226 | + | add(nestResult, 'descendant', ruleSet) |
|
227 | + | } |
|
228 | + | // Adjacent. |
|
229 | + | else if (ruleSet.rule.nestingOperator === '~') { |
|
230 | + | add(nestResult, 'generalSibling', ruleSet) |
|
231 | + | } |
|
232 | + | // Drop top-level nesting (`undefined`), direct child (`>`), adjacent sibling (`+`). |
|
233 | + | } |
|
234 | + | ||
235 | + | return nestResult |
|
236 | + | } |
|
237 | + | ||
238 | + | /** |
|
239 | + | * Combine two lists, if needed. |
|
240 | + | * |
|
241 | + | * This is optimized to create as few lists as possible. |
|
242 | + | * |
|
243 | + | * @param {Array<RuleSet> | undefined} left |
|
244 | + | * @param {Array<RuleSet> | undefined} right |
|
245 | + | * @returns {Array<RuleSet>} |
|
246 | + | */ |
|
247 | + | function combine(left, right) { |
|
248 | + | return left && right && left.length > 0 && right.length > 0 |
|
249 | + | ? [...left, ...right] |
|
250 | + | : left && left.length > 0 |
|
251 | + | ? left |
|
252 | + | : right && right.length > 0 |
|
253 | + | ? right |
|
254 | + | : empty |
|
255 | + | } |
|
256 | + | ||
257 | + | /** |
|
258 | + | * Add a rule to a nesting map. |
|
259 | + | * |
|
260 | + | * @param {Nest} nest |
|
261 | + | * @param {keyof Nest} field |
|
262 | + | * @param {RuleSet} rule |
|
263 | + | */ |
|
264 | + | function add(nest, field, rule) { |
|
265 | + | const list = nest[field] |
|
266 | + | if (list) { |
|
267 | + | list.push(rule) |
|
268 | + | } else { |
|
269 | + | nest[field] = [rule] |
|
270 | + | } |
|
271 | + | } |
|
272 | + | ||
273 | + | /** |
|
274 | + | * Count a node. |
|
275 | + | * |
|
276 | + | * @param {Counts} counts |
|
277 | + | * Counts. |
|
278 | + | * @param {Node} node |
|
279 | + | * Node. |
|
280 | + | * @returns {void} |
|
281 | + | * Nothing. |
|
282 | + | */ |
|
283 | + | function count(counts, node) { |
|
284 | + | // Uppercase to prevent prototype polution, injecting `constructor` or so. |
|
285 | + | // Normalize because HTML is insensitive. |
|
286 | + | const name = node.type.toUpperCase() |
|
287 | + | const count = (counts.types.get(name) || 0) + 1 |
|
288 | + | counts.count++ |
|
289 | + | counts.types.set(name, count) |
|
290 | + | } |
@@ -10,6 +10,7 @@
Loading
10 | 10 | import fauxEsmNthCheck from 'nth-check' |
|
11 | 11 | import {zwitch} from 'zwitch' |
|
12 | 12 | import {parent} from './util.js' |
|
13 | + | import {queryToSelectors, walk} from './walk.js' |
|
13 | 14 | ||
14 | 15 | /** @type {import('nth-check').default} */ |
|
15 | 16 | // @ts-expect-error |
@@ -25,7 +26,7 @@
Loading
25 | 26 | empty, |
|
26 | 27 | 'first-child': firstChild, |
|
27 | 28 | 'first-of-type': firstOfType, |
|
28 | - | has: hasSelector, |
|
29 | + | has, |
|
29 | 30 | 'last-child': lastChild, |
|
30 | 31 | 'last-of-type': lastOfType, |
|
31 | 32 | matches, |
@@ -119,6 +120,33 @@
Loading
119 | 120 | return state.typeIndex === 0 |
|
120 | 121 | } |
|
121 | 122 | ||
123 | + | /** |
|
124 | + | * @param {RulePseudoSelector} query |
|
125 | + | * @param {Node} node |
|
126 | + | * @param {number | undefined} _1 |
|
127 | + | * @param {Parent | undefined} _2 |
|
128 | + | * @param {SelectState} state |
|
129 | + | * @returns {boolean} |
|
130 | + | */ |
|
131 | + | function has(query, node, _1, _2, state) { |
|
132 | + | const fragment = {type: 'root', children: parent(node) ? node.children : []} |
|
133 | + | /** @type {SelectState} */ |
|
134 | + | const childState = { |
|
135 | + | ...state, |
|
136 | + | // Do walk deep. |
|
137 | + | shallow: false, |
|
138 | + | // One result is enough. |
|
139 | + | one: true, |
|
140 | + | scopeNodes: [node], |
|
141 | + | results: [], |
|
142 | + | rootQuery: queryToSelectors(query.value) |
|
143 | + | } |
|
144 | + | ||
145 | + | walk(childState, fragment) |
|
146 | + | ||
147 | + | return childState.results.length > 0 |
|
148 | + | } |
|
149 | + | ||
122 | 150 | /** |
|
123 | 151 | * Check whether a node matches a `:last-child` pseudo. |
|
124 | 152 | * |
@@ -166,20 +194,21 @@
Loading
166 | 194 | * @returns {boolean} |
|
167 | 195 | */ |
|
168 | 196 | function matches(query, node, _1, _2, state) { |
|
169 | - | const {shallow, one, results, any} = state |
|
170 | - | ||
171 | - | state.shallow = false |
|
172 | - | state.one = true |
|
173 | - | state.results = [] |
|
174 | - | ||
175 | - | any(query.value, node, state) |
|
176 | - | const matches = state.results[0] === node |
|
197 | + | /** @type {SelectState} */ |
|
198 | + | const childState = { |
|
199 | + | ...state, |
|
200 | + | // Do walk deep. |
|
201 | + | shallow: false, |
|
202 | + | // One result is enough. |
|
203 | + | one: true, |
|
204 | + | scopeNodes: [node], |
|
205 | + | results: [], |
|
206 | + | rootQuery: queryToSelectors(query.value) |
|
207 | + | } |
|
177 | 208 | ||
178 | - | state.shallow = shallow |
|
179 | - | state.one = one |
|
180 | - | state.results = results |
|
209 | + | walk(childState, node) |
|
181 | 210 | ||
182 | - | return matches |
|
211 | + | return childState.results[0] === node |
|
183 | 212 | } |
|
184 | 213 | ||
185 | 214 | /** |
@@ -355,34 +384,6 @@
Loading
355 | 384 | } |
|
356 | 385 | } |
|
357 | 386 | ||
358 | - | /** |
|
359 | - | * @param {RulePseudoSelector} query |
|
360 | - | * @param {Node} node |
|
361 | - | * @param {number | undefined} _1 |
|
362 | - | * @param {Parent | undefined} _2 |
|
363 | - | * @param {SelectState} state |
|
364 | - | * @returns {boolean} |
|
365 | - | */ |
|
366 | - | function hasSelector(query, node, _1, _2, state) { |
|
367 | - | const fragment = {type: 'root', children: parent(node) ? node.children : []} |
|
368 | - | const {shallow, one, scopeNodes, results, any} = state |
|
369 | - | ||
370 | - | state.shallow = false |
|
371 | - | state.one = true |
|
372 | - | state.scopeNodes = [node] |
|
373 | - | state.results = [] |
|
374 | - | ||
375 | - | any(query.value, fragment, state) |
|
376 | - | const has = state.results.length > 0 |
|
377 | - | ||
378 | - | state.shallow = shallow |
|
379 | - | state.one = one |
|
380 | - | state.scopeNodes = scopeNodes |
|
381 | - | state.results = results |
|
382 | - | ||
383 | - | return has |
|
384 | - | } |
|
385 | - | ||
386 | 387 | /** |
|
387 | 388 | * @param {RulePseudo} query |
|
388 | 389 | * @returns {(value: number) => boolean} |
@@ -5,7 +5,7 @@
Loading
5 | 5 | * @typedef {Record<string, unknown> & {type: string, position?: Position | undefined}} NodeLike |
|
6 | 6 | */ |
|
7 | 7 | ||
8 | - | import {any} from './lib/any.js' |
|
8 | + | import {queryToSelectors, walk} from './lib/walk.js' |
|
9 | 9 | import {parse} from './lib/parse.js' |
|
10 | 10 | import {parent} from './lib/util.js' |
|
11 | 11 |
@@ -25,10 +25,10 @@
Loading
25 | 25 | * Whether `node` matches `selector`. |
|
26 | 26 | */ |
|
27 | 27 | export function matches(selector, node) { |
|
28 | - | const state = createState(node) |
|
28 | + | const state = createState(selector, node) |
|
29 | 29 | state.one = true |
|
30 | 30 | state.shallow = true |
|
31 | - | any(parse(selector), node || undefined, state) |
|
31 | + | walk(state, node || undefined) |
|
32 | 32 | return state.results.length > 0 |
|
33 | 33 | } |
|
34 | 34 |
@@ -48,9 +48,9 @@
Loading
48 | 48 | * This could be `tree` itself. |
|
49 | 49 | */ |
|
50 | 50 | export function select(selector, tree) { |
|
51 | - | const state = createState(tree) |
|
51 | + | const state = createState(selector, tree) |
|
52 | 52 | state.one = true |
|
53 | - | any(parse(selector), tree || undefined, state) |
|
53 | + | walk(state, tree || undefined) |
|
54 | 54 | // To do next major: return `undefined`. |
|
55 | 55 | return state.results[0] || null |
|
56 | 56 | } |
@@ -70,20 +70,23 @@
Loading
70 | 70 | * This could include `tree` itself. |
|
71 | 71 | */ |
|
72 | 72 | export function selectAll(selector, tree) { |
|
73 | - | const state = createState(tree) |
|
74 | - | any(parse(selector), tree || undefined, state) |
|
73 | + | const state = createState(selector, tree) |
|
74 | + | walk(state, tree || undefined) |
|
75 | 75 | return state.results |
|
76 | 76 | } |
|
77 | 77 | ||
78 | 78 | /** |
|
79 | + | * @param {string} selector |
|
80 | + | * Selector to parse. |
|
79 | 81 | * @param {Node | null | undefined} tree |
|
82 | + | * Tree to search. |
|
80 | 83 | * @returns {SelectState} |
|
81 | 84 | */ |
|
82 | - | function createState(tree) { |
|
85 | + | function createState(selector, tree) { |
|
83 | 86 | return { |
|
87 | + | // State of the query. |
|
88 | + | rootQuery: queryToSelectors(parse(selector)), |
|
84 | 89 | results: [], |
|
85 | - | any, |
|
86 | - | iterator: undefined, |
|
87 | 90 | scopeNodes: tree |
|
88 | 91 | ? parent(tree) && |
|
89 | 92 | // Root in nlcst. |
@@ -93,8 +96,8 @@
Loading
93 | 96 | : [], |
|
94 | 97 | one: false, |
|
95 | 98 | shallow: false, |
|
96 | - | index: false, |
|
97 | 99 | found: false, |
|
100 | + | // State in the tree. |
|
98 | 101 | typeIndex: undefined, |
|
99 | 102 | nodeIndex: undefined, |
|
100 | 103 | typeCount: undefined, |
3811951601
3811951601
Sunburst
The inner-most circle is the entire project, moving away from the center are folders then, finally, a single file.
The size and color of each slice is representing the number of statements and the coverage, respectively.
Icicle
The top section represents the entire project. Proceeding with folders and finally individual files.
The size and color of each slice is representing the number of statements and the coverage, respectively.