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 {
nodeName: '#document';
* Document mode.
*
* @see {@link DOCUMENT_MODE} */
mode: DOCUMENT_MODE;
childNodes: ChildNode[];
sourceCodeLocation?: Location | null;
}
export interface DocumentFragment {
nodeName: '#document-fragment';
childNodes: ChildNode[];
sourceCodeLocation?: Location | null;
}
export interface Element {
nodeName: string;
tagName: string;
attrs: Attribute[];
namespaceURI: NS;
sourceCodeLocation?: ElementLocation | null;
parentNode: ParentNode | null;
childNodes: ChildNode[];
}
export interface CommentNode {
nodeName: '#comment';
parentNode: ParentNode | null;
data: string;
sourceCodeLocation?: Location | null;
}
export interface TextNode {
nodeName: '#text';
parentNode: ParentNode | null;
value: string;
sourceCodeLocation?: Location | null;
}
export interface Template extends Element {
nodeName: 'template';
tagName: 'template';
content: DocumentFragment;
}
export interface DocumentType {
nodeName: '#documentType';
parentNode: ParentNode | null;
name: string;
publicId: string;
systemId: string;
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
>;
export const defaultTreeAdapter: TreeAdapter<DefaultTreeAdapterMap> = {
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,
};
},
createTextNode(value: string): TextNode {
return {
nodeName: '#text',
value,
parentNode: null,
};
},
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, defaultTreeAdapter.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, defaultTreeAdapter.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]);
}
}
},
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;
},
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;
},
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');
},
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 };
},
};