css-select
Version:
a CSS selector compiler/engine
138 lines (124 loc) • 4.61 kB
text/typescript
import type { PseudoSelector } from "css-what";
import type { InternalOptions } from "../types.js";
type Pseudo = <Node, ElementNode extends Node>(
element: ElementNode,
options: InternalOptions<Node, ElementNode>,
subselect?: string | null,
) => boolean;
/**
* CSS limits the characters considered as whitespace to space, tab & line
* feed. We add carriage returns as htmlparser2 doesn't normalize them to
* line feeds.
* @see {@link https://www.w3.org/TR/css-text-3/#white-space}
*/
const isDocumentWhiteSpace = /^[ \t\r\n]*$/;
// While filters are precompiled, pseudos get called when they are needed
/** Runtime pseudo selector implementations. */
export const pseudos: Record<string, Pseudo> = {
empty(element, { adapter }) {
const children = adapter.getChildren(element);
return (
// First, make sure the tag does not have any element children.
children.every((element) => !adapter.isTag(element)) &&
// Then, check that the text content is only whitespace.
children.every((element) =>
// FIXME: `getText` call is potentially expensive.
isDocumentWhiteSpace.test(adapter.getText(element)),
)
);
},
"first-child"(element, { adapter, equals }) {
if (adapter.prevElementSibling) {
return adapter.prevElementSibling(element) == null;
}
const firstChild = adapter
.getSiblings(element)
.find((sibling) => adapter.isTag(sibling));
return firstChild != null && equals(element, firstChild);
},
"last-child"(element, { adapter, equals }) {
const siblings = adapter.getSiblings(element);
for (let index = siblings.length - 1; index >= 0; index--) {
if (equals(element, siblings[index])) {
return true;
}
if (adapter.isTag(siblings[index])) {
break;
}
}
return false;
},
"first-of-type"(element, { adapter, equals }) {
const siblings = adapter.getSiblings(element);
const elementName = adapter.getName(element);
for (const currentSibling of siblings) {
if (equals(element, currentSibling)) {
return true;
}
if (
adapter.isTag(currentSibling) &&
adapter.getName(currentSibling) === elementName
) {
break;
}
}
return false;
},
"last-of-type"(element, { adapter, equals }) {
const siblings = adapter.getSiblings(element);
const elementName = adapter.getName(element);
for (let index = siblings.length - 1; index >= 0; index--) {
const currentSibling = siblings[index];
if (equals(element, currentSibling)) {
return true;
}
if (
adapter.isTag(currentSibling) &&
adapter.getName(currentSibling) === elementName
) {
break;
}
}
return false;
},
"only-of-type"(element, { adapter, equals }) {
const elementName = adapter.getName(element);
return adapter
.getSiblings(element)
.every(
(sibling) =>
equals(element, sibling) ||
!adapter.isTag(sibling) ||
adapter.getName(sibling) !== elementName,
);
},
"only-child"(element, { adapter, equals }) {
return adapter
.getSiblings(element)
.every(
(sibling) =>
equals(element, sibling) || !adapter.isTag(sibling),
);
},
};
/**
* Validate pseudo selector argument arity.
* @param pseudoClassCondition Pseudo-function implementation to wrap.
* @param name Name of the pseudo selector.
* @param subselect Subselector passed to the pseudo-function.
* @param argumentIndex Index of the argument parser to apply.
*/
export function verifyPseudoArguments<T extends unknown[]>(
pseudoClassCondition: (...parameters: T) => boolean,
name: string,
subselect: PseudoSelector["data"],
argumentIndex: number,
): void {
if (subselect === null) {
if (pseudoClassCondition.length > argumentIndex) {
throw new Error(`Pseudo-class :${name} requires an argument`);
}
} else if (pseudoClassCondition.length === argumentIndex) {
throw new Error(`Pseudo-class :${name} doesn't have any arguments`);
}
}