svgdom
Version:
Straightforward DOM implementation for SVG, HTML and XML
424 lines (355 loc) • 11.7 kB
JavaScript
import { extend, extendStatic } from '../utils/objectCreationUtils.js'
import { EventTarget } from './EventTarget.js'
import { cloneNode } from '../utils/tagUtils.js'
import { html } from '../utils/namespaces.js'
const nodeTypes = {
ELEMENT_NODE: 1,
ATTRIBUTE_NODE: 2,
TEXT_NODE: 3,
CDATA_SECTION_NODE: 4,
ENTITY_REFERENCE_NODE: 5,
ENTITY_NODE: 6,
PROCESSING_INSTRUCTION_NODE: 7,
COMMENT_NODE: 8,
DOCUMENT_NODE: 9,
DOCUMENT_TYPE_NODE: 10,
DOCUMENT_FRAGMENT_NODE: 11,
NOTATION_NODE: 12
}
export class Node extends EventTarget {
constructor (name = '', props = {}, ns = null) {
super()
// If props.local is true, the element was Node was created with the non-namespace function
// that means whatever was passed as name is the local name even though it might look like a prefix
if (name.includes(':') && !props.local) {
;[ this.prefix, this.localName ] = name.split(':')
} else {
this.localName = name
this.prefix = null
}
// Follow spec and uppercase nodeName for html
this.nodeName = ns === html ? name.toUpperCase() : name
this.namespaceURI = ns
this.nodeType = Node.ELEMENT_NODE
this.nodeValue = props.nodeValue != null ? props.nodeValue : null
this.childNodes = []
this.attrs = props.attrs || new Set()
this.ownerDocument = props.ownerDocument || null
this.parentNode = null
// this.namespaces = {}
// if (this.prefix) {
// this.namespaces[this.prefix] = ns
// } else {
// this.namespaces.default = ns
// }
if (props.childNodes) {
for (let i = 0, il = props.childNodes.length; i < il; ++i) {
this.appendChild(props.childNodes[i])
}
}
}
appendChild (node) {
return this.insertBefore(node)
}
cloneNode (deep = false) {
const clone = cloneNode(this)
if (deep) {
this.childNodes.forEach(function (el) {
const node = el.cloneNode(deep)
clone.appendChild(node)
})
}
return clone
}
contains (node) {
if (node === this) return false
while (node.parentNode) {
if (node === this) return true
node = node.parentNode
}
return false
}
getRootNode () {
if (!this.parentNode || this.nodeType === Node.DOCUMENT_NODE) return this
return this.parentNode.getRootNode()
}
hasChildNodes () {
return !!this.childNodes.length
}
insertBefore (node, before) {
let index = this.childNodes.indexOf(before)
if (index === -1) {
index = this.childNodes.length
}
if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
let child
let oldChild = before
while ((child = node.childNodes.pop())) {
this.insertBefore(child, oldChild)
oldChild = child
}
return node
}
if (node.parentNode) {
node.parentNode.removeChild(node)
}
node.parentNode = this
// Object.setPrototypeOf(node.namespaces.prototype, this.namespaces.prototype)
this.childNodes.splice(index, 0, node)
return node
}
isDefaultNamespace (namespaceURI) {
switch (this.nodeType) {
case Node.ELEMENT_NODE:
if (!this.prefix) {
return this.namespaceURI === namespaceURI
}
if (this.hasAttribute('xmlns')) {
return this.getAttribute('xmlns')
}
// EntityReferences may have to be skipped to get to it
if (this.parentNode) {
return this.parentNode.isDefaultNamespace(namespaceURI)
}
return false
case Node.DOCUMENT_NODE:
return this.documentElement.isDefaultNamespace(namespaceURI)
case Node.ENTITY_NODE:
case Node.NOTATION_NODE:
case Node.DOCUMENT_TYPE_NODE:
case Node.DOCUMENT_FRAGMENT_NODE:
return false
case Node.ATTRIBUTE_NODE:
if (this.ownerElement) {
return this.ownerElement.isDefaultNamespace(namespaceURI)
}
return false
default:
// EntityReferences may have to be skipped to get to it
if (this.parentNode) {
return this.parentNode.isDefaultNamespace(namespaceURI)
}
return false
}
}
isEqualNode (node) {
this.normalize()
node.normalize()
let bool = this.nodeName === node.nodeName
bool = bool && this.localName === node.localName
bool = bool && this.namespaceURI === node.namespaceURI
bool = bool && this.prefix === node.prefix
bool = bool && this.nodeValue === node.nodeValue
bool = bool && this.childNodes.length === node.childNodes.length
// dont check children recursively when the count doesnt event add up
if (!bool) return false
bool = bool && !this.childNodes.reduce((last, curr, index) => {
return last && curr.isEqualNode(node.childNodes[index])
}, true)
// FIXME: Use attr nodes
/* bool = bool && ![ ...this.attrs.entries() ].reduce((last, curr, index) => {
const [ key, val ] = node.attrs.entries()
return last && curr[0] === key && curr[1] === val
}, true) */
/*
TODO:
For two DocumentType nodes to be equal, the following conditions must also be satisfied:
The following string attributes are equal: publicId, systemId, internalSubset.
The entities NamedNodeMaps are equal.
The notations NamedNodeMaps are equal.
*/
if (this.nodeType === Node.DOCUMENT_TYPE_NODE && node.nodeType === Node.DOCUMENT_TYPE_NODE) {
bool = bool && this.publicId === node.publicId
bool = bool && this.systemId === node.systemId
bool = bool && this.internalSubset === node.internalSubset
}
return bool
}
isSameNode (node) {
return this === node
}
lookupNamespacePrefix (namespaceURI, originalElement) {
if (this.namespaceURI && this.namespaceURI === namespaceURI && this.prefix
&& originalElement.lookupNamespaceURI(this.prefix) === namespaceURI) {
return this.prefix
}
for (const [ key, val ] of this.attrs.entries()) {
if (!key.includes(':')) continue
const [ attrPrefix, name ] = key.split(':')
if (attrPrefix === 'xmlns' && val === namespaceURI && originalElement.lookupNamespaceURI(name) === namespaceURI) {
return name
}
}
// EntityReferences may have to be skipped to get to it
if (this.parentNode) {
return this.parentNode.lookupNamespacePrefix(namespaceURI, originalElement)
}
return null
}
lookupNamespaceURI (prefix) {
switch (this.nodeType) {
case Node.ELEMENT_NODE:
if (this.namespaceURI != null && this.prefix === prefix) {
// Note: prefix could be "null" in this case we are looking for default namespace
return this.namespaceURI
}
for (const [ key, val ] of this.attrs.entries()) {
if (!key.includes(':')) continue
const [ attrPrefix, name ] = key.split(':')
if (attrPrefix === 'xmlns' && name === prefix) {
if (val != null) {
return val
}
return null
// FIXME: Look up if prefix or attrPrefix
} else if (name === 'xmlns' && prefix == null) {
if (val != null) {
return val
}
return null
}
}
// EntityReferences may have to be skipped to get to it
if (this.parentNode) {
return this.parentNode.lookupNamespaceURI(prefix)
}
return null
case Node.DOCUMENT_NODE:
return this.documentElement.lookupNamespaceURI(prefix)
case Node.ENTITY_NODE:
case Node.NOTATION_NODE:
case Node.DOCUMENT_TYPE_NODE:
case Node.DOCUMENT_FRAGMENT_NODE:
return null
case Node.ATTRIBUTE_NODE:
if (this.ownerElement) {
return this.ownerElement.lookupNamespaceURI(prefix)
}
return null
default:
// EntityReferences may have to be skipped to get to it
if (this.parentNode) {
return this.parentNode.lookupNamespaceURI(prefix)
}
return null
}
}
lookupPrefix (namespaceURI) {
if (!namespaceURI) {
return null
}
const type = this.nodeType
switch (type) {
case Node.ELEMENT_NODE:
return this.lookupNamespacePrefix(namespaceURI, this)
case Node.DOCUMENT_NODE:
return this.documentElement.lookupNamespacePrefix(namespaceURI)
case Node.ENTITY_NODE :
case Node.NOTATION_NODE:
case Node.DOCUMENT_FRAGMENT_NODE:
case Node.DOCUMENT_TYPE_NODE:
return null // type is unknown
case Node.ATTRIBUTE_NODE:
if (this.ownerElement) {
return this.ownerElement.lookupNamespacePrefix(namespaceURI)
}
return null
default:
// EntityReferences may have to be skipped to get to it
if (this.parentNode) {
return this.parentNode.lookupNamespacePrefix(namespaceURI)
}
return null
}
}
normalize () {
const childNodes = []
for (const node of this.childNodes) {
const last = childNodes.shift()
if (!last) {
if (node.data) {
childNodes.unshift(node)
}
continue
}
if (node.nodeType === Node.TEXT_NODE) {
if (!node.data) {
childNodes.unshift(last)
continue
}
if (last.nodeType === Node.TEXT_NODE) {
const merged = this.ownerDocument.createTextNode(last.data + node.data)
childNodes.push(merged)
continue
}
childNodes.push(last, node)
}
}
childNodes.forEach(node => {
node.parentNode = this
})
this.childNodes = childNodes
// this.childNodes = this.childNodes.forEach((textNodes, node) => {
// // FIXME: If first node is an empty textnode, what do we do? -> spec
// if (!textNodes) return [ node ]
// var last = textNodes.pop()
// if (node.nodeType === Node.TEXT_NODE) {
// if (!node.data) return textNodes
// if (last.nodeType === Node.TEXT_NODE) {
// const merged = this.ownerDocument.createTextNode(last.data + ' ' + node.data)
// textNodes.push(merged)
// return textNodes.concat(merged)
// }
// } else {
// textNodes.push(last, node)
// }
// return textNodes
// }, null)
}
removeChild (node) {
node.parentNode = null
// Object.setPrototypeOf(node, null)
const index = this.childNodes.indexOf(node)
if (index === -1) return node
this.childNodes.splice(index, 1)
return node
}
replaceChild (newChild, oldChild) {
const before = oldChild.nextSibling
this.removeChild(oldChild)
this.insertBefore(newChild, before)
return oldChild
}
get nextSibling () {
const child = this.parentNode && this.parentNode.childNodes[this.parentNode.childNodes.indexOf(this) + 1]
return child || null
}
get previousSibling () {
const child = this.parentNode && this.parentNode.childNodes[this.parentNode.childNodes.indexOf(this) - 1]
return child || null
}
get textContent () {
if (this.nodeType === Node.TEXT_NODE) return this.data
if (this.nodeType === Node.CDATA_SECTION_NODE) return this.data
if (this.nodeType === Node.COMMENT_NODE) return this.data
return this.childNodes.reduce(function (last, current) {
return last + current.textContent
}, '')
}
set textContent (text) {
if (this.nodeType === Node.TEXT_NODE || this.nodeType === Node.CDATA_SECTION_NODE || this.nodeType === Node.COMMENT_NODE) {
this.data = text
return
}
this.childNodes = []
this.appendChild(this.ownerDocument.createTextNode(text))
}
get lastChild () {
return this.childNodes[this.childNodes.length - 1] || null
}
get firstChild () {
return this.childNodes[0] || null
}
}
extendStatic(Node, nodeTypes)
extend(Node, nodeTypes)