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