import { DOCUMENT_MODE, type NS } from '../common/html.js'; import type { Attribute, Location, ElementLocation } from '../common/token.js'; import type { TreeAdapter, TreeAdapterTypeMap } from './interface.js'; export interface Document { /** The name of the node. */ nodeName: '#document'; /** * Document mode. * * @see {@link DOCUMENT_MODE} */ mode: DOCUMENT_MODE; /** The node's children. */ childNodes: ChildNode[]; /** Comment source code location info. Available if location info is enabled. */ sourceCodeLocation?: Location | null; } export interface DocumentFragment { /** The name of the node. */ nodeName: '#document-fragment'; /** The node's children. */ childNodes: ChildNode[]; /** Comment source code location info. Available if location info is enabled. */ sourceCodeLocation?: Location | null; } export interface Element { /** Element tag name. Same as {@link tagName}. */ nodeName: string; /** Element tag name. Same as {@link nodeName}. */ tagName: string; /** List of element attributes. */ attrs: Attribute[]; /** Element namespace. */ namespaceURI: NS; /** Element source code location info, with attributes. Available if location info is enabled. */ sourceCodeLocation?: ElementLocation | null; /** Parent node. */ parentNode: ParentNode | null; /** The node's children. */ childNodes: ChildNode[]; } export interface CommentNode { /** The name of the node. */ nodeName: '#comment'; /** Parent node. */ parentNode: ParentNode | null; /** Comment text. */ data: string; /** Comment source code location info. Available if location info is enabled. */ sourceCodeLocation?: Location | null; } export interface TextNode { nodeName: '#text'; /** Parent node. */ parentNode: ParentNode | null; /** Text content. */ value: string; /** Comment source code location info. Available if location info is enabled. */ sourceCodeLocation?: Location | null; } export interface Template extends Element { nodeName: 'template'; tagName: 'template'; /** The content of a `template` tag. */ content: DocumentFragment; } export interface DocumentType { /** The name of the node. */ nodeName: '#documentType'; /** Parent node. */ parentNode: ParentNode | null; /** Document type name. */ name: string; /** Document type public identifier. */ publicId: string; /** Document type system identifier. */ systemId: string; /** Comment source code location info. Available if location info is enabled. */ sourceCodeLocation?: Location | null; } export type ParentNode = Document | DocumentFragment | Element | Template; export type ChildNode = Element | Template | CommentNode | TextNode | DocumentType; export type Node = ParentNode | ChildNode; export type DefaultTreeAdapterMap = TreeAdapterTypeMap< Node, ParentNode, ChildNode, Document, DocumentFragment, Element, CommentNode, TextNode, Template, DocumentType >; function createTextNode(value: string): TextNode { return { nodeName: '#text', value, parentNode: null, }; } export const defaultTreeAdapter: TreeAdapter = { //Node construction createDocument(): Document { return { nodeName: '#document', mode: DOCUMENT_MODE.NO_QUIRKS, childNodes: [], }; }, createDocumentFragment(): DocumentFragment { return { nodeName: '#document-fragment', childNodes: [], }; }, createElement(tagName: string, namespaceURI: NS, attrs: Attribute[]): Element { return { nodeName: tagName, tagName, attrs, namespaceURI, childNodes: [], parentNode: null, }; }, createCommentNode(data: string): CommentNode { return { nodeName: '#comment', data, parentNode: null, }; }, //Tree mutation appendChild(parentNode: ParentNode, newNode: ChildNode): void { parentNode.childNodes.push(newNode); newNode.parentNode = parentNode; }, insertBefore(parentNode: ParentNode, newNode: ChildNode, referenceNode: ChildNode): void { const insertionIdx = parentNode.childNodes.indexOf(referenceNode); parentNode.childNodes.splice(insertionIdx, 0, newNode); newNode.parentNode = parentNode; }, setTemplateContent(templateElement: Template, contentElement: DocumentFragment): void { templateElement.content = contentElement; }, getTemplateContent(templateElement: Template): DocumentFragment { return templateElement.content; }, setDocumentType(document: Document, name: string, publicId: string, systemId: string): void { const doctypeNode = document.childNodes.find((node): node is DocumentType => node.nodeName === '#documentType'); if (doctypeNode) { doctypeNode.name = name; doctypeNode.publicId = publicId; doctypeNode.systemId = systemId; } else { const node: DocumentType = { nodeName: '#documentType', name, publicId, systemId, parentNode: null, }; defaultTreeAdapter.appendChild(document, node); } }, setDocumentMode(document: Document, mode: DOCUMENT_MODE): void { document.mode = mode; }, getDocumentMode(document: Document): DOCUMENT_MODE { return document.mode; }, detachNode(node: ChildNode): void { if (node.parentNode) { const idx = node.parentNode.childNodes.indexOf(node); node.parentNode.childNodes.splice(idx, 1); node.parentNode = null; } }, insertText(parentNode: ParentNode, text: string): void { if (parentNode.childNodes.length > 0) { const prevNode = parentNode.childNodes[parentNode.childNodes.length - 1]; if (defaultTreeAdapter.isTextNode(prevNode)) { prevNode.value += text; return; } } defaultTreeAdapter.appendChild(parentNode, createTextNode(text)); }, insertTextBefore(parentNode: ParentNode, text: string, referenceNode: ChildNode): void { const prevNode = parentNode.childNodes[parentNode.childNodes.indexOf(referenceNode) - 1]; if (prevNode && defaultTreeAdapter.isTextNode(prevNode)) { prevNode.value += text; } else { defaultTreeAdapter.insertBefore(parentNode, createTextNode(text), referenceNode); } }, adoptAttributes(recipient: Element, attrs: Attribute[]): void { const recipientAttrsMap = new Set(recipient.attrs.map((attr) => attr.name)); for (let j = 0; j < attrs.length; j++) { if (!recipientAttrsMap.has(attrs[j].name)) { recipient.attrs.push(attrs[j]); } } }, //Tree traversing getFirstChild(node: ParentNode): null | ChildNode { return node.childNodes[0]; }, getChildNodes(node: ParentNode): ChildNode[] { return node.childNodes; }, getParentNode(node: ChildNode): null | ParentNode { return node.parentNode; }, getAttrList(element: Element): Attribute[] { return element.attrs; }, //Node data getTagName(element: Element): string { return element.tagName; }, getNamespaceURI(element: Element): NS { return element.namespaceURI; }, getTextNodeContent(textNode: TextNode): string { return textNode.value; }, getCommentNodeContent(commentNode: CommentNode): string { return commentNode.data; }, getDocumentTypeNodeName(doctypeNode: DocumentType): string { return doctypeNode.name; }, getDocumentTypeNodePublicId(doctypeNode: DocumentType): string { return doctypeNode.publicId; }, getDocumentTypeNodeSystemId(doctypeNode: DocumentType): string { return doctypeNode.systemId; }, //Node types isTextNode(node: Node): node is TextNode { return node.nodeName === '#text'; }, isCommentNode(node: Node): node is CommentNode { return node.nodeName === '#comment'; }, isDocumentTypeNode(node: Node): node is DocumentType { return node.nodeName === '#documentType'; }, isElementNode(node: Node): node is Element { return Object.prototype.hasOwnProperty.call(node, 'tagName'); }, // Source code location setNodeSourceCodeLocation(node: Node, location: ElementLocation | null): void { node.sourceCodeLocation = location; }, getNodeSourceCodeLocation(node: Node): ElementLocation | undefined | null { return node.sourceCodeLocation; }, updateNodeSourceCodeLocation(node: Node, endLocation: ElementLocation): void { node.sourceCodeLocation = { ...node.sourceCodeLocation, ...endLocation }; }, };