update css-what 7.0.0

Signed-off-by: Bojiang <jiangbo91@huawei.com>
Change-Id: I94ace9042cbcb23f1bac2cf46d3d3540f85d781b
This commit is contained in:
Bojiang
2025-12-05 11:36:23 +08:00
parent 57393cccc4
commit 61e00c1c38
14 changed files with 5860 additions and 6142 deletions
+32 -11
View File
@@ -1,5 +1,10 @@
{
"extends": ["eslint:recommended", "plugin:node/recommended", "prettier"],
"extends": [
"eslint:recommended",
"prettier",
"plugin:n/recommended",
"plugin:unicorn/recommended"
],
"env": {
"node": true,
"es6": true
@@ -23,10 +28,14 @@
"curly": [2, "multi-line"],
"no-else-return": 2,
"node/no-unsupported-features/es-syntax": [
2,
{ "ignores": ["modules"] }
]
"unicorn/prefer-module": 0,
"unicorn/filename-case": 0,
"unicorn/no-null": 0,
"unicorn/prefer-code-point": 0,
"unicorn/prefer-string-slice": 0,
"unicorn/prefer-add-event-listener": 0,
"unicorn/prefer-at": 0,
"unicorn/prefer-string-replace-all": 0
},
"overrides": [
{
@@ -40,12 +49,9 @@
"sourceType": "module",
"project": "./tsconfig.eslint.json"
},
"settings": {
"node": {
"tryExtensions": [".js", ".json", ".node", ".ts"]
}
},
"rules": {
"curly": [2, "multi-line"],
"@typescript-eslint/prefer-for-of": 0,
"@typescript-eslint/member-ordering": 0,
"@typescript-eslint/explicit-function-return-type": 0,
@@ -65,7 +71,22 @@
"@typescript-eslint/prefer-includes": 2,
"@typescript-eslint/no-unnecessary-condition": 2,
"@typescript-eslint/switch-exhaustiveness-check": 2,
"@typescript-eslint/prefer-nullish-coalescing": 2
"@typescript-eslint/prefer-nullish-coalescing": 2,
"@typescript-eslint/consistent-type-imports": [
2,
{ "fixStyle": "inline-type-imports" }
],
"@typescript-eslint/consistent-type-exports": 2,
"n/no-missing-import": 0,
"n/no-unsupported-features/es-syntax": 0
}
},
{
"files": "*.spec.ts",
"rules": {
"n/no-unsupported-features/node-builtins": 0,
"n/no-unpublished-import": 0
}
}
]
+1 -1
View File
@@ -3,7 +3,7 @@
"Name": "css-what",
"License": "BSD 2-Clause License",
"License File": "LICENSE",
"Version Number": "v6.1.0",
"Version Number": "v7.0.0",
"Owner": "lixingchi1@huawei.com",
"Upstream URL": "https://github.com/fb55/css-what",
"Description": "a CSS selector parser."
+5517 -5917
View File
File diff suppressed because it is too large Load Diff
+58 -36
View File
@@ -1,59 +1,81 @@
{
"author": "Felix Böhm <me@feedic.com> (http://feedic.com)",
"name": "css-what",
"version": "7.0.0",
"description": "a CSS selector parser",
"version": "6.1.0",
"funding": {
"url": "https://github.com/sponsors/fb55"
},
"repository": {
"type": "git",
"url": "https://github.com/fb55/css-what"
},
"main": "lib/commonjs/index.js",
"module": "lib/es/index.js",
"types": "lib/es/index.d.ts",
"funding": {
"url": "https://github.com/sponsors/fb55"
},
"license": "BSD-2-Clause",
"author": "Felix Böhm <me@feedic.com> (http://feedic.com)",
"sideEffects": false,
"type": "commonjs",
"exports": {
"./package.json": "./package.json",
".": {
"import": {
"types": "./dist/esm/index.d.ts",
"default": "./dist/esm/index.js"
},
"require": {
"types": "./dist/commonjs/index.d.ts",
"default": "./dist/commonjs/index.js"
}
}
},
"main": "./dist/commonjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/commonjs/index.d.ts",
"files": [
"lib/**/*"
"dist",
"src"
],
"scripts": {
"test": "npm run test:jest && npm run lint",
"test:jest": "jest",
"lint": "npm run lint:es && npm run lint:prettier",
"lint:es": "eslint src",
"lint:prettier": "npm run prettier -- --check",
"format": "npm run format:es && npm run format:prettier",
"format:es": "npm run lint:es -- --fix",
"format:prettier": "npm run prettier -- --write",
"lint": "npm run lint:tsc && npm run lint:es && npm run lint:prettier",
"lint:es": "eslint src",
"lint:prettier": "npm run prettier -- --check",
"lint:tsc": "tsc --noEmit",
"prepublishOnly": "tshy",
"prettier": "prettier '**/*.{ts,md,json,yml}'",
"build": "tsc && tsc -p tsconfig.es.json",
"prepare": "npm run build"
"test": "npm run test:vi && npm run lint",
"test:vi": "vitest run"
},
"prettier": {
"tabWidth": 4
},
"devDependencies": {
"@types/jest": "^27.4.1",
"@types/node": "^17.0.23",
"@typescript-eslint/eslint-plugin": "^5.17.0",
"@typescript-eslint/parser": "^5.17.0",
"eslint": "^8.12.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-node": "^11.1.0",
"jest": "^27.5.1",
"prettier": "^2.6.1",
"ts-jest": "^27.1.4",
"typescript": "^4.6.3"
"@types/node": "^22.15.30",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@vitest/coverage-v8": "^3.2.4",
"eslint": "^8.57.1",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-n": "^17.20.0",
"eslint-plugin-unicorn": "^55.0.0",
"prettier": "^3.6.2",
"tshy": "^3.0.2",
"typescript": "^5.8.3",
"vitest": "^3.2.4"
},
"engines": {
"node": ">= 6"
},
"license": "BSD-2-Clause",
"jest": {
"preset": "ts-jest",
"roots": [
"src"
]
},
"prettier": {
"tabWidth": 4
"tshy": {
"exclude": [
"**/*.spec.ts",
"**/__fixtures__/*",
"**/__tests__/*",
"**/__snapshots__/*"
],
"exports": {
"./package.json": "./package.json",
".": "./src/index.ts"
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
# css-what
[![Build Status](https://img.shields.io/github/workflow/status/fb55/css-what/Node.js%20CI/master)](https://github.com/fb55/css-what/actions/workflows/nodejs-test.yml)
[![Node.js CI](https://github.com/fb55/css-what/actions/workflows/nodejs-test.yml/badge.svg)](https://github.com/fb55/css-what/actions/workflows/nodejs-test.yml)
[![Coverage](https://img.shields.io/coveralls/github/fb55/css-what/master)](https://coveralls.io/github/fb55/css-what?branch=master)
A CSS selector parser.
+13
View File
@@ -17279,5 +17279,18 @@
"namespace": null
}
]
],
".before\\:h-\\[calc\\(100\\%-28px\\)\\]::before": [
[
{
"type": "attribute",
"name": "class",
"action": "element",
"value": "before:h-[calc(100%-28px)]",
"namespace": null,
"ignoreCase": "quirks"
},
{ "type": "pseudo-element", "name": "before", "data": null }
]
]
}
+30 -25
View File
@@ -1,9 +1,14 @@
import { Selector, SelectorType, AttributeAction, IgnoreCaseMode } from "..";
import {
Selector,
SelectorType,
AttributeAction,
IgnoreCaseMode,
} from "../types";
export const tests: [
selector: string,
expected: Selector[][],
message: string
message: string,
][] = [
// Tag names
[
@@ -141,7 +146,7 @@ export const tests: [
// Escaped whitespace
[
"#\\ > a ",
String.raw`#\ > a `,
[
[
{
@@ -165,7 +170,7 @@ export const tests: [
"Space between escaped space and combinator",
],
[
".\\ ",
String.raw`.\ `,
[
[
{
@@ -197,7 +202,7 @@ export const tests: [
"Special charecters in selector",
],
[
"\\61 ",
String.raw`\61 `,
[
[
{
@@ -210,7 +215,7 @@ export const tests: [
"Numeric escape with space (BMP)",
],
[
"\\1d306\\01d306",
String.raw`\1d306\01d306`,
[
[
{
@@ -223,7 +228,7 @@ export const tests: [
"Numeric escape (outside BMP)",
],
[
"#\\26 B",
String.raw`#\26 B`,
[
[
{
@@ -321,7 +326,7 @@ export const tests: [
"quoted attribute with internal newline",
],
[
"[name=foo\\.baz]",
String.raw`[name=foo\.baz]`,
[
[
{
@@ -337,7 +342,7 @@ export const tests: [
"attribute with escaped dot",
],
[
"[name=foo\\[bar\\]]",
String.raw`[name=foo\[bar\]]`,
[
[
{
@@ -353,7 +358,7 @@ export const tests: [
"attribute with escaped square brackets",
],
[
"[xml\\:test]",
String.raw`[xml\:test]`,
[
[
{
@@ -506,7 +511,7 @@ export const tests: [
"pseudo selector with data",
],
[
':contains("(a((foo\\\\\\))))")',
String.raw`:contains("(a((foo\\\))))")`,
[
[
{
@@ -617,7 +622,7 @@ export const tests: [
"Underscores don't need escaping",
],
[
"[name=foo\\ bar]",
String.raw`[name=foo\ bar]`,
[
[
{
@@ -633,7 +638,7 @@ export const tests: [
"Escaped space",
],
[
"[name=foo\\.baz]",
String.raw`[name=foo\.baz]`,
[
[
{
@@ -649,7 +654,7 @@ export const tests: [
"Escaped dot",
],
[
"[name=foo\\[baz\\]]",
String.raw`[name=foo\[baz\]]`,
[
[
{
@@ -665,7 +670,7 @@ export const tests: [
"Escaped brackets",
],
[
"[data-attr='foo_baz\\']']",
String.raw`[data-attr='foo_baz\']']`,
[
[
{
@@ -681,7 +686,7 @@ export const tests: [
"Escaped quote + right bracket",
],
[
"[data-attr='\\'']",
String.raw`[data-attr='\'']`,
[
[
{
@@ -697,7 +702,7 @@ export const tests: [
"Quoted quote",
],
[
"[data-attr='\\\\']",
String.raw`[data-attr='\\']`,
[
[
{
@@ -713,7 +718,7 @@ export const tests: [
"Quoted backslash",
],
[
"[data-attr='\\\\\\'']",
String.raw`[data-attr='\\\'']`,
[
[
{
@@ -721,7 +726,7 @@ export const tests: [
namespace: null,
action: AttributeAction.Equals,
name: "data-attr",
value: "\\'",
value: String.raw`\'`,
ignoreCase: IgnoreCaseMode.Unknown,
},
],
@@ -729,7 +734,7 @@ export const tests: [
"Quoted backslash quote",
],
[
"[data-attr='\\\\\\\\']",
String.raw`[data-attr='\\\\']`,
[
[
{
@@ -745,7 +750,7 @@ export const tests: [
"Quoted backslash backslash",
],
[
"[data-attr='\\5C\\\\']",
String.raw`[data-attr='\5C\\']`,
[
[
{
@@ -761,7 +766,7 @@ export const tests: [
"Quoted backslash backslash (numeric escape)",
],
[
"[data-attr='\\5C \\\\']",
String.raw`[data-attr='\5C \\']`,
[
[
{
@@ -793,7 +798,7 @@ export const tests: [
"Quoted backslash backslash (numeric escape with trailing tab)",
],
[
"[data-attr='\\04e00']",
String.raw`[data-attr='\04e00']`,
[
[
{
@@ -801,7 +806,7 @@ export const tests: [
namespace: null,
action: AttributeAction.Equals,
name: "data-attr",
value: "\u4e00",
value: "\u4E00",
ignoreCase: IgnoreCaseMode.Unknown,
},
],
@@ -809,7 +814,7 @@ export const tests: [
"Long numeric escape (BMP)",
],
[
"[data-attr='\\01D306A']",
String.raw`[data-attr='\01D306A']`,
[
[
{
+12 -7
View File
@@ -1,5 +1,6 @@
import { readFileSync } from "fs";
import { parse } from ".";
import { readFileSync } from "node:fs";
import { describe, it, expect } from "vitest";
import { parse } from "./parse";
import { tests } from "./__fixtures__/tests";
const broken = [
@@ -29,18 +30,16 @@ const broken = [
describe("Parse", () => {
describe("Own tests", () => {
for (const [selector, expected, message] of tests) {
test(message, () =>
expect(parse(selector)).toStrictEqual(expected)
);
it(message, () => expect(parse(selector)).toStrictEqual(expected));
}
});
describe("Collected selectors (qwery, sizzle, nwmatcher)", () => {
const out = JSON.parse(
readFileSync(`${__dirname}/__fixtures__/out.json`, "utf8")
readFileSync(`${__dirname}/__fixtures__/out.json`, "utf8"),
);
for (const s of Object.keys(out)) {
test(s, () => {
it(s, () => {
expect(parse(s)).toStrictEqual(out[s]);
});
}
@@ -61,4 +60,10 @@ describe("Parse", () => {
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" }],
]);
});
});
+107 -76
View File
@@ -8,7 +8,7 @@ import {
DataType,
} from "./types";
const reName = /^[^\\#]?(?:\\(?:[\da-f]{1,6}\s?|.)|[\w\-\u00b0-\uFFFF])+/;
const reName = /^[^#\\]?(?:\\(?:[\da-f]{1,6}\s?|.)|[\w\u00B0-\uFFFF-])+/;
const reEscape = /\\([\da-f]{1,6}\s?|(\s)|.)/gi;
const enum CharCode {
@@ -26,7 +26,6 @@ const enum CharCode {
QuestionMark = 63,
ExclamationMark = 33,
Slash = 47,
Star = 42,
Equal = 61,
Dollar = 36,
Pipe = 124,
@@ -67,6 +66,19 @@ const unpackPseudos = new Set([
"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
@@ -81,10 +93,12 @@ export function isTraversal(selector: Selector): selector is Traversal {
case SelectorType.Descendant:
case SelectorType.Parent:
case SelectorType.Sibling:
case SelectorType.ColumnCombinator:
case SelectorType.ColumnCombinator: {
return true;
default:
}
default: {
return false;
}
}
}
@@ -92,20 +106,23 @@ 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 = parseInt(escaped, 16) - 0x10000;
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 + 0x10000)
: // Supplemental Plane codepoint (surrogate pair)
String.fromCharCode((high >> 10) | 0xd800, (high & 0x3ff) | 0xdc00);
? // 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(str: string) {
return str.replace(reEscape, funescape);
function unescapeCSS(cssString: string) {
return cssString.replace(reEscape, funescape);
}
function isQuote(c: number): boolean {
@@ -123,10 +140,9 @@ function isWhitespace(c: number): boolean {
}
/**
* Parses `selector`, optionally with the passed `options`.
* Parses `selector`.
*
* @param selector Selector to parse.
* @param options Options for parsing.
* @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.
@@ -146,7 +162,7 @@ export function parse(selector: string): Selector[][] {
function parseSelector(
subselects: Selector[][],
selector: string,
selectorIndex: number
selectorIndex: number,
): number {
let tokens: Selector[] = [];
@@ -155,7 +171,7 @@ function parseSelector(
if (!match) {
throw new Error(
`Expected name, found ${selector.slice(selectorIndex)}`
`Expected name, found ${selector.slice(selectorIndex)}`,
);
}
@@ -178,40 +194,37 @@ function parseSelector(
function readValueWithParenthesis(): string {
selectorIndex += 1;
const start = selectorIndex;
let counter = 1;
for (
;
counter > 0 && selectorIndex < selector.length;
let counter = 1;
selectorIndex < selector.length;
selectorIndex++
) {
if (
selector.charCodeAt(selectorIndex) ===
CharCode.LeftParenthesis &&
!isEscaped(selectorIndex)
) {
counter++;
} else if (
selector.charCodeAt(selectorIndex) ===
CharCode.RightParenthesis &&
!isEscaped(selectorIndex)
) {
counter--;
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;
}
}
}
if (counter) {
throw new Error("Parenthesis not matched");
}
return unescapeCSS(selector.slice(start, selectorIndex - 1));
}
function isEscaped(pos: number): boolean {
let slashCount = 0;
while (selector.charCodeAt(--pos) === CharCode.BackSlash) slashCount++;
return (slashCount & 1) === 1;
throw new Error("Parenthesis not matched");
}
function ensureNotTraversal() {
@@ -254,7 +267,7 @@ function parseSelector(
*/
function finalizeSubselector() {
if (
tokens.length &&
tokens.length > 0 &&
tokens[tokens.length - 1].type === SelectorType.Descendant
) {
tokens.pop();
@@ -357,7 +370,7 @@ function parseSelector(
let action: AttributeAction = AttributeAction.Exists;
const possibleAction = actionTypes.get(
selector.charCodeAt(selectorIndex)
selector.charCodeAt(selectorIndex),
);
if (possibleAction) {
@@ -386,57 +399,65 @@ function parseSelector(
if (action !== "exists") {
if (isQuote(selector.charCodeAt(selectorIndex))) {
const quote = selector.charCodeAt(selectorIndex);
let sectionEnd = selectorIndex + 1;
selectorIndex += 1;
const sectionStart = selectorIndex;
while (
sectionEnd < selector.length &&
(selector.charCodeAt(sectionEnd) !== quote ||
isEscaped(sectionEnd))
selectorIndex < selector.length &&
selector.charCodeAt(selectorIndex) !== quote
) {
sectionEnd += 1;
selectorIndex +=
// Skip next character if it is escaped
selector.charCodeAt(selectorIndex) ===
CharCode.BackSlash
? 2
: 1;
}
if (selector.charCodeAt(sectionEnd) !== quote) {
if (selector.charCodeAt(selectorIndex) !== quote) {
throw new Error("Attribute value didn't end");
}
value = unescapeCSS(
selector.slice(selectorIndex + 1, sectionEnd)
selector.slice(sectionStart, selectorIndex),
);
selectorIndex = sectionEnd + 1;
selectorIndex += 1;
} else {
const valueStart = selectorIndex;
while (
selectorIndex < selector.length &&
((!isWhitespace(
selector.charCodeAt(selectorIndex)
) &&
selector.charCodeAt(selectorIndex) !==
CharCode.RightSquareBracket) ||
isEscaped(selectorIndex))
!isWhitespace(selector.charCodeAt(selectorIndex)) &&
selector.charCodeAt(selectorIndex) !==
CharCode.RightSquareBracket
) {
selectorIndex += 1;
selectorIndex +=
// Skip next character if it is escaped
selector.charCodeAt(selectorIndex) ===
CharCode.BackSlash
? 2
: 1;
}
value = unescapeCSS(
selector.slice(valueStart, selectorIndex)
selector.slice(valueStart, selectorIndex),
);
}
stripWhitespace(0);
// See if we have a force ignore flag
const forceIgnore =
selector.charCodeAt(selectorIndex) | 0x20;
// If the forceIgnore flag is set (either `i` or `s`), use that value
if (forceIgnore === CharCode.LowerS) {
ignoreCase = false;
stripWhitespace(1);
} else if (forceIgnore === CharCode.LowerI) {
ignoreCase = true;
stripWhitespace(1);
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;
}
}
}
@@ -472,10 +493,20 @@ function parseSelector(
? readValueWithParenthesis()
: null,
});
continue;
break;
}
const name = getName(1).toLowerCase();
if (pseudosToPseudoElements.has(name)) {
tokens.push({
type: SelectorType.PseudoElement,
name,
data: null,
});
break;
}
let data: DataType = null;
if (
@@ -485,7 +516,7 @@ function parseSelector(
if (unpackPseudos.has(name)) {
if (isQuote(selector.charCodeAt(selectorIndex + 1))) {
throw new Error(
`Pseudo-selector ${name} cannot be quoted`
`Pseudo-selector ${name} cannot be quoted`,
);
}
@@ -493,7 +524,7 @@ function parseSelector(
selectorIndex = parseSelector(
data,
selector,
selectorIndex + 1
selectorIndex + 1,
);
if (
@@ -501,7 +532,7 @@ function parseSelector(
CharCode.RightParenthesis
) {
throw new Error(
`Missing closing parenthesis in :${name} (${selector})`
`Missing closing parenthesis in :${name} (${selector})`,
);
}
@@ -592,7 +623,7 @@ function parseSelector(
tokens.push(
name === "*"
? { type: SelectorType.Universal, namespace }
: { type: SelectorType.Tag, name, namespace }
: { type: SelectorType.Tag, name, namespace },
);
}
}
+5 -4
View File
@@ -1,11 +1,12 @@
import { readFileSync } from "fs";
import { parse, stringify } from ".";
import { readFileSync } from "node:fs";
import { describe, it, expect } from "vitest";
import { parse, stringify } from "./index";
import { tests } from "./__fixtures__/tests";
describe("Stringify & re-parse", () => {
describe("Own tests", () => {
for (const [selector, expected, message] of tests) {
test(`${message} (${selector})`, () => {
it(`${message} (${selector})`, () => {
expect(parse(stringify(expected))).toStrictEqual(expected);
});
}
@@ -13,7 +14,7 @@ describe("Stringify & re-parse", () => {
it("Collected Selectors (qwery, sizzle, nwmatcher)", () => {
const out = JSON.parse(
readFileSync(`${__dirname}/__fixtures__/out.json`, "utf8")
readFileSync(`${__dirname}/__fixtures__/out.json`, "utf8"),
);
for (const s of Object.keys(out)) {
expect(parse(stringify(out[s]))).toStrictEqual(out[s]);
+68 -43
View File
@@ -1,17 +1,17 @@
import { Selector, SelectorType, AttributeAction } from "./types";
const attribValChars = ["\\", '"'];
const pseudoValChars = [...attribValChars, "(", ")"];
const attribValueChars = ["\\", '"'];
const pseudoValueChars = [...attribValueChars, "(", ")"];
const charsToEscapeInAttributeValue = new Set(
attribValChars.map((c) => c.charCodeAt(0))
attribValueChars.map((c) => c.charCodeAt(0)),
);
const charsToEscapeInPseudoValue = new Set(
pseudoValChars.map((c) => c.charCodeAt(0))
pseudoValueChars.map((c) => c.charCodeAt(0)),
);
const charsToEscapeInName = new Set(
[
...pseudoValChars,
...pseudoValueChars,
"~",
"^",
"$",
@@ -24,7 +24,8 @@ const charsToEscapeInName = new Set(
"]",
" ",
".",
].map((c) => c.charCodeAt(0))
"%",
].map((c) => c.charCodeAt(0)),
);
/**
@@ -34,48 +35,63 @@ const charsToEscapeInName = new Set(
*/
export function stringify(selector: Selector[][]): string {
return selector
.map((token) => token.map(stringifyToken).join(""))
.map((token) =>
token
.map((token, index, array) =>
stringifyToken(token, index, array),
)
.join(""),
)
.join(", ");
}
function stringifyToken(
token: Selector,
index: number,
arr: Selector[]
array: Selector[],
): string {
switch (token.type) {
// Simple types
case SelectorType.Child:
case SelectorType.Child: {
return index === 0 ? "> " : " > ";
case SelectorType.Parent:
}
case SelectorType.Parent: {
return index === 0 ? "< " : " < ";
case SelectorType.Sibling:
}
case SelectorType.Sibling: {
return index === 0 ? "~ " : " ~ ";
case SelectorType.Adjacent:
}
case SelectorType.Adjacent: {
return index === 0 ? "+ " : " + ";
case SelectorType.Descendant:
}
case SelectorType.Descendant: {
return " ";
case SelectorType.ColumnCombinator:
}
case SelectorType.ColumnCombinator: {
return index === 0 ? "|| " : " || ";
case SelectorType.Universal:
}
case SelectorType.Universal: {
// Return an empty string if the selector isn't needed.
return token.namespace === "*" &&
index + 1 < arr.length &&
"name" in arr[index + 1]
index + 1 < array.length &&
"name" in array[index + 1]
? ""
: `${getNamespace(token.namespace)}*`;
}
case SelectorType.Tag:
case SelectorType.Tag: {
return getNamespacedName(token);
}
case SelectorType.PseudoElement:
case SelectorType.PseudoElement: {
return `::${escapeName(token.name, charsToEscapeInName)}${
token.data === null
? ""
: `(${escapeName(token.data, charsToEscapeInPseudoValue)})`
}`;
}
case SelectorType.Pseudo:
case SelectorType.Pseudo: {
return `:${escapeName(token.name, charsToEscapeInName)}${
token.data === null
? ""
@@ -83,11 +99,12 @@ function stringifyToken(
typeof token.data === "string"
? escapeName(
token.data,
charsToEscapeInPseudoValue
charsToEscapeInPseudoValue,
)
: stringify(token.data)
})`
}`;
}
case SelectorType.Attribute: {
if (
@@ -115,7 +132,7 @@ function stringifyToken(
return `[${name}${getActionValue(token.action)}="${escapeName(
token.value,
charsToEscapeInAttributeValue
charsToEscapeInAttributeValue,
)}"${
token.ignoreCase === null ? "" : token.ignoreCase ? " i" : " s"
}]`;
@@ -125,22 +142,30 @@ function stringifyToken(
function getActionValue(action: AttributeAction): string {
switch (action) {
case AttributeAction.Equals:
case AttributeAction.Equals: {
return "";
case AttributeAction.Element:
}
case AttributeAction.Element: {
return "~";
case AttributeAction.Start:
}
case AttributeAction.Start: {
return "^";
case AttributeAction.End:
}
case AttributeAction.End: {
return "$";
case AttributeAction.Any:
}
case AttributeAction.Any: {
return "*";
case AttributeAction.Not:
}
case AttributeAction.Not: {
return "!";
case AttributeAction.Hyphen:
}
case AttributeAction.Hyphen: {
return "|";
case AttributeAction.Exists:
}
default: {
throw new Error("Shouldn't be here");
}
}
}
@@ -150,30 +175,30 @@ function getNamespacedName(token: {
}): string {
return `${getNamespace(token.namespace)}${escapeName(
token.name,
charsToEscapeInName
charsToEscapeInName,
)}`;
}
function getNamespace(namespace: string | null): string {
return namespace !== null
? `${
return namespace === null
? ""
: `${
namespace === "*"
? "*"
: escapeName(namespace, charsToEscapeInName)
}|`
: "";
}|`;
}
function escapeName(str: string, charsToEscape: Set<number>): string {
let lastIdx = 0;
let ret = "";
function escapeName(name: string, charsToEscape: Set<number>): string {
let lastIndex = 0;
let escapedName = "";
for (let i = 0; i < str.length; i++) {
if (charsToEscape.has(str.charCodeAt(i))) {
ret += `${str.slice(lastIdx, i)}\\${str.charAt(i)}`;
lastIdx = i + 1;
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 ret.length > 0 ? ret + str.slice(lastIdx) : str;
return escapedName.length > 0 ? escapedName + name.slice(lastIndex) : name;
}
+7 -6
View File
@@ -4,11 +4,12 @@
* @license BSD-3-Clause (https://github.com/web-platform-tests/wpt/blob/master/LICENSE.md)
*/
import { parse, stringify } from ".";
import { describe, it, expect } from "vitest";
import { parse, stringify } from "./index";
function test_valid_selector(
selector: string,
serialized: string | string[] = selector
serialized: string | string[] = selector,
) {
const result = stringify(parse(selector));
if (Array.isArray(serialized)) {
@@ -110,7 +111,7 @@ describe("Web Platform Tests", () => {
it("The Matches-Any Pseudo-class: ':is()'", () => {
test_valid_selector(
":is(ul,ol,.list) > [hidden]",
":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))");
@@ -140,11 +141,11 @@ describe("Web Platform Tests", () => {
test_valid_selector(":not(:host(:not(.a)))");
test_valid_selector(
":not([disabled][selected])",
":not([disabled][selected])"
":not([disabled][selected])",
);
test_valid_selector(
":not([disabled],[selected])",
":not([disabled], [selected])"
":not([disabled], [selected])",
);
test_invalid_selector(":not()");
@@ -172,7 +173,7 @@ describe("Web Platform Tests", () => {
it("The Specificity-adjustment Pseudo-class: ':where()'", () => {
test_valid_selector(
":where(ul,ol,.list) > [hidden]",
":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))");
+2 -2
View File
@@ -2,8 +2,8 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"target": "ES2019",
"module": "es2015",
"outDir": "lib/es",
"module": "CommonJS",
"outDir": "dist/esm",
"moduleResolution": "node"
}
}
+7 -13
View File
@@ -1,19 +1,21 @@
{
"compilerOptions": {
/* Basic Options */
"target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
"target": "ES2019" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
"module": "CommonJS" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
// "lib": [], /* Specify library files to be included in the compilation. */
"declaration": true /* Generates corresponding '.d.ts' file. */,
"declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */,
// "sourceMap": true, /* Generates corresponding '.map' file. */
"outDir": "lib/commonjs" /* Redirect output structure to the directory. */,
"outDir": "dist/esm" /* Redirect output structure to the directory. */,
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */,
/* Additional Checks */
"isolatedDeclarations": true,
"isolatedModules": true,
"noUnusedLocals": true /* Report errors on unused locals. */,
"noUnusedParameters": true /* Report errors on unused parameters. */,
"noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
@@ -21,14 +23,6 @@
/* Module Resolution Options */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
"resolveJsonModule": true
},
"include": ["src"],
"exclude": [
"**/*.spec.ts",
"**/__fixtures__/*",
"**/__tests__/*",
"**/__snapshots__/*"
]
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
}
}