1import { type TreeAdapterTypeMap, type TreeAdapter, type Token, html } from 'parse5'; 2import { 3 type AnyNode, 4 type ParentNode, 5 type ChildNode, 6 Element, 7 Document, 8 ProcessingInstruction, 9 Comment, 10 Text, 11 isDirective, 12 isText, 13 isComment, 14 isTag, 15} from 'domhandler'; 16 17export type Htmlparser2TreeAdapterMap = TreeAdapterTypeMap< 18 AnyNode, 19 ParentNode, 20 ChildNode, 21 Document, 22 Document, 23 Element, 24 Comment, 25 Text, 26 Element, 27 ProcessingInstruction 28>; 29 30function createTextNode(value: string): Text { 31 return new Text(value); 32} 33 34function enquoteDoctypeId(id: string): string { 35 const quote = id.includes('"') ? "'" : '"'; 36 37 return quote + id + quote; 38} 39 40/** @internal */ 41export function serializeDoctypeContent(name: string, publicId: string, systemId: string): string { 42 let str = '!DOCTYPE '; 43 44 if (name) { 45 str += name; 46 } 47 48 if (publicId) { 49 str += ` PUBLIC ${enquoteDoctypeId(publicId)}`; 50 } else if (systemId) { 51 str += ' SYSTEM'; 52 } 53 54 if (systemId) { 55 str += ` ${enquoteDoctypeId(systemId)}`; 56 } 57 58 return str; 59} 60 61export const adapter: TreeAdapter<Htmlparser2TreeAdapterMap> = { 62 // Re-exports from domhandler 63 isCommentNode: isComment, 64 isElementNode: isTag, 65 isTextNode: isText, 66 67 //Node construction 68 createDocument(): Document { 69 const node = new Document([]); 70 node['x-mode'] = html.DOCUMENT_MODE.NO_QUIRKS; 71 return node; 72 }, 73 74 createDocumentFragment(): Document { 75 return new Document([]); 76 }, 77 78 createElement(tagName: string, namespaceURI: html.NS, attrs: Token.Attribute[]): Element { 79 const attribs = Object.create(null); 80 const attribsNamespace = Object.create(null); 81 const attribsPrefix = Object.create(null); 82 83 for (let i = 0; i < attrs.length; i++) { 84 const attrName = attrs[i].name; 85 86 attribs[attrName] = attrs[i].value; 87 attribsNamespace[attrName] = attrs[i].namespace; 88 attribsPrefix[attrName] = attrs[i].prefix; 89 } 90 91 const node = new Element(tagName, attribs, []); 92 node.namespace = namespaceURI; 93 node['x-attribsNamespace'] = attribsNamespace; 94 node['x-attribsPrefix'] = attribsPrefix; 95 return node; 96 }, 97 98 createCommentNode(data: string): Comment { 99 return new Comment(data); 100 }, 101 102 //Tree mutation 103 appendChild(parentNode: ParentNode, newNode: ChildNode): void { 104 const prev = parentNode.children[parentNode.children.length - 1]; 105 106 if (prev) { 107 prev.next = newNode; 108 newNode.prev = prev; 109 } 110 111 parentNode.children.push(newNode); 112 newNode.parent = parentNode; 113 }, 114 115 insertBefore(parentNode: ParentNode, newNode: ChildNode, referenceNode: ChildNode): void { 116 const insertionIdx = parentNode.children.indexOf(referenceNode); 117 const { prev } = referenceNode; 118 119 if (prev) { 120 prev.next = newNode; 121 newNode.prev = prev; 122 } 123 124 referenceNode.prev = newNode; 125 newNode.next = referenceNode; 126 127 parentNode.children.splice(insertionIdx, 0, newNode); 128 newNode.parent = parentNode; 129 }, 130 131 setTemplateContent(templateElement: Element, contentElement: Document): void { 132 adapter.appendChild(templateElement, contentElement as AnyNode as ChildNode); 133 }, 134 135 getTemplateContent(templateElement: Element): Document { 136 return templateElement.children[0] as AnyNode as Document; 137 }, 138 139 setDocumentType(document: Document, name: string, publicId: string, systemId: string): void { 140 const data = serializeDoctypeContent(name, publicId, systemId); 141 let doctypeNode = document.children.find( 142 (node): node is ProcessingInstruction => isDirective(node) && node.name === '!doctype' 143 ); 144 145 if (doctypeNode) { 146 doctypeNode.data = data ?? null; 147 } else { 148 doctypeNode = new ProcessingInstruction('!doctype', data); 149 adapter.appendChild(document, doctypeNode); 150 } 151 152 doctypeNode['x-name'] = name ?? undefined; 153 doctypeNode['x-publicId'] = publicId ?? undefined; 154 doctypeNode['x-systemId'] = systemId ?? undefined; 155 }, 156 157 setDocumentMode(document: Document, mode: html.DOCUMENT_MODE): void { 158 document['x-mode'] = mode; 159 }, 160 161 getDocumentMode(document: Document): html.DOCUMENT_MODE { 162 return document['x-mode'] as html.DOCUMENT_MODE; 163 }, 164 165 detachNode(node: ChildNode): void { 166 if (node.parent) { 167 const idx = node.parent.children.indexOf(node); 168 const { prev, next } = node; 169 170 node.prev = null; 171 node.next = null; 172 173 if (prev) { 174 prev.next = next; 175 } 176 177 if (next) { 178 next.prev = prev; 179 } 180 181 node.parent.children.splice(idx, 1); 182 node.parent = null; 183 } 184 }, 185 186 insertText(parentNode: ParentNode, text: string): void { 187 const lastChild = parentNode.children[parentNode.children.length - 1]; 188 189 if (lastChild && isText(lastChild)) { 190 lastChild.data += text; 191 } else { 192 adapter.appendChild(parentNode, createTextNode(text)); 193 } 194 }, 195 196 insertTextBefore(parentNode: ParentNode, text: string, referenceNode: ChildNode): void { 197 const prevNode = parentNode.children[parentNode.children.indexOf(referenceNode) - 1]; 198 199 if (prevNode && isText(prevNode)) { 200 prevNode.data += text; 201 } else { 202 adapter.insertBefore(parentNode, createTextNode(text), referenceNode); 203 } 204 }, 205 206 adoptAttributes(recipient: Element, attrs: Token.Attribute[]): void { 207 for (let i = 0; i < attrs.length; i++) { 208 const attrName = attrs[i].name; 209 210 if (typeof recipient.attribs[attrName] === 'undefined') { 211 recipient.attribs[attrName] = attrs[i].value; 212 recipient['x-attribsNamespace']![attrName] = attrs[i].namespace!; 213 recipient['x-attribsPrefix']![attrName] = attrs[i].prefix!; 214 } 215 } 216 }, 217 218 //Tree traversing 219 getFirstChild(node: ParentNode): ChildNode | null { 220 return node.children[0]; 221 }, 222 223 getChildNodes(node: ParentNode): ChildNode[] { 224 return node.children; 225 }, 226 227 getParentNode(node: AnyNode): ParentNode | null { 228 return node.parent; 229 }, 230 231 getAttrList(element: Element): Token.Attribute[] { 232 return element.attributes; 233 }, 234 235 //Node data 236 getTagName(element: Element): string { 237 return element.name; 238 }, 239 240 getNamespaceURI(element: Element): html.NS { 241 return element.namespace as html.NS; 242 }, 243 244 getTextNodeContent(textNode: Text): string { 245 return textNode.data; 246 }, 247 248 getCommentNodeContent(commentNode: Comment): string { 249 return commentNode.data; 250 }, 251 252 getDocumentTypeNodeName(doctypeNode: ProcessingInstruction): string { 253 return doctypeNode['x-name'] ?? ''; 254 }, 255 256 getDocumentTypeNodePublicId(doctypeNode: ProcessingInstruction): string { 257 return doctypeNode['x-publicId'] ?? ''; 258 }, 259 260 getDocumentTypeNodeSystemId(doctypeNode: ProcessingInstruction): string { 261 return doctypeNode['x-systemId'] ?? ''; 262 }, 263 264 //Node types 265 266 isDocumentTypeNode(node: AnyNode): node is ProcessingInstruction { 267 return isDirective(node) && node.name === '!doctype'; 268 }, 269 270 // Source code location 271 setNodeSourceCodeLocation(node: AnyNode, location: Token.ElementLocation | null): void { 272 if (location) { 273 node.startIndex = location.startOffset; 274 node.endIndex = location.endOffset; 275 } 276 277 node.sourceCodeLocation = location; 278 }, 279 280 getNodeSourceCodeLocation(node: AnyNode): Token.ElementLocation | null | undefined { 281 return node.sourceCodeLocation; 282 }, 283 284 updateNodeSourceCodeLocation(node: AnyNode, endLocation: Token.ElementLocation): void { 285 if (endLocation.endOffset != null) node.endIndex = endLocation.endOffset; 286 287 node.sourceCodeLocation = { 288 ...node.sourceCodeLocation, 289 ...endLocation, 290 }; 291 }, 292}; 293