css-select
Version:
a CSS selector compiler/engine
214 lines (191 loc) • 7.56 kB
text/typescript
import { SelectorType } from "css-what";
import { attributeRules } from "./attributes.js";
import { getElementParent } from "./helpers/querying.js";
import { compilePseudoSelector } from "./pseudo-selectors/index.js";
import type {
CompiledQuery,
CompileToken,
InternalOptions,
InternalSelector,
} from "./types.js";
/*
* All available rules
*/
/**
* Compile a single selector token.
* @param next Matcher to run after this matcher succeeds.
* @param selector Selector used to match elements.
* @param options Options that control this operation.
* @param context Context nodes used to scope selector matching.
* @param compileToken Function used to compile nested selector tokens.
* @param hasExpensiveSubselector Whether the selector contains expensive subselectors.
*/
export function compileGeneralSelector<Node, ElementNode extends Node>(
next: CompiledQuery<ElementNode>,
selector: InternalSelector,
options: InternalOptions<Node, ElementNode>,
context: Node[] | undefined,
compileToken: CompileToken<Node, ElementNode>,
hasExpensiveSubselector: boolean,
): CompiledQuery<ElementNode> {
const { adapter, equals, cacheResults } = options;
switch (selector.type) {
case SelectorType.PseudoElement: {
throw new Error("Pseudo-elements are not supported by css-select");
}
case SelectorType.ColumnCombinator: {
throw new Error(
"Column combinators are not yet supported by css-select",
);
}
case SelectorType.Attribute: {
if (selector.namespace != null) {
throw new Error(
"Namespaced attributes are not yet supported by css-select",
);
}
if (!options.xmlMode || options.lowerCaseAttributeNames) {
selector.name = selector.name.toLowerCase();
}
return attributeRules[selector.action](next, selector, options);
}
case SelectorType.Pseudo: {
return compilePseudoSelector(
next,
selector,
options,
context,
compileToken,
);
}
// Tags
case SelectorType.Tag: {
if (selector.namespace != null) {
throw new Error(
"Namespaced tag names are not yet supported by css-select",
);
}
let { name } = selector;
if (!options.xmlMode || options.lowerCaseTags) {
name = name.toLowerCase();
}
return function tag(element: ElementNode): boolean {
return adapter.getName(element) === name && next(element);
};
}
// Traversal
case SelectorType.Descendant: {
if (
!hasExpensiveSubselector ||
cacheResults === false ||
typeof WeakMap === "undefined"
) {
return function descendant(element: ElementNode): boolean {
let current: ElementNode | null = element;
while ((current = getElementParent(current, adapter))) {
if (next(current)) {
return true;
}
}
return false;
};
}
const resultCache = new WeakMap<
// @ts-expect-error `ElementNode` is not extending object
ElementNode,
{ matches: boolean }
>();
return function cachedDescendant(element: ElementNode): boolean {
let current: ElementNode | null = element;
let result: { matches: boolean } | undefined;
while ((current = getElementParent(current, adapter))) {
const cached = resultCache.get(current);
if (cached === undefined) {
result ??= { matches: false };
result.matches = next(current);
resultCache.set(current, result);
if (result.matches) {
return true;
}
} else {
if (result) {
result.matches = cached.matches;
}
return cached.matches;
}
}
return false;
};
}
case "_flexibleDescendant": {
// Include element itself, only used while querying an array
return function flexibleDescendant(element: ElementNode): boolean {
let current: ElementNode | null = element;
do {
if (next(current)) {
return true;
}
current = getElementParent(current, adapter);
} while (current);
return false;
};
}
case SelectorType.Parent: {
return function parent(element: ElementNode): boolean {
return adapter
.getChildren(element)
.some((element) => adapter.isTag(element) && next(element));
};
}
case SelectorType.Child: {
return function child(element: ElementNode): boolean {
const parent = getElementParent(element, adapter);
return parent !== null && next(parent);
};
}
case SelectorType.Sibling: {
return function sibling(element: ElementNode): boolean {
const siblings = adapter.getSiblings(element);
for (const currentSibling of siblings) {
if (equals(element, currentSibling)) {
break;
}
if (adapter.isTag(currentSibling) && next(currentSibling)) {
return true;
}
}
return false;
};
}
case SelectorType.Adjacent: {
if (adapter.prevElementSibling) {
return function adjacent(element: ElementNode): boolean {
// biome-ignore lint/style/noNonNullAssertion: checked by if statement
const previous = adapter.prevElementSibling!(element);
return previous != null && next(previous);
};
}
return function adjacent(element: ElementNode): boolean {
const siblings = adapter.getSiblings(element);
let lastElement: ElementNode | undefined;
for (const currentSibling of siblings) {
if (equals(element, currentSibling)) {
break;
}
if (adapter.isTag(currentSibling)) {
lastElement = currentSibling;
}
}
return !!lastElement && next(lastElement);
};
}
case SelectorType.Universal: {
if (selector.namespace != null && selector.namespace !== "*") {
throw new Error(
"Namespaced universal selectors are not yet supported by css-select",
);
}
return next;
}
}
}