Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 40 additions & 4 deletions src/pseudo-selectors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,28 @@ import { filters } from "./filters.js";
import { pseudos, verifyPseudoArguments } from "./pseudos.js";
import { subselects } from "./subselects.js";

const allFilterNames = new Set(Object.keys(filters));

const filtersWithArguments = new Set([
"contains",
"icontains",
"nth-child",
"nth-last-child",
"nth-of-type",
"nth-last-of-type",
"lang",
]);

for (const filterName of filtersWithArguments) {
if (!allFilterNames.has(filterName)) {
throw new Error(`Unknown filter in filtersWithArguments: ${filterName}`);
}
}

const filtersWithoutArguments = new Set(
[...allFilterNames].filter((filterName) => !filtersWithArguments.has(filterName)),
);

/**
* Compile a pseudo selector into an executable query function.
* @param next Matcher to run after this matcher succeeds.
Expand All @@ -37,8 +59,12 @@ export function compilePseudoSelector<Node, ElementNode extends Node>(
): CompiledQuery<ElementNode> {
const { name, data } = selector;

if (data === null && Object.hasOwn(subselects, name)) {
throw new Error(`Pseudo-class :${name} requires an argument`);
}

if (Array.isArray(data)) {
if (!(name in subselects)) {
if (!Object.hasOwn(subselects, name)) {
throw new Error(`Unknown pseudo-class :${name}(${data})`);
}

Expand All @@ -52,7 +78,9 @@ export function compilePseudoSelector<Node, ElementNode extends Node>(

if (typeof stringPseudo === "string") {
if (data != null) {
throw new Error(`Pseudo ${name} doesn't have any arguments`);
throw new Error(
`Pseudo-class :${name} doesn't have any arguments`,
);
}

// The alias has to be parsed here, to make sure options are respected.
Expand All @@ -66,7 +94,15 @@ export function compilePseudoSelector<Node, ElementNode extends Node>(
return (element) => userPseudo(element, data) && next(element);
}

if (name in filters) {
if (Object.hasOwn(filters, name)) {
if (data === null && filtersWithArguments.has(name)) {
throw new Error(`Pseudo-class :${name} requires an argument`);
}

if (data !== null && filtersWithoutArguments.has(name)) {
throw new Error(`Pseudo-class :${name} doesn't have any arguments`);
}

return filters[name](
next,
data as string,
Expand All @@ -76,7 +112,7 @@ export function compilePseudoSelector<Node, ElementNode extends Node>(
);
}

if (name in pseudos) {
if (Object.hasOwn(pseudos, name)) {
const pseudo = pseudos[name];
verifyPseudoArguments(pseudo, name, data, 2);

Expand Down
76 changes: 76 additions & 0 deletions test/pseudo-classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,82 @@ describe("unmatched", () => {
"Unknown pseudo-class :host-context",
);
});

it("should throw when pseudo-classes are missing required arguments", () => {
expect(() => CSSselect.selectAll(":contains", dom)).toThrow(
"Pseudo-class :contains requires an argument",
);

expect(() => CSSselect.selectAll(":icontains", dom)).toThrow(
"Pseudo-class :icontains requires an argument",
);

expect(() => CSSselect.selectAll(":lang", dom)).toThrow(
"Pseudo-class :lang requires an argument",
);

expect(() => CSSselect.selectAll(":nth-child", dom)).toThrow(
"Pseudo-class :nth-child requires an argument",
);

expect(() => CSSselect.selectAll(":nth-last-child", dom)).toThrow(
"Pseudo-class :nth-last-child requires an argument",
);

expect(() => CSSselect.selectAll(":nth-of-type", dom)).toThrow(
"Pseudo-class :nth-of-type requires an argument",
);

expect(() => CSSselect.selectAll(":nth-last-of-type", dom)).toThrow(
"Pseudo-class :nth-last-of-type requires an argument",
);

expect(() => CSSselect.selectAll(":has", dom)).toThrow(
"Pseudo-class :has requires an argument",
);

expect(() => CSSselect.selectAll(":is", dom)).toThrow(
"Pseudo-class :is requires an argument",
);

expect(() => CSSselect.selectAll(":matches", dom)).toThrow(
"Pseudo-class :matches requires an argument",
);

expect(() => CSSselect.selectAll(":where", dom)).toThrow(
"Pseudo-class :where requires an argument",
);

expect(() => CSSselect.selectAll(":not", dom)).toThrow(
"Pseudo-class :not requires an argument",
);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it("should throw when argument-less pseudo-classes receive arguments", () => {
expect(() => CSSselect.selectAll(":scope(foo)", dom)).toThrow(
"Pseudo-class :scope doesn't have any arguments",
);

expect(() => CSSselect.selectAll(":active(foo)", dom)).toThrow(
"Pseudo-class :active doesn't have any arguments",
);

expect(() => CSSselect.selectAll(":root(foo)", dom)).toThrow(
"Pseudo-class :root doesn't have any arguments",
);

expect(() => CSSselect.selectAll(":hover(foo)", dom)).toThrow(
"Pseudo-class :hover doesn't have any arguments",
);

expect(() => CSSselect.selectAll(":visited(foo)", dom)).toThrow(
"Pseudo-class :visited doesn't have any arguments",
);

expect(() => CSSselect.selectAll(":enabled(foo)", dom)).toThrow(
"Pseudo-class :enabled doesn't have any arguments",
);
});
Comment thread
RedZapdos123 marked this conversation as resolved.
});

describe(":first-child", () => {
Expand Down