This commit is contained in:
CHEVALLIER Abel
2025-11-13 16:23:22 +01:00
parent de9c515a47
commit cb235644dc
34924 changed files with 3811102 additions and 0 deletions

17296
node_modules/css-what/src/__fixtures__/out.json generated vendored Normal file

File diff suppressed because it is too large Load Diff

1028
node_modules/css-what/src/__fixtures__/tests.ts generated vendored Normal file

File diff suppressed because it is too large Load Diff

3
node_modules/css-what/src/index.ts generated vendored Normal file
View File

@@ -0,0 +1,3 @@
export * from "./types.js";
export { isTraversal, parse } from "./parse.js";
export { stringify } from "./stringify.js";

69
node_modules/css-what/src/parse.spec.ts generated vendored Normal file
View File

@@ -0,0 +1,69 @@
import { readFileSync } from "node:fs";
import { describe, it, expect } from "vitest";
import { parse } from "./parse.js";
import { tests } from "./__fixtures__/tests.js";
const broken = [
"[",
"(",
"{",
"()",
"<>",
"{}",
",",
",a",
"a,",
"[id=012345678901234567890123456789",
"input[name=foo b]",
"input[name!foo]",
"input[name|]",
"input[name=']",
"input[name=foo[baz]]",
':has("p")',
":has(p",
":foo(p()",
"#",
"##foo",
"/*",
];
describe("Parse", () => {
describe("Own tests", () => {
for (const [selector, expected, message] of tests) {
it(message, () => expect(parse(selector)).toStrictEqual(expected));
}
});
describe("Collected selectors (qwery, sizzle, nwmatcher)", () => {
const out = JSON.parse(
readFileSync(`${__dirname}/__fixtures__/out.json`, "utf8"),
);
for (const s of Object.keys(out)) {
it(s, () => {
expect(parse(s)).toStrictEqual(out[s]);
});
}
});
describe("Broken selectors", () => {
for (const selector of broken) {
it(`should not parse — ${selector}`, () => {
expect(() => parse(selector)).toThrow(Error);
});
}
});
it("should ignore comments", () => {
expect(parse("/* comment1 */ /**/ foo /*comment2*/")).toEqual([
[{ name: "foo", namespace: null, type: "tag" }],
]);
expect(() => parse("/*/")).toThrowError("Comment was not terminated");
});
it("should support legacy pseudo-elements with single colon", () => {
expect(parse(":before")).toEqual([
[{ name: "before", data: null, type: "pseudo-element" }],
]);
});
});

634
node_modules/css-what/src/parse.ts generated vendored Normal file
View File

