Files
website/node_modules/hast-util-select/lib/walk.js
2024-05-06 17:15:30 -04:00

330 lines
7.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @typedef {import('css-selector-parser').AstRule} AstRule
*
* @typedef {import('hast').Element} Element
* @typedef {import('hast').Nodes} Nodes
* @typedef {import('hast').Parents} Parents
*
* @typedef {import('./index.js').State} State
*/
/**
* @typedef Counts
* Info on elements in a parent.
* @property {number} count
* Number of elements.
* @property {Map<string, number>} types
* Number of elements by tag name.
*
* @typedef Nest
* Rule sets by nesting.
* @property {Array<AstRule> | undefined} adjacentSibling
* `a + b`
* @property {Array<AstRule> | undefined} descendant
* `a b`
* @property {Array<AstRule> | undefined} directChild
* `a > b`
* @property {Array<AstRule> | undefined} generalSibling
* `a ~ b`
*/
import {enterState} from './enter-state.js'
import {test} from './test.js'
/** @type {Array<never>} */
const empty = []
/**
* Walk a tree.
*
* @param {State} state
* State.
* @param {Nodes | undefined} tree
* Tree.
*/
export function walk(state, tree) {
if (tree) {
one(state, [], tree, undefined, undefined, tree)
}
}
/**
* Add a rule to a nesting map.
*
* @param {Nest} nest
* Nesting.
* @param {keyof Nest} field
* Field.
* @param {AstRule} rule
* Rule.
*/
function add(nest, field, rule) {
const list = nest[field]
if (list) {
list.push(rule)
} else {
nest[field] = [rule]
}
}
/**
* Check in a parent.
*
* @param {State} state
* State.
* @param {Nest} nest
* Nesting.
* @param {Parents} node
* Parent.
* @param {Nodes} tree
* Tree.
* @returns {undefined}
* Nothing.
*/
function all(state, nest, node, tree) {
const fromParent = combine(nest.descendant, nest.directChild)
/** @type {Array<AstRule> | undefined} */
let fromSibling
let index = -1
/**
* Total counts.
* @type {Counts}
*/
const total = {count: 0, types: new Map()}
/**
* Counts of previous siblings.
* @type {Counts}
*/
const before = {count: 0, types: new Map()}
while (++index < node.children.length) {
count(total, node.children[index])
}
index = -1
while (++index < node.children.length) {
const child = node.children[index]
// Uppercase to prevent prototype polution, injecting `constructor` or so.
// Normalize because HTML is insensitive.
const name =
child.type === 'element' ? child.tagName.toUpperCase() : undefined
// Before counting further elements:
state.elementIndex = before.count
state.typeIndex = name ? before.types.get(name) || 0 : 0
// After counting all elements.
state.elementCount = total.count
state.typeCount = name ? total.types.get(name) : 0
// Only apply if this is a parent, this should be an element, but we check
// for parents so that we delve into custom nodes too.
if ('children' in child) {
const forSibling = combine(fromParent, fromSibling)
const nest = one(
state,
forSibling,
node.children[index],
index,
node,
tree
)
fromSibling = combine(nest.generalSibling, nest.adjacentSibling)
}
// We found one thing, and one is enough.
if (state.one && state.found) {
break
}
count(before, node.children[index])
}
}
/**
* Apply selectors to an element.
*
* @param {State} state
* Current state.
* @param {Array<AstRule>} rules
* Rules to apply.
* @param {Element} node
* Element to apply rules to.
* @param {number | undefined} index
* Index of `node` in `parent`.
* @param {Parents | undefined} parent
* Parent of `node`.
* @returns {Nest}
* Further rules.
*/
function applySelectors(state, rules, node, index, parent) {
/** @type {Nest} */
const nestResult = {
adjacentSibling: undefined,
descendant: undefined,
directChild: undefined,
generalSibling: undefined
}
let selectorIndex = -1
while (++selectorIndex < rules.length) {
const rule = rules[selectorIndex]
// We found one thing, and one is enough.
if (state.one && state.found) {
break
}
// When shallow, we dont allow nested rules.
// Idea: we could allow a stack of parents?
// Might get quite complex though.
if (state.shallow && rule.nestedRule) {
throw new Error('Expected selector without nesting')
}
// If this rule matches:
if (test(rule, node, index, parent, state)) {
const nest = rule.nestedRule
// Are there more?
if (nest) {
/** @type {keyof Nest} */
const label =
nest.combinator === '+'
? 'adjacentSibling'
: nest.combinator === '~'
? 'generalSibling'
: nest.combinator === '>'
? 'directChild'
: 'descendant'
add(nestResult, label, nest)
} else {
// We have a match!
state.found = true
if (!state.results.includes(node)) {
state.results.push(node)
}
}
}
// Descendant.
if (rule.combinator === undefined) {
add(nestResult, 'descendant', rule)
}
// Adjacent.
else if (rule.combinator === '~') {
add(nestResult, 'generalSibling', rule)
}
// Drop direct child (`>`), adjacent sibling (`+`).
}
return nestResult
}
/**
* Combine two lists, if needed.
*
* This is optimized to create as few lists as possible.
*
* @param {Array<AstRule> | undefined} left
* Rules.
* @param {Array<AstRule> | undefined} right
* Rules.
* @returns {Array<AstRule>}
* Rules.
*/
function combine(left, right) {
return left && right && left.length > 0 && right.length > 0
? [...left, ...right]
: left && left.length > 0
? left
: right && right.length > 0
? right
: empty
}
/**
* Count a node.
*
* @param {Counts} counts
* Counts.
* @param {Nodes} node
* Node (were looking for elements).
* @returns {undefined}
* Nothing.
*/
function count(counts, node) {
if (node.type === 'element') {
// Uppercase to prevent prototype polution, injecting `constructor` or so.
// Normalize because HTML is insensitive.
const name = node.tagName.toUpperCase()
const count = (counts.types.get(name) || 0) + 1
counts.count++
counts.types.set(name, count)
}
}
/**
* Check a node.
*
* @param {State} state
* State.
* @param {Array<AstRule>} currentRules
* Rules.
* @param {Nodes} node
* Node.
* @param {number | undefined} index
* Index of `node` in `parent`.
* @param {Parents | undefined} parent
* Parent of `node`.
* @param {Nodes} tree
* Tree.
* @returns {Nest}
* Nesting.
*/
function one(state, currentRules, node, index, parent, tree) {
/** @type {Nest} */
let nestResult = {
adjacentSibling: undefined,
descendant: undefined,
directChild: undefined,
generalSibling: undefined
}
const exit = enterState(state, node)
if (node.type === 'element') {
let rootRules = state.rootQuery.rules
// Remove direct child rules if this is the root.
// This only happens for a `:has()` rule, which can be like
// `a:has(> b)`.
if (parent && parent !== tree) {
rootRules = state.rootQuery.rules.filter(
(d) =>
d.combinator === undefined ||
(d.combinator === '>' && parent === tree)
)
}
nestResult = applySelectors(
state,
// Try the root rules for this element too.
combine(currentRules, rootRules),
node,
index,
parent
)
}
// If this is a parent, and we want to delve into them, and we havent found
// our single result yet.
if ('children' in node && !state.shallow && !(state.one && state.found)) {
all(state, nestResult, node, tree)
}
exit()
return nestResult
}