• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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