349 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			349 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| '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);
 | |
| };
 |