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