'use strict'; const doctype = require('parse5/lib/common/doctype'); const { DOCUMENT_MODE } = require('parse5/lib/common/html'); //Conversion tables for DOM Level1 structure emulation const nodeTypes = { element: 1, text: 3, cdata: 4, comment: 8 }; const nodePropertyShorthands = { tagName: 'name', childNodes: 'children', parentNode: 'parent', previousSibling: 'prev', nextSibling: 'next', nodeValue: 'data' }; //Node class Node { constructor(props) { for (const key of Object.keys(props)) { this[key] = props[key]; } } get firstChild() { const children = this.children; return (children && children[0]) || null; } get lastChild() { const children = this.children; return (children && children[children.length - 1]) || null; } get nodeType() { return nodeTypes[this.type] || nodeTypes.element; } } Object.keys(nodePropertyShorthands).forEach(key => { const shorthand = nodePropertyShorthands[key]; Object.defineProperty(Node.prototype, key, { get: function() { return this[shorthand] || null; }, set: function(val) { this[shorthand] = val; return val; } }); }); //Node construction exports.createDocument = function() { return new Node({ type: 'root', name: 'root', parent: null, prev: null, next: null, children: [], 'x-mode': DOCUMENT_MODE.NO_QUIRKS }); }; exports.createDocumentFragment = function() { return new Node({ type: 'root', name: 'root', parent: null, prev: null, next: null, children: [] }); }; exports.createElement = function(tagName, namespaceURI, attrs) { const attribs = Object.create(null); const attribsNamespace = Object.create(null); const attribsPrefix = Object.create(null); for (let i = 0; i < attrs.length; i++) { const attrName = attrs[i].name; attribs[attrName] = attrs[i].value; attribsNamespace[attrName] = attrs[i].namespace; attribsPrefix[attrName] = attrs[i].prefix; } return new Node({ type: tagName === 'script' || tagName === 'style' ? tagName : 'tag', name: tagName, namespace: namespaceURI, attribs: attribs, 'x-attribsNamespace': attribsNamespace, 'x-attribsPrefix': attribsPrefix, children: [], parent: null, prev: null, next: null }); }; exports.createCommentNode = function(data) { return new Node({ type: 'comment', data: data, parent: null, prev: null, next: null }); }; const createTextNode = function(value) { return new Node({ type: 'text', data: value, parent: null, prev: null, next: null }); }; //Tree mutation const appendChild = (exports.appendChild = function(parentNode, newNode) { const prev = parentNode.children[parentNode.children.length - 1]; if (prev) { prev.next = newNode; newNode.prev = prev; } parentNode.children.push(newNode); newNode.parent = parentNode; }); const insertBefore = (exports.insertBefore = function(parentNode, newNode, referenceNode) { const insertionIdx = parentNode.children.indexOf(referenceNode); const prev = referenceNode.prev; if (prev) { prev.next = newNode; newNode.prev = prev; } referenceNode.prev = newNode; newNode.next = referenceNode; parentNode.children.splice(insertionIdx, 0, newNode); newNode.parent = parentNode; }); exports.setTemplateContent = function(templateElement, contentElement) { appendChild(templateElement, contentElement); }; exports.getTemplateContent = function(templateElement) { return templateElement.children[0]; }; exports.setDocumentType = function(document, name, publicId, systemId) { const data = doctype.serializeContent(name, publicId, systemId); let doctypeNode = null; for (let i = 0; i < document.children.length; i++) { if (document.children[i].type === 'directive' && document.children[i].name === '!doctype') { doctypeNode = document.children[i]; break; } } if (doctypeNode) { doctypeNode.data = data; doctypeNode['x-name'] = name; doctypeNode['x-publicId'] = publicId; doctypeNode['x-systemId'] = systemId; } else { appendChild( document, new Node({ type: 'directive', name: '!doctype', data: data, 'x-name': name, 'x-publicId': publicId, 'x-systemId': systemId }) ); } }; exports.setDocumentMode = function(document, mode) { document['x-mode'] = mode; }; exports.getDocumentMode = function(document) { return document['x-mode']; }; exports.detachNode = function(node) { if (node.parent) { const idx = node.parent.children.indexOf(node); const prev = node.prev; const next = node.next; node.prev = null; node.next = null; if (prev) { prev.next = next; } if (next) { next.prev = prev; } node.parent.children.splice(idx, 1); node.parent = null; } }; exports.insertText = function(parentNode, text) { const lastChild = parentNode.children[parentNode.children.length - 1]; if (lastChild && lastChild.type === 'text') { lastChild.data += text; } else { appendChild(parentNode, createTextNode(text)); } }; exports.insertTextBefore = function(parentNode, text, referenceNode) { const prevNode = parentNode.children[parentNode.children.indexOf(referenceNode) - 1]; if (prevNode && prevNode.type === 'text') { prevNode.data += text; } else { insertBefore(parentNode, createTextNode(text), referenceNode); } }; exports.adoptAttributes = function(recipient, attrs) { for (let i = 0; i < attrs.length; i++) { const attrName = attrs[i].name; if (typeof recipient.attribs[attrName] === 'undefined') { recipient.attribs[attrName] = attrs[i].value; recipient['x-attribsNamespace'][attrName] = attrs[i].namespace; recipient['x-attribsPrefix'][attrName] = attrs[i].prefix; } } }; //Tree traversing exports.getFirstChild = function(node) { return node.children[0]; }; exports.getChildNodes = function(node) { return node.children; }; exports.getParentNode = function(node) { return node.parent; }; exports.getAttrList = function(element) { const attrList = []; for (const name in element.attribs) { attrList.push({ name: name, value: element.attribs[name], namespace: element['x-attribsNamespace'][name], prefix: element['x-attribsPrefix'][name] }); } return attrList; }; //Node data exports.getTagName = function(element) { return element.name; }; exports.getNamespaceURI = function(element) { return element.namespace; }; exports.getTextNodeContent = function(textNode) { return textNode.data; }; exports.getCommentNodeContent = function(commentNode) { return commentNode.data; }; exports.getDocumentTypeNodeName = function(doctypeNode) { return doctypeNode['x-name']; }; exports.getDocumentTypeNodePublicId = function(doctypeNode) { return doctypeNode['x-publicId']; }; exports.getDocumentTypeNodeSystemId = function(doctypeNode) { return doctypeNode['x-systemId']; }; //Node types exports.isTextNode = function(node) { return node.type === 'text'; }; exports.isCommentNode = function(node) { return node.type === 'comment'; }; exports.isDocumentTypeNode = function(node) { return node.type === 'directive' && node.name === '!doctype'; }; exports.isElementNode = function(node) { return !!node.attribs; }; // Source code location exports.setNodeSourceCodeLocation = function(node, location) { node.sourceCodeLocation = location; }; exports.getNodeSourceCodeLocation = function(node) { return node.sourceCodeLocation; }; exports.updateNodeSourceCodeLocation = function(node, endLocation) { node.sourceCodeLocation = Object.assign(node.sourceCodeLocation, endLocation); };