Skip to main content
Module

x/deno_dom/src/dom/node.ts

Browser DOM & HTML parser in Deno
Extremely Popular
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
import { CTOR_KEY } from "../constructor-lock.ts";import { NodeList, NodeListMutator, nodeListMutatorSym } from "./node-list.ts";import { HTMLCollection, HTMLCollectionMutator, HTMLCollectionMutatorSym } from "./html-collection.ts";import type { Element } from "./element.ts";import type { Document } from "./document.ts";
export class EventTarget { addEventListener() { // TODO }
removeEventListener() { // TODO }
dispatchEvent() { // TODO }}
export enum NodeType { 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,}
const nodesAndTextNodes = (nodes: (Node | any)[], parentNode: Node) => { return nodes.map(n => { let node = n;
if (!(n instanceof Node)) { node = new Text("" + n); }
node._setParent(parentNode, true); return node; });}
export class Node extends EventTarget { public nodeValue: string | null; public childNodes: NodeList; public parentNode: Node | null = null; public parentElement: Element | null; #childNodesMutator: NodeListMutator; #ownerDocument: Document | null = null; private _ancestors = new Set<Node>();
// Instance constants defined after Node // class body below to avoid clutter static ELEMENT_NODE = NodeType.ELEMENT_NODE; static ATTRIBUTE_NODE = NodeType.ATTRIBUTE_NODE; static TEXT_NODE = NodeType.TEXT_NODE; static CDATA_SECTION_NODE = NodeType.CDATA_SECTION_NODE; static ENTITY_REFERENCE_NODE = NodeType.ENTITY_REFERENCE_NODE; static ENTITY_NODE = NodeType.ENTITY_NODE; static PROCESSING_INSTRUCTION_NODE = NodeType.PROCESSING_INSTRUCTION_NODE; static COMMENT_NODE = NodeType.COMMENT_NODE; static DOCUMENT_NODE = NodeType.DOCUMENT_NODE; static DOCUMENT_TYPE_NODE = NodeType.DOCUMENT_TYPE_NODE; static DOCUMENT_FRAGMENT_NODE = NodeType.DOCUMENT_FRAGMENT_NODE; static NOTATION_NODE = NodeType.NOTATION_NODE;
constructor( public nodeName: string, public nodeType: NodeType, parentNode: Node | null, key: typeof CTOR_KEY, ) { if (key !== CTOR_KEY) { throw new TypeError("Illegal constructor."); } super();
this.nodeValue = null; this.childNodes = new NodeList(); this.#childNodesMutator = this.childNodes[nodeListMutatorSym](); this.parentElement = <Element> parentNode;
if (parentNode) { parentNode.appendChild(this); } }
_getChildNodesMutator(): NodeListMutator { return this.#childNodesMutator; }
_setParent(newParent: Node | null, force = false) { const sameParent = this.parentNode === newParent; const shouldUpdateParentAndAncestors = !sameParent || force;
if (shouldUpdateParentAndAncestors) { this.parentNode = newParent;
if (newParent) { if (!sameParent) { // If this a document node or another non-element node // then parentElement should be set to null if (newParent.nodeType === NodeType.ELEMENT_NODE) { this.parentElement = newParent as unknown as Element; } else { this.parentElement = null; }
this._setOwnerDocument(newParent.#ownerDocument); }
// Add parent chain to ancestors this._ancestors = new Set(newParent._ancestors); this._ancestors.add(newParent); } else { this.parentElement = null; this._ancestors.clear(); }
// Update ancestors for child nodes for (const child of this.childNodes) { child._setParent(this, shouldUpdateParentAndAncestors); } } }
_assertNotAncestor(child: Node) { // Check this child isn't an ancestor if (child.contains(this)) { throw new DOMException("The new child is an ancestor of the parent"); } }
_setOwnerDocument(document: Document | null) { if (this.#ownerDocument !== document) { this.#ownerDocument = document;
for (const child of this.childNodes) { child._setOwnerDocument(document); } } }
contains(child: Node) { return child._ancestors.has(this) || child === this; }
get ownerDocument() { return this.#ownerDocument; }
get textContent(): string { let out = "";
for (const child of this.childNodes) { switch (child.nodeType) { case NodeType.TEXT_NODE: out += child.nodeValue; break; case NodeType.ELEMENT_NODE: out += child.textContent; break; } }
return out; }
set textContent(content: string) { for (const child of this.childNodes) { child._setParent(null); }
this._getChildNodesMutator().splice(0, this.childNodes.length); this.appendChild(new Text(content)); }
get firstChild() { return this.childNodes[0] || null; }
get lastChild() { return this.childNodes[this.childNodes.length - 1] || null; }
hasChildNodes() { return this.firstChild !== null; }
cloneNode(deep: boolean = false): this { const copy = this._shallowClone();
copy._setOwnerDocument(this.ownerDocument);
if (deep) { for (const child of this.childNodes) { copy.appendChild(child.cloneNode(true)); } }
return copy as this; }
_shallowClone(): Node { throw new Error("Illegal invocation"); }
remove() { const parent = this.parentNode;
if (parent) { const nodeList = parent._getChildNodesMutator(); const idx = nodeList.indexOf(this); nodeList.splice(idx, 1); this._setParent(null); } }
appendChild(child: Node): Node { return child._appendTo(this); }
_appendTo(parentNode: Node) { parentNode._assertNotAncestor(this); // FIXME: Should this really be a method? const oldParentNode = this.parentNode;
// Check if we already own this child if (oldParentNode === parentNode) { if (parentNode._getChildNodesMutator().indexOf(this) !== -1) { return this; } } else if (oldParentNode) { this.remove(); }
this._setParent(parentNode, true); parentNode._getChildNodesMutator().push(this);
return this; }
removeChild(child: Node) { // Just copy Firefox's error messages if (child && typeof child === "object") { if (child.parentNode === this) { return child.remove(); } else { throw new DOMException("Node.removeChild: The node to be removed is not a child of this node"); } } else { throw new TypeError("Node.removeChild: Argument 1 is not an object."); } }
replaceChild(newChild: Node, oldChild: Node): Node { if (oldChild.parentNode !== this) { throw new Error("Old child's parent is not the current node."); }
oldChild.replaceWith(newChild); return oldChild; }
private insertBeforeAfter(nodes: (Node | string)[], side: number) { const parentNode = this.parentNode!; const mutator = parentNode._getChildNodesMutator(); const index = mutator.indexOf(this); nodes = nodesAndTextNodes(nodes, parentNode);
mutator.splice(index + side, 0, ...(<Node[]> nodes)); }
before(...nodes: (Node | string)[]) { if (this.parentNode) { this.insertBeforeAfter(nodes, 0); } }
after(...nodes: (Node | string)[]) { if (this.parentNode) { this.insertBeforeAfter(nodes, 1); } }
insertBefore(newNode: Node, refNode: Node | null): Node { this._assertNotAncestor(newNode); const mutator = this._getChildNodesMutator();
if (refNode === null) { this.appendChild(newNode); return newNode; }
const index = mutator.indexOf(refNode); if (index === -1) { throw new Error("DOMException: Child to insert before is not a child of this node"); }
const oldParentNode = newNode.parentNode; newNode._setParent(this, oldParentNode !== this); mutator.splice(index, 0, newNode);
return newNode; }
replaceWith(...nodes: (Node | string)[]) { if (this.parentNode) { const parentNode = this.parentNode; const mutator = parentNode._getChildNodesMutator(); const index = mutator.indexOf(this); nodes = nodesAndTextNodes(nodes, parentNode);
mutator.splice(index, 1, ...(<Node[]> nodes)); this._setParent(null); } }
get children(): HTMLCollection { const collection = new HTMLCollection(); const mutator = collection[HTMLCollectionMutatorSym]();
for (const child of this.childNodes) { if (child.nodeType === NodeType.ELEMENT_NODE) { mutator.push(<Element> child); } }
return collection; }
get nextSibling(): Node | null { const parent = this.parentNode;
if (!parent) { return null; }
const index = parent._getChildNodesMutator().indexOf(this); const next: Node | null = parent.childNodes[index + 1] || null;
return next; }
get previousSibling(): Node | null { const parent = this.parentNode;
if (!parent) { return null; }
const index = parent._getChildNodesMutator().indexOf(this); const prev: Node | null = parent.childNodes[index - 1] || null;
return prev; }
// Node.compareDocumentPosition()'s bitmask values public static DOCUMENT_POSITION_DISCONNECTED = 1 as const; public static DOCUMENT_POSITION_PRECEDING = 2 as const; public static DOCUMENT_POSITION_FOLLOWING = 4 as const; public static DOCUMENT_POSITION_CONTAINS = 8 as const; public static DOCUMENT_POSITION_CONTAINED_BY = 16 as const; public static DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 32 as const;
/** * FIXME: Does not implement attribute node checks * ref: https://dom.spec.whatwg.org/#dom-node-comparedocumentposition * MDN: https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition */ compareDocumentPosition(other: Node) { if (other === this) { return 0; }
// Note: major browser implementations differ in their rejection error of // non-Node or nullish values so we just copy the most relevant error message // from Firefox if (!(other instanceof Node)) { throw new TypeError("Node.compareDocumentPosition: Argument 1 does not implement interface Node."); }
let node1Root = other; let node2Root = this as Node; const node1Hierarchy = [node1Root]; const node2Hierarchy = [node2Root]; while (node1Root.parentNode ?? node2Root.parentNode) { node1Root = node1Root.parentNode ? (node1Hierarchy.push(node1Root.parentNode), node1Root.parentNode) : node1Root; node2Root = node2Root.parentNode ? (node2Hierarchy.push(node2Root.parentNode), node2Root.parentNode) : node2Root; }
// Check if they don't share the same root node if (node1Root !== node2Root) { return Node.DOCUMENT_POSITION_DISCONNECTED | Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC | Node.DOCUMENT_POSITION_PRECEDING; }
const longerHierarchy = node1Hierarchy.length > node2Hierarchy.length ? node1Hierarchy : node2Hierarchy; const shorterHierarchy = longerHierarchy === node1Hierarchy ? node2Hierarchy : node1Hierarchy;
// Check if either is a container of the other if (longerHierarchy[longerHierarchy.length - shorterHierarchy.length] === shorterHierarchy[0]) { return longerHierarchy === node1Hierarchy // other is a child of this ? Node.DOCUMENT_POSITION_CONTAINED_BY | Node.DOCUMENT_POSITION_FOLLOWING // this is a child of other : Node.DOCUMENT_POSITION_CONTAINS | Node.DOCUMENT_POSITION_PRECEDING; }
// Find their first common ancestor and see whether they // are preceding or following const longerStart = longerHierarchy.length - shorterHierarchy.length; for (let i = shorterHierarchy.length - 1; i >= 0; i--) { const shorterHierarchyNode = shorterHierarchy[i]; const longerHierarchyNode = longerHierarchy[longerStart + i];
// We found the first common ancestor if (longerHierarchyNode !== shorterHierarchyNode) { const siblings = shorterHierarchyNode.parentNode!._getChildNodesMutator();
if (siblings.indexOf(shorterHierarchyNode) < siblings.indexOf(longerHierarchyNode)) { // Shorter is before longer if (shorterHierarchy === node1Hierarchy) { // Other is before this return Node.DOCUMENT_POSITION_PRECEDING; } else { // This is before other return Node.DOCUMENT_POSITION_FOLLOWING; } } else { // Longer is before shorter if (longerHierarchy === node1Hierarchy) { // Other is before this return Node.DOCUMENT_POSITION_PRECEDING; } else { // Other is after this return Node.DOCUMENT_POSITION_FOLLOWING; } } } }
// FIXME: Should probably throw here because this // point should be unreachable code as per the // intended logic return Node.DOCUMENT_POSITION_FOLLOWING; }}
// Node instance `nodeType` enum constantsexport interface Node { ELEMENT_NODE: NodeType; ATTRIBUTE_NODE: NodeType; TEXT_NODE: NodeType; CDATA_SECTION_NODE: NodeType; ENTITY_REFERENCE_NODE: NodeType; ENTITY_NODE: NodeType; PROCESSING_INSTRUCTION_NODE: NodeType; COMMENT_NODE: NodeType; DOCUMENT_NODE: NodeType; DOCUMENT_TYPE_NODE: NodeType; DOCUMENT_FRAGMENT_NODE: NodeType; NOTATION_NODE: NodeType;}
Node.prototype.ELEMENT_NODE = NodeType.ELEMENT_NODE;Node.prototype.ATTRIBUTE_NODE = NodeType.ATTRIBUTE_NODE;Node.prototype.TEXT_NODE = NodeType.TEXT_NODE;Node.prototype.CDATA_SECTION_NODE = NodeType.CDATA_SECTION_NODE;Node.prototype.ENTITY_REFERENCE_NODE = NodeType.ENTITY_REFERENCE_NODE;Node.prototype.ENTITY_NODE = NodeType.ENTITY_NODE;Node.prototype.PROCESSING_INSTRUCTION_NODE = NodeType.PROCESSING_INSTRUCTION_NODE;Node.prototype.COMMENT_NODE = NodeType.COMMENT_NODE;Node.prototype.DOCUMENT_NODE = NodeType.DOCUMENT_NODE;Node.prototype.DOCUMENT_TYPE_NODE = NodeType.DOCUMENT_TYPE_NODE;Node.prototype.DOCUMENT_FRAGMENT_NODE = NodeType.DOCUMENT_FRAGMENT_NODE;Node.prototype.NOTATION_NODE = NodeType.NOTATION_NODE;
export class CharacterData extends Node { constructor( public data: string, nodeName: string, nodeType: NodeType, parentNode: Node | null, key: typeof CTOR_KEY, ) { super( nodeName, nodeType, parentNode, key, );
this.nodeValue = data; }
get length(): number { return this.data.length; }
// TODO: Implement NonDocumentTypeChildNode.nextElementSibling, etc // ref: https://developer.mozilla.org/en-US/docs/Web/API/CharacterData}
export class Text extends CharacterData { constructor( text: string = "", ) { super( text, "#text", NodeType.TEXT_NODE, null, CTOR_KEY, );
this.nodeValue = text; }
_shallowClone(): Node { return new Text(this.textContent); }
get textContent(): string { return <string> this.nodeValue; }}
export class Comment extends CharacterData { constructor( text: string = "", ) { super( text, "#comment", NodeType.COMMENT_NODE, null, CTOR_KEY, );
this.nodeValue = text; }
_shallowClone(): Node { return new Comment(this.textContent); }
get textContent(): string { return <string> this.nodeValue; }}