| 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; | |
| } |