| '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); |
| }; |