• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import { type TreeAdapterTypeMap, type TreeAdapter, type Token, html } from 'parse5';
2import {
3    type AnyNode,
4    type ParentNode,
5    type ChildNode,
6    Element,
7    Document,
8    ProcessingInstruction,
9    Comment,
10    Text,
11    isDirective,
12    isText,
13    isComment,
14    isTag,
15} from 'domhandler';
16
17export type Htmlparser2TreeAdapterMap = TreeAdapterTypeMap<
18    AnyNode,
19    ParentNode,
20    ChildNode,
21    Document,
22    Document,
23    Element,
24    Comment,
25    Text,
26    Element,
27    ProcessingInstruction
28>;
29
30function createTextNode(value: string): Text {
31    return new Text(value);
32}
33
34function enquoteDoctypeId(id: string): string {
35    const quote = id.includes('"') ? "'" : '"';
36
37    return quote + id + quote;
38}
39
40/** @internal */
41export function serializeDoctypeContent(name: string, publicId: string, systemId: string): string {
42    let str = '!DOCTYPE ';
43
44    if (name) {
45        str += name;
46    }
47
48    if (publicId) {
49        str += ` PUBLIC ${enquoteDoctypeId(publicId)}`;
50    } else if (systemId) {
51        str += ' SYSTEM';
52    }
53
54    if (systemId) {
55        str += ` ${enquoteDoctypeId(systemId)}`;
56    }
57
58    return str;
59}
60
61export const adapter: TreeAdapter<Htmlparser2TreeAdapterMap> = {
62    // Re-exports from domhandler
63    isCommentNode: isComment,
64    isElementNode: isTag,
65    isTextNode: isText,
66
67    //Node construction
68    createDocument(): Document {
69        const node = new Document([]);
70        node['x-mode'] = html.DOCUMENT_MODE.NO_QUIRKS;
71        return node;
72    },
73
74    createDocumentFragment(): Document {
75        return new Document([]);
76    },
77
78    createElement(tagName: string, namespaceURI: html.NS, attrs: Token.Attribute[]): Element {
79        const attribs = Object.create(null);
80        const attribsNamespace = Object.create(null);
81        const attribsPrefix = Object.create(null);
82
83        for (let i = 0; i < attrs.length; i++) {
84            const attrName = attrs[i].name;
85
86            attribs[attrName] = attrs[i].value;
87            attribsNamespace[attrName] = attrs[i].namespace;
88            attribsPrefix[attrName] = attrs[i].prefix;
89        }
90
91        const node = new Element(tagName, attribs, []);
92        node.namespace = namespaceURI;
93        node['x-attribsNamespace'] = attribsNamespace;
94        node['x-attribsPrefix'] = attribsPrefix;
95        return node;
96    },
97
98    createCommentNode(data: string): Comment {
99        return new Comment(data);
100    },
101
102    //Tree mutation
103    appendChild(parentNode: ParentNode, newNode: ChildNode): void {
104        const prev = parentNode.children[parentNode.children.length - 1];
105
106        if (prev) {
107            prev.next = newNode;
108            newNode.prev = prev;
109        }
110
111        parentNode.children.push(newNode);
112        newNode.parent = parentNode;
113    },
114
115    insertBefore(parentNode: ParentNode, newNode: ChildNode, referenceNode: ChildNode): void {
116        const insertionIdx = parentNode.children.indexOf(referenceNode);
117        const { prev } = referenceNode;
118
119        if (prev) {
120            prev.next = newNode;
121            newNode.prev = prev;
122        }
123
124        referenceNode.prev = newNode;
125        newNode.next = referenceNode;
126
127        parentNode.children.splice(insertionIdx, 0, newNode);
128        newNode.parent = parentNode;
129    },
130
131    setTemplateContent(templateElement: Element, contentElement: Document): void {
132        adapter.appendChild(templateElement, contentElement as AnyNode as ChildNode);
133    },
134
135    getTemplateContent(templateElement: Element): Document {
136        return templateElement.children[0] as AnyNode as Document;
137    },
138
139    setDocumentType(document: Document, name: string, publicId: string, systemId: string): void {
140        const data = serializeDoctypeContent(name, publicId, systemId);
141        let doctypeNode = document.children.find(
142            (node): node is ProcessingInstruction => isDirective(node) && node.name === '!doctype'
143        );
144
145        if (doctypeNode) {
146            doctypeNode.data = data ?? null;
147        } else {
148            doctypeNode = new ProcessingInstruction('!doctype', data);
149            adapter.appendChild(document, doctypeNode);
150        }
151
152        doctypeNode['x-name'] = name ?? undefined;
153        doctypeNode['x-publicId'] = publicId ?? undefined;
154        doctypeNode['x-systemId'] = systemId ?? undefined;
155    },
156
157    setDocumentMode(document: Document, mode: html.DOCUMENT_MODE): void {
158        document['x-mode'] = mode;
159    },
160
161    getDocumentMode(document: Document): html.DOCUMENT_MODE {
162        return document['x-mode'] as html.DOCUMENT_MODE;
163    },
164
165    detachNode(node: ChildNode): void {
166        if (node.parent) {
167            const idx = node.parent.children.indexOf(node);
168            const { prev, next } = node;
169
170            node.prev = null;
171            node.next = null;
172
173            if (prev) {
174                prev.next = next;
175            }
176
177            if (next) {
178                next.prev = prev;
179            }
180
181            node.parent.children.splice(idx, 1);
182            node.parent = null;
183        }
184    },
185
186    insertText(parentNode: ParentNode, text: string): void {
187        const lastChild = parentNode.children[parentNode.children.length - 1];
188
189        if (lastChild && isText(lastChild)) {
190            lastChild.data += text;
191        } else {
192            adapter.appendChild(parentNode, createTextNode(text));
193        }
194    },
195
196    insertTextBefore(parentNode: ParentNode, text: string, referenceNode: ChildNode): void {
197        const prevNode = parentNode.children[parentNode.children.indexOf(referenceNode) - 1];
198
199        if (prevNode && isText(prevNode)) {
200            prevNode.data += text;
201        } else {
202            adapter.insertBefore(parentNode, createTextNode(text), referenceNode);
203        }
204    },
205
206    adoptAttributes(recipient: Element, attrs: Token.Attribute[]): void {
207        for (let i = 0; i < attrs.length; i++) {
208            const attrName = attrs[i].name;
209
210            if (typeof recipient.attribs[attrName] === 'undefined') {
211                recipient.attribs[attrName] = attrs[i].value;
212                recipient['x-attribsNamespace']![attrName] = attrs[i].namespace!;
213                recipient['x-attribsPrefix']![attrName] = attrs[i].prefix!;
214            }
215        }
216    },
217
218    //Tree traversing
219    getFirstChild(node: ParentNode): ChildNode | null {
220        return node.children[0];
221    },
222
223    getChildNodes(node: ParentNode): ChildNode[] {
224        return node.children;
225    },
226
227    getParentNode(node: AnyNode): ParentNode | null {
228        return node.parent;
229    },
230
231    getAttrList(element: Element): Token.Attribute[] {
232        return element.attributes;
233    },
234
235    //Node data
236    getTagName(element: Element): string {
237        return element.name;
238    },
239
240    getNamespaceURI(element: Element): html.NS {
241        return element.namespace as html.NS;
242    },
243
244    getTextNodeContent(textNode: Text): string {
245        return textNode.data;
246    },
247
248    getCommentNodeContent(commentNode: Comment): string {
249        return commentNode.data;
250    },
251
252    getDocumentTypeNodeName(doctypeNode: ProcessingInstruction): string {
253        return doctypeNode['x-name'] ?? '';
254    },
255
256    getDocumentTypeNodePublicId(doctypeNode: ProcessingInstruction): string {
257        return doctypeNode['x-publicId'] ?? '';
258    },
259
260    getDocumentTypeNodeSystemId(doctypeNode: ProcessingInstruction): string {
261        return doctypeNode['x-systemId'] ?? '';
262    },
263
264    //Node types
265
266    isDocumentTypeNode(node: AnyNode): node is ProcessingInstruction {
267        return isDirective(node) && node.name === '!doctype';
268    },
269
270    // Source code location
271    setNodeSourceCodeLocation(node: AnyNode, location: Token.ElementLocation | null): void {
272        if (location) {
273            node.startIndex = location.startOffset;
274            node.endIndex = location.endOffset;
275        }
276
277        node.sourceCodeLocation = location;
278    },
279
280    getNodeSourceCodeLocation(node: AnyNode): Token.ElementLocation | null | undefined {
281        return node.sourceCodeLocation;
282    },
283
284    updateNodeSourceCodeLocation(node: AnyNode, endLocation: Token.ElementLocation): void {
285        if (endLocation.endOffset != null) node.endIndex = endLocation.endOffset;
286
287        node.sourceCodeLocation = {
288            ...node.sourceCodeLocation,
289            ...endLocation,
290        };
291    },
292};
293