@@ -0,0 +1,634 @@
import {
type Selector,
SelectorType,
type AttributeSelector,
type Traversal,
AttributeAction,
type TraversalType,
type DataType,
} from "./types.js";
const reName = /^[^#\\]?(?:\\(?:[\da-f]{1,6}\s?|.)|[\w\u00B0-\uFFFF-])+/;
const reEscape = /\\([\da-f]{1,6}\s?|(\s)|.)/gi;
const enum CharCode {
LeftParenthesis = 40,
RightParenthesis = 41,
LeftSquareBracket = 91,
RightSquareBracket = 93,
Comma = 44,
Period = 46,
Colon = 58,
SingleQuote = 39,
DoubleQuote = 34,
Plus = 43,
Tilde = 126,
QuestionMark = 63,
ExclamationMark = 33,
Slash = 47,
Equal = 61,
Dollar = 36,
Pipe = 124,
Circumflex = 94,
Asterisk = 42,
GreaterThan = 62,
LessThan = 60,
Hash = 35,
LowerI = 105,
LowerS = 115,
BackSlash = 92,
// Whitespace
Space = 32,
Tab = 9,
NewLine = 10,
FormFeed = 12,
CarriageReturn = 13,
}
const actionTypes = new Map<number, AttributeAction>([
[CharCode.Tilde, AttributeAction.Element],
[CharCode.Circumflex, AttributeAction.Start],
[CharCode.Dollar, AttributeAction.End],
[CharCode.Asterisk, AttributeAction.Any],
[CharCode.ExclamationMark, AttributeAction.Not],
[CharCode.Pipe, AttributeAction.Hyphen],
]);
// Pseudos, whose data property is parsed as well.
const unpackPseudos = new Set([
"has",
"not",
"matches",
"is",
"where",
"host",
"host-context",
]);
/**
* Pseudo elements defined in CSS Level 1 and CSS Level 2 can be written with
* a single colon; eg. :before will turn into ::before.
*
* @see {@link https://www.w3.org/TR/2018/WD-selectors-4-20181121/#pseudo-element-syntax}
*/
const pseudosToPseudoElements = new Set([
"before",
"after",
"first-line",
"first-letter",
]);
/**
* Checks whether a specific selector is a traversal.
* This is useful eg. in swapping the order of elements that
* are not traversals.
*
* @param selector Selector to check.
*/
export function isTraversal(selector: Selector): selector is Traversal {
switch (selector.type) {
case SelectorType.Adjacent:
case SelectorType.Child:
case SelectorType.Descendant:
case SelectorType.Parent:
case SelectorType.Sibling:
case SelectorType.ColumnCombinator: {
return true;
}
default: {
return false;
}
}
}
const stripQuotesFromPseudos = new Set(["contains", "icontains"]);
// Unescape function taken from https://github.com/jquery/sizzle/blob/master/src/sizzle.js#L152
function funescape(_: string, escaped: string, escapedWhitespace?: string) {
const high = Number.parseInt(escaped, 16) - 0x1_00_00;
// NaN means non-codepoint
return high !== high || escapedWhitespace
? escaped
: high < 0
? // BMP codepoint
String.fromCharCode(high + 0x1_00_00)
: // Supplemental Plane codepoint (surrogate pair)
String.fromCharCode(
(high >> 10) | 0xd8_00,
(high & 0x3_ff) | 0xdc_00,
);
}
function unescapeCSS(cssString: string) {
return cssString.replace(reEscape, funescape);
}
function isQuote(c: number): boolean {
return c === CharCode.SingleQuote || c === CharCode.DoubleQuote;
}
function isWhitespace(c: number): boolean {
return (
c === CharCode.Space ||
c === CharCode.Tab ||
c === CharCode.NewLine ||
c === CharCode.FormFeed ||
c === CharCode.CarriageReturn
);
}
/**
* Parses `selector`.
*
* @param selector Selector to parse.
* @returns Returns a two-dimensional array.
* The first dimension represents selectors separated by commas (eg. `sub1, sub2`),
* the second contains the relevant tokens for that selector.
*/
export function parse(selector: string): Selector[][] {
const subselects: Selector[][] = [];
const endIndex = parseSelector(subselects, `${selector}`, 0);
if (endIndex < selector.length) {
throw new Error(`Unmatched selector: ${selector.slice(endIndex)}`);
}
return subselects;
}
function parseSelector(
subselects: Selector[][],
selector: string,
selectorIndex: number,
): number {
let tokens: Selector[] = [];
function getName(offset: number): string {
const match = selector.slice(selectorIndex + offset).match(reName);
if (!match) {
throw new Error(
`Expected name, found ${selector.slice(selectorIndex)}`,
);
}
const [name] = match;
selectorIndex += offset + name.length;
return unescapeCSS(name);
}
function stripWhitespace(offset: number) {
selectorIndex += offset;
while (
selectorIndex < selector.length &&
isWhitespace(selector.charCodeAt(selectorIndex))
) {
selectorIndex++;
}
}
function readValueWithParenthesis(): string {
selectorIndex += 1;
const start = selectorIndex;
for (
let counter = 1;
selectorIndex < selector.length;
selectorIndex++
) {
switch (selector.charCodeAt(selectorIndex)) {
case CharCode.BackSlash: {
// Skip next character
selectorIndex += 1;
break;
}
case CharCode.LeftParenthesis: {
counter += 1;
break;
}
case CharCode.RightParenthesis: {
counter -= 1;
if (counter === 0) {
return unescapeCSS(
selector.slice(start, selectorIndex++),
);
}
break;
}
}
}
throw new Error("Parenthesis not matched");
}
function ensureNotTraversal() {
if (tokens.length > 0 && isTraversal(tokens[tokens.length - 1])) {
throw new Error("Did not expect successive traversals.");
}
}
function addTraversal(type: TraversalType) {
if (
tokens.length > 0 &&
tokens[tokens.length - 1].type === SelectorType.Descendant
) {
tokens[tokens.length - 1].type = type;
return;
}
ensureNotTraversal();
tokens.push({ type });
}
function addSpecialAttribute(name: string, action: AttributeAction) {
tokens.push({
type: SelectorType.Attribute,
name,
action,
value: getName(1),
namespace: null,
ignoreCase: "quirks",
});
}
/**
* We have finished parsing the current part of the selector.
*
* Remove descendant tokens at the end if they exist,
* and return the last index, so that parsing can be
* picked up from here.
*/
function finalizeSubselector() {
if (
tokens.length > 0 &&
tokens[tokens.length - 1].type === SelectorType.Descendant
) {
tokens.pop();
}
if (tokens.length === 0) {
throw new Error("Empty sub-selector");
}
subselects.push(tokens);
}
stripWhitespace(0);
if (selector.length === selectorIndex) {
return selectorIndex;
}
loop: while (selectorIndex < selector.length) {
const firstChar = selector.charCodeAt(selectorIndex);
switch (firstChar) {
// Whitespace
case CharCode.Space:
case CharCode.Tab:
case CharCode.NewLine:
case CharCode.FormFeed:
case CharCode.CarriageReturn: {
if (
tokens.length === 0 ||
tokens[0].type !== SelectorType.Descendant
) {
ensureNotTraversal();
tokens.push({ type: SelectorType.Descendant });
}
stripWhitespace(1);
break;
}
// Traversals
case CharCode.GreaterThan: {
addTraversal(SelectorType.Child);
stripWhitespace(1);
break;
}
case CharCode.LessThan: {
addTraversal(SelectorType.Parent);
stripWhitespace(1);
break;
}
case CharCode.Tilde: {
addTraversal(SelectorType.Sibling);
stripWhitespace(1);
break;
}
case CharCode.Plus: {
addTraversal(SelectorType.Adjacent);
stripWhitespace(1);
break;
}
// Special attribute selectors: .class, #id
case CharCode.Period: {
addSpecialAttribute("class", AttributeAction.Element);
break;
}
case CharCode.Hash: {
addSpecialAttribute("id", AttributeAction.Equals);
break;
}
case CharCode.LeftSquareBracket: {
stripWhitespace(1);
// Determine attribute name and namespace
let name: string;
let namespace: string | null = null;
if (selector.charCodeAt(selectorIndex) === CharCode.Pipe) {
// Equivalent to no namespace
name = getName(1);
} else if (selector.startsWith("*|", selectorIndex)) {
namespace = "*";
name = getName(2);
} else {
name = getName(0);
if (
selector.charCodeAt(selectorIndex) === CharCode.Pipe &&
selector.charCodeAt(selectorIndex + 1) !==
CharCode.Equal
) {
namespace = name;
name = getName(1);
}
}
stripWhitespace(0);
// Determine comparison operation
let action: AttributeAction = AttributeAction.Exists;
const possibleAction = actionTypes.get(
selector.charCodeAt(selectorIndex),
);
if (possibleAction) {
action = possibleAction;
if (
selector.charCodeAt(selectorIndex + 1) !==
CharCode.Equal
) {
throw new Error("Expected `=`");
}
stripWhitespace(2);
} else if (
selector.charCodeAt(selectorIndex) === CharCode.Equal
) {
action = AttributeAction.Equals;
stripWhitespace(1);
}
// Determine value
let value = "";
let ignoreCase: boolean | null = null;
if (action !== "exists") {
if (isQuote(selector.charCodeAt(selectorIndex))) {
const quote = selector.charCodeAt(selectorIndex);
selectorIndex += 1;
const sectionStart = selectorIndex;
while (
selectorIndex < selector.length &&
selector.charCodeAt(selectorIndex) !== quote
) {
selectorIndex +=
// Skip next character if it is escaped
selector.charCodeAt(selectorIndex) ===
CharCode.BackSlash
? 2
: 1;
}
if (selector.charCodeAt(selectorIndex) !== quote) {
throw new Error("Attribute value didn't end");
}
value = unescapeCSS(
selector.slice(sectionStart, selectorIndex),
);
selectorIndex += 1;
} else {
const valueStart = selectorIndex;
while (
selectorIndex < selector.length &&
!isWhitespace(selector.charCodeAt(selectorIndex)) &&
selector.charCodeAt(selectorIndex) !==
CharCode.RightSquareBracket
) {
selectorIndex +=
// Skip next character if it is escaped
selector.charCodeAt(selectorIndex) ===
CharCode.BackSlash
? 2
: 1;
}
value = unescapeCSS(
selector.slice(valueStart, selectorIndex),
);
}
stripWhitespace(0);
// See if we have a force ignore flag
switch (selector.charCodeAt(selectorIndex) | 0x20) {
// If the forceIgnore flag is set (either `i` or `s`), use that value
case CharCode.LowerI: {
ignoreCase = true;
stripWhitespace(1);
break;
}
case CharCode.LowerS: {
ignoreCase = false;
stripWhitespace(1);
break;
}
}
}
if (
selector.charCodeAt(selectorIndex) !==
CharCode.RightSquareBracket
) {
throw new Error("Attribute selector didn't terminate");
}
selectorIndex += 1;
const attributeSelector: AttributeSelector = {
type: SelectorType.Attribute,
name,
action,
value,
namespace,
ignoreCase,
};
tokens.push(attributeSelector);
break;
}
case CharCode.Colon: {
if (selector.charCodeAt(selectorIndex + 1) === CharCode.Colon) {
tokens.push({
type: SelectorType.PseudoElement,
name: getName(2).toLowerCase(),
data:
selector.charCodeAt(selectorIndex) ===
CharCode.LeftParenthesis
? readValueWithParenthesis()
: null,
});
break;
}
const name = getName(1).toLowerCase();
if (pseudosToPseudoElements.has(name)) {
tokens.push({
type: SelectorType.PseudoElement,
name,
data: null,
});
break;
}
let data: DataType = null;
if (
selector.charCodeAt(selectorIndex) ===
CharCode.LeftParenthesis
) {
if (unpackPseudos.has(name)) {
if (isQuote(selector.charCodeAt(selectorIndex + 1))) {
throw new Error(
`Pseudo-selector ${name} cannot be quoted`,
);
}
data = [];
selectorIndex = parseSelector(
data,
selector,
selectorIndex + 1,
);
if (
selector.charCodeAt(selectorIndex) !==
CharCode.RightParenthesis
) {
throw new Error(
`Missing closing parenthesis in :${name} (${selector})`,
);
}
selectorIndex += 1;
} else {
data = readValueWithParenthesis();
if (stripQuotesFromPseudos.has(name)) {
const quot = data.charCodeAt(0);
if (
quot === data.charCodeAt(data.length - 1) &&
isQuote(quot)
) {
data = data.slice(1, -1);
}
}
data = unescapeCSS(data);
}
}
tokens.push({ type: SelectorType.Pseudo, name, data });
break;
}
case CharCode.Comma: {
finalizeSubselector();
tokens = [];
stripWhitespace(1);
break;
}
default: {
if (selector.startsWith("/*", selectorIndex)) {
const endIndex = selector.indexOf("*/", selectorIndex + 2);
if (endIndex < 0) {
throw new Error("Comment was not terminated");
}
selectorIndex = endIndex + 2;
// Remove leading whitespace
if (tokens.length === 0) {
stripWhitespace(0);
}
break;
}
let namespace = null;
let name: string;
if (firstChar === CharCode.Asterisk) {
selectorIndex += 1;
name = "*";
} else if (firstChar === CharCode.Pipe) {
name = "";
if (
selector.charCodeAt(selectorIndex + 1) === CharCode.Pipe
) {
addTraversal(SelectorType.ColumnCombinator);
stripWhitespace(2);
break;
}
} else if (reName.test(selector.slice(selectorIndex))) {
name = getName(0);
} else {
break loop;
}
if (
selector.charCodeAt(selectorIndex) === CharCode.Pipe &&
selector.charCodeAt(selectorIndex + 1) !== CharCode.Pipe
) {
namespace = name;
if (
selector.charCodeAt(selectorIndex + 1) ===
CharCode.Asterisk
) {
name = "*";
selectorIndex += 2;
} else {
name = getName(1);
}
}
tokens.push(
name === "*"
? { type: SelectorType.Universal, namespace }
: { type: SelectorType.Tag, name, namespace },
);
}
}
}
finalizeSubselector();
return selectorIndex;
}

23
node_modules/css-what/src/stringify.spec.ts generated vendored Normal file
View File

@@ -0,0 +1,23 @@
import { readFileSync } from "node:fs";
import { describe, it, expect } from "vitest";
import { parse, stringify } from "./index.js";
import { tests } from "./__fixtures__/tests.js";
describe("Stringify & re-parse", () => {
describe("Own tests", () => {
for (const [selector, expected, message] of tests) {
it(`${message} (${selector})`, () => {
expect(parse(stringify(expected))).toStrictEqual(expected);
});
}
});
it("Collected Selectors (qwery, sizzle, nwmatcher)", () => {
const out = JSON.parse(
readFileSync(`${__dirname}/__fixtures__/out.json`, "utf8"),
);
for (const s of Object.keys(out)) {
expect(parse(stringify(out[s]))).toStrictEqual(out[s]);
}
});
});

204
node_modules/css-what/src/stringify.ts generated vendored Normal file
View File

@@ -0,0 +1,204 @@
import { type Selector, SelectorType, AttributeAction } from "./types.js";
const attribValueChars = ["\\", '"'];
const pseudoValueChars = [...attribValueChars, "(", ")"];
const charsToEscapeInAttributeValue = new Set(
attribValueChars.map((c) => c.charCodeAt(0)),
);
const charsToEscapeInPseudoValue = new Set(
pseudoValueChars.map((c) => c.charCodeAt(0)),
);
const charsToEscapeInName = new Set(
[
...pseudoValueChars,
"~",
"^",
"$",
"*",
"+",
"!",
"|",
":",
"[",
"]",
" ",
".",
"%",
].map((c) => c.charCodeAt(0)),
);
/**
* Turns `selector` back into a string.
*
* @param selector Selector to stringify.
*/
export function stringify(selector: Selector[][]): string {
return selector
.map((token) =>
token
.map((token, index, array) =>
stringifyToken(token, index, array),
)
.join(""),
)
.join(", ");
}
function stringifyToken(
token: Selector,
index: number,
array: Selector[],
): string {
switch (token.type) {
// Simple types
case SelectorType.Child: {
return index === 0 ? "> " : " > ";
}
case SelectorType.Parent: {
return index === 0 ? "< " : " < ";
}
case SelectorType.Sibling: {
return index === 0 ? "~ " : " ~ ";
}
case SelectorType.Adjacent: {
return index === 0 ? "+ " : " + ";
}
case SelectorType.Descendant: {
return " ";
}
case SelectorType.ColumnCombinator: {
return index === 0 ? "|| " : " || ";
}
case SelectorType.Universal: {
// Return an empty string if the selector isn't needed.
return token.namespace === "*" &&
index + 1 < array.length &&
"name" in array[index + 1]
? ""
: `${getNamespace(token.namespace)}*`;
}
case SelectorType.Tag: {
return getNamespacedName(token);
}
case SelectorType.PseudoElement: {
return `::${escapeName(token.name, charsToEscapeInName)}${
token.data === null
? ""
: `(${escapeName(token.data, charsToEscapeInPseudoValue)})`
}`;
}
case SelectorType.Pseudo: {
return `:${escapeName(token.name, charsToEscapeInName)}${
token.data === null
? ""
: `(${
typeof token.data === "string"
? escapeName(
token.data,
charsToEscapeInPseudoValue,
)
: stringify(token.data)
})`
}`;
}
case SelectorType.Attribute: {
if (
token.name === "id" &&
token.action === AttributeAction.Equals &&
token.ignoreCase === "quirks" &&
!token.namespace
) {
return `#${escapeName(token.value, charsToEscapeInName)}`;
}
if (
token.name === "class" &&
token.action === AttributeAction.Element &&
token.ignoreCase === "quirks" &&
!token.namespace
) {
return `.${escapeName(token.value, charsToEscapeInName)}`;
}
const name = getNamespacedName(token);
if (token.action === AttributeAction.Exists) {
return `[${name}]`;
}
return `[${name}${getActionValue(token.action)}="${escapeName(
token.value,
charsToEscapeInAttributeValue,
)}"${
token.ignoreCase === null ? "" : token.ignoreCase ? " i" : " s"
}]`;
}
}
}
function getActionValue(action: AttributeAction): string {
switch (action) {
case AttributeAction.Equals: {
return "";
}
case AttributeAction.Element: {
return "~";
}
case AttributeAction.Start: {
return "^";
}
case AttributeAction.End: {
return "$";
}
case AttributeAction.Any: {
return "*";
}
case AttributeAction.Not: {
return "!";
}
case AttributeAction.Hyphen: {
return "|";
}
default: {
throw new Error("Shouldn't be here");
}
}
}
function getNamespacedName(token: {
name: string;
namespace: string | null;
}): string {
return `${getNamespace(token.namespace)}${escapeName(
token.name,
charsToEscapeInName,
)}`;
}
function getNamespace(namespace: string | null): string {
return namespace === null
? ""
: `${
namespace === "*"
? "*"
: escapeName(namespace, charsToEscapeInName)
}|`;
}
function escapeName(name: string, charsToEscape: Set<number>): string {
let lastIndex = 0;
let escapedName = "";
for (let index = 0; index < name.length; index++) {
if (charsToEscape.has(name.charCodeAt(index))) {
escapedName += `${name.slice(lastIndex, index)}\\${name.charAt(index)}`;
lastIndex = index + 1;
}
}
return escapedName.length > 0 ? escapedName + name.slice(lastIndex) : name;
}

94
node_modules/css-what/src/types.ts generated vendored Normal file
View File

@@ -0,0 +1,94 @@
export type Selector =
| PseudoSelector
| PseudoElement
| AttributeSelector
| TagSelector
| UniversalSelector
| Traversal;
export enum SelectorType {
Attribute = "attribute",
Pseudo = "pseudo",
PseudoElement = "pseudo-element",
Tag = "tag",
Universal = "universal",
// Traversals
Adjacent = "adjacent",
Child = "child",
Descendant = "descendant",
Parent = "parent",
Sibling = "sibling",
ColumnCombinator = "column-combinator",
}
/**
* Modes for ignore case.
*
* This could be updated to an enum, and the object is
* the current stand-in that will allow code to be updated
* without big changes.
*/
export const IgnoreCaseMode = {
Unknown: null,
QuirksMode: "quirks",
IgnoreCase: true,
CaseSensitive: false,
} as const;
export interface AttributeSelector {
type: SelectorType.Attribute;
name: string;
action: AttributeAction;
value: string;
ignoreCase: "quirks" | boolean | null;
namespace: string | null;
}
export type DataType = Selector[][] | null | string;
export interface PseudoSelector {
type: SelectorType.Pseudo;
name: string;
data: DataType;
}
export interface PseudoElement {
type: SelectorType.PseudoElement;
name: string;
data: string | null;
}
export interface TagSelector {
type: SelectorType.Tag;
name: string;
namespace: string | null;
}
export interface UniversalSelector {
type: SelectorType.Universal;
namespace: string | null;
}
export interface Traversal {
type: TraversalType;
}
export enum AttributeAction {
Any = "any",
Element = "element",
End = "end",
Equals = "equals",
Exists = "exists",
Hyphen = "hyphen",
Not = "not",
Start = "start",
}
export type TraversalType =
| SelectorType.Adjacent
| SelectorType.Child
| SelectorType.Descendant
| SelectorType.Parent
| SelectorType.Sibling
| SelectorType.ColumnCombinator;

185
node_modules/css-what/src/wpt.spec.ts generated vendored Normal file
View File

@@ -0,0 +1,185 @@
/**
* @fileoverview CSS Selector parsing tests from WPT
* @see https://github.com/web-platform-tests/wpt/tree/0bb883967c888261a8372923fd61eb5ad14305b2/css/selectors/parsing
* @license BSD-3-Clause (https://github.com/web-platform-tests/wpt/blob/master/LICENSE.md)
*/
import { describe, it, expect } from "vitest";
import { parse, stringify } from "./index.js";
function test_valid_selector(
selector: string,
serialized: string | string[] = selector,
) {
const result = stringify(parse(selector));
if (Array.isArray(serialized)) {
// Should be a part of the array
expect(serialized).toContain(result);
} else {
expect(result).toStrictEqual(serialized);
}
}
function test_invalid_selector(selector: string) {
expect(() => parse(selector)).toThrow(Error);
}
describe("Web Platform Tests", () => {
it("Attribute selectors", () => {
// Attribute presence and value selectors
test_valid_selector("[att]");
test_valid_selector("[att=val]", '[att="val"]');
test_valid_selector("[att~=val]", '[att~="val"]');
test_valid_selector("[att|=val]", '[att|="val"]');
test_valid_selector("h1[title]");
test_valid_selector("span[class='example']", 'span[class="example"]');
test_valid_selector("a[hreflang=fr]", 'a[hreflang="fr"]');
test_valid_selector("a[hreflang|='en']", 'a[hreflang|="en"]');
// Substring matching attribute selectors
test_valid_selector("[att^=val]", '[att^="val"]');
test_valid_selector("[att$=val]", '[att$="val"]');
test_valid_selector("[att*=val]", '[att*="val"]');
test_valid_selector('object[type^="image/"]');
test_valid_selector('a[href$=".html"]');
test_valid_selector('p[title*="hello"]');
// From Attribute selectors and namespaces examples in spec:
test_valid_selector("[*|att]");
test_valid_selector("[|att]", "[att]");
});
it("Child combinators", () => {
test_valid_selector("body > p");
test_valid_selector("div ol>li p", "div ol > li p");
});
it("Class selectors", () => {
test_valid_selector("*.pastoral", ["*.pastoral", ".pastoral"]);
test_valid_selector(".pastoral", ["*.pastoral", ".pastoral"]);
test_valid_selector("h1.pastoral");
test_valid_selector("p.pastoral.marine");
});
it("Descendant combinator", () => {
test_valid_selector("h1 em");
test_valid_selector("div * p");
test_valid_selector("div p *[href]", ["div p *[href]", "div p [href]"]);
});
it(":focus-visible pseudo-class", () => {
test_valid_selector(":focus-visible");
test_valid_selector("a:focus-visible");
test_valid_selector(":focus:not(:focus-visible)");
});
it("The relational pseudo-class", () => {
test_valid_selector(":has(a)");
test_valid_selector(":has(#a)");
test_valid_selector(":has(.a)");
test_valid_selector(":has([a])");
test_valid_selector(':has([a="b"])');
test_valid_selector(':has([a|="b"])');
test_valid_selector(":has(:hover)");
test_valid_selector("*:has(.a)", ["*:has(.a)", ":has(.a)"]);
test_valid_selector(".a:has(.b)");
test_valid_selector(".a:has(> .b)");
test_valid_selector(".a:has(~ .b)");
test_valid_selector(".a:has(+ .b)");
test_valid_selector(".a:has(.b) .c");
test_valid_selector(".a .b:has(.c)");
test_valid_selector(".a .b:has(.c .d)");
test_valid_selector(".a .b:has(.c .d) .e");
test_valid_selector(".a:has(.b:has(.c))");
test_valid_selector(".a:has(.b:is(.c .d))");
test_valid_selector(".a:has(.b:is(.c:has(.d) .e))");
test_valid_selector(".a:is(.b:has(.c) .d)");
test_valid_selector(".a:not(:has(.b))");
test_valid_selector(".a:has(:not(.b))");
test_valid_selector(".a:has(.b):has(.c)");
test_valid_selector("*|*:has(*)", ":has(*)");
test_valid_selector(":has(*|*)");
test_invalid_selector(".a:has()");
});
it("ID selectors", () => {
test_valid_selector("h1#chapter1");
test_valid_selector("#chapter1");
test_valid_selector("*#z98y", ["*#z98y", "#z98y"]);
});
it("The Matches-Any Pseudo-class: ':is()'", () => {
test_valid_selector(
":is(ul,ol,.list) > [hidden]",
":is(ul, ol, .list) > [hidden]",
);
test_valid_selector(":is(:hover,:focus)", ":is(:hover, :focus)");
test_valid_selector("a:is(:not(:hover))");
test_valid_selector(":is(#a)");
test_valid_selector(".a.b ~ :is(.c.d ~ .e.f)");
test_valid_selector(".a.b ~ .c.d:is(span.e + .f, .g.h > .i.j .k)");
});
it("The negation pseudo-class", () => {
test_valid_selector("button:not([disabled])");
test_valid_selector("*:not(foo)", ["*:not(foo)", ":not(foo)"]);
test_valid_selector(":not(:link):not(:visited)");
test_valid_selector("*|*:not(*)", ":not(*)");
test_valid_selector(":not(:hover)");
test_valid_selector(":not(*|*)");
test_valid_selector("foo:not(bar)");
test_valid_selector(":not(:not(foo))");
test_valid_selector(":not(.a .b)");
test_valid_selector(":not(.a + .b)");
test_valid_selector(":not(.a .b ~ c)");
test_valid_selector(":not(span.a, div.b)");
test_valid_selector(":not(.a .b ~ c, .d .e)");
test_valid_selector(":not(:host)");
test_valid_selector(":not(:host(.a))");
test_valid_selector(":host(:not(.a))");
test_valid_selector(":not(:host(:not(.a)))");
test_valid_selector(
":not([disabled][selected])",
":not([disabled][selected])",
);
test_valid_selector(
":not([disabled],[selected])",
":not([disabled], [selected])",
);
test_invalid_selector(":not()");
test_invalid_selector(":not(:not())");
});
it("Sibling combinators", () => {
test_valid_selector("math + p");
test_valid_selector("h1.opener + h2");
test_valid_selector("h1 ~ pre");
});
it("Universal selector", () => {
test_valid_selector("*");
test_valid_selector("div :first-child", [
"div *:first-child",
"div :first-child",
]);
test_valid_selector("div *:first-child", [
"div *:first-child",
"div :first-child",
]);
});
it("The Specificity-adjustment Pseudo-class: ':where()'", () => {
test_valid_selector(
":where(ul,ol,.list) > [hidden]",
":where(ul, ol, .list) > [hidden]",
);
test_valid_selector(":where(:hover,:focus)", ":where(:hover, :focus)");
test_valid_selector("a:where(:not(:hover))");
test_valid_selector(":where(#a)");
test_valid_selector(".a.b ~ :where(.c.d ~ .e.f)");
test_valid_selector(".a.b ~ .c.d:where(span.e + .f, .g.h > .i.j .k)");
});
});