1import { DOCUMENT_MODE, type NS } from '../common/html.js'; 2import type { Attribute, Location, ElementLocation } from '../common/token.js'; 3import type { TreeAdapter, TreeAdapterTypeMap } from './interface.js'; 4 5export interface Document { 6 /** The name of the node. */ 7 nodeName: '#document'; 8 /** 9 * Document mode. 10 * 11 * @see {@link DOCUMENT_MODE} */ 12 mode: DOCUMENT_MODE; 13 /** The node's children. */ 14 childNodes: ChildNode[]; 15 /** Comment source code location info. Available if location info is enabled. */ 16 sourceCodeLocation?: Location | null; 17} 18 19export interface DocumentFragment { 20 /** The name of the node. */ 21 nodeName: '#document-fragment'; 22 /** The node's children. */ 23 childNodes: ChildNode[]; 24 /** Comment source code location info. Available if location info is enabled. */ 25 sourceCodeLocation?: Location | null; 26} 27 28export interface Element { 29 /** Element tag name. Same as {@link tagName}. */ 30 nodeName: string; 31 /** Element tag name. Same as {@link nodeName}. */ 32 tagName: string; 33 /** List of element attributes. */ 34 attrs: Attribute[]; 35 /** Element namespace. */ 36 namespaceURI: NS; 37 /** Element source code location info, with attributes. Available if location info is enabled. */ 38 sourceCodeLocation?: ElementLocation | null; 39 /** Parent node. */ 40 parentNode: ParentNode | null; 41 /** The node's children. */ 42 childNodes: ChildNode[]; 43} 44 45export interface CommentNode { 46 /** The name of the node. */ 47 nodeName: '#comment'; 48 /** Parent node. */ 49 parentNode: ParentNode | null; 50 /** Comment text. */ 51 data: string; 52 /** Comment source code location info. Available if location info is enabled. */ 53 sourceCodeLocation?: Location | null; 54} 55 56export interface TextNode { 57 nodeName: '#text'; 58 /** Parent node. */ 59 parentNode: ParentNode | null; 60 /** Text content. */ 61 value: string; 62 /** Comment source code location info. Available if location info is enabled. */ 63 sourceCodeLocation?: Location | null; 64} 65 66export interface Template extends Element { 67 nodeName: 'template'; 68 tagName: 'template'; 69 /** The content of a `template` tag. */ 70 content: DocumentFragment; 71} 72 73export interface DocumentType { 74 /** The name of the node. */ 75 nodeName: '#documentType'; 76 /** Parent node. */ 77 parentNode: ParentNode | null; 78 /** Document type name. */ 79 name: string; 80 /** Document type public identifier. */ 81 publicId: string; 82 /** Document type system identifier. */ 83 systemId: string; 84 /** Comment source code location info. Available if location info is enabled. */ 85 sourceCodeLocation?: Location | null; 86} 87 88export type ParentNode = Document | DocumentFragment | Element | Template; 89export type ChildNode = Element | Template | CommentNode | TextNode | DocumentType; 90export type Node = ParentNode | ChildNode; 91 92export type DefaultTreeAdapterMap = TreeAdapterTypeMap< 93 Node, 94 ParentNode, 95 ChildNode, 96 Document, 97 DocumentFragment, 98 Element, 99 CommentNode, 100 TextNode, 101 Template, 102 DocumentType 103>; 104 105function createTextNode(value: string): TextNode { 106 return { 107 nodeName: '#text', 108 value, 109 parentNode: null, 110 }; 111} 112 113export const defaultTreeAdapter: TreeAdapter<DefaultTreeAdapterMap> = { 114 //Node construction 115 createDocument(): Document { 116 return { 117 nodeName: '#document', 118 mode: DOCUMENT_MODE.NO_QUIRKS, 119 childNodes: [], 120 }; 121 }, 122 123 createDocumentFragment(): DocumentFragment { 124 return { 125 nodeName: '#document-fragment', 126 childNodes: [], 127 }; 128 }, 129 130 createElement(tagName: string, namespaceURI: NS, attrs: Attribute[]): Element { 131 return { 132 nodeName: tagName, 133 tagName, 134 attrs, 135 namespaceURI, 136 childNodes: [], 137 parentNode: null, 138 }; 139 }, 140 141 createCommentNode(data: string): CommentNode { 142 return { 143 nodeName: '#comment', 144 data, 145 parentNode: null, 146 }; 147 }, 148 149 //Tree mutation 150 appendChild(parentNode: ParentNode, newNode: ChildNode): void { 151 parentNode.childNodes.push(newNode); 152 newNode.parentNode = parentNode; 153 }, 154 155 insertBefore(parentNode: ParentNode, newNode: ChildNode, referenceNode: ChildNode): void { 156 const insertionIdx = parentNode.childNodes.indexOf(referenceNode); 157 158 parentNode.childNodes.splice(insertionIdx, 0, newNode); 159 newNode.parentNode = parentNode; 160 }, 161 162 setTemplateContent(templateElement: Template, contentElement: DocumentFragment): void { 163 templateElement.content = contentElement; 164 }, 165 166 getTemplateContent(templateElement: Template): DocumentFragment { 167 return templateElement.content; 168 }, 169 170 setDocumentType(document: Document, name: string, publicId: string, systemId: string): void { 171 const doctypeNode = document.childNodes.find((node): node is DocumentType => node.nodeName === '#documentType'); 172 173 if (doctypeNode) { 174 doctypeNode.name = name; 175 doctypeNode.publicId = publicId; 176 doctypeNode.systemId = systemId; 177 } else { 178 const node: DocumentType = { 179 nodeName: '#documentType', 180 name, 181 publicId, 182 systemId, 183 parentNode: null, 184 }; 185 defaultTreeAdapter.appendChild(document, node); 186 } 187 }, 188 189 setDocumentMode(document: Document, mode: DOCUMENT_MODE): void { 190 document.mode = mode; 191 }, 192 193 getDocumentMode(document: Document): DOCUMENT_MODE { 194 return document.mode; 195 }, 196 197 detachNode(node: ChildNode): void { 198 if (node.parentNode) { 199 const idx = node.parentNode.childNodes.indexOf(node); 200 201 node.parentNode.childNodes.splice(idx, 1); 202 node.parentNode = null; 203 } 204 }, 205 206 insertText(parentNode: ParentNode, text: string): void { 207 if (parentNode.childNodes.length > 0) { 208 const prevNode = parentNode.childNodes[parentNode.childNodes.length - 1]; 209 210 if (defaultTreeAdapter.isTextNode(prevNode)) { 211 prevNode.value += text; 212 return; 213 } 214 } 215 216 defaultTreeAdapter.appendChild(parentNode, createTextNode(text)); 217 }, 218 219 insertTextBefore(parentNode: ParentNode, text: string, referenceNode: ChildNode): void { 220 const prevNode = parentNode.childNodes[parentNode.childNodes.indexOf(referenceNode) - 1]; 221 222 if (prevNode && defaultTreeAdapter.isTextNode(prevNode)) { 223 prevNode.value += text; 224 } else { 225 defaultTreeAdapter.insertBefore(parentNode, createTextNode(text), referenceNode); 226 } 227 }, 228 229 adoptAttributes(recipient: Element, attrs: Attribute[]): void { 230 const recipientAttrsMap = new Set(recipient.attrs.map((attr) => attr.name)); 231 232 for (let j = 0; j < attrs.length; j++) { 233 if (!recipientAttrsMap.has(attrs[j].name)) { 234 recipient.attrs.push(attrs[j]); 235 } 236 } 237 }, 238 239 //Tree traversing 240 getFirstChild(node: ParentNode): null | ChildNode { 241 return node.childNodes[0]; 242 }, 243 244 getChildNodes(node: ParentNode): ChildNode[] { 245 return node.childNodes; 246 }, 247 248 getParentNode(node: ChildNode): null | ParentNode { 249 return node.parentNode; 250 }, 251 252 getAttrList(element: Element): Attribute[] { 253 return element.attrs; 254 }, 255 256 //Node data 257 getTagName(element: Element): string { 258 return element.tagName; 259 }, 260 261 getNamespaceURI(element: Element): NS { 262 return element.namespaceURI; 263 }, 264 265 getTextNodeContent(textNode: TextNode): string { 266 return textNode.value; 267 }, 268 269 getCommentNodeContent(commentNode: CommentNode): string { 270 return commentNode.data; 271 }, 272 273 getDocumentTypeNodeName(doctypeNode: DocumentType): string { 274 return doctypeNode.name; 275 }, 276 277 getDocumentTypeNodePublicId(doctypeNode: DocumentType): string { 278 return doctypeNode.publicId; 279 }, 280 281 getDocumentTypeNodeSystemId(doctypeNode: DocumentType): string { 282 return doctypeNode.systemId; 283 }, 284 285 //Node types 286 isTextNode(node: Node): node is TextNode { 287 return node.nodeName === '#text'; 288 }, 289 290 isCommentNode(node: Node): node is CommentNode { 291 return node.nodeName === '#comment'; 292 }, 293 294 isDocumentTypeNode(node: Node): node is DocumentType { 295 return node.nodeName === '#documentType'; 296 }, 297 298 isElementNode(node: Node): node is Element { 299 return Object.prototype.hasOwnProperty.call(node, 'tagName'); 300 }, 301 302 // Source code location 303 setNodeSourceCodeLocation(node: Node, location: ElementLocation | null): void { 304 node.sourceCodeLocation = location; 305 }, 306 307 getNodeSourceCodeLocation(node: Node): ElementLocation | undefined | null { 308 return node.sourceCodeLocation; 309 }, 310 311 updateNodeSourceCodeLocation(node: Node, endLocation: ElementLocation): void { 312 node.sourceCodeLocation = { ...node.sourceCodeLocation, ...endLocation }; 313 }, 314}; 315