1import { Selector, SelectorType, AttributeAction } from "./types"; 2 3const attribValChars = ["\\", '"']; 4const pseudoValChars = [...attribValChars, "(", ")"]; 5 6const charsToEscapeInAttributeValue = new Set( 7 attribValChars.map((c) => c.charCodeAt(0)) 8); 9const charsToEscapeInPseudoValue = new Set( 10 pseudoValChars.map((c) => c.charCodeAt(0)) 11); 12const charsToEscapeInName = new Set( 13 [ 14 ...pseudoValChars, 15 "~", 16 "^", 17 "$", 18 "*", 19 "+", 20 "!", 21 "|", 22 ":", 23 "[", 24 "]", 25 " ", 26 ".", 27 ].map((c) => c.charCodeAt(0)) 28); 29 30/** 31 * Turns `selector` back into a string. 32 * 33 * @param selector Selector to stringify. 34 */ 35export function stringify(selector: Selector[][]): string { 36 return selector 37 .map((token) => token.map(stringifyToken).join("")) 38 .join(", "); 39} 40 41function stringifyToken( 42 token: Selector, 43 index: number, 44 arr: Selector[] 45): string { 46 switch (token.type) { 47 // Simple types 48 case SelectorType.Child: 49 return index === 0 ? "> " : " > "; 50 case SelectorType.Parent: 51 return index === 0 ? "< " : " < "; 52 case SelectorType.Sibling: 53 return index === 0 ? "~ " : " ~ "; 54 case SelectorType.Adjacent: 55 return index === 0 ? "+ " : " + "; 56 case SelectorType.Descendant: 57 return " "; 58 case SelectorType.ColumnCombinator: 59 return index === 0 ? "|| " : " || "; 60 case SelectorType.Universal: 61 // Return an empty string if the selector isn't needed. 62 return token.namespace === "*" && 63 index + 1 < arr.length && 64 "name" in arr[index + 1] 65 ? "" 66 : `${getNamespace(token.namespace)}*`; 67 68 case SelectorType.Tag: 69 return getNamespacedName(token); 70 71 case SelectorType.PseudoElement: 72 return `::${escapeName(token.name, charsToEscapeInName)}${ 73 token.data === null 74 ? "" 75 : `(${escapeName(token.data, charsToEscapeInPseudoValue)})` 76 }`; 77 78 case SelectorType.Pseudo: 79 return `:${escapeName(token.name, charsToEscapeInName)}${ 80 token.data === null 81 ? "" 82 : `(${ 83 typeof token.data === "string" 84 ? escapeName( 85 token.data, 86 charsToEscapeInPseudoValue 87 ) 88 : stringify(token.data) 89 })` 90 }`; 91 92 case SelectorType.Attribute: { 93 if ( 94 token.name === "id" && 95 token.action === AttributeAction.Equals && 96 token.ignoreCase === "quirks" && 97 !token.namespace 98 ) { 99 return `#${escapeName(token.value, charsToEscapeInName)}`; 100 } 101 if ( 102 token.name === "class" && 103 token.action === AttributeAction.Element && 104 token.ignoreCase === "quirks" && 105 !token.namespace 106 ) { 107 return `.${escapeName(token.value, charsToEscapeInName)}`; 108 } 109 110 const name = getNamespacedName(token); 111 112 if (token.action === AttributeAction.Exists) { 113 return `[${name}]`; 114 } 115 116 return `[${name}${getActionValue(token.action)}="${escapeName( 117 token.value, 118 charsToEscapeInAttributeValue 119 )}"${ 120 token.ignoreCase === null ? "" : token.ignoreCase ? " i" : " s" 121 }]`; 122 } 123 } 124} 125 126function getActionValue(action: AttributeAction): string { 127 switch (action) { 128 case AttributeAction.Equals: 129 return ""; 130 case AttributeAction.Element: 131 return "~"; 132 case AttributeAction.Start: 133 return "^"; 134 case AttributeAction.End: 135 return "$"; 136 case AttributeAction.Any: 137 return "*"; 138 case AttributeAction.Not: 139 return "!"; 140 case AttributeAction.Hyphen: 141 return "|"; 142 case AttributeAction.Exists: 143 throw new Error("Shouldn't be here"); 144 } 145} 146 147function getNamespacedName(token: { 148 name: string; 149 namespace: string | null; 150}): string { 151 return `${getNamespace(token.namespace)}${escapeName( 152 token.name, 153 charsToEscapeInName 154 )}`; 155} 156 157function getNamespace(namespace: string | null): string { 158 return namespace !== null 159 ? `${ 160 namespace === "*" 161 ? "*" 162 : escapeName(namespace, charsToEscapeInName) 163 }|` 164 : ""; 165} 166 167function escapeName(str: string, charsToEscape: Set<number>): string { 168 let lastIdx = 0; 169 let ret = ""; 170 171 for (let i = 0; i < str.length; i++) { 172 if (charsToEscape.has(str.charCodeAt(i))) { 173 ret += `${str.slice(lastIdx, i)}\\${str.charAt(i)}`; 174 lastIdx = i + 1; 175 } 176 } 177 178 return ret.length > 0 ? ret + str.slice(lastIdx) : str; 179} 180