• 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 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