• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3const doctype = require('parse5/lib/common/doctype');
4const { DOCUMENT_MODE } = require('parse5/lib/common/html');
5
6//Conversion tables for DOM Level1 structure emulation
7const nodeTypes = {
8    element: 1,
9    text: 3,
10    cdata: 4,
11    comment: 8
12};
13
14const nodePropertyShorthands = {
15    tagName: 'name',
16    childNodes: 'children',
17    parentNode: 'parent',
18    previousSibling: 'prev',
19    nextSibling: 'next',
20    nodeValue: 'data'
21};
22
23//Node
24class Node {
25    constructor(props) {
26        for (const key of Object.keys(props)) {
27            this[key] = props[key];
28        }
29    }
30
31    get firstChild() {
32        const children = this.children;
33
34        return (children && children[0]) || null;
35    }
36
37    get lastChild() {
38        const children = this.children;
39
40        return (children && children[children.length - 1]) || null;
41    }
42
43    get nodeType() {
44        return nodeTypes[this.type] || nodeTypes.element;
45    }
46}
47
48Object.keys(nodePropertyShorthands).forEach(key => {
49    const shorthand = nodePropertyShorthands[key];
50
51    Object.defineProperty(Node.prototype, key, {
52        get: function() {
53            return this[shorthand] || null;
54        },
55        set: function(val) {
56            this[shorthand] = val;
57            return val;
58        }
59    });
60});
61
62//Node construction
63exports.createDocument = function() {
64    return new Node({
65        type: 'root',
66        name: 'root',
67        parent: null,
68        prev: null,
69        next: null,
70        children: [],
71        'x-mode': DOCUMENT_MODE.NO_QUIRKS
72    });
73};
74
75exports.createDocumentFragment = function() {
76    return new Node({
77        type: 'root',
78        name: 'root',
79        parent: null,
80        prev: null,
81        next: null,
82        children: []
83    });
84};
85
86exports.createElement = function(tagName, namespaceURI, attrs) {
87    const attribs = Object.create(null);
88    const attribsNamespace = Object.create(null);
89    const attribsPrefix = Object.create(null);
90
91    for (let i = 0; i < attrs.length; i++) {
92        const attrName = attrs[i].name;
93
94        attribs[attrName] = attrs[i].value;
95        attribsNamespace[attrName] = attrs[i].namespace;
96        attribsPrefix[attrName] = attrs[i].prefix;
97    }
98
99    return new Node({
100        type: tagName === 'script' || tagName === 'style' ? tagName : 'tag',
101        name: tagName,
102        namespace: namespaceURI,
103        attribs: attribs,
104        'x-attribsNamespace': attribsNamespace,
105        'x-attribsPrefix': attribsPrefix,
106        children: [],
107        parent: null,
108        prev: null,
109        next: null
110    });
111};
112
113exports.createCommentNode = function(data) {
114    return new Node({
115        type: 'comment',
116        data: data,
117        parent: null,
118        prev: null,
119        next: null
120    });
121};
122
123const createTextNode = function(value) {
124    return new Node({
125        type: 'text',
126        data: value,
127        parent: null,
128        prev: null,
129        next: null
130    });
131};
132
133//Tree mutation
134const appendChild = (exports.appendChild = function(parentNode, newNode) {
135    const prev = parentNode.children[parentNode.children.length - 1];
136
137    if (prev) {
138        prev.next = newNode;
139        newNode.prev = prev;
140    }
141
142    parentNode.children.push(newNode);
143    newNode.parent = parentNode;
144});
145
146const insertBefore = (exports.insertBefore = function(parentNode, newNode, referenceNode) {
147    const insertionIdx = parentNode.children.indexOf(referenceNode);
148    const prev = referenceNode.prev;
149
150    if (prev) {
151        prev.next = newNode;
152        newNode.prev = prev;
153    }
154
155    referenceNode.prev = newNode;
156    newNode.next = referenceNode;
157
158    parentNode.children.splice(insertionIdx, 0, newNode);
159    newNode.parent = parentNode;
160});
161
162exports.setTemplateContent = function(templateElement, contentElement) {
163    appendChild(templateElement, contentElement);
164};
165
166exports.getTemplateContent = function(templateElement) {
167    return templateElement.children[0];
168};
169
170exports.setDocumentType = function(document, name, publicId, systemId) {
171    const data = doctype.serializeContent(name, publicId, systemId);
172    let doctypeNode = null;
173
174    for (let i = 0; i < document.children.length; i++) {
175        if (document.children[i].type === 'directive' && document.children[i].name === '!doctype') {
176            doctypeNode = document.children[i];
177            break;
178        }
179    }
180
181    if (doctypeNode) {
182        doctypeNode.data = data;
183        doctypeNode['x-name'] = name;
184        doctypeNode['x-publicId'] = publicId;
185        doctypeNode['x-systemId'] = systemId;
186    } else {
187        appendChild(
188            document,
189            new Node({
190                type: 'directive',
191                name: '!doctype',
192                data: data,
193                'x-name': name,
194                'x-publicId': publicId,
195                'x-systemId': systemId
196            })
197        );
198    }
199};
200
201exports.setDocumentMode = function(document, mode) {
202    document['x-mode'] = mode;
203};
204
205exports.getDocumentMode = function(document) {
206    return document['x-mode'];
207};
208
209exports.detachNode = function(node) {
210    if (node.parent) {
211        const idx = node.parent.children.indexOf(node);
212        const prev = node.prev;
213        const next = node.next;
214
215        node.prev = null;
216        node.next = null;
217
218        if (prev) {
219            prev.next = next;
220        }
221
222        if (next) {
223            next.prev = prev;
224        }
225
226        node.parent.children.splice(idx, 1);
227        node.parent = null;
228    }
229};
230
231exports.insertText = function(parentNode, text) {
232    const lastChild = parentNode.children[parentNode.children.length - 1];
233
234    if (lastChild && lastChild.type === 'text') {
235        lastChild.data += text;
236    } else {
237        appendChild(parentNode, createTextNode(text));
238    }
239};
240
241exports.insertTextBefore = function(parentNode, text, referenceNode) {
242    const prevNode = parentNode.children[parentNode.children.indexOf(referenceNode) - 1];
243
244    if (prevNode && prevNode.type === 'text') {
245        prevNode.data += text;
246    } else {
247        insertBefore(parentNode, createTextNode(text), referenceNode);
248    }
249};
250
251exports.adoptAttributes = function(recipient, attrs) {
252    for (let i = 0; i < attrs.length; i++) {
253        const attrName = attrs[i].name;
254
255        if (typeof recipient.attribs[attrName] === 'undefined') {
256            recipient.attribs[attrName] = attrs[i].value;
257            recipient['x-attribsNamespace'][attrName] = attrs[i].namespace;
258            recipient['x-attribsPrefix'][attrName] = attrs[i].prefix;
259        }
260    }
261};
262
263//Tree traversing
264exports.getFirstChild = function(node) {
265    return node.children[0];
266};
267
268exports.getChildNodes = function(node) {
269    return node.children;
270};
271
272exports.getParentNode = function(node) {
273    return node.parent;
274};
275
276exports.getAttrList = function(element) {
277    const attrList = [];
278
279    for (const name in element.attribs) {
280        attrList.push({
281            name: name,
282            value: element.attribs[name],
283            namespace: element['x-attribsNamespace'][name],
284            prefix: element['x-attribsPrefix'][name]
285        });
286    }
287
288    return attrList;
289};
290
291//Node data
292exports.getTagName = function(element) {
293    return element.name;
294};
295
296exports.getNamespaceURI = function(element) {
297    return element.namespace;
298};
299
300exports.getTextNodeContent = function(textNode) {
301    return textNode.data;
302};
303
304exports.getCommentNodeContent = function(commentNode) {
305    return commentNode.data;
306};
307
308exports.getDocumentTypeNodeName = function(doctypeNode) {
309    return doctypeNode['x-name'];
310};
311
312exports.getDocumentTypeNodePublicId = function(doctypeNode) {
313    return doctypeNode['x-publicId'];
314};
315
316exports.getDocumentTypeNodeSystemId = function(doctypeNode) {
317    return doctypeNode['x-systemId'];
318};
319
320//Node types
321exports.isTextNode = function(node) {
322    return node.type === 'text';
323};
324
325exports.isCommentNode = function(node) {
326    return node.type === 'comment';
327};
328
329exports.isDocumentTypeNode = function(node) {
330    return node.type === 'directive' && node.name === '!doctype';
331};
332
333exports.isElementNode = function(node) {
334    return !!node.attribs;
335};
336
337// Source code location
338exports.setNodeSourceCodeLocation = function(node, location) {
339    node.sourceCodeLocation = location;
340};
341
342exports.getNodeSourceCodeLocation = function(node) {
343    return node.sourceCodeLocation;
344};
345
346exports.updateNodeSourceCodeLocation = function(node, endLocation) {
347    node.sourceCodeLocation = Object.assign(node.sourceCodeLocation, endLocation);
348};
349