css-select
Version:
a CSS selector compiler/engine
153 lines (134 loc) • 5.35 kB
text/typescript
import * as boolbase from "boolbase";
import type { Selector } from "css-what";
import { cacheParentResults } from "../helpers/cache.js";
import { findOne, getNextSiblings } from "../helpers/querying.js";
import { includesScopePseudo, isTraversal } from "../helpers/selectors.js";
import type { CompiledQuery, CompileToken, InternalOptions } from "../types.js";
/** Used as a placeholder for :has. Will be replaced with the actual element. */
export const PLACEHOLDER_ELEMENT = {};
type Subselect = <Node, ElementNode extends Node>(
next: CompiledQuery<ElementNode>,
subselect: Selector[][],
options: InternalOptions<Node, ElementNode>,
context: Node[] | undefined,
compileToken: CompileToken<Node, ElementNode>,
) => CompiledQuery<ElementNode>;
/**
* Check if the selector has any properties that rely on the current element.
* If not, we can cache the result of the selector.
*
* We can't cache selectors that start with a traversal (e.g. `>`, `+`, `~`),
* or include a `:scope`.
*
* @param selector - The selector to check.
* @returns Whether the selector has any properties that rely on the current element.
*/
function hasDependsOnCurrentElement(selector: Selector[][]) {
return selector.some(
(sel) =>
sel.length > 0 &&
(isTraversal(sel[0]) || sel.some(includesScopePseudo)),
);
}
function copyOptions<Node, ElementNode extends Node>(
options: InternalOptions<Node, ElementNode>,
): InternalOptions<Node, ElementNode> {
// Not copied: context, rootFunc
return {
xmlMode: !!options.xmlMode,
lowerCaseAttributeNames: !!options.lowerCaseAttributeNames,
lowerCaseTags: !!options.lowerCaseTags,
quirksMode: !!options.quirksMode,
cacheResults: !!options.cacheResults,
pseudos: options.pseudos,
adapter: options.adapter,
equals: options.equals,
};
}
const is: Subselect = (next, token, options, context, compileToken) => {
const func = compileToken(token, copyOptions(options), context);
return func === boolbase.trueFunc
? next
: func === boolbase.falseFunc
? boolbase.falseFunc
: (elem) => func(elem) && next(elem);
};
/*
* :not, :has, :is, :matches and :where have to compile selectors
* doing this in src/pseudos.ts would lead to circular dependencies,
* so we add them here
*/
export const subselects: Record<string, Subselect> = {
is,
/**
* `:matches` and `:where` are aliases for `:is`.
*/
matches: is,
where: is,
not(next, token, options, context, compileToken) {
const func = compileToken(token, copyOptions(options), context);
return func === boolbase.falseFunc
? next
: func === boolbase.trueFunc
? boolbase.falseFunc
: (elem) => !func(elem) && next(elem);
},
has<Node, ElementNode extends Node>(
next: CompiledQuery<ElementNode>,
subselect: Selector[][],
options: InternalOptions<Node, ElementNode>,
_context: Node[] | undefined,
compileToken: CompileToken<Node, ElementNode>,
): CompiledQuery<ElementNode> {
const { adapter } = options;
const opts = copyOptions(options);
opts.relativeSelector = true;
const context = subselect.some((s) => s.some(isTraversal))
? // Used as a placeholder. Will be replaced with the actual element.
[PLACEHOLDER_ELEMENT as unknown as ElementNode]
: undefined;
const skipCache = hasDependsOnCurrentElement(subselect);
const compiled = compileToken(subselect, opts, context);
if (compiled === boolbase.falseFunc) {
return boolbase.falseFunc;
}
// If `compiled` is `trueFunc`, we can skip this.
if (context && compiled !== boolbase.trueFunc) {
return skipCache
? (elem) => {
if (!next(elem)) {
return false;
}
context[0] = elem;
const childs = adapter.getChildren(elem);
return (
findOne(
compiled,
compiled.shouldTestNextSiblings
? [
...childs,
...getNextSiblings(elem, adapter),
]
: childs,
options,
) !== null
);
}
: cacheParentResults(next, options, (elem) => {
context[0] = elem;
return (
findOne(
compiled,
adapter.getChildren(elem),
options,
) !== null
);
});
}
const hasOne = (elem: ElementNode) =>
findOne(compiled, adapter.getChildren(elem), options) !== null;
return skipCache
? (elem) => next(elem) && hasOne(elem)
: cacheParentResults(next, options, hasOne);
},
};