unist-util-select
Advanced tools
Comparing version 4.0.3 to 5.0.0
@@ -29,3 +29,3 @@ /** | ||
* Tree to search. | ||
* @returns {Node | null} | ||
* @returns {Node | undefined} | ||
* First node in `tree` that matches `selector` or `null` if nothing is | ||
@@ -39,3 +39,3 @@ * found. | ||
tree?: Node | NodeLike | null | undefined | ||
): Node | null | ||
): Node | undefined | ||
/** | ||
@@ -42,0 +42,0 @@ * Select all nodes that match `selector` in the given `tree`. |
13
index.js
@@ -5,8 +5,11 @@ /** | ||
* @typedef {import('./lib/types.js').SelectState} SelectState | ||
*/ | ||
/** | ||
* @typedef {Record<string, unknown> & {type: string, position?: Position | undefined}} NodeLike | ||
*/ | ||
import {queryToSelectors, walk} from './lib/walk.js' | ||
import {parse} from './lib/parse.js' | ||
import {parent} from './lib/util.js' | ||
import {walk} from './lib/walk.js' | ||
@@ -45,3 +48,3 @@ /** | ||
* Tree to search. | ||
* @returns {Node | null} | ||
* @returns {Node | undefined} | ||
* First node in `tree` that matches `selector` or `null` if nothing is | ||
@@ -56,4 +59,3 @@ * found. | ||
walk(state, tree || undefined) | ||
// To do next major: return `undefined`. | ||
return state.results[0] || null | ||
return state.results[0] | ||
} | ||
@@ -87,2 +89,3 @@ | ||
* @returns {SelectState} | ||
* State. | ||
*/ | ||
@@ -92,3 +95,3 @@ function createState(selector, tree) { | ||
// State of the query. | ||
rootQuery: queryToSelectors(parse(selector)), | ||
rootQuery: parse(selector), | ||
results: [], | ||
@@ -95,0 +98,0 @@ scopeNodes: tree |
/** | ||
* @param {Rule} query | ||
* @param {AstRule} query | ||
* @param {Node} node | ||
* @returns {boolean} | ||
*/ | ||
export function attribute(query: Rule, node: Node): boolean | ||
export type Rule = import('./types.js').Rule | ||
export type RuleAttr = import('./types.js').RuleAttr | ||
export function attribute(query: AstRule, node: Node): boolean | ||
export type AstAttribute = import('css-selector-parser').AstAttribute | ||
export type AstRule = import('css-selector-parser').AstRule | ||
export type Node = import('./types.js').Node |
/** | ||
* @typedef {import('./types.js').Rule} Rule | ||
* @typedef {import('./types.js').RuleAttr} RuleAttr | ||
* @typedef {import('css-selector-parser').AstAttribute} AstAttribute | ||
* @typedef {import('css-selector-parser').AstRule} AstRule | ||
* @typedef {import('./types.js').Node} Node | ||
*/ | ||
import {unreachable} from 'devlop' | ||
import {zwitch} from 'zwitch' | ||
import {indexable} from './util.js' | ||
/** @type {(query: RuleAttr, node: Node) => boolean} */ | ||
/** @type {(query: AstAttribute, node: Node) => boolean} */ | ||
const handle = zwitch('operator', { | ||
@@ -24,3 +26,3 @@ unknown: unknownOperator, | ||
/** | ||
* @param {Rule} query | ||
* @param {AstRule} query | ||
* @param {Node} node | ||
@@ -32,4 +34,6 @@ * @returns {boolean} | ||
while (++index < query.attrs.length) { | ||
if (!handle(query.attrs[index], node)) return false | ||
if (query.attributes) { | ||
while (++index < query.attributes.length) { | ||
if (!handle(query.attributes[index], node)) return false | ||
} | ||
} | ||
@@ -45,3 +49,3 @@ | ||
* | ||
* @param {RuleAttr} query | ||
* @param {AstAttribute} query | ||
* @param {Node} node | ||
@@ -51,3 +55,3 @@ * @returns {boolean} | ||
function exists(query, node) { | ||
// @ts-expect-error: Looks like a record. | ||
indexable(node) | ||
return node[query.name] !== null && node[query.name] !== undefined | ||
@@ -61,3 +65,3 @@ } | ||
* | ||
* @param {RuleAttr} query | ||
* @param {AstAttribute} query | ||
* @param {Node} node | ||
@@ -67,4 +71,5 @@ * @returns {boolean} | ||
function exact(query, node) { | ||
// @ts-expect-error: Looks like a record. | ||
return exists(query, node) && String(node[query.name]) === query.value | ||
const queryValue = attributeValue(query) | ||
indexable(node) | ||
return exists(query, node) && String(node[query.name]) === queryValue | ||
} | ||
@@ -80,3 +85,3 @@ | ||
* | ||
* @param {RuleAttr} query | ||
* @param {AstAttribute} query | ||
* @param {Node} node | ||
@@ -86,4 +91,3 @@ * @returns {boolean} | ||
function containsArray(query, node) { | ||
/** @type {unknown} */ | ||
// @ts-expect-error: Looks like a record. | ||
indexable(node) | ||
const value = node[query.name] | ||
@@ -93,7 +97,9 @@ | ||
// If this is an array, and the query is contained in it, return true. | ||
const queryValue = attributeValue(query) | ||
// If this is an array, and the query is contained in it, return `true`. | ||
// Coverage comment in place because TS turns `Array.isArray(unknown)` | ||
// into `Array<any>` instead of `Array<unknown>`. | ||
// type-coverage:ignore-next-line | ||
if (Array.isArray(value) && value.includes(query.value)) { | ||
if (Array.isArray(value) && value.includes(queryValue)) { | ||
return true | ||
@@ -103,3 +109,3 @@ } | ||
// For all other values, return whether this is an exact match. | ||
return String(value) === query.value | ||
return String(value) === queryValue | ||
} | ||
@@ -112,3 +118,3 @@ | ||
* | ||
* @param {RuleAttr} query | ||
* @param {AstAttribute} query | ||
* @param {Node} node | ||
@@ -118,5 +124,5 @@ * @returns {boolean} | ||
function begins(query, node) { | ||
/** @type {unknown} */ | ||
// @ts-expect-error: Looks like a record. | ||
indexable(node) | ||
const value = node[query.name] | ||
const queryValue = attributeValue(query) | ||
@@ -126,3 +132,3 @@ return Boolean( | ||
typeof value === 'string' && | ||
value.slice(0, query.value.length) === query.value | ||
value.slice(0, queryValue.length) === queryValue | ||
) | ||
@@ -136,3 +142,3 @@ } | ||
* | ||
* @param {RuleAttr} query | ||
* @param {AstAttribute} query | ||
* @param {Node} node | ||
@@ -142,5 +148,5 @@ * @returns {boolean} | ||
function ends(query, node) { | ||
/** @type {unknown} */ | ||
// @ts-expect-error: Looks like a record. | ||
indexable(node) | ||
const value = node[query.name] | ||
const queryValue = attributeValue(query) | ||
@@ -150,3 +156,3 @@ return Boolean( | ||
typeof value === 'string' && | ||
value.slice(-query.value.length) === query.value | ||
value.slice(-queryValue.length) === queryValue | ||
) | ||
@@ -160,3 +166,3 @@ } | ||
* | ||
* @param {RuleAttr} query | ||
* @param {AstAttribute} query | ||
* @param {Node} node | ||
@@ -166,7 +172,8 @@ * @returns {boolean} | ||
function containsString(query, node) { | ||
/** @type {unknown} */ | ||
// @ts-expect-error: Looks like a record. | ||
indexable(node) | ||
const value = node[query.name] | ||
const queryValue = attributeValue(query) | ||
return Boolean( | ||
query.value && typeof value === 'string' && value.includes(query.value) | ||
typeof value === 'string' && queryValue && value.includes(queryValue) | ||
) | ||
@@ -185,1 +192,17 @@ } | ||
} | ||
/** | ||
* @param {AstAttribute} query | ||
* @returns {string} | ||
*/ | ||
function attributeValue(query) { | ||
const queryValue = query.value | ||
/* c8 ignore next 4 -- never happens with our config */ | ||
if (!queryValue) unreachable('Attribute values should be defined') | ||
if (queryValue.type === 'Substitution') { | ||
unreachable('Substitutions are not enabled') | ||
} | ||
return queryValue.value | ||
} |
/** | ||
* @param {string} selector | ||
* @returns {Selectors | RuleSet | null} | ||
* @returns {AstSelector} | ||
*/ | ||
export function parse(selector: string): Selectors | RuleSet | null | ||
export type Selectors = import('./types.js').Selectors | ||
export type RuleSet = import('./types.js').RuleSet | ||
export function parse(selector: string): AstSelector | ||
export type AstSelector = import('css-selector-parser').AstSelector |
/** | ||
* @typedef {import('./types.js').Selectors} Selectors | ||
* @typedef {import('./types.js').RuleSet} RuleSet | ||
* @typedef {import('css-selector-parser').AstSelector} AstSelector | ||
*/ | ||
import {CssSelectorParser} from 'css-selector-parser' | ||
import {createParser} from 'css-selector-parser' | ||
const parser = new CssSelectorParser() | ||
const cssSelectorParse = createParser({syntax: 'selectors-4'}) | ||
parser.registerAttrEqualityMods('~', '^', '$', '*') | ||
parser.registerSelectorPseudos('any', 'matches', 'not', 'has') | ||
parser.registerNestingOperators('>', '+', '~') | ||
/** | ||
* @param {string} selector | ||
* @returns {Selectors | RuleSet | null} | ||
* @returns {AstSelector} | ||
*/ | ||
@@ -23,3 +18,3 @@ export function parse(selector) { | ||
return parser.parse(selector) | ||
return cssSelectorParse(selector) | ||
} |
/** | ||
* Check whether an node matches pseudo selectors. | ||
* | ||
* @param {Rule} query | ||
* @param {AstRule} query | ||
* @param {Node} node | ||
@@ -12,3 +12,3 @@ * @param {number | undefined} index | ||
export function pseudo( | ||
query: Rule, | ||
query: AstRule, | ||
node: Node, | ||
@@ -20,9 +20,8 @@ index: number | undefined, | ||
export namespace pseudo { | ||
const needsIndex: string[] | ||
let needsIndex: string[] | ||
} | ||
export type Rule = import('./types.js').Rule | ||
export type RulePseudo = import('./types.js').RulePseudo | ||
export type RulePseudoSelector = import('./types.js').RulePseudoSelector | ||
export type Parent = import('./types.js').Parent | ||
export type AstRule = import('css-selector-parser').AstRule | ||
export type AstPseudoClass = import('css-selector-parser').AstPseudoClass | ||
export type Node = import('unist').Node | ||
export type Parent = import('unist').Parent | ||
export type SelectState = import('./types.js').SelectState | ||
export type Node = import('./types.js').Node |
/** | ||
* @typedef {import('./types.js').Rule} Rule | ||
* @typedef {import('./types.js').RulePseudo} RulePseudo | ||
* @typedef {import('./types.js').RulePseudoSelector} RulePseudoSelector | ||
* @typedef {import('./types.js').Parent} Parent | ||
* @typedef {import('css-selector-parser').AstRule} AstRule | ||
* @typedef {import('css-selector-parser').AstPseudoClass} AstPseudoClass | ||
* @typedef {import('unist').Node} Node | ||
* @typedef {import('unist').Parent} Parent | ||
* @typedef {import('./types.js').SelectState} SelectState | ||
* @typedef {import('./types.js').Node} Node | ||
*/ | ||
import {unreachable} from 'devlop' | ||
import fauxEsmNthCheck from 'nth-check' | ||
import {zwitch} from 'zwitch' | ||
import {parent} from './util.js' | ||
import {queryToSelectors, walk} from './walk.js' | ||
import {walk} from './walk.js' | ||
/** @type {import('nth-check').default} */ | ||
// @ts-expect-error | ||
// @ts-expect-error: `nth-check` types are wrong. | ||
const nthCheck = fauxEsmNthCheck.default || fauxEsmNthCheck | ||
/** @type {(rule: Rule | RulePseudo, node: Node, index: number | undefined, parent: Parent | undefined, state: SelectState) => boolean} */ | ||
/** @type {(rule: AstPseudoClass, node: Node, index: number | undefined, parent: Parent | undefined, state: SelectState) => boolean} */ | ||
const handle = zwitch('name', { | ||
// @ts-expect-error: always known. | ||
unknown: unknownPseudo, | ||
invalid: invalidPseudo, | ||
handlers: { | ||
any: matches, | ||
is, | ||
blank: empty, | ||
@@ -32,3 +33,2 @@ empty, | ||
'last-of-type': lastOfType, | ||
matches, | ||
not, | ||
@@ -52,3 +52,3 @@ 'nth-child': nthChild, | ||
'last-of-type', | ||
'matches', | ||
'is', | ||
'not', | ||
@@ -66,3 +66,3 @@ 'nth-child', | ||
* | ||
* @param {Rule} query | ||
* @param {AstRule} query | ||
* @param {Node} node | ||
@@ -75,7 +75,9 @@ * @param {number | undefined} index | ||
export function pseudo(query, node, index, parent, state) { | ||
const pseudos = query.pseudos | ||
let offset = -1 | ||
while (++offset < pseudos.length) { | ||
if (!handle(pseudos[offset], node, index, parent, state)) return false | ||
if (query.pseudoClasses) { | ||
while (++offset < query.pseudoClasses.length) { | ||
if (!handle(query.pseudoClasses[offset], node, index, parent, state)) | ||
return false | ||
} | ||
} | ||
@@ -89,3 +91,3 @@ | ||
* | ||
* @param {RulePseudo} _1 | ||
* @param {AstPseudoClass} _1 | ||
* @param {Node} node | ||
@@ -101,3 +103,3 @@ * @returns {boolean} | ||
* | ||
* @param {RulePseudo} query | ||
* @param {AstPseudoClass} query | ||
* @param {Node} _1 | ||
@@ -117,3 +119,3 @@ * @param {number | undefined} _2 | ||
* | ||
* @param {RulePseudo} query | ||
* @param {AstPseudoClass} query | ||
* @param {Node} _1 | ||
@@ -131,3 +133,3 @@ * @param {number | undefined} _2 | ||
/** | ||
* @param {RulePseudoSelector} query | ||
* @param {AstPseudoClass} query | ||
* @param {Node} node | ||
@@ -140,2 +142,9 @@ * @param {number | undefined} _1 | ||
function has(query, node, _1, _2, state) { | ||
const argument = query.argument | ||
/* c8 ignore next 3 -- never happens with our config */ | ||
if (!argument || argument.type !== 'Selector') { | ||
unreachable('`:has` has selectors') | ||
} | ||
const fragment = {type: 'root', children: parent(node) ? node.children : []} | ||
@@ -153,3 +162,3 @@ /** @type {SelectState} */ | ||
results: [], | ||
rootQuery: queryToSelectors(query.value) | ||
rootQuery: argument | ||
} | ||
@@ -165,3 +174,3 @@ | ||
* | ||
* @param {RulePseudo} query | ||
* @param {AstPseudoClass} query | ||
* @param {Node} _1 | ||
@@ -184,3 +193,3 @@ * @param {number | undefined} _2 | ||
* | ||
* @param {RulePseudo} query | ||
* @param {AstPseudoClass} query | ||
* @param {Node} _1 | ||
@@ -201,5 +210,5 @@ * @param {number | undefined} _2 | ||
/** | ||
* Check whether a node `:matches` further selectors. | ||
* Check whether a node `:is` further selectors. | ||
* | ||
* @param {RulePseudoSelector} query | ||
* @param {AstPseudoClass} query | ||
* @param {Node} node | ||
@@ -211,3 +220,10 @@ * @param {number | undefined} _1 | ||
*/ | ||
function matches(query, node, _1, _2, state) { | ||
function is(query, node, _1, _2, state) { | ||
const argument = query.argument | ||
/* c8 ignore next 3 -- never happens with our config */ | ||
if (!argument || argument.type !== 'Selector') { | ||
unreachable('`:is` has selectors') | ||
} | ||
/** @type {SelectState} */ | ||
@@ -224,3 +240,3 @@ const childState = { | ||
results: [], | ||
rootQuery: queryToSelectors(query.value) | ||
rootQuery: argument | ||
} | ||
@@ -236,3 +252,3 @@ | ||
* | ||
* @param {RulePseudoSelector} query | ||
* @param {AstPseudoClass} query | ||
* @param {Node} node | ||
@@ -245,3 +261,3 @@ * @param {number | undefined} index | ||
function not(query, node, index, parent, state) { | ||
return !matches(query, node, index, parent, state) | ||
return !is(query, node, index, parent, state) | ||
} | ||
@@ -252,3 +268,3 @@ | ||
* | ||
* @param {RulePseudo} query | ||
* @param {AstPseudoClass} query | ||
* @param {Node} _1 | ||
@@ -269,3 +285,3 @@ * @param {number | undefined} _2 | ||
* | ||
* @param {RulePseudo} query | ||
* @param {AstPseudoClass} query | ||
* @param {Node} _1 | ||
@@ -290,3 +306,3 @@ * @param {number | undefined} _2 | ||
* | ||
* @param {RulePseudo} query | ||
* @param {AstPseudoClass} query | ||
* @param {Node} _1 | ||
@@ -311,3 +327,3 @@ * @param {number | undefined} _2 | ||
* | ||
* @param {RulePseudo} query | ||
* @param {AstPseudoClass} query | ||
* @param {Node} _1 | ||
@@ -328,3 +344,3 @@ * @param {number | undefined} _2 | ||
* | ||
* @param {RulePseudo} query | ||
* @param {AstPseudoClass} query | ||
* @param {Node} _1 | ||
@@ -344,3 +360,3 @@ * @param {number | undefined} _2 | ||
* | ||
* @param {RulePseudo} query | ||
* @param {AstPseudoClass} query | ||
* @param {Node} _1 | ||
@@ -360,3 +376,3 @@ * @param {number | undefined} _2 | ||
* | ||
* @param {RulePseudo} _1 | ||
* @param {AstPseudoClass} _1 | ||
* @param {Node} node | ||
@@ -374,3 +390,3 @@ * @param {number | undefined} _2 | ||
* | ||
* @param {RulePseudo} _1 | ||
* @param {AstPseudoClass} _1 | ||
* @param {Node} node | ||
@@ -393,13 +409,7 @@ * @param {number | undefined} _2 | ||
/** | ||
* @param {unknown} query | ||
* @param {AstPseudoClass} query | ||
* @returns {never} | ||
*/ | ||
function unknownPseudo(query) { | ||
// @ts-expect-error: indexable. | ||
if (query.name) { | ||
// @ts-expect-error: indexable. | ||
throw new Error('Unknown pseudo-selector `' + query.name + '`') | ||
} | ||
throw new Error('Unexpected pseudo-element or empty pseudo-class') | ||
throw new Error('Unknown pseudo-selector `' + query.name + '`') | ||
} | ||
@@ -409,3 +419,3 @@ | ||
* @param {SelectState} state | ||
* @param {RulePseudo} query | ||
* @param {AstPseudoClass} query | ||
*/ | ||
@@ -419,3 +429,3 @@ function assertDeep(state, query) { | ||
/** | ||
* @param {RulePseudo} query | ||
* @param {AstPseudoClass} query | ||
* @returns {(value: number) => boolean} | ||
@@ -429,4 +439,10 @@ */ | ||
if (!fn) { | ||
// @ts-expect-error: always string. | ||
fn = nthCheck(query.value) | ||
const value = query.argument | ||
/* c8 ignore next 3 -- never happens with our config */ | ||
if (!value || value.type !== 'Formula') { | ||
unreachable('`:nth` has a formula') | ||
} | ||
fn = nthCheck(value.a + 'n+' + value.b) | ||
// @ts-expect-error: cache. | ||
@@ -433,0 +449,0 @@ query._cachedFn = fn |
/** | ||
* @param {Rule} query | ||
* @param {AstRule} query | ||
* @param {Node} node | ||
@@ -10,3 +10,3 @@ * @param {number | undefined} index | ||
export function test( | ||
query: Rule, | ||
query: AstRule, | ||
node: Node, | ||
@@ -17,5 +17,5 @@ index: number | undefined, | ||
): boolean | ||
export type Rule = import('./types.js').Rule | ||
export type Node = import('./types.js').Node | ||
export type Parent = import('./types.js').Parent | ||
export type AstRule = import('css-selector-parser').AstRule | ||
export type Node = import('unist').Node | ||
export type Parent = import('unist').Parent | ||
export type SelectState = import('./types.js').SelectState |
/** | ||
* @typedef {import('./types.js').Rule} Rule | ||
* @typedef {import('./types.js').Node} Node | ||
* @typedef {import('./types.js').Parent} Parent | ||
* @typedef {import('css-selector-parser').AstRule} AstRule | ||
* @typedef {import('unist').Node} Node | ||
* @typedef {import('unist').Parent} Parent | ||
* @typedef {import('./types.js').SelectState} SelectState | ||
@@ -9,7 +9,6 @@ */ | ||
import {attribute} from './attribute.js' | ||
import {name} from './name.js' | ||
import {pseudo} from './pseudo.js' | ||
/** | ||
* @param {Rule} query | ||
* @param {AstRule} query | ||
* @param {Node} node | ||
@@ -22,11 +21,16 @@ * @param {number | undefined} index | ||
export function test(query, node, index, parent, state) { | ||
if (query.id) throw new Error('Invalid selector: id') | ||
if (query.ids) throw new Error('Invalid selector: id') | ||
if (query.classNames) throw new Error('Invalid selector: class') | ||
if (query.pseudoElement) { | ||
throw new Error('Invalid selector: `::' + query.pseudoElement + '`') | ||
} | ||
return Boolean( | ||
node && | ||
(!query.tagName || name(query, node)) && | ||
(!query.attrs || attribute(query, node)) && | ||
(!query.pseudos || pseudo(query, node, index, parent, state)) | ||
(!query.tag || | ||
query.tag.type === 'WildcardTag' || | ||
query.tag.name === node.type) && | ||
(!query.attributes || attribute(query, node)) && | ||
(!query.pseudoClasses || pseudo(query, node, index, parent, state)) | ||
) | ||
} |
@@ -1,68 +0,4 @@ | ||
/** | ||
* Any node. | ||
*/ | ||
export type AstSelector = import('css-selector-parser').AstSelector | ||
export type Node = import('unist').Node | ||
/** | ||
* Node with children. | ||
*/ | ||
export type Parent = import('unist').Parent | ||
/** | ||
* Multiple selectors. | ||
*/ | ||
export type Selectors = import('css-selector-parser').Selectors | ||
/** | ||
* One rule. | ||
*/ | ||
export type Rule = import('css-selector-parser').Rule | ||
/** | ||
* Multiple rules. | ||
*/ | ||
export type RuleSet = import('css-selector-parser').RuleSet | ||
/** | ||
* Pseudo rule. | ||
*/ | ||
export type RulePseudo = import('css-selector-parser').RulePseudo | ||
/** | ||
* Attribute value type. | ||
*/ | ||
export type AttrValueType = import('css-selector-parser').AttrValueType | ||
/** | ||
* Fix for types from `css-selector-parser`. | ||
*/ | ||
export type RuleAttr = { | ||
/** | ||
* Attribute name. | ||
*/ | ||
name: string | ||
/** | ||
* Operator, such as `'|='`, missing when for example `[x]`. | ||
*/ | ||
operator?: string | undefined | ||
/** | ||
* Attribute value type. | ||
*/ | ||
valueType?: AttrValueType | undefined | ||
/** | ||
* Attribute value. | ||
*/ | ||
value?: string | undefined | ||
} | ||
/** | ||
* More specific type for registered selector pseudos. | ||
*/ | ||
export type RulePseudoSelector = { | ||
/** | ||
* Name of pseudo, such as `'matches'`. | ||
*/ | ||
name: string | ||
/** | ||
* Set to `'selector'`, because `value` is a compiled selector. | ||
*/ | ||
valueType: 'selector' | ||
/** | ||
* Selector. | ||
*/ | ||
value: Selectors | RuleSet | ||
} | ||
/** | ||
* Current state. | ||
@@ -74,3 +10,3 @@ */ | ||
*/ | ||
rootQuery: Selectors | ||
rootQuery: AstSelector | ||
/** | ||
@@ -77,0 +13,0 @@ * Matches. |
/** | ||
* @typedef {import('css-selector-parser').AstSelector} AstSelector | ||
* @typedef {import('unist').Node} Node | ||
* Any node. | ||
* @typedef {import('unist').Parent} Parent | ||
* Node with children. | ||
* | ||
* @typedef {import('css-selector-parser').Selectors} Selectors | ||
* Multiple selectors. | ||
* @typedef {import('css-selector-parser').Rule} Rule | ||
* One rule. | ||
* @typedef {import('css-selector-parser').RuleSet} RuleSet | ||
* Multiple rules. | ||
* @typedef {import('css-selector-parser').RulePseudo} RulePseudo | ||
* Pseudo rule. | ||
* @typedef {import('css-selector-parser').AttrValueType} AttrValueType | ||
* Attribute value type. | ||
* | ||
* @typedef RuleAttr | ||
* Fix for types from `css-selector-parser`. | ||
* @property {string} name | ||
* Attribute name. | ||
* @property {string | undefined} [operator] | ||
* Operator, such as `'|='`, missing when for example `[x]`. | ||
* @property {AttrValueType | undefined} [valueType] | ||
* Attribute value type. | ||
* @property {string | undefined} [value] | ||
* Attribute value. | ||
* | ||
* @typedef RulePseudoSelector | ||
* More specific type for registered selector pseudos. | ||
* @property {string} name | ||
* Name of pseudo, such as `'matches'`. | ||
* @property {'selector'} valueType | ||
* Set to `'selector'`, because `value` is a compiled selector. | ||
* @property {Selectors | RuleSet} value | ||
* Selector. | ||
* | ||
*/ | ||
/** | ||
* @typedef SelectState | ||
* Current state. | ||
* @property {Selectors} rootQuery | ||
* @property {AstSelector} rootQuery | ||
* Original root selectors. | ||
@@ -42,0 +11,0 @@ * @property {Array<Node>} results |
/** | ||
* @typedef {import('./types.js').Node} Node | ||
* @typedef {import('./types.js').Parent} Parent | ||
* TypeScript helper to check if something is indexable (any object is | ||
* indexable in JavaScript). | ||
* | ||
* @param {unknown} value | ||
* Thing to check. | ||
* @returns {asserts value is Record<string, unknown>} | ||
* Nothing. | ||
* @throws {Error} | ||
* When `value` is not an object. | ||
*/ | ||
export function indexable( | ||
value: unknown | ||
): asserts value is Record<string, unknown> | ||
/** | ||
@@ -9,9 +19,4 @@ * @param {Node} node | ||
*/ | ||
export function parent( | ||
node: Node | ||
): node is import('unist').Parent< | ||
import('unist').Node<import('unist').Data>, | ||
import('unist').Data | ||
> | ||
export type Node = import('./types.js').Node | ||
export type Parent = import('./types.js').Parent | ||
export function parent(node: Node): node is import('unist').Parent | ||
export type Node = import('unist').Node | ||
export type Parent = import('unist').Parent |
/** | ||
* @typedef {import('./types.js').Node} Node | ||
* @typedef {import('./types.js').Parent} Parent | ||
* @typedef {import('unist').Node} Node | ||
* @typedef {import('unist').Parent} Parent | ||
*/ | ||
import {unreachable} from 'devlop' | ||
/** | ||
* TypeScript helper to check if something is indexable (any object is | ||
* indexable in JavaScript). | ||
* | ||
* @param {unknown} value | ||
* Thing to check. | ||
* @returns {asserts value is Record<string, unknown>} | ||
* Nothing. | ||
* @throws {Error} | ||
* When `value` is not an object. | ||
*/ | ||
export function indexable(value) { | ||
// Always called when something is an object, this is just for TS. | ||
/* c8 ignore next 3 */ | ||
if (!value || typeof value !== 'object') { | ||
unreachable('Expected object') | ||
} | ||
} | ||
/** | ||
* @param {Node} node | ||
@@ -11,4 +32,4 @@ * @returns {node is Parent} | ||
export function parent(node) { | ||
// @ts-expect-error: looks like a record. | ||
indexable(node) | ||
return Array.isArray(node.children) | ||
} |
/** | ||
* Turn a query into a uniform object. | ||
* | ||
* @param {Selectors | RuleSet | null} query | ||
* @returns {Selectors} | ||
*/ | ||
export function queryToSelectors(query: Selectors | RuleSet | null): Selectors | ||
/** | ||
* Walk a tree. | ||
@@ -15,7 +8,6 @@ * | ||
export function walk(state: SelectState, tree: Node | undefined): void | ||
export type Node = import('./types.js').Node | ||
export type Parent = import('./types.js').Parent | ||
export type RuleSet = import('./types.js').RuleSet | ||
export type AstRule = import('css-selector-parser').AstRule | ||
export type Node = import('unist').Node | ||
export type Parent = import('unist').Parent | ||
export type SelectState = import('./types.js').SelectState | ||
export type Selectors = import('./types.js').Selectors | ||
/** | ||
@@ -28,15 +20,15 @@ * Rule sets by nesting. | ||
*/ | ||
descendant: Array<RuleSet> | undefined | ||
descendant: Array<AstRule> | undefined | ||
/** | ||
* `a > b` | ||
*/ | ||
directChild: Array<RuleSet> | undefined | ||
directChild: Array<AstRule> | undefined | ||
/** | ||
* `a + b` | ||
*/ | ||
adjacentSibling: Array<RuleSet> | undefined | ||
adjacentSibling: Array<AstRule> | undefined | ||
/** | ||
* `a ~ b` | ||
*/ | ||
generalSibling: Array<RuleSet> | undefined | ||
generalSibling: Array<AstRule> | undefined | ||
} | ||
@@ -43,0 +35,0 @@ /** |
106
lib/walk.js
/** | ||
* @typedef {import('./types.js').Node} Node | ||
* @typedef {import('./types.js').Parent} Parent | ||
* @typedef {import('./types.js').RuleSet} RuleSet | ||
* @typedef {import('css-selector-parser').AstRule} AstRule | ||
* @typedef {import('unist').Node} Node | ||
* @typedef {import('unist').Parent} Parent | ||
* @typedef {import('./types.js').SelectState} SelectState | ||
* @typedef {import('./types.js').Selectors} Selectors | ||
* | ||
* @typedef Nest | ||
* Rule sets by nesting. | ||
* @property {Array<RuleSet> | undefined} descendant | ||
* @property {Array<AstRule> | undefined} descendant | ||
* `a b` | ||
* @property {Array<RuleSet> | undefined} directChild | ||
* @property {Array<AstRule> | undefined} directChild | ||
* `a > b` | ||
* @property {Array<RuleSet> | undefined} adjacentSibling | ||
* @property {Array<AstRule> | undefined} adjacentSibling | ||
* `a + b` | ||
* @property {Array<RuleSet> | undefined} generalSibling | ||
* @property {Array<AstRule> | undefined} generalSibling | ||
* `a ~ b` | ||
@@ -34,20 +33,2 @@ * | ||
/** | ||
* 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. | ||
@@ -60,3 +41,3 @@ * | ||
if (tree) { | ||
one(state, [], tree, undefined, undefined) | ||
one(state, [], tree, undefined, undefined, tree) | ||
} | ||
@@ -69,9 +50,10 @@ } | ||
* @param {SelectState} state | ||
* @param {Array<RuleSet>} currentRules | ||
* @param {Array<AstRule>} currentRules | ||
* @param {Node} node | ||
* @param {number | undefined} index | ||
* @param {Parent | undefined} parentNode | ||
* @param {Node} tree | ||
* @returns {Nest} | ||
*/ | ||
function one(state, currentRules, node, index, parentNode) { | ||
function one(state, currentRules, node, index, parentNode, tree) { | ||
/** @type {Nest} */ | ||
@@ -85,6 +67,19 @@ let nestResult = { | ||
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 (parentNode && parentNode !== tree) { | ||
rootRules = state.rootQuery.rules.filter( | ||
(d) => | ||
d.combinator === undefined || | ||
(d.combinator === '>' && parentNode === tree) | ||
) | ||
} | ||
nestResult = applySelectors( | ||
state, | ||
// Try the root rules for this node too. | ||
combine(currentRules, state.rootQuery.selectors), | ||
combine(currentRules, rootRules), | ||
node, | ||
@@ -98,3 +93,3 @@ index, | ||
if (parent(node) && !state.shallow && !(state.one && state.found)) { | ||
all(state, nestResult, node) | ||
all(state, nestResult, node, tree) | ||
} | ||
@@ -111,7 +106,8 @@ | ||
* @param {Parent} node | ||
* @returns {void} | ||
* @param {Node} tree | ||
* @returns {undefined} | ||
*/ | ||
function all(state, nest, node) { | ||
function all(state, nest, node, tree) { | ||
const fromParent = combine(nest.descendant, nest.directChild) | ||
/** @type {Array<RuleSet> | undefined} */ | ||
/** @type {Array<AstRule> | undefined} */ | ||
let fromSibling | ||
@@ -149,3 +145,3 @@ let index = -1 | ||
const forSibling = combine(fromParent, fromSibling) | ||
const nest = one(state, forSibling, node.children[index], index, node) | ||
const nest = one(state, forSibling, node.children[index], index, node, tree) | ||
fromSibling = combine(nest.generalSibling, nest.adjacentSibling) | ||
@@ -167,3 +163,3 @@ | ||
* Current state. | ||
* @param {Array<RuleSet>} rules | ||
* @param {Array<AstRule>} rules | ||
* Rules to apply. | ||
@@ -190,3 +186,3 @@ * @param {Node} node | ||
while (++selectorIndex < rules.length) { | ||
const ruleSet = rules[selectorIndex] | ||
const rule = rules[selectorIndex] | ||
@@ -201,3 +197,3 @@ // We found one thing, and one is enough. | ||
// Might get quite complex though. | ||
if (state.shallow && ruleSet.rule.rule) { | ||
if (state.shallow && rule.nestedRule) { | ||
throw new Error('Expected selector without nesting') | ||
@@ -207,19 +203,17 @@ } | ||
// If this rule matches: | ||
if (test(ruleSet.rule, node, index, parent, state)) { | ||
const nest = ruleSet.rule.rule | ||
if (test(rule, node, index, parent, state)) { | ||
const nest = rule.nestedRule | ||
// Are there more? | ||
if (nest) { | ||
/** @type {RuleSet} */ | ||
const rule = {type: 'ruleSet', rule: nest} | ||
/** @type {keyof Nest} */ | ||
const label = | ||
nest.nestingOperator === '+' | ||
nest.combinator === '+' | ||
? 'adjacentSibling' | ||
: nest.nestingOperator === '~' | ||
: nest.combinator === '~' | ||
? 'generalSibling' | ||
: nest.nestingOperator === '>' | ||
: nest.combinator === '>' | ||
? 'directChild' | ||
: 'descendant' | ||
add(nestResult, label, rule) | ||
add(nestResult, label, nest) | ||
} else { | ||
@@ -236,10 +230,10 @@ // We have a match! | ||
// Descendant. | ||
if (ruleSet.rule.nestingOperator === null) { | ||
add(nestResult, 'descendant', ruleSet) | ||
if (rule.combinator === undefined) { | ||
add(nestResult, 'descendant', rule) | ||
} | ||
// Adjacent. | ||
else if (ruleSet.rule.nestingOperator === '~') { | ||
add(nestResult, 'generalSibling', ruleSet) | ||
else if (rule.combinator === '~') { | ||
add(nestResult, 'generalSibling', rule) | ||
} | ||
// Drop top-level nesting (`undefined`), direct child (`>`), adjacent sibling (`+`). | ||
// Drop direct child (`>`), adjacent sibling (`+`). | ||
} | ||
@@ -255,5 +249,5 @@ | ||
* | ||
* @param {Array<RuleSet> | undefined} left | ||
* @param {Array<RuleSet> | undefined} right | ||
* @returns {Array<RuleSet>} | ||
* @param {Array<AstRule> | undefined} left | ||
* @param {Array<AstRule> | undefined} right | ||
* @returns {Array<AstRule>} | ||
*/ | ||
@@ -275,3 +269,3 @@ function combine(left, right) { | ||
* @param {keyof Nest} field | ||
* @param {RuleSet} rule | ||
* @param {AstRule} rule | ||
*/ | ||
@@ -294,3 +288,3 @@ function add(nest, field, rule) { | ||
* Node. | ||
* @returns {void} | ||
* @returns {undefined} | ||
* Nothing. | ||
@@ -297,0 +291,0 @@ */ |
{ | ||
"name": "unist-util-select", | ||
"version": "4.0.3", | ||
"version": "5.0.0", | ||
"description": "unist utility to select nodes with CSS-like selectors", | ||
@@ -43,4 +43,3 @@ "license": "MIT", | ||
"type": "module", | ||
"main": "index.js", | ||
"types": "index.d.ts", | ||
"exports": "./index.js", | ||
"files": [ | ||
@@ -52,4 +51,5 @@ "lib/", | ||
"dependencies": { | ||
"@types/unist": "^2.0.0", | ||
"css-selector-parser": "^1.0.0", | ||
"@types/unist": "^3.0.0", | ||
"css-selector-parser": "^2.0.0", | ||
"devlop": "^1.1.0", | ||
"nth-check": "^2.0.0", | ||
@@ -59,4 +59,4 @@ "zwitch": "^2.0.0" | ||
"devDependencies": { | ||
"@types/node": "^18.0.0", | ||
"c8": "^7.0.0", | ||
"@types/node": "^20.0.0", | ||
"c8": "^8.0.0", | ||
"prettier": "^2.0.0", | ||
@@ -66,5 +66,5 @@ "remark-cli": "^11.0.0", | ||
"type-coverage": "^2.0.0", | ||
"typescript": "^4.0.0", | ||
"typescript": "^5.0.0", | ||
"unist-builder": "^3.0.0", | ||
"xo": "^0.53.0" | ||
"xo": "^0.54.0" | ||
}, | ||
@@ -76,18 +76,25 @@ "scripts": { | ||
"test-api": "node --conditions development test/index.js", | ||
"test-coverage": "c8 --check-coverage --100 --reporter lcov npm run test-api", | ||
"test-coverage": "c8 --100 --reporter lcov npm run test-api", | ||
"test": "npm run build && npm run format && npm run test-coverage" | ||
}, | ||
"prettier": { | ||
"tabWidth": 2, | ||
"useTabs": false, | ||
"singleQuote": true, | ||
"bracketSpacing": false, | ||
"semi": false, | ||
"trailingComma": "none" | ||
"singleQuote": true, | ||
"tabWidth": 2, | ||
"trailingComma": "none", | ||
"useTabs": false | ||
}, | ||
"remarkConfig": { | ||
"plugins": [ | ||
"remark-preset-wooorm" | ||
] | ||
}, | ||
"typeCoverage": { | ||
"atLeast": 100, | ||
"detail": true, | ||
"ignoreCatch": true, | ||
"strict": true | ||
}, | ||
"xo": { | ||
"prettier": true, | ||
"rules": { | ||
"max-params": "off" | ||
}, | ||
"overrides": [ | ||
@@ -99,17 +106,13 @@ { | ||
"rules": { | ||
"no-await-in-loop": 0 | ||
"import/no-unassigned-import": "off", | ||
"max-nested-callbacks": "off", | ||
"no-await-in-loop": "off" | ||
} | ||
} | ||
] | ||
}, | ||
"remarkConfig": { | ||
"plugins": [ | ||
"preset-wooorm" | ||
] | ||
}, | ||
"typeCoverage": { | ||
"atLeast": 100, | ||
"detail": true, | ||
"strict": true | ||
], | ||
"rules": { | ||
"max-params": "off" | ||
}, | ||
"prettier": true | ||
} | ||
} |
@@ -57,3 +57,3 @@ # unist-util-select | ||
This package is [ESM only][esm]. | ||
In Node.js (version 14.14+, 16.0+), install with [npm][]: | ||
In Node.js (version 16+), install with [npm][]: | ||
@@ -67,3 +67,3 @@ ```sh | ||
```js | ||
import {matches, select, selectAll} from "https://esm.sh/unist-util-select@4" | ||
import {matches, select, selectAll} from "https://esm.sh/unist-util-select@5" | ||
``` | ||
@@ -75,3 +75,3 @@ | ||
<script type="module"> | ||
import {matches, select, selectAll} from "https://esm.sh/unist-util-select@4?bundle" | ||
import {matches, select, selectAll} from "https://esm.sh/unist-util-select@5?bundle" | ||
</script> | ||
@@ -96,3 +96,3 @@ ``` | ||
matches('blockquote, list', tree) // => true | ||
console.log(matches('blockquote, list', tree)) // => true | ||
@@ -108,3 +108,4 @@ console.log(select('code ~ :nth-child(even)', tree)) | ||
This package exports the identifiers `matches`, `select`, and `selectAll`. | ||
This package exports the identifiers [`matches`][api-matches], | ||
[`select`][api-select], and [`selectAll`][api-select-all]. | ||
There is no default export. | ||
@@ -157,3 +158,3 @@ | ||
First node in `tree` that matches `selector` or `null` if nothing is found. | ||
First node in `tree` that matches `selector` or `undefined` if nothing is found. | ||
@@ -255,6 +256,4 @@ This could be `tree` itself. | ||
if there’s an array on the tree, otherwise same as attribute equality) | ||
* [x] `:any()` (functional pseudo-class, use `:matches` instead) | ||
* [x] `:has()` (functional pseudo-class) | ||
Relative selectors (`:has(> img)`) are not supported, but `:scope` is | ||
* [x] `:matches()` (functional pseudo-class) | ||
* [x] `:is()` (functional pseudo-class) | ||
* [x] `:has()` (functional pseudo-class; also supports `a:has(> b)`) | ||
* [x] `:not()` (functional pseudo-class) | ||
@@ -281,2 +280,3 @@ * [x] `:blank` (pseudo-class, blank and empty are the same: a parent without | ||
* \* — not supported in `matches` | ||
* `:any()` and `:matches()` are renamed to `:is()` in CSS | ||
@@ -290,7 +290,10 @@ ## Types | ||
Projects maintained by the unified collective are compatible with all maintained | ||
Projects maintained by the unified collective are compatible with maintained | ||
versions of Node.js. | ||
As of now, that is Node.js 14.14+ and 16.0+. | ||
Our projects sometimes work with older versions, but this is not guaranteed. | ||
When we cut a new major release, we drop support for unmaintained versions of | ||
Node. | ||
This means we try to keep the current release line, `unist-util-select@^5`, | ||
compatible with Node.js 16. | ||
## Related | ||
@@ -335,5 +338,5 @@ | ||
[size-badge]: https://img.shields.io/bundlephobia/minzip/unist-util-select.svg | ||
[size-badge]: https://img.shields.io/badge/dynamic/json?label=minzipped%20size&query=$.size.compressedSize&url=https://deno.bundlejs.com/?q=unist-util-select | ||
[size]: https://bundlephobia.com/result?p=unist-util-select | ||
[size]: https://bundlejs.com/?q=unist-util-select | ||
@@ -379,1 +382,7 @@ [sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg | ||
[hast-util-select]: https://github.com/syntax-tree/hast-util-select | ||
[api-matches]: #matchesselector-node | ||
[api-select]: #selectselector-tree | ||
[api-select-all]: #selectallselector-tree |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
378
48423
5
19
1252
+ Addeddevlop@^1.1.0
+ Added@types/unist@3.0.3(transitive)
+ Addedcss-selector-parser@2.3.2(transitive)
+ Addeddequal@2.0.3(transitive)
+ Addeddevlop@1.1.0(transitive)
- Removed@types/unist@2.0.11(transitive)
- Removedcss-selector-parser@1.4.1(transitive)
Updated@types/unist@^3.0.0
Updatedcss-selector-parser@^2.0.0