UNPKG

css-what

Version:
205 lines (187 loc) 5.68 kB
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; }