syntax-tree / unist-util-select
Showing 5 of 9 files from the diff.

@@ -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,

@@ -13,7 +13,7 @@
Loading
13 13
14 14
/**
15 15
 * @param {string} selector
16 -
 * @returns {Selectors | RuleSet | undefined}
16 +
 * @returns {Selectors | RuleSet | null}
17 17
 */
18 18
export function parse(selector) {
19 19
  if (typeof selector !== 'string') {

@@ -4,7 +4,7 @@
Loading
4 4
 */
5 5
6 6
/**
7 -
 * Check whether an element has a type.
7 +
 * Check whether a node has a type.
8 8
 *
9 9
 * @param {Rule} query
10 10
 * @param {Node} node
Files Coverage
lib 100.00%
index.js 100.00%
Project Totals (8 files) 100.00%
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.
Grid
Each block represents a single file in the project. The size and color of each block is represented by the number of statements and the coverage, respectively.
Loading