css-select
Version:
a CSS selector compiler/engine
131 lines (117 loc) • 4.09 kB
text/typescript
import type { PseudoSelector } from "css-what";
import type { InternalOptions } from "../types.js";
type Pseudo = <Node, ElementNode extends Node>(
elem: 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
export const pseudos: Record<string, Pseudo> = {
empty(elem, { adapter }) {
const children = adapter.getChildren(elem);
return (
// First, make sure the tag does not have any element children.
children.every((elem) => !adapter.isTag(elem)) &&
// Then, check that the text content is only whitespace.
children.every((elem) =>
// FIXME: `getText` call is potentially expensive.
isDocumentWhiteSpace.test(adapter.getText(elem)),
)
);
},
"first-child"(elem, { adapter, equals }) {
if (adapter.prevElementSibling) {
return adapter.prevElementSibling(elem) == null;
}
const firstChild = adapter
.getSiblings(elem)
.find((elem) => adapter.isTag(elem));
return firstChild != null && equals(elem, firstChild);
},
"last-child"(elem, { adapter, equals }) {
const siblings = adapter.getSiblings(elem);
for (let i = siblings.length - 1; i >= 0; i--) {
if (equals(elem, siblings[i])) {
return true;
}
if (adapter.isTag(siblings[i])) {
break;
}
}
return false;
},
"first-of-type"(elem, { adapter, equals }) {
const siblings = adapter.getSiblings(elem);
const elemName = adapter.getName(elem);
for (let i = 0; i < siblings.length; i++) {
const currentSibling = siblings[i];
if (equals(elem, currentSibling)) {
return true;
}
if (
adapter.isTag(currentSibling) &&
adapter.getName(currentSibling) === elemName
) {
break;
}
}
return false;
},
"last-of-type"(elem, { adapter, equals }) {
const siblings = adapter.getSiblings(elem);
const elemName = adapter.getName(elem);
for (let i = siblings.length - 1; i >= 0; i--) {
const currentSibling = siblings[i];
if (equals(elem, currentSibling)) {
return true;
}
if (
adapter.isTag(currentSibling) &&
adapter.getName(currentSibling) === elemName
) {
break;
}
}
return false;
},
"only-of-type"(elem, { adapter, equals }) {
const elemName = adapter.getName(elem);
return adapter
.getSiblings(elem)
.every(
(sibling) =>
equals(elem, sibling) ||
!adapter.isTag(sibling) ||
adapter.getName(sibling) !== elemName,
);
},
"only-child"(elem, { adapter, equals }) {
return adapter
.getSiblings(elem)
.every(
(sibling) => equals(elem, sibling) || !adapter.isTag(sibling),
);
},
};
export function verifyPseudoArgs<T extends Array<unknown>>(
func: (...args: T) => boolean,
name: string,
subselect: PseudoSelector["data"],
argIndex: number,
): void {
if (subselect === null) {
if (func.length > argIndex) {
throw new Error(`Pseudo-class :${name} requires an argument`);
}
} else if (func.length === argIndex) {
throw new Error(`Pseudo-class :${name} doesn't have any arguments`);
}
}