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