diff --git a/index.js b/index.js index 1d5ed55..66f210c 100644 --- a/index.js +++ b/index.js @@ -5,7 +5,7 @@ * @typedef {Record & {type: string, position?: Position | undefined}} NodeLike */ -import {any} from './lib/any.js' +import {queryToSelectors, walk} from './lib/walk.js' import {parse} from './lib/parse.js' import {parent} from './lib/util.js' @@ -25,10 +25,10 @@ import {parent} from './lib/util.js' * Whether `node` matches `selector`. */ export function matches(selector, node) { - const state = createState(node) + const state = createState(selector, node) state.one = true state.shallow = true - any(parse(selector), node || undefined, state) + walk(state, node || undefined) return state.results.length > 0 } @@ -48,9 +48,9 @@ export function matches(selector, node) { * This could be `tree` itself. */ export function select(selector, tree) { - const state = createState(tree) + const state = createState(selector, tree) state.one = true - any(parse(selector), tree || undefined, state) + walk(state, tree || undefined) // To do next major: return `undefined`. return state.results[0] || null } @@ -70,20 +70,23 @@ export function select(selector, tree) { * This could include `tree` itself. */ export function selectAll(selector, tree) { - const state = createState(tree) - any(parse(selector), tree || undefined, state) + const state = createState(selector, tree) + walk(state, tree || undefined) return state.results } /** + * @param {string} selector + * Selector to parse. * @param {Node | null | undefined} tree + * Tree to search. * @returns {SelectState} */ -function createState(tree) { +function createState(selector, tree) { return { + // State of the query. + rootQuery: queryToSelectors(parse(selector)), results: [], - any, - iterator: undefined, scopeNodes: tree ? parent(tree) && // Root in nlcst. @@ -93,8 +96,8 @@ function createState(tree) { : [], one: false, shallow: false, - index: false, found: false, + // State in the tree. typeIndex: undefined, nodeIndex: undefined, typeCount: undefined, diff --git a/lib/any.js b/lib/any.js deleted file mode 100644 index 3e9e1ec..0000000 --- a/lib/any.js +++ /dev/null @@ -1,176 +0,0 @@ -/** - * @typedef {import('./types.js').Selectors} Selectors - * @typedef {import('./types.js').Rule} Rule - * @typedef {import('./types.js').RuleSet} RuleSet - * @typedef {import('./types.js').Node} Node - * @typedef {import('./types.js').SelectIterator} SelectIterator - * @typedef {import('./types.js').SelectState} SelectState - */ - -import {zwitch} from 'zwitch' -import {nest} from './nest.js' -import {pseudo} from './pseudo.js' -import {test} from './test.js' - -/** @type {(query: Selectors | RuleSet | Rule, node: Node, state: SelectState) => Array} */ -const type = zwitch('type', { - unknown: unknownType, - invalid: invalidType, - handlers: {selectors, ruleSet, rule} -}) - -/** - * Handle an optional query and node. - * - * @param {Selectors | RuleSet | Rule | undefined} query - * Thing to find. - * @param {Node | undefined} node - * Tree. - * @param {SelectState} state - * State. - * @returns {void} - */ -export function any(query, node, state) { - if (query && node) { - type(query, node, state) - } -} - -/** - * Handle selectors. - * - * @param {Selectors} query - * Multiple selectors. - * @param {Node} node - * Tree. - * @param {SelectState} state - * State. - * @returns {void} - */ -function selectors(query, node, state) { - let index = -1 - - while (++index < query.selectors.length) { - const set = query.selectors[index] - rule(set.rule, node, state) - } -} - -/** - * Handle a selector. - * - * @param {RuleSet} query - * One selector. - * @param {Node} node - * Tree. - * @param {SelectState} state - * State. - * @returns {void} - */ -function ruleSet(query, node, state) { - return rule(query.rule, node, state) -} - -/** - * Handle a rule. - * - * @param {Rule} query - * One rule. - * @param {Node} tree - * Tree. - * @param {SelectState} state - * State. - * @returns {void} - */ -function rule(query, tree, state) { - if (state.shallow && query.rule) { - throw new Error('Expected selector without nesting') - } - - nest( - query, - tree, - 0, - undefined, - configure(query, { - ...state, - iterator, - index: needsIndex(query) - }) - ) - - /** @type {SelectIterator} */ - function iterator(query, node, index, parent, state) { - if (test(query, node, index, parent, state)) { - if (query.rule) { - nest(query.rule, node, index, parent, { - ...state, - iterator, - index: needsIndex(query.rule) - }) - } else { - if (!state.results.includes(node)) state.results.push(node) - state.found = true - } - } - } -} - -/** - * Check if indexing is needed. - * - * @param {Rule} query - * @returns {boolean} - */ -function needsIndex(query) { - const pseudos = query.pseudos || [] - let index = -1 - - while (++index < pseudos.length) { - if (pseudo.needsIndex.includes(pseudos[index].name)) { - return true - } - } - - return false -} - -/** - * @template {SelectState} S - * @param {Rule} query - * @param {S} state - * @returns {S} - */ -function configure(query, state) { - const pseudos = query.pseudos || [] - let index = -1 - - while (++index < pseudos.length) { - if (pseudo.needsIndex.includes(pseudos[index].name)) { - state.index = true - break - } - } - - return state -} - -// Shouldn’t be called, all data is handled. -/** - * @param {unknown} query - * @returns {never} - */ -/* c8 ignore next 4 */ -function unknownType(query) { - // @ts-expect-error: `type` guaranteed. - throw new Error('Unknown type `' + query.type + '`') -} - -// Shouldn’t be called, parser gives correct data. -/** - * @returns {never} - */ -/* c8 ignore next 3 */ -function invalidType() { - throw new Error('Invalid type') -} diff --git a/lib/name.js b/lib/name.js index 0fa428d..34a75f0 100644 --- a/lib/name.js +++ b/lib/name.js @@ -4,7 +4,7 @@ */ /** - * Check whether an element has a type. + * Check whether a node has a type. * * @param {Rule} query * @param {Node} node diff --git a/lib/nest.js b/lib/nest.js deleted file mode 100644 index 7fa3090..0000000 --- a/lib/nest.js +++ /dev/null @@ -1,332 +0,0 @@ -/** - * @typedef {import('./types.js').Rule} Rule - * @typedef {import('./types.js').Node} Node - * @typedef {import('./types.js').Parent} Parent - * @typedef {import('./types.js').SelectState} SelectState - * @typedef {import('./types.js').SelectIterator} SelectIterator - */ - -import {zwitch} from 'zwitch' -import {parent} from './util.js' - -const own = {}.hasOwnProperty - -/** @type {(query: Rule, node: Node, index: number | undefined, parent: Parent | undefined, state: SelectState) => void} */ -const handle = zwitch('nestingOperator', { - unknown: unknownNesting, - // @ts-expect-error: hush. - invalid: topScan, // `undefined` is the top query selector. - handlers: { - null: descendant, // `null` is the descendant combinator. - '>': directChild, - '+': adjacentSibling, - '~': generalSibling - } -}) - -/** - * Nest a rule. - * - * @param {Rule} query - * @param {Node} node - * @param {number | undefined} index - * @param {Parent | undefined} parent - * @param {SelectState} state - */ -export function nest(query, node, index, parent, state) { - handle(query, node, index, parent, state) -} - -/** - * Top-most scan. - * - * @param {Rule} query - * @param {Node} node - * @param {number | undefined} index - * @param {Parent | undefined} parent - * @param {SelectState} state - */ -function topScan(query, node, index, parent, state) { - if (!state.iterator) { - throw new Error('Expected `iterator` to be defined') - } - - // Shouldn’t happen. - /* c8 ignore next 3 */ - if (typeof index !== 'number') { - throw new TypeError('Expected `index` to be defined') - } - - state.iterator(query, node, index, parent, state) - if (!state.shallow) descendant(query, node, index, parent, state) -} - -/** - * Handle a descendant nesting operator (`a b`). - * - * @param {Rule} query - * @param {Node} node - * @param {number | undefined} index - * @param {Parent | undefined} parent - * @param {SelectState} state - */ -function descendant(query, node, index, parent, state) { - // Shouldn’t happen. - /* c8 ignore next 3 */ - if (!state.iterator) { - throw new Error('Expected `iterator` to be defined') - } - - const previous = state.iterator - - state.iterator = descendantIterator - directChild(query, node, index, parent, state) - - /** @type {SelectIterator} */ - function descendantIterator(query, node, index, parent, state) { - state.iterator = previous - previous(query, node, index, parent, state) - state.iterator = descendantIterator - - if (state.one && state.found) return - - directChild(query, node, index, parent, state) - } -} - -/** - * Handle a direct child nesting operator (`a > b`). - * - * Also reused by normal descendant operators. - * - * @param {Rule} query - * @param {Node} node - * @param {number | undefined} _1 - * @param {Parent | undefined} _2 - * @param {SelectState} state - */ -function directChild(query, node, _1, _2, state) { - if (!parent(node) || node.children.length === 0) return - new WalkIterator(query, node, state).each(undefined, undefined).done() -} - -/** - * Handle an adjacent sibling nesting operator (`a + b`). - * - * @param {Rule} query - * @param {Node} _ - * @param {number | undefined} index - * @param {Parent | undefined} parent - * @param {SelectState} state - */ -function adjacentSibling(query, _, index, parent, state) { - // Shouldn’t happen. - /* c8 ignore next 3 */ - if (typeof index !== 'number') { - throw new TypeError('Expected `index` to be defined') - } - - // Shouldn’t happen. - /* c8 ignore next */ - if (!parent) return - - new WalkIterator(query, parent, state) - .prefillTypeIndex(0, ++index) - .each(index, ++index) - .prefillTypeIndex(index, undefined) - .done() -} - -/** - * Handle a general sibling nesting operator (`a ~ b`). - * - * @param {Rule} query - * @param {Node} _ - * @param {number | undefined} index - * @param {Parent | undefined} parent - * @param {SelectState} state - */ -function generalSibling(query, _, index, parent, state) { - // Shouldn’t happen. - /* c8 ignore next 3 */ - if (typeof index !== 'number') { - throw new TypeError('Expected `index` to be defined') - } - - // Shouldn’t happen. - /* c8 ignore next */ - if (!parent) return - - new WalkIterator(query, parent, state) - .prefillTypeIndex(0, ++index) - .each(index, undefined) - .done() -} - -// Shouldn’t be called, parser gives correct data. -/** - * @param {unknown} query - * @returns {never} - */ -/* c8 ignore next 4 */ -function unknownNesting(query) { - // @ts-expect-error: `nestingOperator` guaranteed. - throw new Error('Unexpected nesting `' + query.nestingOperator + '`') -} - -class WalkIterator { - /** - * Handles typeIndex and typeCount properties for every walker. - * - * @param {Rule} query - * @param {Parent} parent - * @param {SelectState} state - */ - constructor(query, parent, state) { - /** @type {Rule} */ - this.query = query - /** @type {Parent} */ - this.parent = parent - /** @type {SelectState} */ - this.state = state - /** @type {TypeIndex | undefined} */ - this.typeIndex = state.index ? new TypeIndex() : undefined - /** @type {Array} */ - this.delayed = [] - } - - /** - * @param {number | undefined} x - * @param {number | undefined} y - * @returns {this} - */ - prefillTypeIndex(x, y) { - let [start, end] = this.defaults(x, y) - - if (this.typeIndex) { - while (start < end) { - this.typeIndex.index(this.parent.children[start]) - start++ - } - } - - return this - } - - /** - * @param {number | undefined} x - * @param {number | undefined} y - * @returns {this} - */ - each(x, y) { - const [start, end] = this.defaults(x, y) - const child = this.parent.children[start] - /** @type {number} */ - let index - /** @type {number} */ - let nodeIndex - - if (start >= end) return this - - if (this.typeIndex) { - nodeIndex = this.typeIndex.nodes - index = this.typeIndex.index(child) - this.delayed.push(delay) - } else { - // Shouldn’t happen. - /* c8 ignore next 3 */ - if (!this.state.iterator) { - throw new Error('Expected `iterator` to be defined') - } - - this.state.iterator(this.query, child, start, this.parent, this.state) - } - - // Stop if we’re looking for one node and it’s already found. - if (this.state.one && this.state.found) return this - - return this.each(start + 1, end) - - /** - * @this {WalkIterator} - */ - function delay() { - // Shouldn’t happen. - /* c8 ignore next 3 */ - if (!this.typeIndex) { - throw new TypeError('Expected `typeIndex` to be defined') - } - - // Shouldn’t happen. - /* c8 ignore next 3 */ - if (!this.state.iterator) { - throw new Error('Expected `iterator` to be defined') - } - - this.state.typeIndex = index - this.state.nodeIndex = nodeIndex - this.state.typeCount = this.typeIndex.count(child) - this.state.nodeCount = this.typeIndex.nodes - this.state.iterator(this.query, child, start, this.parent, this.state) - } - } - - /** - * Done! - * @returns {this} - */ - done() { - let index = -1 - - while (++index < this.delayed.length) { - this.delayed[index].call(this) - if (this.state.one && this.state.found) break - } - - return this - } - - /** - * @param {number | undefined} start - * @param {number | undefined} end - * @returns {[number, number]} - */ - defaults(start, end) { - if (start === undefined || start < 0) start = 0 - if (end === undefined || end > this.parent.children.length) - end = this.parent.children.length - return [start, end] - } -} - -class TypeIndex { - constructor() { - /** @type {Record} */ - this.counts = {} - /** @type {number} */ - this.nodes = 0 - } - - /** - * @param {Node} node - * @returns {number} - */ - index(node) { - const type = node.type - - this.nodes++ - - if (!own.call(this.counts, type)) this.counts[type] = 0 - - // Note: `++` is intended to be postfixed! - return this.counts[type]++ - } - - /** - * @param {Node} node - * @returns {number | undefined} - */ - count(node) { - return this.counts[node.type] - } -} diff --git a/lib/parse.js b/lib/parse.js index 429d7a8..44a3cef 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -13,7 +13,7 @@ parser.registerNestingOperators('>', '+', '~') /** * @param {string} selector - * @returns {Selectors | RuleSet | undefined} + * @returns {Selectors | RuleSet | null} */ export function parse(selector) { if (typeof selector !== 'string') { diff --git a/lib/pseudo.js b/lib/pseudo.js index 4de65f0..d14c17a 100644 --- a/lib/pseudo.js +++ b/lib/pseudo.js @@ -10,6 +10,7 @@ import fauxEsmNthCheck from 'nth-check' import {zwitch} from 'zwitch' import {parent} from './util.js' +import {queryToSelectors, walk} from './walk.js' /** @type {import('nth-check').default} */ // @ts-expect-error @@ -25,7 +26,7 @@ const handle = zwitch('name', { empty, 'first-child': firstChild, 'first-of-type': firstOfType, - has: hasSelector, + has, 'last-child': lastChild, 'last-of-type': lastOfType, matches, @@ -119,6 +120,33 @@ function firstOfType(query, _1, _2, _3, state) { return state.typeIndex === 0 } +/** + * @param {RulePseudoSelector} query + * @param {Node} node + * @param {number | undefined} _1 + * @param {Parent | undefined} _2 + * @param {SelectState} state + * @returns {boolean} + */ +function has(query, node, _1, _2, state) { + const fragment = {type: 'root', children: parent(node) ? node.children : []} + /** @type {SelectState} */ + const childState = { + ...state, + // Do walk deep. + shallow: false, + // One result is enough. + one: true, + scopeNodes: [node], + results: [], + rootQuery: queryToSelectors(query.value) + } + + walk(childState, fragment) + + return childState.results.length > 0 +} + /** * Check whether a node matches a `:last-child` pseudo. * @@ -166,20 +194,21 @@ function lastOfType(query, _1, _2, _3, state) { * @returns {boolean} */ function matches(query, node, _1, _2, state) { - const {shallow, one, results, any} = state - - state.shallow = false - state.one = true - state.results = [] - - any(query.value, node, state) - const matches = state.results[0] === node + /** @type {SelectState} */ + const childState = { + ...state, + // Do walk deep. + shallow: false, + // One result is enough. + one: true, + scopeNodes: [node], + results: [], + rootQuery: queryToSelectors(query.value) + } - state.shallow = shallow - state.one = one - state.results = results + walk(childState, node) - return matches + return childState.results[0] === node } /** @@ -355,34 +384,6 @@ function assertDeep(state, query) { } } -/** - * @param {RulePseudoSelector} query - * @param {Node} node - * @param {number | undefined} _1 - * @param {Parent | undefined} _2 - * @param {SelectState} state - * @returns {boolean} - */ -function hasSelector(query, node, _1, _2, state) { - const fragment = {type: 'root', children: parent(node) ? node.children : []} - const {shallow, one, scopeNodes, results, any} = state - - state.shallow = false - state.one = true - state.scopeNodes = [node] - state.results = [] - - any(query.value, fragment, state) - const has = state.results.length > 0 - - state.shallow = shallow - state.one = one - state.scopeNodes = scopeNodes - state.results = results - - return has -} - /** * @param {RulePseudo} query * @returns {(value: number) => boolean} diff --git a/lib/types.js b/lib/types.js index 779834f..1addb0c 100644 --- a/lib/types.js +++ b/lib/types.js @@ -4,8 +4,6 @@ * @typedef {import('unist').Parent} Parent * Node with children. * - * @typedef {import('css-selector-parser').Selector} Selector - * One selector. * @typedef {import('css-selector-parser').Selectors} Selectors * Multiple selectors. * @typedef {import('css-selector-parser').Rule} Rule @@ -39,21 +37,16 @@ * * @typedef SelectState * Current state. + * @property {Selectors} rootQuery + * Original root selectors. * @property {Array} results * Matches. - * @property {(query: Selectors | RuleSet | Rule, node: Node | undefined, state: SelectState) => void} any - * To do: Remove. - * @property {SelectIterator | undefined} iterator - * Current iterator. - * To do: Remove. * @property {Array} scopeNodes * Nodes in scope. * @property {boolean} one * Whether we can stop looking after we found one node. * @property {boolean} shallow * Whether we only allow selectors without nesting. - * @property {boolean} index - * Whether we need to index siblings. * @property {boolean} found * Whether we found at least one match. * @property {number | undefined} typeIndex @@ -64,19 +57,6 @@ * Track siblings: there are `n` siblings with this node’s type. * @property {number | undefined} nodeCount * Track siblings: there are `n` siblings. - * - * @callback SelectIterator - * An iterator. - * @param {Rule} query - * Current rule. - * @param {Node} node - * Current node. - * @param {number} index - * Index of `node` in `parent. - * @param {Parent | undefined} parent - * Parent of `node`. - * @param {SelectState} state - * Current state. */ export {} diff --git a/lib/walk.js b/lib/walk.js new file mode 100644 index 0000000..79360db --- /dev/null +++ b/lib/walk.js @@ -0,0 +1,290 @@ +/** + * @typedef {import('./types.js').Node} Node + * @typedef {import('./types.js').Parent} Parent + * @typedef {import('./types.js').RuleSet} RuleSet + * @typedef {import('./types.js').SelectState} SelectState + * @typedef {import('./types.js').Selectors} Selectors + * + * @typedef Nest + * Rule sets by nesting. + * @property {Array | undefined} descendant + * `a b` + * @property {Array | undefined} directChild + * `a > b` + * @property {Array | undefined} adjacentSibling + * `a + b` + * @property {Array | undefined} generalSibling + * `a ~ b` + * + * @typedef Counts + * Info on nodes in a parent. + * @property {number} count + * Number of nodes. + * @property {Map} types + * Number of nodes by type. + */ + +import {test} from './test.js' +import {parent} from './util.js' + +/** @type {Array} */ +const empty = [] + +/** + * Turn a query into a uniform object. + * + * @param {Selectors | RuleSet | null} query + * @returns {Selectors} + */ +export function queryToSelectors(query) { + if (query === null) { + return {type: 'selectors', selectors: []} + } + + if (query.type === 'ruleSet') { + return {type: 'selectors', selectors: [query]} + } + + return query +} + +/** + * Walk a tree. + * + * @param {SelectState} state + * @param {Node | undefined} tree + */ +export function walk(state, tree) { + if (tree) { + one(state, [], tree, undefined, undefined) + } +} + +/** + * Check a node. + * + * @param {SelectState} state + * @param {Array} currentRules + * @param {Node} node + * @param {number | undefined} index + * @param {Parent | undefined} parentNode + * @returns {Nest} + */ +function one(state, currentRules, node, index, parentNode) { + /** @type {Nest} */ + let nestResult = { + directChild: undefined, + descendant: undefined, + adjacentSibling: undefined, + generalSibling: undefined + } + + nestResult = applySelectors( + state, + // Try the root rules for this node too. + combine(currentRules, state.rootQuery.selectors), + node, + index, + parentNode + ) + + // If this is a parent, and we want to delve into them, and we haven’t found + // our single result yet. + if (parent(node) && !state.shallow && !(state.one && state.found)) { + all(state, nestResult, node) + } + + return nestResult +} + +/** + * Check a node. + * + * @param {SelectState} state + * @param {Nest} nest + * @param {Parent} node + * @returns {void} + */ +function all(state, nest, node) { + const fromParent = combine(nest.descendant, nest.directChild) + /** @type {Array | 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. + const name = child.type.toUpperCase() + // Before counting further nodes: + state.nodeIndex = before.count + state.typeIndex = before.types.get(name) || 0 + // After counting all nodes. + state.nodeCount = total.count + state.typeCount = total.types.get(name) + + // Only apply if this is a parent. + const forSibling = combine(fromParent, fromSibling) + const nest = one(state, forSibling, node.children[index], index, node) + 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 a node. + * + * @param {SelectState} state + * Current state. + * @param {Array} rules + * Rules to apply. + * @param {Node} node + * Node to apply rules to. + * @param {number | undefined} index + * Index of node in parent. + * @param {Parent | undefined} parent + * Parent of node. + * @returns {Nest} + * Further rules. + */ +function applySelectors(state, rules, node, index, parent) { + /** @type {Nest} */ + const nestResult = { + directChild: undefined, + descendant: undefined, + adjacentSibling: undefined, + generalSibling: undefined + } + let selectorIndex = -1 + + while (++selectorIndex < rules.length) { + const ruleSet = rules[selectorIndex] + + // We found one thing, and one is enough. + if (state.one && state.found) { + break + } + + // When shallow, we don’t allow nested rules. + // Idea: we could allow a stack of parents? + // Might get quite complex though. + if (state.shallow && ruleSet.rule.rule) { + throw new Error('Expected selector without nesting') + } + + // If this rule matches: + if (test(ruleSet.rule, node, index, parent, state)) { + const nest = ruleSet.rule.rule + + // Are there more? + if (nest) { + /** @type {RuleSet} */ + const rule = {type: 'ruleSet', rule: nest} + /** @type {keyof Nest} */ + const label = + nest.nestingOperator === '+' + ? 'adjacentSibling' + : nest.nestingOperator === '~' + ? 'generalSibling' + : nest.nestingOperator === '>' + ? 'directChild' + : 'descendant' + add(nestResult, label, rule) + } else { + // We have a match! + state.found = true + + if (!state.results.includes(node)) { + state.results.push(node) + } + } + } + + // Descendant. + if (ruleSet.rule.nestingOperator === null) { + add(nestResult, 'descendant', ruleSet) + } + // Adjacent. + else if (ruleSet.rule.nestingOperator === '~') { + add(nestResult, 'generalSibling', ruleSet) + } + // Drop top-level nesting (`undefined`), direct child (`>`), adjacent sibling (`+`). + } + + return nestResult +} + +/** + * Combine two lists, if needed. + * + * This is optimized to create as few lists as possible. + * + * @param {Array | undefined} left + * @param {Array | undefined} right + * @returns {Array} + */ +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 +} + +/** + * Add a rule to a nesting map. + * + * @param {Nest} nest + * @param {keyof Nest} field + * @param {RuleSet} rule + */ +function add(nest, field, rule) { + const list = nest[field] + if (list) { + list.push(rule) + } else { + nest[field] = [rule] + } +} + +/** + * Count a node. + * + * @param {Counts} counts + * Counts. + * @param {Node} node + * Node. + * @returns {void} + * Nothing. + */ +function count(counts, node) { + // Uppercase to prevent prototype polution, injecting `constructor` or so. + // Normalize because HTML is insensitive. + const name = node.type.toUpperCase() + const count = (counts.types.get(name) || 0) + 1 + counts.count++ + counts.types.set(name, count) +} diff --git a/test/all.js b/test/all.js index 5e142de..7138e88 100644 --- a/test/all.js +++ b/test/all.js @@ -76,12 +76,12 @@ test('all together now', () => { ]) ), [ + u('b', {e: 3}, 'Alpha'), u('b', {d: 1}, 'Charlie'), - u('c', {d: 2, e: 4}, 'Foxtrot'), u('c', 'Delta'), + u('c', {d: 2, e: 4}, 'Foxtrot'), u('b', 'Golf'), - u('c', 'Hotel'), - u('b', {e: 3}, 'Alpha') + u('c', 'Hotel') ] ) })