|
/// <reference no-default-lib="true" /> |
|
// deno-lint-ignore-file no-unused-vars no-explicit-any |
|
|
|
import { |
|
inspect, |
|
type InspectOptions, |
|
type InspectOptionsStylized, |
|
// highlight, |
|
indexOf, |
|
pop, |
|
push, |
|
shift, |
|
slice, |
|
splice, |
|
unshift, |
|
} from "./utils.ts"; |
|
import { XMLFormatter } from "./format.ts"; |
|
|
|
// #region internal |
|
|
|
type AnyNode = Element | CDATASection | Attr | Document | DocumentType | DocumentFragment | ProcessingInstruction | Comment | Text | Notation | Entity | EntityReference; |
|
|
|
let createHTMLCollection!: (nodes: NodeList | Element[]) => HTMLCollection; |
|
let createTreeWalker!: ( |
|
root: Node, |
|
whatToShow?: number, |
|
filter?: NodeFilter | null, |
|
) => TreeWalker; |
|
let createLocation!: (url: string | URL, base?: string | URL) => Location; |
|
let createDOMStringList!: (strings: string[]) => DOMStringList; |
|
let createDOMTokenList!: ( |
|
element: Element, |
|
attr: Attr | undefined, |
|
strings: string[], |
|
) => DOMTokenList; |
|
let createDOMStringMap: (element: Element) => DOMStringMap; |
|
|
|
let getHTMLCollectionNodes: (hc: HTMLCollection) => Element[]; |
|
|
|
let setDocumentDomImplementation: ( |
|
doc: Document, |
|
impl: DOMImplementation, |
|
) => DOMImplementation; |
|
let setDocumentDefaultView: (doc: Document, view: Window) => Window; |
|
let setDocumentElement: (doc: Document, el: Element) => Element; |
|
let setDocumentLocation: (doc: Document, loc: Location) => Location; |
|
let setOwnerDocument: (node: Node, doc: Document) => void; |
|
let setOwnerElement: (node: Node, element: Element) => void; |
|
let setParentNode: (node: Node, parent: Node | null) => void; |
|
let setChildNodes: (node: Node, children: NodeList | null) => void; |
|
let setAttributes: (element: Element, attrs: NamedNodeMap) => void; |
|
let setNamespaceURI: (node: Node, uri: string | null) => void; |
|
|
|
const _ = {} as { |
|
getDOMMatrixBuffer(matrix: DOMMatrixReadOnly): ArrayBuffer; |
|
getDOMMatrixData(matrix: DOMMatrixReadOnly): Float64Array; |
|
getDOMMatrixDataView(matrix: DOMMatrixReadOnly): DataView; |
|
getDOMMatrixIndex(matrix: DOMMatrixReadOnly, index: number): number; |
|
setDOMMatrixBuffer(matrix: DOMMatrixReadOnly, buffer: ArrayBuffer): void; |
|
setDocumentDefaultView(doc: Document, view: Window): Window; |
|
setDocumentElement(doc: Document, el: Element): Element; |
|
setDocumentLocation(doc: Document, loc: Location): Location; |
|
setOwnerDocument(node: Node, doc: Document): void; |
|
setOwnerElement(node: Node, element: Element): void; |
|
setParentNode(node: Node, parent: Node | null): void; |
|
setChildNodes(node: Node, children: NodeList | null): void; |
|
setAttributes(element: Element, attrs: NamedNodeMap): void; |
|
setNamespaceURI(node: Node, uri: string | null): void; |
|
createHTMLCollection(nodes: NodeList | Element[]): HTMLCollection; |
|
createTreeWalker( |
|
root: Node, |
|
whatToShow?: number, |
|
filter?: NodeFilter | null, |
|
): TreeWalker; |
|
createLocation(url: string | URL, base?: string | URL): Location; |
|
createDOMStringList(strings: string[]): DOMStringList; |
|
createDOMTokenList( |
|
element: Element, |
|
attr: Attr | undefined, |
|
strings: string[], |
|
): DOMTokenList; |
|
createDOMStringMap(element: Element): DOMStringMap; |
|
getHTMLCollectionNodes(hc: HTMLCollection): Element[]; |
|
setDocumentDomImplementation( |
|
doc: Document, |
|
impl: DOMImplementation, |
|
): DOMImplementation; |
|
} |
|
|
|
// #region Window + Globals |
|
|
|
const _globalThis: typeof globalThis = (0, eval)("this"); |
|
const _window = _globalThis.window; |
|
const Window: typeof globalThis.Window = _globalThis.Window; |
|
const WindowPrototype = Window.prototype; |
|
|
|
// #endregion Window + Globals |
|
|
|
// #region WebIDL |
|
namespace webidl { |
|
const _brand: unique symbol = Symbol("[[webidl.brand]]"); |
|
|
|
export type brand = typeof _brand; |
|
export const brand: brand = Reflect.ownKeys(new EventTarget()).find((k): k is brand => |
|
typeof k === "symbol" && k.description === "[[webidl.brand]]" |
|
) ?? _brand; |
|
|
|
export function createBranded< |
|
const T extends object, |
|
const P extends object, |
|
>(proto: P, props: T): T & P { |
|
const obj = Object.create(proto); |
|
Object.assign(obj, props); |
|
return obj; |
|
} |
|
|
|
export function assertBranded< |
|
const T extends object, |
|
const P extends object, |
|
>(obj: T & P): asserts obj is T & P { |
|
if (!(brand in obj) || obj[webidl.brand] !== webidl.brand) throw new TypeError(); |
|
} |
|
|
|
export function illegalConstructor(): never { |
|
throw new TypeError("Illegal constructor"); |
|
} |
|
} |
|
// #endregion WebIDL |
|
|
|
export class DOMImplementation { |
|
#features: { [key: string]: any }; |
|
readonly [webidl.brand]: webidl.brand = webidl.brand; |
|
|
|
constructor(features?: { [key: string]: any }) { |
|
this.#features = {}; |
|
if (features) { |
|
for (const feature in features) { |
|
this.#features = features[feature]; |
|
} |
|
} |
|
} |
|
|
|
hasFeature(feature: string, version: string): boolean { |
|
const versions = this.#features[feature.toLowerCase()]; |
|
if (versions && (!version || version in versions)) { |
|
return true; |
|
} else { |
|
return false; |
|
} |
|
} |
|
|
|
createDocument( |
|
namespaceURI: string, |
|
qualifiedName: string, |
|
doctype: DocumentType, |
|
): Document { |
|
const doc = new Document(); |
|
setDocumentDomImplementation(doc, this); |
|
if (doctype) { |
|
Object.assign(doc, { doctype }); |
|
doc.appendChild(doctype); |
|
} |
|
const root = doc.createElementNS(namespaceURI, qualifiedName); |
|
doc.appendChild(root); |
|
setDocumentElement(doc, root); |
|
return doc; |
|
} |
|
|
|
createHTMLDocument(title?: string): Document { |
|
const doc = this.createDocument( |
|
"http://www.w3.org/1999/xhtml", |
|
"html", |
|
this.createDocumentType("html", "", ""), |
|
); |
|
const root = doc.documentElement; |
|
const head = doc.createElement("head"); |
|
const body = doc.createElement("body"); |
|
if (title) { |
|
const titleEl = doc.createElement("title"); |
|
titleEl.textContent = title; |
|
head.appendChild(titleEl); |
|
} |
|
root.appendChild(head); |
|
root.appendChild(body); |
|
Object.assign(doc, { body, head }); |
|
return doc; |
|
} |
|
|
|
createDocumentType( |
|
qualifiedName: string, |
|
publicId: string, |
|
systemId: string, |
|
): DocumentType { |
|
const node = new DocumentType(); |
|
node.name = qualifiedName; |
|
// @ts-ignore readonly |
|
node.nodeName = qualifiedName; |
|
node.publicId = publicId; |
|
node.systemId = systemId; |
|
return node; |
|
} |
|
} |
|
|
|
const ELEMENT_NODE = 1; |
|
const ATTRIBUTE_NODE = 2; |
|
const TEXT_NODE = 3; |
|
const CDATA_SECTION_NODE = 4; |
|
const ENTITY_REFERENCE_NODE = 5; |
|
const ENTITY_NODE = 6; |
|
const PROCESSING_INSTRUCTION_NODE = 7; |
|
const COMMENT_NODE = 8; |
|
const DOCUMENT_NODE = 9; |
|
const DOCUMENT_TYPE_NODE = 10; |
|
const DOCUMENT_FRAGMENT_NODE = 11; |
|
const NOTATION_NODE = 12; |
|
|
|
const DOCUMENT_POSITION_DISCONNECTED = 0x01; |
|
const DOCUMENT_POSITION_PRECEDING = 0x02; |
|
const DOCUMENT_POSITION_FOLLOWING = 0x04; |
|
const DOCUMENT_POSITION_CONTAINS = 0x08; |
|
const DOCUMENT_POSITION_CONTAINED_BY = 0x10; |
|
const DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 0x20; |
|
|
|
type NodeTypes = |
|
| Node.ELEMENT_NODE |
|
| Node.ATTRIBUTE_NODE |
|
| Node.TEXT_NODE |
|
| Node.CDATA_SECTION_NODE |
|
| Node.ENTITY_REFERENCE_NODE |
|
| Node.ENTITY_NODE |
|
| Node.PROCESSING_INSTRUCTION_NODE |
|
| Node.COMMENT_NODE |
|
| Node.DOCUMENT_NODE |
|
| Node.DOCUMENT_TYPE_NODE |
|
| Node.DOCUMENT_FRAGMENT_NODE |
|
| Node.NOTATION_NODE; |
|
|
|
declare const kType: unique symbol; |
|
|
|
// flavored number type for the nodeType property. keeps typescript happy |
|
// with assignability between Node and its subclasses. For example, we know |
|
// that both Element and Attr extend from Node; so if we have a parameter on a |
|
// function with a type of `Node`, we _should_ be able to assign an Element or |
|
// an Attr to it (among others). But if nodeType was just typed as `number`, |
|
// it would throw a fit since both Element and Attr have types that are more |
|
// specific than that (1 and 2, respectively). |
|
export type NodeType = NodeTypes | (number & { [kType]?: never }); |
|
// the available node types ^^^^ ^^^^^ base type / |
|
// -------- optional == flavor, required == webidl.brand _/ |
|
|
|
// #region Nodes |
|
|
|
export abstract class Node extends EventTarget { |
|
declare readonly ELEMENT_NODE: Node.ELEMENT_NODE; |
|
declare readonly ATTRIBUTE_NODE: Node.ATTRIBUTE_NODE; |
|
declare readonly TEXT_NODE: Node.TEXT_NODE; |
|
declare readonly CDATA_SECTION_NODE: Node.CDATA_SECTION_NODE; |
|
declare readonly ENTITY_REFERENCE_NODE: Node.ENTITY_REFERENCE_NODE; |
|
declare readonly ENTITY_NODE: Node.ENTITY_NODE; |
|
declare readonly PROCESSING_INSTRUCTION_NODE: |
|
Node.PROCESSING_INSTRUCTION_NODE; |
|
declare readonly COMMENT_NODE: Node.COMMENT_NODE; |
|
declare readonly DOCUMENT_NODE: Node.DOCUMENT_NODE; |
|
declare readonly DOCUMENT_TYPE_NODE: Node.DOCUMENT_TYPE_NODE; |
|
declare readonly DOCUMENT_FRAGMENT_NODE: Node.DOCUMENT_FRAGMENT_NODE; |
|
declare readonly NOTATION_NODE: Node.NOTATION_NODE; |
|
|
|
static readonly #_defaultNsMap = { |
|
"": "http://www.w3.org/1999/xhtml", |
|
xml: "http://www.w3.org/XML/1998/namespace", |
|
xmlns: "http://www.w3.org/2000/xmlns/", |
|
}; |
|
readonly #_nsMap: { [key: string]: string } = { ...Node.#_defaultNsMap }; |
|
|
|
readonly nodeType: NodeType = Node.ELEMENT_NODE; |
|
nodeName = ""; |
|
nodeValue: string | null = null; |
|
|
|
#namespaceURI: string | null = null; |
|
#ownerDocument: Document | null = null; |
|
#parentNode: Node | null = null; |
|
#childNodes: NodeList | null = null; |
|
|
|
readonly [webidl.brand]: webidl.brand = webidl.brand; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, Node.prototype); |
|
} |
|
|
|
static { |
|
_.setParentNode = (node, parent) => node.#parentNode = parent; |
|
_.setChildNodes = (node, children) => node.#childNodes = children; |
|
_.setOwnerDocument = (node, doc) => node.#ownerDocument = doc; |
|
_.setNamespaceURI = (node, uri) => node.#namespaceURI = uri; |
|
} |
|
|
|
get ownerDocument(): Document | null { |
|
return this.#ownerDocument ??= isDocument(this) |
|
? this |
|
: isDocument(this.parentNode) |
|
? this.parentNode |
|
: this.parentNode?.ownerDocument ?? null; |
|
} |
|
|
|
set ownerDocument( |
|
value: Document | (Node & { ownerDocument: Document }) | null, |
|
) { |
|
if (!isDocument(this) && value != null) { |
|
if (isDocument(value)) { |
|
this.#ownerDocument = value; |
|
} else if (isDocument(value.ownerDocument)) { |
|
this.#ownerDocument = value.ownerDocument; |
|
} else { |
|
throw new TypeError("Invalid ownerDocument value"); |
|
} |
|
} |
|
} |
|
|
|
get childNodes(): NodeList { |
|
return this.#childNodes ??= new NodeList(); |
|
} |
|
|
|
set childNodes(value: NodeList | Node[] | null) { |
|
if (value == null) { |
|
this.#childNodes = null; |
|
} else if (value instanceof NodeList) { |
|
this.#childNodes = value; |
|
} else { |
|
this.#childNodes = new NodeList(); |
|
push(this.childNodes, ...value); |
|
} |
|
} |
|
|
|
get parentNode(): Node | null { |
|
return this.#parentNode ?? null; |
|
} |
|
|
|
set parentNode(value: Node | null) { |
|
this.#parentNode = value; |
|
} |
|
|
|
get firstChild(): Node | null { |
|
if (this.childNodes?.length) return this.childNodes[0]; |
|
return null; |
|
} |
|
|
|
set firstChild(node: Node | null) { |
|
if (!this.childNodes) this.childNodes = new NodeList(); |
|
this.childNodes[0] = node; |
|
} |
|
|
|
get lastChild(): Node | null { |
|
if (this.childNodes) { |
|
const { length } = this.childNodes; |
|
return this.childNodes[length - 1] ?? null; |
|
} |
|
return null; |
|
} |
|
|
|
set lastChild(node: Node | null) { |
|
if (!this.childNodes) this.childNodes = new NodeList(); |
|
const { length } = this.childNodes; |
|
const index = Math.max(0, length - 1); |
|
this.childNodes[index] = node; |
|
} |
|
|
|
get previousSibling(): Node | null { |
|
if (this.parentNode && this.parentNode.childNodes) { |
|
const index = indexOf(this.parentNode.childNodes, this); |
|
if (index > 0) return this.parentNode.childNodes[index - 1] ?? null; |
|
} |
|
return null; |
|
} |
|
|
|
set previousSibling(node: Node | null) { |
|
if (this.parentNode && this.parentNode.childNodes) { |
|
const index = indexOf(this.parentNode.childNodes, this); |
|
if (index > 0) this.parentNode.childNodes[index - 1] = node; |
|
} |
|
} |
|
|
|
get nextSibling(): Node | null { |
|
if (this.parentNode && this.parentNode.childNodes) { |
|
const { childNodes } = this.parentNode, { length } = childNodes; |
|
const index = indexOf(childNodes, this); |
|
if (index >= 0 && index < length - 1) return childNodes[index + 1]; |
|
} |
|
return null; |
|
} |
|
|
|
set nextSibling(node: Node | null) { |
|
if (this.parentNode && this.parentNode.childNodes) { |
|
const { childNodes } = this.parentNode; |
|
const index = indexOf(childNodes, this); |
|
if (index >= 0) childNodes[index + 1] = node; |
|
} |
|
} |
|
|
|
get textContent(): string | null { |
|
return this.nodeValue; |
|
} |
|
|
|
set textContent(value: string | null) { |
|
this.nodeValue = value; |
|
} |
|
|
|
get qualifiedName(): string { |
|
return this.prefix |
|
? `${this.prefix}:${this.localName}` |
|
: this.localName ?? ""; |
|
} |
|
|
|
get namespaceURI(): string { |
|
return this.#namespaceURI ||= this.lookupNamespaceURI("") ?? ""; |
|
} |
|
|
|
set namespaceURI(namespaceURI: string | null) { |
|
if (namespaceURI) { |
|
this.#namespaceURI = namespaceURI; |
|
const prefix = this.lookupPrefix(namespaceURI); |
|
if (!prefix) this.#_nsMap[""] = namespaceURI; |
|
} else { |
|
this.#namespaceURI = null!; |
|
} |
|
} |
|
|
|
get prefix(): string | null { |
|
return this.nodeName.includes(":") ? this.nodeName.split(":")[0] : null; |
|
} |
|
|
|
set prefix(prefix: string | null) { |
|
this.nodeName = prefix ? `${prefix}:${this.localName}` : this.localName; |
|
} |
|
|
|
get localName(): string { |
|
return this.nodeName.includes(":") |
|
? this.nodeName.split(":")[1] |
|
: this.nodeName; |
|
} |
|
|
|
set localName(localName: string) { |
|
this.nodeName = this.prefix ? `${this.prefix}:${localName}` : localName; |
|
} |
|
|
|
insertBefore<TNewNode extends Node, TOldNode extends Node>( |
|
newChild: TNewNode, |
|
refChild: TOldNode | null, |
|
): TNewNode; |
|
insertBefore(newChild: Node, refChild: Node | null): Node; |
|
insertBefore(newChild: Node, refChild: Node | null): Node { |
|
_insertBefore(this, newChild, refChild); |
|
_.setParentNode(newChild, this); |
|
_.setOwnerDocument(newChild, this.ownerDocument!); |
|
_.setNamespaceURI(newChild, this.namespaceURI); |
|
return newChild; |
|
} |
|
|
|
replaceChild<TNewNode extends Node, TOldNode extends Node>( |
|
newChild: TNewNode, |
|
oldChild: TOldNode, |
|
): TOldNode; |
|
replaceChild(newChild: Node, oldChild: Node): Node; |
|
replaceChild(newChild: Node, oldChild: Node): Node { |
|
if (!this.contains(oldChild)) { |
|
throw new DOMException( |
|
"The node to be replaced is not a child of this node.", |
|
"NotFoundError", |
|
); |
|
} |
|
this.insertBefore(newChild, oldChild); |
|
this.removeChild(oldChild); |
|
_.setParentNode(newChild, this); |
|
return oldChild; |
|
} |
|
|
|
removeChild<TNode extends Node>(oldChild: TNode): TNode; |
|
removeChild(oldChild: Node): Node; |
|
removeChild(oldChild: Node): Node { |
|
if (this.childNodes == null || !this.childNodes.length) { |
|
throw new DOMException("Node has no children", "HierarchyRequestError"); |
|
} |
|
const index = indexOf(this.childNodes, oldChild); |
|
if (index >= 0) { |
|
splice(this.childNodes, index, 1); |
|
oldChild.parentNode = null; |
|
oldChild.nextSibling = null; |
|
oldChild.previousSibling = null; |
|
} |
|
return oldChild; |
|
} |
|
|
|
appendChild<TNode extends Node>(newChild: TNode): TNode; |
|
appendChild(newChild: Node): Node; |
|
appendChild(newChild: Node): Node { |
|
return this.insertBefore(newChild, null); |
|
} |
|
|
|
remove(): void { |
|
if (this.parentNode) { |
|
this.parentNode.removeChild(this); |
|
} else { |
|
throw new DOMException("Node has no parent", "NotFoundError"); |
|
} |
|
} |
|
|
|
append(...nodes: (Node | string)[]): void { |
|
for (let node of nodes) { |
|
if (typeof node === "string") node = new Text(node); |
|
this.appendChild(node); |
|
} |
|
} |
|
|
|
prepend(...nodes: (Node | string)[]): void { |
|
for (let node of nodes) { |
|
if (typeof node === "string") node = new Text(node); |
|
this.insertBefore(node, this.firstChild); |
|
} |
|
} |
|
|
|
before(...nodes: (Node | string)[]): void { |
|
if (this.parentNode) { |
|
for (let node of nodes) { |
|
if (typeof node === "string") node = new Text(node); |
|
this.parentNode.insertBefore(node, this); |
|
} |
|
} |
|
} |
|
|
|
after(...nodes: (Node | string)[]): void { |
|
if (this.parentNode) { |
|
for (let node of nodes) { |
|
if (typeof node === "string") node = new Text(node); |
|
this.parentNode.insertBefore(node, this.nextSibling); |
|
} |
|
} |
|
} |
|
|
|
replaceWith(...nodes: (Node | string)[]): void { |
|
if (this.parentNode) { |
|
for (let node of nodes) { |
|
if (typeof node === "string") node = new Text(node); |
|
this.parentNode.insertBefore(node, this); |
|
} |
|
this.remove(); |
|
} |
|
} |
|
|
|
hasChildNodes(): boolean { |
|
return !!this.childNodes?.length && this.firstChild != null; |
|
} |
|
|
|
cloneNode(deep: boolean): Node { |
|
let node: |
|
| Element |
|
| Text |
|
| Comment |
|
| CDATASection |
|
| ProcessingInstruction |
|
| Attr |
|
| DocumentType |
|
| DocumentFragment |
|
| Document; |
|
if (isElement(this)) { |
|
node = new Element(); |
|
node.textContent = this.textContent; |
|
|
|
for (const attr of this.attributes ?? []) { |
|
node.setAttributeNode(attr.cloneNode(true) as Attr); |
|
} |
|
|
|
if (deep) { |
|
node.childNodes = new NodeList(); |
|
for (const child of this.childNodes ?? []) { |
|
node.appendChild(child.cloneNode(true)); |
|
} |
|
} |
|
} else if (isText(this)) { |
|
node = new Text(this.data); |
|
} else if (isComment(this)) { |
|
node = new Comment(this.data); |
|
} else if (isCDATASection(this)) { |
|
node = new CDATASection(this.data); |
|
} else if (isProcessingInstruction(this)) { |
|
node = this.ownerDocument!.createProcessingInstruction( |
|
this.target, |
|
this.data, |
|
); |
|
} else if (isAttr(this)) { |
|
node = new Attr(); |
|
} else if (isDocumentType(this)) { |
|
const document = this.ownerDocument!; |
|
node = document.implementation.createDocumentType( |
|
this.qualifiedName, |
|
this.publicId, |
|
this.systemId, |
|
); |
|
} else if (isDocumentFragment(this)) { |
|
node = new DocumentFragment(); |
|
} else if (isDocument(this)) { |
|
node = this.implementation.createDocument( |
|
this.namespaceURI ?? "", |
|
this.qualifiedName, |
|
this.doctype!, |
|
); |
|
} else { |
|
throw new Error("Invalid node type"); |
|
} |
|
|
|
// node.#parentNode = this.#parentNode; |
|
_.setParentNode(node, this.parentNode); |
|
// node.#ownerDocument = this.#ownerDocument; |
|
_.setOwnerDocument(node, this.ownerDocument); |
|
// node.#namespaceURI = this.#namespaceURI; |
|
_.setNamespaceURI(node, this.namespaceURI); |
|
|
|
node.localName = this.localName ?? this.nodeName; |
|
node.prefix = this.prefix; |
|
// @ts-ignore readonly |
|
node.nodeName = this.nodeName; |
|
node.nodeValue = this.nodeValue; |
|
|
|
return node; |
|
} |
|
|
|
normalize(): void { |
|
let child = this.firstChild; |
|
while (child) { |
|
const next = child.nextSibling; |
|
if (next && isText(next) && isText(child)) { |
|
const wholeText = child.wholeText; |
|
this.removeChild(next); |
|
child.appendData(next.data); |
|
} else { |
|
child.normalize(); |
|
child = next; |
|
} |
|
} |
|
} |
|
|
|
isSupported(feature: string, version: string): boolean { |
|
return !!this.ownerDocument?.implementation.hasFeature(feature, version); |
|
} |
|
|
|
lookupPrefix(namespaceURI: string): string | null { |
|
// deno-lint-ignore no-this-alias |
|
let el: Node | null = this; |
|
while (el) { |
|
const map = el instanceof Node ? el.#_nsMap : null; |
|
if (map) { |
|
for (const n in map) { |
|
if (map[n] == namespaceURI) { |
|
return n; |
|
} |
|
} |
|
} |
|
el = el.nodeType == 2 ? (el as Attr).ownerDocument : el.parentNode; |
|
} |
|
return null; |
|
} |
|
|
|
lookupNamespaceURI(prefix: string): string | null { |
|
// deno-lint-ignore no-this-alias |
|
let el: Node | null = this; |
|
while (el) { |
|
const map = el instanceof Node ? el.#_nsMap : null; |
|
if (map) { |
|
if (prefix in map) { |
|
return map[prefix]; |
|
} |
|
} |
|
el = el.nodeType == 2 ? (el as Attr).ownerDocument : el.parentNode; |
|
} |
|
return null; |
|
} |
|
|
|
isDefaultNamespace(namespaceURI: string): boolean { |
|
const prefix = this.lookupPrefix(namespaceURI); |
|
return prefix === null; |
|
} |
|
|
|
isEqualNode(node: Node): boolean { |
|
if (this.nodeType !== node.nodeType) return false; |
|
if (this.nodeName !== node.nodeName) return false; |
|
if (this.nodeValue !== node.nodeValue) return false; |
|
if (this.namespaceURI !== node.namespaceURI) return false; |
|
if (this.prefix !== node.prefix) return false; |
|
if (this.localName !== node.localName) return false; |
|
if (this.childNodes?.length !== node.childNodes?.length) return false; |
|
if (isElement(this) && isElement(node)) { |
|
if (this.attributes?.length !== node.attributes?.length) return false; |
|
if (this.tagName !== node.tagName) return false; |
|
if (this.children.length !== node.children.length) return false; |
|
} |
|
return true; |
|
} |
|
|
|
isSameNode(node: Node): boolean { |
|
// TODO: improve this garbage |
|
return this === node; |
|
} |
|
|
|
compareDocumentPosition(other: Node): number { |
|
// TODO: implement |
|
return 0; |
|
} |
|
|
|
contains(other: Node): boolean { |
|
let node: Node | null = other; |
|
while (node) { |
|
if (node === this) return true; |
|
node = node.parentNode; |
|
} |
|
return false; |
|
} |
|
|
|
getRootNode(options?: GetRootNodeOptions): Node { |
|
// deno-lint-ignore no-this-alias |
|
let node: Node = this; |
|
if (options?.composed) { |
|
while (node.parentNode) node = node.parentNode; |
|
} else { |
|
while (node.parentNode && !isDocument(node.parentNode)) { |
|
node = node.parentNode; |
|
} |
|
} |
|
return node; |
|
} |
|
|
|
static { |
|
const constants = { |
|
ELEMENT_NODE, |
|
ATTRIBUTE_NODE, |
|
TEXT_NODE, |
|
CDATA_SECTION_NODE, |
|
ENTITY_REFERENCE_NODE, |
|
ENTITY_NODE, |
|
PROCESSING_INSTRUCTION_NODE, |
|
COMMENT_NODE, |
|
DOCUMENT_NODE, |
|
DOCUMENT_TYPE_NODE, |
|
DOCUMENT_FRAGMENT_NODE, |
|
NOTATION_NODE, |
|
DOCUMENT_POSITION_DISCONNECTED, |
|
DOCUMENT_POSITION_PRECEDING, |
|
DOCUMENT_POSITION_FOLLOWING, |
|
DOCUMENT_POSITION_CONTAINS, |
|
DOCUMENT_POSITION_CONTAINED_BY, |
|
DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC, |
|
} as const; |
|
|
|
const keys = Object.keys(constants) as (keyof typeof constants)[]; |
|
|
|
const descriptors = keys.reduce( |
|
(o, k) => ({ |
|
...o, |
|
[k]: { |
|
value: constants[k], |
|
enumerable: false, |
|
writable: false, |
|
configurable: false, |
|
}, |
|
}), |
|
Object.create(null) as PropertyDescriptorMap, |
|
); |
|
|
|
Object.defineProperties(Node, descriptors); |
|
Object.defineProperties(Node.prototype, descriptors); |
|
} |
|
} |
|
|
|
export declare namespace Node { |
|
export const ELEMENT_NODE = 1; |
|
export type ELEMENT_NODE = 1; |
|
export const ATTRIBUTE_NODE = 2; |
|
export type ATTRIBUTE_NODE = 2; |
|
export const TEXT_NODE = 3; |
|
export type TEXT_NODE = 3; |
|
export const CDATA_SECTION_NODE = 4; |
|
export type CDATA_SECTION_NODE = 4; |
|
export const ENTITY_REFERENCE_NODE = 5; |
|
export type ENTITY_REFERENCE_NODE = 5; |
|
export const ENTITY_NODE = 6; |
|
export type ENTITY_NODE = 6; |
|
export const PROCESSING_INSTRUCTION_NODE = 7; |
|
export type PROCESSING_INSTRUCTION_NODE = 7; |
|
export const COMMENT_NODE = 8; |
|
export type COMMENT_NODE = 8; |
|
export const DOCUMENT_NODE = 9; |
|
export type DOCUMENT_NODE = 9; |
|
export const DOCUMENT_TYPE_NODE = 10; |
|
export type DOCUMENT_TYPE_NODE = 10; |
|
export const DOCUMENT_FRAGMENT_NODE = 11; |
|
export type DOCUMENT_FRAGMENT_NODE = 11; |
|
export const NOTATION_NODE = 12; |
|
export type NOTATION_NODE = 12; |
|
export const DOCUMENT_POSITION_DISCONNECTED = 0x01; |
|
export type DOCUMENT_POSITION_DISCONNECTED = 0x01; |
|
export const DOCUMENT_POSITION_PRECEDING = 0x02; |
|
export type DOCUMENT_POSITION_PRECEDING = 0x02; |
|
export const DOCUMENT_POSITION_FOLLOWING = 0x04; |
|
export type DOCUMENT_POSITION_FOLLOWING = 0x04; |
|
export const DOCUMENT_POSITION_CONTAINS = 0x08; |
|
export type DOCUMENT_POSITION_CONTAINS = 0x08; |
|
export const DOCUMENT_POSITION_CONTAINED_BY = 0x10; |
|
export type DOCUMENT_POSITION_CONTAINED_BY = 0x10; |
|
export const DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 0x20; |
|
export type DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 0x20; |
|
} |
|
|
|
export interface ARIAMixin { |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaAtomic) */ |
|
ariaAtomic: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaAutoComplete) */ |
|
ariaAutoComplete: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaBusy) */ |
|
ariaBusy: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaChecked) */ |
|
ariaChecked: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaColCount) */ |
|
ariaColCount: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaColIndex) */ |
|
ariaColIndex: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaColSpan) */ |
|
ariaColSpan: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaCurrent) */ |
|
ariaCurrent: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaDisabled) */ |
|
ariaDisabled: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaExpanded) */ |
|
ariaExpanded: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaHasPopup) */ |
|
ariaHasPopup: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaHidden) */ |
|
ariaHidden: string | null; |
|
ariaInvalid: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaKeyShortcuts) */ |
|
ariaKeyShortcuts: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaLabel) */ |
|
ariaLabel: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaLevel) */ |
|
ariaLevel: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaLive) */ |
|
ariaLive: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaModal) */ |
|
ariaModal: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaMultiLine) */ |
|
ariaMultiLine: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaMultiSelectable) */ |
|
ariaMultiSelectable: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaOrientation) */ |
|
ariaOrientation: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaPlaceholder) */ |
|
ariaPlaceholder: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaPosInSet) */ |
|
ariaPosInSet: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaPressed) */ |
|
ariaPressed: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaReadOnly) */ |
|
ariaReadOnly: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaRequired) */ |
|
ariaRequired: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaRoleDescription) */ |
|
ariaRoleDescription: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaRowCount) */ |
|
ariaRowCount: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaRowIndex) */ |
|
ariaRowIndex: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaRowSpan) */ |
|
ariaRowSpan: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaSelected) */ |
|
ariaSelected: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaSetSize) */ |
|
ariaSetSize: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaSort) */ |
|
ariaSort: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaValueMax) */ |
|
ariaValueMax: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaValueMin) */ |
|
ariaValueMin: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaValueNow) */ |
|
ariaValueNow: string | null; |
|
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/ariaValueText) */ |
|
ariaValueText: string | null; |
|
role: string | null; |
|
} |
|
// deno-lint-ignore no-empty-interface |
|
export interface Element extends ARIAMixin {} |
|
|
|
export class Element extends Node { |
|
public readonly nodeType: Node.ELEMENT_NODE = Node.ELEMENT_NODE; |
|
declare public nodeName: string; |
|
declare public nodeValue: string | null; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, Element.prototype); |
|
} |
|
|
|
#attributes?: NamedNodeMap; |
|
#classList?: DOMTokenList; |
|
#children?: HTMLCollection | null = null; |
|
#dataset?: DOMStringMap | undefined; |
|
|
|
get dataset(): DOMStringMap { |
|
return this.#dataset ??= createDOMStringMap(this); |
|
} |
|
|
|
get attributes(): NamedNodeMap { |
|
return this.#attributes ??= new NamedNodeMap(this); |
|
} |
|
|
|
get tagName(): string { |
|
return this.nodeName.toUpperCase(); |
|
} |
|
|
|
get isConnected(): boolean { |
|
return !!this.ownerDocument; |
|
} |
|
|
|
get children(): HTMLCollection { |
|
return createHTMLCollection(this.childNodes ?? []); |
|
} |
|
|
|
get childElementCount(): number { |
|
return this.children.length; |
|
} |
|
|
|
get parentElement(): Element | null { |
|
return this.parentNode instanceof Element ? this.parentNode : null; |
|
} |
|
|
|
get firstElementChild(): Element | null { |
|
return this.children[0] ?? null; |
|
} |
|
|
|
get lastElementChild(): Element | null { |
|
return this.children[this.children.length - 1] ?? null; |
|
} |
|
|
|
get previousElementSibling(): Element | null { |
|
const { parentElement } = this; |
|
if (parentElement) { |
|
const index = indexOf(parentElement.children, this); |
|
if (index > 0) return parentElement.children[index - 1]; |
|
} |
|
return null; |
|
} |
|
|
|
get nextElementSibling(): Element | null { |
|
const { parentElement } = this; |
|
if (parentElement) { |
|
const index = indexOf(parentElement.children, this); |
|
if (index >= 0 && index < parentElement.children.length - 1) { |
|
return parentElement.children[index + 1]; |
|
} |
|
} |
|
return null; |
|
} |
|
|
|
get innerHTML(): string { |
|
return [...this.children].map((node) => node.outerHTML).join("") ?? ""; |
|
} |
|
|
|
set innerHTML(html: string) { |
|
const parser = new DOMParser(); |
|
const doc = parser.parseFromString(html, "text/html"); |
|
this.childNodes = doc.body?.childNodes ?? null; |
|
} |
|
|
|
get innerText(): string { |
|
let text = ""; |
|
const stack: Node[] = [...this.childNodes ?? []]; |
|
while (stack.length) { |
|
const node = stack.pop(); |
|
if (isNode(node)) { |
|
if (isElement(node)) { |
|
text += node.innerText; // recurse |
|
} else if (isText(node)) { |
|
text += node.data; // accumulate text content |
|
} else if (isCDATASection(node)) { |
|
text += node.data; // accumulate raw text content |
|
} else if ( |
|
isComment(node) || |
|
isProcessingInstruction(node) || |
|
isDocumentType(node) || |
|
isNotation(node) |
|
) { |
|
text += ""; // strip comments and friends |
|
} else if (isDocument(node) || isDocumentFragment(node)) { |
|
push(stack, ...node.childNodes ?? []); |
|
} else { |
|
continue; |
|
} |
|
} |
|
} |
|
return text; |
|
} |
|
|
|
set innerText(text: string | Text) { |
|
this.childNodes ??= new NodeList(); |
|
if (typeof text === "string") text = new Text(text); |
|
splice(this.childNodes, 0, this.childNodes.length, text); |
|
} |
|
|
|
get outerHTML(): string { |
|
const attrs = this.attributes; |
|
const tagName = this.tagName.toLowerCase(); |
|
let html = `<${tagName}`; |
|
for (let i = 0; i < attrs.length; i++) { |
|
const attr = attrs.item(i); |
|
if (attr && attr.value) html += ` ${attr.name}="${attr.value}"`; |
|
} |
|
return `${html}>${this.innerHTML}</${tagName}>`; |
|
} |
|
|
|
set outerHTML(html: string) { |
|
const parser = new DOMParser(); |
|
const doc = parser.parseFromString(html, "text/html"); |
|
const root = doc.documentElement; |
|
if (root && root instanceof Element) { |
|
this.childNodes = root.childNodes; |
|
this.#children = root.#children; |
|
} |
|
} |
|
|
|
get id(): string { |
|
return this.getAttribute("id"); |
|
} |
|
|
|
set id(id: string) { |
|
this.setAttribute("id", id); |
|
} |
|
|
|
get className(): string { |
|
return this.classList.value; |
|
} |
|
|
|
set className(className: string) { |
|
this.classList.value = className; |
|
} |
|
|
|
get classList(): DOMTokenList { |
|
if (!this.#classList) { |
|
let classNameAttr = this.getAttributeNode("class"); |
|
if (!classNameAttr) { |
|
classNameAttr = new Attr(); |
|
classNameAttr.name = classNameAttr.nodeName = "class"; |
|
classNameAttr.value = ""; |
|
classNameAttr.ownerElement = this; |
|
this.attributes.setNamedItem(classNameAttr); |
|
} |
|
this.#classList = createDOMTokenList( |
|
this, |
|
classNameAttr, |
|
classNameAttr.value.split(/\s+/), |
|
); |
|
} |
|
return this.#classList; |
|
} |
|
|
|
getAttribute(name: string): string { |
|
return this.getAttributeNode(name)?.value ?? ""; |
|
} |
|
|
|
getAttributeNames(): string[] { |
|
const attrs = this.attributes; |
|
const ret: string[] = []; |
|
for (let i = 0; i < attrs.length; i++) { |
|
const attr = attrs.item(i); |
|
if (attr) ret.push(attr.name); |
|
} |
|
return ret; |
|
} |
|
|
|
getAttributeNode(name: string): Attr | null { |
|
const attrs = this.attributes; |
|
if (attrs) { |
|
for (let i = 0; i < attrs.length; i++) { |
|
const attr = attrs.item(i); |
|
if (attr && attr.name === name) { |
|
return attr; |
|
} |
|
} |
|
} |
|
return null; |
|
} |
|
|
|
getAttributeNodeNS(namespaceURI: string, localName: string): Attr | null { |
|
const attrs = this.attributes; |
|
if (attrs) { |
|
for (let i = 0; i < attrs.length; i++) { |
|
const attr = attrs.item(i); |
|
if ( |
|
attr && |
|
attr.localName === localName && |
|
attr.namespaceURI === namespaceURI |
|
) { |
|
return attr; |
|
} |
|
} |
|
} |
|
return null; |
|
} |
|
|
|
getAttributeNS(namespaceURI: string, localName: string): string { |
|
return this.getAttributeNodeNS(namespaceURI, localName)?.value ?? ""; |
|
} |
|
|
|
getElementsByTagName(tagname: string): Element[] { |
|
return this.getElementsByTagNameNS("*", tagname); |
|
} |
|
|
|
getElementsByTagNameNS( |
|
namespace: string | null, |
|
localName: string, |
|
): Element[] { |
|
const qualifiedName = namespace ? `${namespace}:${localName}` : localName; |
|
const elements: Element[] = []; |
|
const stack: Element[] = [this]; |
|
while (stack.length) { |
|
const el = stack.pop(); |
|
if (el) { |
|
if (el.nodeType === ELEMENT_NODE) { |
|
if (el.qualifiedName === qualifiedName || localName === "*") { |
|
push(elements, el); |
|
} |
|
push(stack, ...el.children ?? []); |
|
} |
|
} |
|
} |
|
return elements; |
|
} |
|
|
|
hasAttribute(name: string): boolean { |
|
return this.getAttributeNode(name) !== null; |
|
} |
|
|
|
hasAttributeNS(namespaceURI: string, localName: string): boolean { |
|
return this.getAttributeNodeNS(namespaceURI, localName) !== null; |
|
} |
|
|
|
hasAttributes(): boolean { |
|
return this.attributes.length > 0; |
|
} |
|
|
|
removeAttribute(name: string): void { |
|
const attr = this.getAttributeNode(name); |
|
if (attr) { |
|
this.removeAttributeNode(attr); |
|
} |
|
} |
|
|
|
removeAttributeNode(oldAttr: Attr): Attr | null { |
|
const attrs = this.attributes; |
|
if (attrs) { |
|
for (let i = 0; i < attrs.length; i++) { |
|
const attr = attrs.item(i); |
|
if (attr === oldAttr) { |
|
attrs.removeNamedItem(attr.name); |
|
return attr; |
|
} |
|
} |
|
} |
|
return null; |
|
} |
|
|
|
removeAttributeNodeNS(oldAttr: Attr): Attr | null { |
|
const attrs = this.attributes; |
|
if (attrs) { |
|
for (let i = 0; i < attrs.length; i++) { |
|
const attr = attrs.item(i); |
|
if ( |
|
attr && |
|
attr.localName === oldAttr.localName && |
|
attr.namespaceURI === oldAttr.namespaceURI |
|
) { |
|
attrs.removeNamedItemNS(attr.namespaceURI, attr.localName); |
|
return attr; |
|
} |
|
} |
|
} |
|
return null; |
|
} |
|
|
|
removeAttributeNS(namespaceURI: string, localName: string): void { |
|
const attr = this.getAttributeNodeNS(namespaceURI, localName); |
|
if (attr) this.removeAttributeNode(attr); |
|
} |
|
|
|
setAttribute(name: string, value: string): void { |
|
const attr = this.ownerDocument?.createAttribute(name); |
|
if (attr) { |
|
attr.value = value; |
|
this.setAttributeNode(attr); |
|
} |
|
} |
|
|
|
setAttributeNode(newAttr: Attr): Attr | null { |
|
const attrs = this.attributes; |
|
if (attrs) { |
|
for (let i = 0; i < attrs.length; i++) { |
|
const attr = attrs.item(i); |
|
if (attr && attr.name === newAttr.name) { |
|
attrs.setNamedItem(newAttr); |
|
return attr; |
|
} |
|
} |
|
attrs.setNamedItem(newAttr); |
|
} |
|
return null; |
|
} |
|
|
|
setAttributeNodeNS(newAttr: Attr): Attr | null { |
|
const attrs = this.attributes; |
|
if (attrs) { |
|
for (let i = 0; i < attrs.length; i++) { |
|
const attr = attrs.item(i); |
|
if ( |
|
attr && |
|
attr.namespaceURI === newAttr.namespaceURI && |
|
attr.localName === newAttr.localName |
|
) { |
|
attrs.setNamedItem(newAttr); |
|
return attr; |
|
} |
|
} |
|
attrs.setNamedItem(newAttr); |
|
} |
|
return null; |
|
} |
|
|
|
setAttributeNS( |
|
namespaceURI: string, |
|
qualifiedName: string, |
|
value: string, |
|
): void { |
|
const attr = this.ownerDocument?.createAttributeNS( |
|
namespaceURI, |
|
qualifiedName, |
|
); |
|
if (attr) { |
|
attr.value = value; |
|
this.setAttributeNode(attr); |
|
} |
|
} |
|
|
|
appendChild<TNode extends Node>(newChild: TNode): TNode; |
|
appendChild(newChild: Node): Node; |
|
appendChild(newChild: Node): Node { |
|
if (isDocumentFragment(newChild)) { |
|
return this.insertBefore(newChild, null); |
|
} else { |
|
push(this.childNodes ??= new NodeList(), newChild); |
|
return newChild; |
|
} |
|
} |
|
|
|
insertBefore<TNewNode extends Node, TOldNode extends Node>( |
|
newChild: TNewNode, |
|
refChild: TOldNode | null, |
|
): TNewNode; |
|
insertBefore(newChild: Node, refChild: Node | null): Node; |
|
insertBefore(newChild: Node, refChild: Node | null): Node { |
|
if (isDocumentFragment(newChild)) { |
|
return _insertBefore(this, newChild, refChild); |
|
} else { |
|
return _insertBefore(this, [newChild], refChild); |
|
} |
|
} |
|
|
|
closest<K extends keyof HTMLElementTagNameMap>( |
|
selector: K, |
|
): HTMLElementTagNameMap[K] | null; |
|
closest<K extends keyof SVGElementTagNameMap>( |
|
selector: K, |
|
): SVGElementTagNameMap[K] | null; |
|
closest<E extends Element = Element>(selector: string): E | null; |
|
closest(selector: string): Element | null { |
|
// deno-lint-ignore no-this-alias |
|
let el: Node | null = this; |
|
while (el) { |
|
if (isElement(el)) { |
|
if (el.matches(selector)) return el; |
|
el = el.parentNode; |
|
} |
|
} |
|
return null; |
|
} |
|
|
|
matches(selector: string): boolean { |
|
const ATTR_SELECTOR_RE = |
|
/\[(?<attr>[\w\-]+)(?<modifier>[*^~|$]|)=(['"])(?<value>[^\3]+?)\3\]/y; |
|
const PSEUDO_SELECTOR_RE = /:(?<pseudo>[\w\-]+)(\((?<inner>[^)]+)\))?/y; |
|
const ID_SELECTOR_RE = /(?<tag>.+?)#(?<id>[^\s.]+)(?<rest>.*)/y; |
|
const CLASS_SELECTOR_RE = |
|
/(?<tag>.+?)(?<className>(?:\.([^\s#.:(\[]+))+)(?<rest>.*)/y; |
|
const selectors = selector.split(","); |
|
return selectors.some((selector) => { |
|
const trimmed = selector.trim(); |
|
if (trimmed === "*") return true; |
|
if (trimmed === this.tagName.toLowerCase()) return true; |
|
let match = ID_SELECTOR_RE.exec(trimmed); |
|
if (match && match.groups) { |
|
const { tag, id, rest } = match.groups; |
|
if (tag && tag !== this.tagName.toLowerCase()) return false; |
|
if (id && id !== this.id) return false; |
|
if (rest) return this.matches(rest); |
|
return true; |
|
} |
|
match = CLASS_SELECTOR_RE.exec(trimmed); |
|
if (match && match.groups) { |
|
const { tag, className, rest } = match.groups; |
|
if (tag && tag !== this.tagName.toLowerCase()) return false; |
|
if (className) { |
|
const classNames = className.split("."); |
|
return classNames.every((className) => |
|
this.classList.contains(className) |
|
); |
|
} |
|
if (rest) return this.matches(rest); |
|
return true; |
|
} |
|
match = ATTR_SELECTOR_RE.exec(trimmed); |
|
if (match && match.groups) { |
|
const { attr, modifier, value } = match.groups; |
|
const attribute = this.getAttribute(attr); |
|
if (modifier === "=" && attribute !== value) return false; |
|
if (modifier === "~" && !attribute.includes(value)) return false; |
|
if (modifier === "|" && !attribute.startsWith(value)) return false; |
|
if (modifier === "^" && !attribute.startsWith(value)) return false; |
|
if (modifier === "$" && !attribute.endsWith(value)) return false; |
|
if (modifier === "*" && !attribute.includes(value)) return false; |
|
return true; |
|
} |
|
match = PSEUDO_SELECTOR_RE.exec(trimmed); |
|
if (match && match.groups) { |
|
const { pseudo, inner } = match.groups; |
|
if (pseudo === "not") { |
|
if (!inner) return false; |
|
return !this.matches(inner); |
|
} |
|
if (pseudo === "has") { |
|
if (!inner) return false; |
|
return this.querySelectorAll(inner).length > 0; |
|
} |
|
if (pseudo === "is") { |
|
if (!inner) return false; |
|
return this.matches(inner); |
|
} |
|
return false; |
|
} |
|
match = /^[\w\-]+$/.exec(trimmed); |
|
if (match) return this.tagName.toLowerCase() === trimmed; |
|
return false; |
|
}); |
|
} |
|
|
|
querySelector<K extends keyof SVGElementTagNameMap>( |
|
selectors: K, |
|
): SVGElementTagNameMap[K] | null; |
|
querySelector<K extends keyof HTMLElementTagNameMap>( |
|
selectors: K, |
|
): HTMLElementTagNameMap[K] | null; |
|
querySelector<T extends Element = Element>(selectors: string): T | null; |
|
querySelector(selectors: string): Element | null { |
|
return this.querySelectorAll(selectors)[0] ?? null; |
|
} |
|
|
|
querySelectorAll<K extends keyof SVGElementTagNameMap>( |
|
selectors: K, |
|
): SVGElementTagNameMap[K][]; |
|
querySelectorAll<K extends keyof HTMLElementTagNameMap>( |
|
selectors: K, |
|
): HTMLElementTagNameMap[K][]; |
|
querySelectorAll<T extends Element = Element>( |
|
selectors: string, |
|
): T[]; |
|
querySelectorAll(selector: string): Element[] { |
|
const selectors = selector.split(/\s*,\s*/); |
|
const elements: Element[] = []; |
|
for (let selector of selectors) { |
|
selector = selector.trim(); |
|
_querySelectorAll(this, (node) => node.matches(selector), elements); |
|
} |
|
return elements; |
|
} |
|
|
|
declare public readonly [Symbol.toStringTag]: string; |
|
|
|
[Symbol.for("nodejs.util.inspect.custom")]( |
|
depth: number | null, |
|
options: InspectOptionsStylized, |
|
): string { |
|
const { stylize, ...opts } = { |
|
colors: true, |
|
compact: 3, |
|
getters: true, |
|
showHidden: false, |
|
depth: depth ?? 2, |
|
customInspect: false, |
|
...options, |
|
} satisfies InspectOptionsStylized; |
|
|
|
// const name = this[Symbol.toStringTag] || this.constructor.name; |
|
const tag = this.tagName.toLowerCase(); |
|
const classes = [...this.classList].join("."); |
|
if (depth && depth < 0) { |
|
return `${ |
|
opts.colors ? `\x1b[1m${stylize(tag, "special")}\x1b[22m` : tag |
|
}${ |
|
this.id |
|
? opts.colors ? stylize(`#${this.id}`, "number") : `#${this.id}` |
|
: "" |
|
}${opts.colors ? options.stylize(classes, "undefined") : classes}`; |
|
} else { |
|
return XMLFormatter.format(this.outerHTML, { |
|
finalNewLine: false, |
|
newLine: "\n", |
|
useTabs: false, |
|
}); |
|
} |
|
} |
|
|
|
static { |
|
Object.defineProperties(this.prototype, { |
|
[Symbol.toStringTag]: { |
|
value: "Element", |
|
configurable: true, |
|
enumerable: false, |
|
writable: false, |
|
}, |
|
}); |
|
} |
|
} |
|
|
|
export class Attr extends Node { |
|
public nodeName = ""; |
|
public readonly nodeType: Node.ATTRIBUTE_NODE = Node.ATTRIBUTE_NODE; |
|
public name = ""; |
|
public value = ""; |
|
public specified = true; |
|
public ownerElement: Element | null = null; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, Attr.prototype); |
|
} |
|
} |
|
|
|
export abstract class CharacterData extends Node { |
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, CharacterData.prototype); |
|
} |
|
|
|
get data(): string { |
|
return this.nodeValue ??= ""; |
|
} |
|
|
|
set data(data: string) { |
|
this.nodeValue = data; |
|
} |
|
|
|
get length(): number { |
|
return this.data.length; |
|
} |
|
|
|
set length(length: number) { |
|
length = +length >>> 0; |
|
if (length < 0 || !isFinite(length) || isNaN(length)) { |
|
throw new DOMException("Invalid length", "InvalidCharacterError"); |
|
} |
|
const data = this.data; |
|
if (length < data.length) { |
|
this.data = data.slice(0, length); |
|
} else if (length > data.length) { |
|
this.data = data.padEnd(length, "\u{0}"); |
|
} |
|
} |
|
|
|
substringData(offset: number, count: number): string { |
|
return this.data.substring(offset, offset + count); |
|
} |
|
|
|
appendData(text: string): void { |
|
this.data += text; |
|
} |
|
|
|
replaceData(offset: number, count: number, text: string): void { |
|
const start = this.data.substring(0, offset); |
|
const end = this.data.substring(offset + count); |
|
text = start + text + end; |
|
this.nodeValue = this.data = text; |
|
this.length = text.length; |
|
} |
|
|
|
insertData(offset: number, text: string): void { |
|
this.replaceData(offset, 0, text); |
|
} |
|
|
|
deleteData(offset: number, count: number): void { |
|
this.replaceData(offset, count, ""); |
|
} |
|
|
|
override appendChild(_newChild: Node): Node { |
|
throw new Error("Cannot append child to CharacterData node"); |
|
} |
|
|
|
override insertBefore(_newChild: Node, _refChild: Node | null): Node { |
|
throw new Error("Cannot insert child before CharacterData node"); |
|
} |
|
|
|
override replaceChild(_newChild: Node, _oldChild: Node): Node { |
|
throw new Error("Cannot replace child of CharacterData node"); |
|
} |
|
|
|
override removeChild(_oldChild: Node): Node { |
|
throw new Error("Cannot remove child from CharacterData node"); |
|
} |
|
|
|
override hasChildNodes(): false { |
|
return false; |
|
} |
|
} |
|
|
|
export class Text extends CharacterData { |
|
readonly nodeName = "#text"; |
|
readonly nodeType: Node.TEXT_NODE = Node.TEXT_NODE; |
|
|
|
constructor(data?: string) { |
|
super(); |
|
this.data = data ?? ""; |
|
Object.setPrototypeOf(this, Text.prototype); |
|
} |
|
|
|
splitText(offset: number): Text { |
|
const text = this.data; |
|
const newText = text.substring(offset); |
|
const newNode = new Text(); |
|
newNode.ownerDocument = this.ownerDocument; |
|
newNode.appendData(newText); |
|
if (this.parentNode) { |
|
const nextSibling = this.nextSibling; |
|
if (nextSibling) { |
|
this.parentNode.insertBefore(newNode, nextSibling); |
|
} else { |
|
this.parentNode.appendChild(newNode); |
|
} |
|
} |
|
this.deleteData(offset, text.length - offset); |
|
return newNode; |
|
} |
|
|
|
get wholeText(): string { |
|
let text = ""; |
|
// deno-lint-ignore no-this-alias |
|
let node: Node | null = this; |
|
// append data to the left |
|
while (node && isText(node)) { |
|
text = node.data + text; |
|
node = node.previousSibling; |
|
} |
|
// append data to the right |
|
node = this; |
|
while (node && isText(node)) { |
|
text += node.data; |
|
node = node.nextSibling; |
|
} |
|
return text; |
|
} |
|
} |
|
|
|
export class CDATASection extends CharacterData { |
|
readonly nodeName = "#cdata-section"; |
|
readonly nodeType: Node.CDATA_SECTION_NODE = Node.CDATA_SECTION_NODE; |
|
|
|
constructor(data?: string) { |
|
super(); |
|
this.data = data ?? ""; |
|
Object.setPrototypeOf(this, CDATASection.prototype); |
|
} |
|
} |
|
|
|
export class EntityReference extends Node { |
|
readonly nodeName = "#entity-reference"; |
|
readonly nodeType: Node.ENTITY_REFERENCE_NODE = Node.ENTITY_REFERENCE_NODE; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, EntityReference.prototype); |
|
} |
|
} |
|
|
|
export class Entity extends Node { |
|
readonly nodeName = "#entity"; |
|
readonly nodeType: Node.ENTITY_NODE = Node.ENTITY_NODE; |
|
readonly publicId: string = ""; |
|
readonly systemId: string = ""; |
|
readonly notationName: string = ""; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, Entity.prototype); |
|
} |
|
} |
|
|
|
export class ProcessingInstruction extends Node { |
|
readonly nodeName = "#processing-instruction"; |
|
readonly nodeType: Node.PROCESSING_INSTRUCTION_NODE = |
|
Node.PROCESSING_INSTRUCTION_NODE; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, ProcessingInstruction.prototype); |
|
} |
|
|
|
public nodeValue = ""; |
|
readonly target: string = ""; |
|
|
|
get data(): string { |
|
return this.nodeValue; |
|
} |
|
|
|
set data(data: string) { |
|
this.nodeValue = data; |
|
} |
|
|
|
get length(): number { |
|
return this.nodeValue.length; |
|
} |
|
} |
|
|
|
export class Comment extends CharacterData { |
|
readonly nodeName = "#comment"; |
|
readonly nodeType: Node.COMMENT_NODE = Node.COMMENT_NODE; |
|
|
|
constructor(data?: string) { |
|
super(); |
|
this.data = data ?? ""; |
|
Object.setPrototypeOf(this, Comment.prototype); |
|
} |
|
} |
|
|
|
export class Location { |
|
#url: URL = new URL("about:blank"); |
|
|
|
static { |
|
createLocation = (url, base) => { |
|
const location = new Location(); |
|
location.#url = new URL(url, base); |
|
return location; |
|
}; |
|
} |
|
|
|
get href(): string { |
|
return this.#url.href; |
|
} |
|
|
|
set href(href: string) { |
|
throw new DOMException(`Cannot set "location.href".`, "NotSupportedError"); |
|
} |
|
|
|
get protocol(): string { |
|
return this.#url.protocol; |
|
} |
|
|
|
set protocol(protocol: string) { |
|
throw new DOMException( |
|
`Cannot set "location.protocol".`, |
|
"NotSupportedError", |
|
); |
|
} |
|
|
|
get host(): string { |
|
return this.#url.host; |
|
} |
|
|
|
set host(host: string) { |
|
throw new DOMException(`Cannot set "location.host".`, "NotSupportedError"); |
|
} |
|
|
|
get hostname(): string { |
|
return this.#url.hostname; |
|
} |
|
|
|
set hostname(hostname: string) { |
|
throw new DOMException( |
|
`Cannot set "location.hostname".`, |
|
"NotSupportedError", |
|
); |
|
} |
|
|
|
get port(): string { |
|
return this.#url.port; |
|
} |
|
|
|
set port(port: string) { |
|
throw new DOMException(`Cannot set "location.port".`, "NotSupportedError"); |
|
} |
|
|
|
get pathname(): string { |
|
return this.#url.pathname; |
|
} |
|
|
|
set pathname(pathname: string) { |
|
throw new DOMException( |
|
`Cannot set "location.pathname".`, |
|
"NotSupportedError", |
|
); |
|
} |
|
|
|
get search(): string { |
|
return this.#url.search; |
|
} |
|
|
|
set search(search: string) { |
|
throw new DOMException( |
|
`Cannot set "location.search".`, |
|
"NotSupportedError", |
|
); |
|
} |
|
|
|
get hash(): string { |
|
return this.#url.hash; |
|
} |
|
|
|
set hash(hash: string) { |
|
throw new DOMException(`Cannot set "location.hash".`, "NotSupportedError"); |
|
} |
|
|
|
#ancestorOrigins: DOMStringList | undefined; |
|
|
|
get ancestorOrigins(): DOMStringList { |
|
return this.#ancestorOrigins ??= createDOMStringList([]); |
|
} |
|
|
|
toString(): string { |
|
return this.#url.toString(); |
|
} |
|
} |
|
|
|
export class Document extends Node { |
|
readonly nodeName = "#document"; |
|
readonly nodeType: Node.DOCUMENT_NODE = Node.DOCUMENT_NODE; |
|
|
|
#defaultView: Window | null = window; |
|
#documentElement: Element | undefined; |
|
#implementation: DOMImplementation | undefined; |
|
#location: Location | undefined; |
|
|
|
static { |
|
setDocumentDomImplementation = (doc, impl) => doc.#implementation = impl; |
|
setDocumentDefaultView = (doc, view) => doc.#defaultView = view; |
|
setDocumentElement = (doc, el) => doc.#documentElement = el; |
|
setDocumentLocation = (doc, loc) => doc.#location = loc; |
|
} |
|
|
|
readonly doctype: DocumentType | null; |
|
readonly head: HTMLHeadElement | null = null; |
|
readonly body: HTMLBodyElement | null = null; |
|
readonly compatMode: string = "CSS1Compat"; |
|
|
|
declare readonly domain: string; |
|
declare readonly referrer: string; |
|
|
|
constructor() { |
|
super(); |
|
this.doctype = null!; |
|
Object.setPrototypeOf(this, Document.prototype); |
|
} |
|
|
|
get title(): string { |
|
if (this.head) { |
|
return this.head.querySelector("title")?.textContent ?? ""; |
|
} |
|
return ""; |
|
} |
|
|
|
set title(title: string | Text) { |
|
if (!this.head) { |
|
const head = this.createElement("head"); |
|
Object.assign(this, { head }); |
|
// insert the head before the first element |
|
const firstChild = this.firstChild; |
|
if (firstChild) { |
|
this.insertBefore(head, firstChild); |
|
} else { |
|
this.appendChild(head); |
|
} |
|
} |
|
let titleElement = this.head!.querySelector("title"); |
|
if (titleElement) titleElement.remove(); |
|
titleElement = this.createElement("title"); |
|
title = isText(title) ? title : new Text(String(title)); |
|
titleElement.appendChild(title); |
|
this.head!.appendChild(titleElement); |
|
} |
|
|
|
get implementation(): DOMImplementation { |
|
return this.#implementation ??= new DOMImplementation(); |
|
} |
|
|
|
get documentURI(): string { |
|
return this.location?.href; |
|
} |
|
|
|
get defaultView(): Window | null { |
|
return this.#defaultView ??= window; |
|
} |
|
|
|
get location(): Location { |
|
return this.#location ??= createLocation("about:blank"); |
|
} |
|
|
|
get URL(): string { |
|
return this.location.href; |
|
} |
|
|
|
get documentElement(): Element { |
|
if (!this.#documentElement) { |
|
this.#documentElement = this.createElementNS( |
|
this.namespaceURI as "http://www.w3.org/1999/xhtml", |
|
this.namespaceURI === "http://www.w3.org/1999/xhtml" ? "html" : "root", |
|
); |
|
push(this.childNodes, this.#documentElement); |
|
} |
|
return this.#documentElement; |
|
} |
|
|
|
get children(): HTMLCollection { |
|
return this.documentElement.children; |
|
} |
|
|
|
get childElementCount(): number { |
|
return this.documentElement.childElementCount; |
|
} |
|
|
|
get firstElementChild(): Element | null { |
|
return this.documentElement.firstElementChild; |
|
} |
|
|
|
get lastElementChild(): Element | null { |
|
return this.documentElement.lastElementChild; |
|
} |
|
|
|
get childNodes(): NodeList { |
|
return this.documentElement.childNodes; |
|
} |
|
|
|
get firstChild(): Node | null { |
|
return this.documentElement.firstChild; |
|
} |
|
|
|
get lastChild(): Node | null { |
|
return this.documentElement.lastChild; |
|
} |
|
|
|
get nextSibling(): Node | null { |
|
return this.documentElement.nextSibling; |
|
} |
|
|
|
get previousSibling(): Node | null { |
|
return this.documentElement.previousSibling; |
|
} |
|
|
|
get innerHTML(): string { |
|
return this.documentElement.innerHTML; |
|
} |
|
|
|
set innerHTML(html: string) { |
|
this.documentElement.innerHTML = html; |
|
} |
|
|
|
get outerHTML(): string { |
|
return this.documentElement.outerHTML; |
|
} |
|
|
|
set outerHTML(html: string) { |
|
this.documentElement.outerHTML = html; |
|
} |
|
|
|
get innerText(): string { |
|
return this.documentElement.innerText; |
|
} |
|
|
|
set innerText(text: string | Text) { |
|
this.documentElement.innerText = text; |
|
} |
|
|
|
get textContent(): string { |
|
return this.documentElement.textContent ?? ""; |
|
} |
|
|
|
set textContent(text: string | Text) { |
|
this.documentElement.textContent = isText(text) ? text.wholeText : text; |
|
} |
|
|
|
createAttribute(name: string): Attr { |
|
const attr = new Attr(); |
|
_.setOwnerDocument(attr, this); |
|
attr.name = name; |
|
attr.nodeName = name; |
|
attr.value = ""; |
|
attr.specified = true; |
|
return attr; |
|
} |
|
|
|
createAttributeNS(namespaceURI: string, qualifiedName: string): Attr { |
|
const attr = new Attr(); |
|
_.setOwnerDocument(attr, this); |
|
attr.namespaceURI = namespaceURI; |
|
attr.prefix = null; |
|
attr.localName = qualifiedName; |
|
attr.name = qualifiedName; |
|
attr.nodeName = qualifiedName; |
|
attr.value = ""; |
|
attr.specified = true; |
|
return attr; |
|
} |
|
|
|
createCDATASection(data: string): CDATASection { |
|
const cdata = new CDATASection(data); |
|
_.setOwnerDocument(cdata, this); |
|
return cdata; |
|
} |
|
|
|
createComment(data: string): Comment { |
|
const comment = new Comment(data); |
|
_.setOwnerDocument(comment, this); |
|
return comment; |
|
} |
|
|
|
createDocumentFragment(): DocumentFragment { |
|
const fragment = new DocumentFragment(); |
|
_.setOwnerDocument(fragment, this); |
|
_.setChildNodes(fragment, new NodeList()); |
|
return fragment; |
|
} |
|
|
|
createEntity(name: string): Entity { |
|
const entity = new Entity(); |
|
_.setOwnerDocument(entity, this); |
|
const nodeName = name; |
|
Object.assign(entity, { nodeName }); |
|
return entity; |
|
} |
|
|
|
createEntityReference(name: string): EntityReference { |
|
const entity = new EntityReference(); |
|
_.setOwnerDocument(entity, this); |
|
const nodeName = name; |
|
Object.assign(entity, { nodeName }); |
|
return entity; |
|
} |
|
|
|
createProcessingInstruction( |
|
target: string, |
|
data: string, |
|
): ProcessingInstruction { |
|
const pi = new ProcessingInstruction(); |
|
_.setOwnerDocument(pi, this); |
|
Object.assign(pi, { target, data }); |
|
return pi; |
|
} |
|
|
|
createTextNode(data: string): Text { |
|
const text = new Text(data); |
|
_.setOwnerDocument(text, this); |
|
return text; |
|
} |
|
|
|
createElement<K extends keyof SVGElementTagNameMap>( |
|
tagName: K, |
|
): SVGElementTagNameMap[K]; |
|
createElement<K extends keyof HTMLElementTagNameMap>( |
|
tagName: K, |
|
): HTMLElementTagNameMap[K]; |
|
createElement(tagName: string): Element; |
|
createElement(tagName: string): Element { |
|
const element = new Element(); |
|
_.setOwnerDocument(element, this); |
|
element.nodeName = tagName; |
|
element.localName = tagName; |
|
element.childNodes = new NodeList(); |
|
return element; |
|
} |
|
|
|
createElementNS<K extends keyof SVGElementTagNameMap>( |
|
namespaceURI: "http://www.w3.org/2000/svg", |
|
tagName: K, |
|
): SVGElementTagNameMap[K]; |
|
createElementNS( |
|
namespaceURI: "http://www.w3.org/2000/svg", |
|
qualifiedName: string, |
|
): SVGElement; |
|
createElementNS<K extends keyof HTMLElementTagNameMap>( |
|
namespaceURI: "http://www.w3.org/1999/xhtml", |
|
tagName: K, |
|
): HTMLElementTagNameMap[K]; |
|
createElementNS( |
|
namespaceURI: "http://www.w3.org/1999/xhtml", |
|
qualifiedName: string, |
|
): HTMLElement; |
|
createElementNS(namespaceURI: string, qualifiedName: string): Element; |
|
createElementNS(namespaceURI: string, qualifiedName: string): Element { |
|
const element = new Element(); |
|
_.setOwnerDocument(element, this); |
|
element.nodeName = qualifiedName; |
|
element.namespaceURI = namespaceURI; |
|
element.prefix = null; |
|
element.localName = qualifiedName; |
|
element.childNodes = new NodeList(); |
|
return element; |
|
} |
|
|
|
createTreeWalker( |
|
root: Node, |
|
whatToShow?: number, |
|
filter?: NodeFilter | null, |
|
): TreeWalker { |
|
return createTreeWalker(root, whatToShow, filter); |
|
} |
|
|
|
getElementById(id: string): Element | null { |
|
return this.documentElement.querySelector(`[id="${id}"]`); |
|
} |
|
|
|
getElementsByName<T extends HTMLElement = HTMLElement>(name: string): T[] { |
|
return this.documentElement.querySelectorAll<T>(`[name="${name}"]`); |
|
} |
|
|
|
getElementsByTagName<K extends keyof SVGElementTagNameMap>( |
|
tagName: K, |
|
): SVGElementTagNameMap[K][]; |
|
getElementsByTagName<K extends keyof HTMLElementTagNameMap>( |
|
tagName: K, |
|
): HTMLElementTagNameMap[K][]; |
|
getElementsByTagName(tagName: string): Element[]; |
|
getElementsByTagName(tagName: string): Element[] { |
|
return this.getElementsByTagNameNS("*", tagName); |
|
} |
|
|
|
getElementsByTagNameNS<K extends keyof SVGElementTagNameMap>( |
|
namespaceURI: "http://www.w3.org/2000/svg", |
|
tagName: K, |
|
): SVGElementTagNameMap[K][]; |
|
getElementsByTagNameNS( |
|
namespaceURI: "http://www.w3.org/2000/svg", |
|
tagName: string, |
|
): SVGElement[]; |
|
getElementsByTagNameNS<K extends keyof HTMLElementTagNameMap>( |
|
namespaceURI: "http://www.w3.org/1999/xhtml", |
|
tagName: K, |
|
): HTMLElementTagNameMap[K][]; |
|
getElementsByTagNameNS( |
|
namespaceURI: "http://www.w3.org/1999/xhtml", |
|
tagName: string, |
|
): HTMLElement[]; |
|
getElementsByTagNameNS(namespaceURI: string, tagName: string): Element[]; |
|
getElementsByTagNameNS(namespaceURI: string, tagName: string): Element[] { |
|
const qualifiedName = namespaceURI && namespaceURI !== "*" |
|
? `${namespaceURI}:${tagName}` |
|
: tagName; |
|
const elements: Element[] = []; |
|
const stack: Element[] = [this.documentElement]; |
|
while (stack.length) { |
|
const el = stack.pop(); |
|
if (el && isElement(el)) { |
|
if ( |
|
el.qualifiedName === qualifiedName || tagName === "*" || |
|
(namespaceURI === "*" && el.localName === tagName) |
|
) { |
|
push(elements, el); |
|
} |
|
push(stack, ...el.children ?? []); |
|
} |
|
} |
|
return elements; |
|
} |
|
|
|
getElementsByClassName<K extends keyof SVGElementTagNameMap>( |
|
classNames: string, |
|
): SVGElementTagNameMap[K][]; |
|
getElementsByClassName<K extends keyof HTMLElementTagNameMap>( |
|
classNames: string, |
|
): HTMLElementTagNameMap[K][]; |
|
getElementsByClassName(className: string): Element[] { |
|
return this.querySelectorAll(`[class="${className}"]`); |
|
} |
|
|
|
querySelector<K extends keyof SVGElementTagNameMap>( |
|
selectors: K, |
|
): SVGElementTagNameMap[K] | null; |
|
querySelector<K extends keyof HTMLElementTagNameMap>( |
|
selectors: K, |
|
): HTMLElementTagNameMap[K] | null; |
|
querySelector<T extends Element = Element>(selectors: string): T | null; |
|
querySelector(selectors: string): Element | null { |
|
return this.querySelectorAll(selectors)[0] ?? null; |
|
} |
|
|
|
querySelectorAll<K extends keyof SVGElementTagNameMap>( |
|
selectors: K, |
|
): SVGElementTagNameMap[K][]; |
|
querySelectorAll<K extends keyof HTMLElementTagNameMap>( |
|
selectors: K, |
|
): HTMLElementTagNameMap[K][]; |
|
querySelectorAll<T extends Element = Element>( |
|
selectors: string, |
|
): T[]; |
|
querySelectorAll(selector: string): Element[] { |
|
return this.documentElement.querySelectorAll(selector); |
|
} |
|
|
|
elementsFromPoint(_x: number, _y: number): Element[] { |
|
// TODO: implement |
|
return []; |
|
} |
|
|
|
getSelection(): Selection | null { |
|
// TODO: implement |
|
return null; |
|
} |
|
|
|
hasFocus(): boolean { |
|
// TODO: implement |
|
return false; |
|
} |
|
|
|
declare public readonly [Symbol.toStringTag]: "Document"; |
|
|
|
[Symbol.for("nodejs.util.inspect.custom")]( |
|
depth: number | null, |
|
options: InspectOptionsStylized, |
|
): string { |
|
const { stylize, ...opts } = { |
|
colors: true, |
|
compact: 3, |
|
getters: true, |
|
showHidden: false, |
|
depth: depth ?? 2, |
|
customInspect: false, |
|
...options, |
|
} satisfies InspectOptionsStylized; |
|
|
|
const name = this[Symbol.toStringTag] || this.constructor.name; |
|
depth = depth ?? options.depth ?? null; |
|
if (depth && depth < 0) { |
|
return stylize( |
|
`[${name}: ${this.documentElement.tagName.toLowerCase()}]`, |
|
"special", |
|
); |
|
} else { |
|
const html = this.outerHTML; |
|
return html; |
|
// return opts.colors |
|
// ? highlight(html, { |
|
// finalNewLine: false, |
|
// newLine: "\n", |
|
// useTabs: false, |
|
// }) |
|
// : html; |
|
} |
|
} |
|
|
|
static { |
|
Object.defineProperties(this.prototype, { |
|
[Symbol.toStringTag]: { |
|
value: "Document", |
|
configurable: true, |
|
enumerable: false, |
|
writable: false, |
|
}, |
|
}); |
|
} |
|
} |
|
|
|
export class DocumentType extends Node { |
|
public readonly nodeName = "#document-type"; |
|
public readonly nodeType: Node.DOCUMENT_TYPE_NODE = Node.DOCUMENT_TYPE_NODE; |
|
public name = ""; |
|
public publicId = ""; |
|
public systemId = ""; |
|
public internalSubset = ""; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, DocumentType.prototype); |
|
} |
|
|
|
get outerHTML(): string { |
|
return `<!DOCTYPE ${this.name}${ |
|
this.publicId ? ` PUBLIC "${this.publicId}"` : "" |
|
}${this.systemId ? ` "${this.systemId}"` : ""}>`; |
|
} |
|
|
|
get innerHTML(): string { |
|
return this.outerHTML; |
|
} |
|
|
|
get textContent(): string { |
|
return this.outerHTML; |
|
} |
|
} |
|
|
|
export class DocumentFragment extends Node { |
|
readonly nodeName = "#document-fragment"; |
|
readonly nodeType: Node.DOCUMENT_FRAGMENT_NODE = Node.DOCUMENT_FRAGMENT_NODE; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, DocumentFragment.prototype); |
|
} |
|
|
|
getElementById(id: string): Element | null { |
|
return this.querySelector(`[id="${id}"]`); |
|
} |
|
|
|
querySelector<K extends keyof SVGElementTagNameMap>( |
|
selectors: K, |
|
): SVGElementTagNameMap[K] | null; |
|
querySelector<K extends keyof HTMLElementTagNameMap>( |
|
selectors: K, |
|
): HTMLElementTagNameMap[K] | null; |
|
querySelector<T extends Element = Element>(selectors: string): T | null; |
|
querySelector(selectors: string): Element | null { |
|
return this.querySelectorAll(selectors)[0] ?? null; |
|
} |
|
|
|
querySelectorAll<K extends keyof SVGElementTagNameMap>( |
|
selectors: K, |
|
): SVGElementTagNameMap[K][]; |
|
querySelectorAll<K extends keyof HTMLElementTagNameMap>( |
|
selectors: K, |
|
): HTMLElementTagNameMap[K][]; |
|
querySelectorAll<T extends Element = Element>( |
|
selectors: string, |
|
): T[]; |
|
querySelectorAll(selector: string): Element[] { |
|
const elements: Element[] = []; |
|
_querySelectorAll(this, (node) => node.matches(selector), elements); |
|
return elements; |
|
} |
|
} |
|
|
|
export class Notation extends Node { |
|
readonly nodeName = "#notation"; |
|
readonly nodeType: Node.NOTATION_NODE = Node.NOTATION_NODE; |
|
readonly publicId: string = ""; |
|
readonly systemId: string = ""; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, Notation.prototype); |
|
} |
|
} |
|
|
|
// #endregion Nodes |
|
|
|
// #region Collections |
|
|
|
export class NodeList { |
|
/** Represents a Node in the DOM tree. */ |
|
[index: number]: Node | null; |
|
|
|
/** The number of {@link Node}s in this {@link NodeList}. */ |
|
public length = 0; |
|
|
|
/** |
|
* Returns the {@link Node} at the given index, or `null` if the index is out |
|
* of range. If the |
|
*/ |
|
public item(index: number): Node | null { |
|
index = +index; |
|
if (isNaN(index) || !isFinite(index)) return this.item(0); |
|
if (index < 0 || index >= this.length) return null; |
|
return this[index] || null; |
|
} |
|
|
|
public forEach( |
|
callbackfn: (value: Node, key: number, list: NodeList) => void, |
|
thisArg?: unknown, |
|
): void { |
|
assert( |
|
typeof callbackfn === "function", |
|
"NodeList: callbackfn must be a function", |
|
); |
|
for (let i = 0; i < this.length; i++) { |
|
callbackfn.call(thisArg, this[i]!, i, this); |
|
} |
|
} |
|
|
|
public *keys(): IterableIterator<number> { |
|
for (let i = 0; i < this.length; i++) yield i; |
|
} |
|
|
|
public *values(): IterableIterator<Node> { |
|
for (const index of this.keys()) yield this[index]!; |
|
} |
|
|
|
public *entries(): IterableIterator<[number, Node]> { |
|
for (const index of this.keys()) yield [index, this[index]!]; |
|
} |
|
|
|
public [Symbol.iterator](): IterableIterator<Node> { |
|
return this.values(); |
|
} |
|
|
|
declare public readonly [Symbol.toStringTag]: "NodeList"; |
|
|
|
get [webidl.brand](): webidl.brand { |
|
return webidl.brand; |
|
} |
|
|
|
[Symbol.for("nodejs.util.inspect.custom")]( |
|
depth: number | null, |
|
options: InspectOptionsStylized, |
|
): string { |
|
const { stylize, ...opts } = { |
|
colors: true, |
|
compact: 3, |
|
getters: true, |
|
showHidden: false, |
|
depth: depth ?? 2, |
|
customInspect: false, |
|
...options, |
|
} satisfies InspectOptionsStylized; |
|
|
|
const name = this[Symbol.toStringTag] || this.constructor.name; |
|
const values = [...this.values()]; |
|
const size = this.length; |
|
if (depth && depth < 0) { |
|
return stylize(`[${name} (${size})]`, "special"); |
|
} else { |
|
return `${name} (${size}) ${inspect(values, opts)}`; |
|
} |
|
} |
|
|
|
static { |
|
Object.defineProperties(this.prototype, { |
|
[Symbol.toStringTag]: { |
|
value: "NodeList", |
|
configurable: true, |
|
enumerable: false, |
|
writable: false, |
|
}, |
|
[Symbol.iterator]: { |
|
value: this.prototype.values, |
|
configurable: true, |
|
enumerable: false, |
|
writable: true, |
|
}, |
|
}); |
|
} |
|
} |
|
|
|
export class NamedNodeMap { |
|
#ownerElement: Element | null; |
|
#attributes: Attr[]; |
|
|
|
get [webidl.brand](): webidl.brand { |
|
return webidl.brand; |
|
} |
|
|
|
constructor(ownerElement: Element | null) { |
|
this.#ownerElement = ownerElement; |
|
this.#attributes = []; |
|
|
|
const attributes: Attr[] = []; |
|
this.#attributes = attributes; |
|
|
|
const forbidden = [ |
|
"length", |
|
"item", |
|
"getNamedItem", |
|
"setNamedItem", |
|
"removeNamedItem", |
|
"getNamedItemNS", |
|
"setNamedItemNS", |
|
"removeNamedItemNS", |
|
"keys", |
|
"values", |
|
"entries", |
|
Symbol.iterator, |
|
]; |
|
|
|
return new Proxy(this, { |
|
get: (_, prop) => { |
|
if (prop === "length") return attributes.length; |
|
if (prop === "item") return this.item.bind(this); |
|
if (prop === "getNamedItem") return this.getNamedItem.bind(this); |
|
if (prop === "setNamedItem") return this.setNamedItem.bind(this); |
|
if (prop === "removeNamedItem") { |
|
return this.removeNamedItem.bind(this); |
|
} |
|
if (prop === "getNamedItemNS") { |
|
return this.getNamedItemNS.bind(this); |
|
} |
|
if (prop === "setNamedItemNS") { |
|
return this.setNamedItemNS.bind(this); |
|
} |
|
if (prop === "removeNamedItemNS") { |
|
return this.removeNamedItemNS.bind(this); |
|
} |
|
if (prop === "keys") return this.keys.bind(this); |
|
if (prop === "values") return this.values.bind(this); |
|
if (prop === "entries") return this.entries.bind(this); |
|
if (prop === Symbol.iterator) return this.values.bind(this); |
|
if (typeof prop === "string") { |
|
const attr = this.getNamedItem(prop); |
|
if (attr) return attr.value; |
|
} |
|
return Reflect.get(this, prop, this); |
|
}, |
|
set: (_, prop, value) => { |
|
if (forbidden.includes(prop)) return false; |
|
if (typeof prop === "string") { |
|
let attr = this.getNamedItem(prop); |
|
if (attr) { |
|
attr.value = value; |
|
} else { |
|
attr = new Attr(); |
|
attr.ownerElement = ownerElement; |
|
attr.name = prop; |
|
attr.nodeName = prop; |
|
attr.value = value; |
|
this.setNamedItem(attr); |
|
} |
|
return true; |
|
} |
|
return Reflect.set(this, prop, value); |
|
}, |
|
has: (_, prop) => { |
|
if (forbidden.includes(prop)) return true; |
|
if (typeof prop === "string") { |
|
return this.getNamedItem(prop) !== null; |
|
} |
|
return Reflect.has(this, prop); |
|
}, |
|
ownKeys: () => { |
|
const keys = Reflect.ownKeys(this); |
|
for (let i = 0; i < attributes.length; i++) { |
|
push(keys, String(i)); |
|
} |
|
return [...new Set(keys)].filter((k) => !forbidden.includes(k)); |
|
}, |
|
getOwnPropertyDescriptor: (_, prop) => { |
|
if (forbidden.includes(prop)) { |
|
if (prop === "length") { |
|
return { |
|
enumerable: false, |
|
configurable: true, |
|
get: () => this.length, |
|
set: (_) => {/*noop*/}, |
|
}; |
|
} |
|
let value = this[prop as keyof this]; |
|
if (typeof value === "function") value = value.bind(this); |
|
return { |
|
enumerable: false, |
|
configurable: true, |
|
writable: false, |
|
value, |
|
}; |
|
} |
|
if (typeof prop === "string") { |
|
const attr = this.getNamedItem(prop); |
|
if (attr) { |
|
return { |
|
value: attr.value, |
|
writable: true, |
|
enumerable: true, |
|
configurable: true, |
|
}; |
|
} |
|
} |
|
return Reflect.getOwnPropertyDescriptor(this, prop); |
|
}, |
|
deleteProperty: (_, prop) => { |
|
if (forbidden.includes(prop)) return false; |
|
if (typeof prop === "string") { |
|
return this.removeNamedItem(prop) !== null; |
|
} |
|
return Reflect.deleteProperty(this, prop); |
|
}, |
|
defineProperty: (_, prop, descriptor) => { |
|
if (forbidden.includes(prop)) return false; |
|
if (typeof prop === "string") { |
|
const attr = this.getNamedItem(prop); |
|
if (attr) { |
|
attr.value = descriptor.value; |
|
} else { |
|
const attr = new Attr(); |
|
attr.ownerElement = ownerElement; |
|
attr.name = prop; |
|
attr.nodeName = prop; |
|
attr.value = descriptor.value; |
|
this.setNamedItem(attr); |
|
} |
|
return true; |
|
} |
|
return Reflect.defineProperty(this, prop, descriptor); |
|
}, |
|
}); |
|
} |
|
|
|
get length(): number { |
|
return this.#attributes.length; |
|
} |
|
|
|
item(index: number): Attr | null { |
|
return this.#attributes[index] || null; |
|
} |
|
|
|
getNamedItem(name: string): Attr | null { |
|
for (const attr of this.#attributes) { |
|
if (attr.name === name) { |
|
return attr; |
|
} |
|
} |
|
return null; |
|
} |
|
|
|
setNamedItem(attr: Attr): Attr { |
|
if (attr.ownerElement) attr.ownerElement.removeAttributeNode(attr); |
|
attr.ownerElement = this.#ownerElement; |
|
this.#attributes.push(attr); |
|
return attr; |
|
} |
|
|
|
removeNamedItem(name: string): Attr | null { |
|
for (let i = 0; i < this.#attributes.length; i++) { |
|
const attr = this.#attributes[i]; |
|
if (attr.name === name) { |
|
this.#attributes.splice(i, 1); |
|
attr.ownerElement = null; |
|
return attr; |
|
} |
|
} |
|
return null; |
|
} |
|
|
|
getNamedItemNS(namespaceURI: string, localName: string): Attr | null { |
|
for (const attr of this.#attributes) { |
|
if ( |
|
attr.namespaceURI === namespaceURI && attr.localName === localName |
|
) { |
|
return attr; |
|
} |
|
} |
|
return null; |
|
} |
|
|
|
setNamedItemNS(attr: Attr): Attr { |
|
if (attr.ownerElement) attr.ownerElement.removeAttributeNode(attr); |
|
attr.ownerElement = this.#ownerElement; |
|
this.#attributes.push(attr); |
|
return attr; |
|
} |
|
|
|
removeNamedItemNS(namespaceURI: string, localName: string): Attr | null { |
|
for (let i = 0; i < this.#attributes.length; i++) { |
|
const attr = this.#attributes[i]; |
|
if ( |
|
attr.namespaceURI === namespaceURI && attr.localName === localName |
|
) { |
|
this.#attributes.splice(i, 1); |
|
attr.ownerElement = null; |
|
return attr; |
|
} |
|
} |
|
return null; |
|
} |
|
|
|
*keys(): IterableIterator<string> { |
|
for (const attr of this.#attributes) yield attr.name; |
|
} |
|
|
|
*values(): IterableIterator<Attr> { |
|
for (const attr of this.#attributes) yield attr; |
|
} |
|
|
|
*entries(): IterableIterator<[string, Attr]> { |
|
for (const attr of this.#attributes) yield [attr.name, attr]; |
|
} |
|
|
|
[Symbol.iterator](): IterableIterator<Attr> { |
|
return this.values(); |
|
} |
|
|
|
declare public readonly [Symbol.toStringTag]: "NamedNodeMap"; |
|
|
|
static { |
|
Object.defineProperties(this.prototype, { |
|
[Symbol.toStringTag]: { |
|
value: "NamedNodeMap", |
|
configurable: true, |
|
enumerable: false, |
|
writable: false, |
|
}, |
|
[Symbol.iterator]: { |
|
value: this.prototype.values, |
|
configurable: true, |
|
enumerable: false, |
|
writable: true, |
|
}, |
|
}); |
|
} |
|
} |
|
|
|
export class DOMStringList { |
|
[index: number]: string; |
|
|
|
#values: string[]; |
|
|
|
constructor() { |
|
const values: string[] = []; |
|
this.#values = values; |
|
|
|
return new Proxy(this, { |
|
get: (_, prop) => { |
|
if (typeof prop === "string") { |
|
const index = +prop; |
|
if (!isNaN(index) && isFinite(index)) return this.item(index); |
|
} |
|
return Reflect.get(this, prop); |
|
}, |
|
set: (_, prop, value) => { |
|
if (typeof prop === "string") { |
|
const index = +prop; |
|
if (!isNaN(index) && isFinite(index)) { |
|
return Reflect.set(this, index, value); |
|
} |
|
} |
|
return Reflect.set(this, prop, value); |
|
}, |
|
has: (_, prop) => { |
|
if (typeof prop === "string") { |
|
const index = +prop; |
|
if (!isNaN(index) && isFinite(index)) { |
|
return this.item(index) !== null; |
|
} |
|
} |
|
return Reflect.has(this, prop); |
|
}, |
|
ownKeys: () => { |
|
const keys = Reflect.ownKeys(this); |
|
for (let i = 0; i < values.length; i++) { |
|
push(keys, String(i)); |
|
} |
|
return [...new Set(keys)]; |
|
}, |
|
getOwnPropertyDescriptor: (_, prop) => { |
|
if (typeof prop === "string") { |
|
const index = +prop; |
|
if (!isNaN(index) && isFinite(index)) { |
|
const value = this.item(index); |
|
if (value !== null) { |
|
return { |
|
value, |
|
writable: true, |
|
enumerable: true, |
|
configurable: true, |
|
}; |
|
} |
|
} |
|
} |
|
return Reflect.getOwnPropertyDescriptor(this, prop); |
|
}, |
|
}); |
|
} |
|
|
|
get [webidl.brand](): webidl.brand { |
|
return webidl.brand; |
|
} |
|
|
|
get length(): number { |
|
return this.#values.length; |
|
} |
|
|
|
item(index: number): string | null { |
|
return this.#values[index] || null; |
|
} |
|
|
|
contains(string: string): boolean { |
|
return this.#values.includes(string); |
|
} |
|
|
|
*keys(): IterableIterator<number> { |
|
yield* this.#values.keys(); |
|
} |
|
|
|
*values(): IterableIterator<string> { |
|
yield* this.#values.values(); |
|
} |
|
|
|
*entries(): IterableIterator<[number, string]> { |
|
yield* this.#values.entries(); |
|
} |
|
|
|
toString(): string { |
|
return this.#values.join(" "); |
|
} |
|
|
|
[Symbol.iterator](): IterableIterator<string> { |
|
return this.values(); |
|
} |
|
|
|
declare public readonly [Symbol.toStringTag]: "DOMStringList"; |
|
|
|
[Symbol.for("nodejs.util.inspect.custom")]( |
|
depth: number | null, |
|
options: InspectOptionsStylized, |
|
): string { |
|
const { stylize, ...opts } = { |
|
colors: true, |
|
compact: 3, |
|
getters: true, |
|
showHidden: false, |
|
depth: depth ?? 2, |
|
customInspect: false, |
|
...options, |
|
} satisfies InspectOptionsStylized; |
|
|
|
const name = this[Symbol.toStringTag] || this.constructor.name; |
|
const values = Object.assign({}, [...this.values()]); |
|
const size = this.length; |
|
if (depth && depth < 0) { |
|
return stylize(`[${name} (${size})]`, "special"); |
|
} else { |
|
return `${name} (${size}) ${inspect(values, opts)}`; |
|
} |
|
} |
|
|
|
static { |
|
Object.defineProperties(this.prototype, { |
|
[Symbol.toStringTag]: { |
|
value: "DOMStringList", |
|
configurable: true, |
|
enumerable: false, |
|
writable: false, |
|
}, |
|
[Symbol.iterator]: { |
|
value: this.prototype.values, |
|
configurable: true, |
|
enumerable: false, |
|
writable: true, |
|
}, |
|
}); |
|
|
|
createDOMStringList = (strings) => { |
|
const list = new DOMStringList(); |
|
list.#values = strings; |
|
return list; |
|
}; |
|
} |
|
} |
|
|
|
export class DOMTokenList { |
|
#element: Element; |
|
#attribute?: Attr; |
|
#values: string[] = []; |
|
|
|
constructor(element: Element, attribute?: Attr) { |
|
this.#element = element; |
|
if (attribute) { |
|
this.#attribute = attribute; |
|
this.value = attribute.value; |
|
} |
|
} |
|
|
|
get [webidl.brand](): webidl.brand { |
|
return webidl.brand; |
|
} |
|
|
|
get value(): string { |
|
return this.#values.filter(Boolean).join(" "); |
|
} |
|
|
|
set value(value: string | string[]) { |
|
if (typeof value === "string") value = value.split(/\s+/); |
|
if (Array.isArray(value)) { |
|
this.#values = value; |
|
if (this.#attribute) this.#attribute.value = value.join(" "); |
|
} else { |
|
throw new DOMException("Invalid value", "SyntaxError"); |
|
} |
|
} |
|
|
|
get length(): number { |
|
return this.value.split(/\s+/).length; |
|
} |
|
|
|
get ownerElement(): Element { |
|
return this.#element; |
|
} |
|
|
|
item(index: number): string | null { |
|
return this.#values[index] || null; |
|
} |
|
|
|
contains(token: string): boolean { |
|
return this.#values.includes(token); |
|
} |
|
|
|
add(...tokens: string[]): void { |
|
const value = this.#values; |
|
for (const token of tokens) { |
|
if (!value.includes(token)) value.push(token); |
|
} |
|
this.value = value.join(" "); |
|
} |
|
|
|
remove(...tokens: string[]): void { |
|
const values = this.#values; |
|
for (const token of tokens) { |
|
const index = indexOf(values, token); |
|
if (index >= 0) splice(values, index, 1); |
|
} |
|
this.value = values.join(" "); |
|
} |
|
|
|
toggle(token: string, force?: boolean): boolean { |
|
const value = this.#values; |
|
const index = indexOf(value, token); |
|
if (index >= 0) { |
|
if (force) return true; |
|
splice(value, index, 1); |
|
} else { |
|
if (force === false) return false; |
|
push(value, token); |
|
} |
|
this.value = value.join(" "); |
|
return true; |
|
} |
|
|
|
replace(oldToken: string, newToken: string): boolean { |
|
const value = this.#values; |
|
const index = indexOf(value, oldToken); |
|
if (index >= 0) { |
|
splice(value, index, 1, newToken); |
|
this.value = value.join(" "); |
|
return true; |
|
} else { |
|
return false; |
|
} |
|
} |
|
|
|
supports(token: string): boolean { |
|
// this is dumb |
|
return token ? true : false; |
|
} |
|
|
|
toString(): string { |
|
return this.value; |
|
} |
|
|
|
*keys(): IterableIterator<number> { |
|
yield* this.#values.keys(); |
|
} |
|
|
|
*values(): IterableIterator<string> { |
|
yield* this.#values.values(); |
|
} |
|
|
|
*entries(): IterableIterator<[number, string]> { |
|
yield* this.#values.entries(); |
|
} |
|
|
|
[Symbol.iterator](): IterableIterator<string> { |
|
return this.values(); |
|
} |
|
|
|
declare public readonly [Symbol.toStringTag]: "DOMTokenList"; |
|
|
|
static { |
|
Object.defineProperties(this.prototype, { |
|
[Symbol.toStringTag]: { |
|
value: "DOMTokenList", |
|
configurable: true, |
|
enumerable: false, |
|
writable: false, |
|
}, |
|
[Symbol.iterator]: { |
|
value: this.prototype.values, |
|
configurable: true, |
|
enumerable: false, |
|
writable: true, |
|
}, |
|
}); |
|
|
|
createDOMTokenList = (element, attr, strings) => { |
|
const list = new DOMTokenList(element, attr); |
|
list.#element = element; |
|
list.#attribute = attr; |
|
list.#values = strings; |
|
return list; |
|
}; |
|
} |
|
} |
|
|
|
export class DOMStringMap { |
|
[key: string]: string | undefined; |
|
|
|
#element!: Element; |
|
|
|
static readonly #camelize = (str: string): string => |
|
str.replace(/(?<=[a-z0-9])-([a-z])/g, (_, c) => c.toUpperCase()); |
|
|
|
static readonly #kebabify = (str: string): string => |
|
str.replace( |
|
/(?<=[a-z0-9])([A-Z])(?=[a-z])/g, |
|
(_, c) => `-${c.toLowerCase()}`, |
|
); |
|
|
|
constructor() { |
|
const $element = () => this.#element; |
|
const camelize = (s: string, p = "data") => |
|
DOMStringMap.#camelize(s.replace(new RegExp(`^${p}-`), "")); |
|
const kebabify = (s: string, p = "data") => |
|
[p, DOMStringMap.#kebabify(s)].filter(Boolean).join("-"); |
|
|
|
const forbidden = [ |
|
"constructor", |
|
"prototype", |
|
"hasOwnProperty", |
|
"toString", |
|
"valueOf", |
|
"__proto__", |
|
"isPrototypeOf", |
|
]; |
|
return new Proxy(this, { |
|
get: (_, p) => { |
|
const el = $element(); |
|
if (typeof p === "string") { |
|
const key = kebabify(p); |
|
if (!forbidden.includes(p) && !forbidden.includes(key)) { |
|
if (el.hasAttribute(key)) { |
|
const attr = el.getAttribute(key); |
|
Reflect.set(this, p, attr); |
|
return attr; |
|
} |
|
if (Reflect.has(this, key)) return Reflect.get(this, key); |
|
} |
|
} |
|
return Reflect.get(this, p); |
|
}, |
|
set: (_, p, v) => { |
|
if (typeof p === "string") { |
|
const el = $element(), key = kebabify(p); |
|
if (!forbidden.includes(p) && !forbidden.includes(key)) { |
|
el.setAttribute(key, v); |
|
return Reflect.set(this, p, v); |
|
} |
|
} |
|
return Reflect.set(this, p, v); |
|
}, |
|
deleteProperty: (_, p) => { |
|
if (typeof p === "string") { |
|
const el = $element(), key = kebabify(p); |
|
if (!forbidden.includes(p) && !forbidden.includes(key)) { |
|
if (el.hasAttribute(key)) el.removeAttribute(key); |
|
} |
|
} |
|
return Reflect.deleteProperty(this, p); |
|
}, |
|
defineProperty: (_, p, attributes) => { |
|
if (typeof p === "string") { |
|
const el = $element(), key = kebabify(p); |
|
if (!forbidden.includes(p) && !forbidden.includes(key)) { |
|
el.setAttribute(key, attributes.value); |
|
return Reflect.defineProperty(this, key, attributes); |
|
} |
|
} |
|
return Reflect.defineProperty(this, p, attributes); |
|
}, |
|
ownKeys: () => { |
|
const el = $element(); |
|
const keys = new Set<string>(); |
|
for (const key of Reflect.ownKeys(this)) keys.add(key as string); |
|
for (const key of el.getAttributeNames()) { |
|
if (!key.startsWith("data-")) continue; |
|
const p = camelize(key); |
|
if (!forbidden.includes(p) && !forbidden.includes(key)) keys.add(p); |
|
} |
|
return [...keys]; |
|
}, |
|
getOwnPropertyDescriptor: (_, p) => { |
|
if (typeof p === "string") { |
|
const el = $element(), key = kebabify(p); |
|
if (!forbidden.includes(p) && !forbidden.includes(key)) { |
|
if (el.hasAttribute(key)) { |
|
const attr = el.getAttribute(key); |
|
return { |
|
configurable: true, |
|
enumerable: true, |
|
writable: true, |
|
value: attr, |
|
}; |
|
} |
|
} |
|
} |
|
return Reflect.getOwnPropertyDescriptor(this, p); |
|
}, |
|
}); |
|
} |
|
|
|
get [webidl.brand](): webidl.brand { |
|
return webidl.brand; |
|
} |
|
|
|
declare public readonly [Symbol.toStringTag]: "DOMStringMap"; |
|
|
|
static { |
|
createDOMStringMap = (element) => { |
|
const map = new DOMStringMap(); |
|
map.#element = element; |
|
return map; |
|
}; |
|
|
|
Object.defineProperties(this.prototype, { |
|
[Symbol.toStringTag]: { |
|
value: "DOMStringMap", |
|
configurable: true, |
|
enumerable: false, |
|
writable: false, |
|
}, |
|
}); |
|
} |
|
} |
|
|
|
export interface NodeFilter { |
|
(node: Node): number; |
|
} |
|
|
|
export abstract class NodeFilter { |
|
static readonly FILTER_ACCEPT = 1; |
|
static readonly FILTER_REJECT = 2; |
|
static readonly FILTER_SKIP = 3; |
|
static readonly SHOW_ALL = 0xFFFFFFFF; |
|
static readonly SHOW_ELEMENT = 0x01; |
|
static readonly SHOW_ATTRIBUTE = 0x02; |
|
static readonly SHOW_TEXT = 0x04; |
|
static readonly SHOW_CDATA_SECTION = 0x08; |
|
static readonly SHOW_ENTITY_REFERENCE = 0x10; |
|
static readonly SHOW_ENTITY = 0x20; |
|
static readonly SHOW_PROCESSING_INSTRUCTION = 0x40; |
|
static readonly SHOW_COMMENT = 0x80; |
|
static readonly SHOW_DOCUMENT = 0x100; |
|
static readonly SHOW_DOCUMENT_TYPE = 0x200; |
|
static readonly SHOW_DOCUMENT_FRAGMENT = 0x400; |
|
static readonly SHOW_NOTATION = 0x800; |
|
|
|
acceptNode(node: Node): number { |
|
return node instanceof Node |
|
? NodeFilter.FILTER_ACCEPT |
|
: NodeFilter.FILTER_SKIP; |
|
} |
|
|
|
private constructor() { |
|
if (new.target === NodeFilter) { |
|
throw new TypeError("Illegal constructor"); |
|
} |
|
} |
|
|
|
get [webidl.brand](): webidl.brand { |
|
return webidl.brand; |
|
} |
|
} |
|
|
|
export class TreeWalker { |
|
readonly root!: Node; |
|
readonly whatToShow!: number; |
|
readonly filter!: NodeFilter | null; |
|
currentNode!: Node; |
|
|
|
constructor() { |
|
Object.setPrototypeOf(this, TreeWalker.prototype); |
|
} |
|
|
|
get [webidl.brand](): webidl.brand { |
|
return webidl.brand; |
|
} |
|
|
|
nextNode(): Node | null { |
|
const currentNode = this.currentNode; |
|
if (currentNode === null) { |
|
return null; |
|
} |
|
return this.#traverse(currentNode); |
|
} |
|
|
|
previousNode(): Node | null { |
|
const currentNode = this.currentNode; |
|
if (currentNode === null) { |
|
return null; |
|
} |
|
return this.#traverse(currentNode, true); |
|
} |
|
|
|
#traverse(currentNode: Node, previous = false): Node | null { |
|
let node = previous ? currentNode.previousSibling : currentNode.nextSibling; |
|
while (node === null) { |
|
if (currentNode.parentNode === null) { |
|
return null; |
|
} |
|
currentNode = currentNode.parentNode; |
|
if (currentNode === this.root) { |
|
return null; |
|
} |
|
node = previous ? currentNode.previousSibling : currentNode.nextSibling; |
|
} |
|
if (this.filter === null || this.filter.acceptNode(node) === 1) { |
|
this.currentNode = node; |
|
return node; |
|
} |
|
return this.#traverse(node, previous); |
|
} |
|
|
|
static { |
|
createTreeWalker = (root, whatToShow, filter) => { |
|
const walker = new TreeWalker(); |
|
walker.currentNode = root; |
|
Object.assign(walker, { root, whatToShow, filter }); |
|
return walker; |
|
}; |
|
} |
|
} |
|
|
|
// extend from deno's built-in formdata class. |
|
export class FormData extends globalThis.FormData { |
|
#form?: HTMLFormElement; |
|
#submitter?: HTMLElement; |
|
|
|
constructor(form?: HTMLFormElement, submitter?: HTMLElement) { |
|
// deno's formdata does not allow passing in a form element, |
|
// and will throw a type error if we pass it in here. |
|
super(); |
|
Object.setPrototypeOf(this, FormData.prototype); |
|
if (form && form instanceof HTMLFormElement) this.#form = form; |
|
if (submitter && submitter instanceof HTMLElement) { |
|
this.#submitter = submitter; |
|
submitter.addEventListener("click", (e) => { |
|
e.preventDefault(); |
|
this.submit(); |
|
}); |
|
} |
|
} |
|
|
|
get [webidl.brand](): webidl.brand { |
|
return webidl.brand; |
|
} |
|
|
|
get form(): HTMLFormElement | undefined { |
|
return this.#form; |
|
} |
|
|
|
get submitter(): HTMLElement | undefined { |
|
return this.#submitter; |
|
} |
|
|
|
async submit(): Promise<void> { |
|
if (this.form) { |
|
const { form } = this; |
|
const action = form.action; |
|
const method = form.method || "GET"; |
|
// deno-lint-ignore no-this-alias |
|
const body = this; |
|
const target = form.target; |
|
const submitter = this.#submitter; |
|
const event = new Event("submit", { cancelable: true }); |
|
if (submitter) { |
|
Object.defineProperty(event, "submitter", { |
|
value: submitter, |
|
enumerable: true, |
|
configurable: true, |
|
writable: true, |
|
}); |
|
} |
|
if (form.dispatchEvent(event)) { |
|
const res = await fetch(action, { method, body }).catch( |
|
(e) => e.name !== "AbortError" && Promise.reject(e), |
|
); |
|
await form.submit(); |
|
} |
|
} |
|
} |
|
|
|
declare readonly [Symbol.toStringTag]: "FormData"; |
|
|
|
static { |
|
Object.defineProperties(this.prototype, { |
|
[Symbol.toStringTag]: { |
|
value: "FormData", |
|
configurable: true, |
|
enumerable: false, |
|
writable: false, |
|
}, |
|
}); |
|
} |
|
} |
|
|
|
export class HTMLCollection { |
|
[index: number]: Element | null; |
|
|
|
#nodes: Element[] = []; |
|
|
|
constructor() { |
|
Object.setPrototypeOf(this, HTMLCollection.prototype); |
|
} |
|
|
|
get [webidl.brand](): webidl.brand { |
|
return webidl.brand; |
|
} |
|
|
|
item(index: number): Element | null { |
|
return this.#nodes[index] ?? null; |
|
} |
|
|
|
namedItem(name: string): Element | null { |
|
for (const node of this.#nodes) { |
|
if (node instanceof Element && node.id === name) { |
|
return node; |
|
} |
|
} |
|
return null; |
|
} |
|
|
|
get length(): number { |
|
return this.#nodes.length; |
|
} |
|
|
|
[Symbol.iterator](): IterableIterator<Element> { |
|
return this.values(); |
|
} |
|
|
|
*values(): IterableIterator<Element> { |
|
for (const node of this.#nodes) { |
|
if (node instanceof Element) yield node; |
|
} |
|
} |
|
|
|
*keys(): IterableIterator<number> { |
|
for (let i = 0; i < this.#nodes.length; i++) yield i; |
|
} |
|
|
|
*entries(): IterableIterator<[number, Element]> { |
|
for (const index of this.keys()) { |
|
yield [index, this.#nodes[index] as Element]; |
|
} |
|
} |
|
|
|
get [Symbol.toStringTag](): string { |
|
return "HTMLCollection"; |
|
} |
|
|
|
static { |
|
createHTMLCollection = (nodes) => { |
|
const list = new HTMLCollection(); |
|
list.#nodes = [...nodes].filter(isElement); |
|
return list; |
|
}; |
|
|
|
getHTMLCollectionNodes = (hc) => hc.#nodes; |
|
} |
|
} |
|
|
|
export class HTMLFormControlsCollection extends HTMLCollection { |
|
#form!: HTMLFormElement; |
|
|
|
constructor(node: Node) { |
|
super(); |
|
if (isElement(node)) this.#form = node as unknown as HTMLFormElement; |
|
Object.setPrototypeOf(this, HTMLFormControlsCollection.prototype); |
|
} |
|
|
|
get [webidl.brand](): webidl.brand { |
|
return webidl.brand; |
|
} |
|
|
|
get form(): HTMLFormElement | undefined { |
|
return this.#form; |
|
} |
|
} |
|
|
|
export class HTMLSelectOptionsCollection extends HTMLCollection { |
|
get #elements(): Element[] { |
|
return getHTMLCollectionNodes(this); |
|
} |
|
|
|
constructor(node: Node) { |
|
super(); |
|
if (isElement(node)) { |
|
for (const child of node.children) { |
|
if (child instanceof HTMLOptionElement) { |
|
this.#elements.push(child); |
|
} |
|
} |
|
} |
|
Object.setPrototypeOf(this, HTMLSelectOptionsCollection.prototype); |
|
} |
|
|
|
namedItem(name: string): HTMLOptionElement | null { |
|
for (const node of this) { |
|
if (node instanceof HTMLOptionElement) { |
|
if (node.value === name || node.text === name) { |
|
return node; |
|
} |
|
} |
|
} |
|
return null; |
|
} |
|
} |
|
|
|
// #endregion Collections |
|
|
|
// #region Geometry |
|
|
|
export class DOMMatrixReadOnly { |
|
static fromFloat32Array(float32Array: Float32Array): DOMMatrixReadOnly { |
|
if (float32Array.length !== 16) { |
|
throw new Error("A 4x4 matrix must have 16 elements"); |
|
} |
|
const float64Array = Float64Array.from(float32Array); |
|
return new DOMMatrixReadOnly(Array.from(float64Array)); |
|
} |
|
|
|
static fromFloat64Array(float64Array: Float64Array): DOMMatrixReadOnly { |
|
if (float64Array.length !== 16) { |
|
throw new Error("A 4x4 matrix must have 16 elements"); |
|
} |
|
return new DOMMatrixReadOnly(Array.from(float64Array)); |
|
} |
|
|
|
static fromMatrix( |
|
matrix: DOMMatrixReadOnly | DOMMatrixInit, |
|
): DOMMatrixReadOnly { |
|
if (matrix instanceof DOMMatrixReadOnly) { |
|
return new DOMMatrixReadOnly(Array.from(matrix.#data)); |
|
} else { |
|
const { a, b, c, d, e, f } = matrix; |
|
// deno-fmt-ignore |
|
return new DOMMatrixReadOnly([ |
|
a, b, 0, 0, |
|
c, d, 0, 0, |
|
0, 0, 1, 0, |
|
e, f, 0, 1, |
|
]); |
|
} |
|
} |
|
|
|
static [Symbol.hasInstance](that: unknown): that is DOMMatrixReadOnly { |
|
return Function[Symbol.hasInstance].call(this, that) || ( |
|
that != null && typeof that === "object" && !Array.isArray(that) && |
|
["x", "y", "width", "height"].every((p) => |
|
p in that && typeof that[p as keyof typeof that] === "number" && |
|
!Number.isNaN(that[p as keyof typeof that]) |
|
) |
|
); |
|
} |
|
|
|
declare readonly [Symbol.toStringTag]: string; |
|
|
|
// 4x4 matrix |
|
// => 16 cells (each cell is a 64-bit double-precision floating point) |
|
// => 16 * 8 bytes per cell (8 x 8 = 64 bits) |
|
// => 128 bytes of total needed for each matrix's data buffer |
|
#buffer: ArrayBuffer = new ArrayBuffer(128); |
|
#data: Float64Array = new Float64Array(this.#buffer); |
|
#view: DataView = new DataView(this.#buffer); |
|
|
|
/** Used for subclassing purposes only. */ |
|
#ctor: typeof DOMMatrixReadOnly = DOMMatrixReadOnly; |
|
|
|
constructor(init?: ArrayLike<number> | undefined); |
|
constructor(init: ArrayLike<number>); |
|
constructor(init: DOMMatrixInit); |
|
constructor(init?: ArrayLike<number> | DOMMatrixInit | undefined); |
|
constructor(init?: ArrayLike<number> | DOMMatrixInit) { |
|
this.#ctor = new.target ?? this.constructor as typeof DOMMatrixReadOnly; |
|
|
|
if (init == null) { |
|
// Identity matrix |
|
for (let i = 0; i < 16; i++) this.#data[i] = 0; |
|
// deno-fmt-ignore |
|
this.#data[0] = this.#data[5] = this.#data[10] = this.#data[15] = 1; |
|
} else if (typeof init === "object") { |
|
if ("length" in init && typeof init.length === "number") { |
|
if (init.length === 16) { |
|
for (let i = 0; i < 16; i++) this.#data[i] = init[i]; |
|
} |
|
} else if (["a", "b", "c", "d", "e", "f"].every((p) => p in init)) { |
|
const { a, b, c, d, e, f } = init as DOMMatrixInit; |
|
// deno-fmt-ignore |
|
this.#data.set([a, b, 0, 0, c, d, 0, 0, 0, 0, 1, 0, e, f, 0, 1]); |
|
} else { |
|
throw new TypeError("Invalid DOMMatrixInit object."); |
|
} |
|
} |
|
} |
|
|
|
get a(): number { |
|
webidl.assertBranded(this); |
|
return this.m11; |
|
} |
|
|
|
get b(): number { |
|
webidl.assertBranded(this); |
|
return this.m12; |
|
} |
|
|
|
get c(): number { |
|
webidl.assertBranded(this); |
|
return this.m21; |
|
} |
|
|
|
get d(): number { |
|
webidl.assertBranded(this); |
|
return this.m22; |
|
} |
|
|
|
get e(): number { |
|
webidl.assertBranded(this); |
|
return this.m41; |
|
} |
|
|
|
get f(): number { |
|
webidl.assertBranded(this); |
|
return this.m42; |
|
} |
|
|
|
/** |
|
* Returns true if the matrix is two-dimensional (2D), meaning it has no |
|
* perspective components (m13, m14, m23, m24, m31, m32, m33, m34). This |
|
* is equivalent to checking that all of the `z` and `w` axis values are |
|
* set to their default values of `0` and `1`, respectively. |
|
* |
|
* Regarding 2D vs 4x4 matrices, this is a rough idea of how they translate: |
|
* |
|
* ``` |
|
* 2D (3x3) → 4x4 |
|
* ======================= |
|
* [a c e] → [a 0 c 0] |
|
* [b d f] → [b 0 d 0] |
|
* [0 0 1] → [0 0 0 0] |
|
* [ ] → [e f 0 1] |
|
* ``` |
|
* |
|
* @see https://drafts.fxtf.org/geometry/#dom-dommatrixreadonly-is2d |
|
*/ |
|
get is2D(): boolean { |
|
webidl.assertBranded(this); |
|
for (const z of [2, 6, 8, 9, 10, 14]) { |
|
if (z !== 10 && this.#data[z] !== 0) return false; |
|
if (z === 10 && this.#data[z] !== 1) return false; |
|
} |
|
return true; |
|
} |
|
|
|
/** |
|
* Returns true if the matrix is an identity matrix, and false otherwise. |
|
* An identity matrix is simply a matrix in which no transformation is |
|
* applied. The identity matrix is equivalent to a matrix that contains |
|
* its default values as follows: |
|
* |
|
* ``` |
|
* [1 0 0 0] // m11* m12 m13 m14 |
|
* [0 1 0 0] // m21 m22* m23 m24 |
|
* [0 0 1 0] // m31 m32 m33* m34 |
|
* [0 0 0 1] // m41 m42 m43 m44* |
|
* ``` |
|
*/ |
|
get isIdentity(): boolean { |
|
webidl.assertBranded(this); |
|
const on = new Set([0, 5, 10, 15]); |
|
for (let i = 0; i < 16; i++) { |
|
if (on.has(i) && this.#data[i] !== 1) return false; |
|
if (!on.has(i) && this.#data[i] !== 0) return false; |
|
} |
|
return true; |
|
} |
|
|
|
get m11(): number { |
|
webidl.assertBranded(this); |
|
return this.#data[0]; |
|
} |
|
|
|
get m12(): number { |
|
webidl.assertBranded(this); |
|
return this.#data[1]; |
|
} |
|
|
|
get m13(): number { |
|
webidl.assertBranded(this); |
|
return this.#data[2]; |
|
} |
|
|
|
get m14(): number { |
|
webidl.assertBranded(this); |
|
return this.#data[3]; |
|
} |
|
|
|
get m21(): number { |
|
webidl.assertBranded(this); |
|
return this.#data[4]; |
|
} |
|
|
|
get m22(): number { |
|
webidl.assertBranded(this); |
|
return this.#data[5]; |
|
} |
|
|
|
get m23(): number { |
|
webidl.assertBranded(this); |
|
return this.#data[6]; |
|
} |
|
|
|
get m24(): number { |
|
webidl.assertBranded(this); |
|
return this.#data[7]; |
|
} |
|
|
|
get m31(): number { |
|
webidl.assertBranded(this); |
|
return this.#data[8]; |
|
} |
|
|
|
get m32(): number { |
|
webidl.assertBranded(this); |
|
return this.#data[9]; |
|
} |
|
|
|
get m33(): number { |
|
webidl.assertBranded(this); |
|
return this.#data[10]; |
|
} |
|
|
|
get m34(): number { |
|
webidl.assertBranded(this); |
|
return this.#data[11]; |
|
} |
|
|
|
get m41(): number { |
|
webidl.assertBranded(this); |
|
return this.#data[12]; |
|
} |
|
|
|
get m42(): number { |
|
webidl.assertBranded(this); |
|
return this.#data[13]; |
|
} |
|
|
|
get m43(): number { |
|
webidl.assertBranded(this); |
|
return this.#data[14]; |
|
} |
|
|
|
get m44(): number { |
|
webidl.assertBranded(this); |
|
return this.#data[15]; |
|
} |
|
|
|
flipX(): DOMMatrixReadOnly { |
|
webidl.assertBranded(this); |
|
|
|
// deno-fmt-ignore |
|
const flipXMatrix = new this.#ctor([ |
|
-1, 0, 0, 0, |
|
0, 1, 0, 0, |
|
0, 0, 1, 0, |
|
0, 0, 0, 1, |
|
]); |
|
return this.multiply(flipXMatrix); |
|
} |
|
|
|
flipY(): DOMMatrixReadOnly { |
|
webidl.assertBranded(this); |
|
|
|
// deno-fmt-ignore |
|
const flipYMatrix = new this.#ctor([ |
|
1, 0, 0, 0, |
|
0, -1, 0, 0, |
|
0, 0, 1, 0, |
|
0, 0, 0, 1, |
|
]); |
|
return this.multiply(flipYMatrix); |
|
} |
|
|
|
inverse(): DOMMatrixReadOnly { |
|
webidl.assertBranded(this); |
|
const m = this.#data; |
|
const result = new Float64Array(16); |
|
let det = 0; |
|
|
|
for (let i = 0; i < 4; i++) { |
|
const minors = this.#calculateMinors(i); |
|
for (let j = 0; j < 4; j++) { |
|
// calculate determinant & adjugate |
|
// (transpose of cofactor matrix) |
|
const adjugate = (i + j) % 2 === 0; |
|
const index = i * 4 + j; |
|
det += (adjugate ? m[index] : -m[index]) * minors[j]; |
|
|
|
result[i + j * 4] = adjugate ? minors[j] : -minors[j]; |
|
} |
|
} |
|
|
|
// Check for singular matrix (determinant is zero) |
|
if (det === 0) throw new TypeError("Cannot invert a singular matrix."); |
|
|
|
// Multiply the adjugate matrix by 1/determinant |
|
for (let i = 0; i < 16; i++) { |
|
result[i] /= det; |
|
} |
|
|
|
return new this.#ctor(Array.from(result)); |
|
} |
|
|
|
#calculateMinors(index: number): Float64Array { |
|
webidl.assertBranded(this); |
|
|
|
const m = this.#data; |
|
const minors = new Float64Array(4); |
|
|
|
for (let i = 0; i < 4; i++) { |
|
const m33 = new Float64Array(9); |
|
|
|
for (let j = 0; j < 4; j++) { |
|
if (j === index) continue; |
|
|
|
m33[(j < index ? j : j - 1) * 3 + 0] = m[(j * 4) + 1]; |
|
m33[(j < index ? j : j - 1) * 3 + 1] = m[(j * 4) + 2]; |
|
m33[(j < index ? j : j - 1) * 3 + 2] = m[(j * 4) + 3]; |
|
} |
|
|
|
minors[i] = m33[0] * (m33[4] * m33[8] - m33[5] * m33[7]) - |
|
m33[1] * (m33[3] * m33[8] - m33[5] * m33[6]) + |
|
m33[2] * (m33[3] * m33[7] - m33[4] * m33[6]); |
|
} |
|
|
|
return minors; |
|
} |
|
|
|
multiply(matrix: DOMMatrixReadOnly): DOMMatrixReadOnly { |
|
webidl.assertBranded(this); |
|
|
|
const result = new this.#ctor(); |
|
|
|
for (let row = 0; row < 4; row++) { |
|
for (let col = 0; col < 4; col++) { |
|
let sum = 0; |
|
for (let i = 0; i < 4; i++) { |
|
sum += this.#data[row * 4 + i] * matrix.#data[i * 4 + col]; |
|
} |
|
result.#data[row * 4 + col] = sum; |
|
} |
|
} |
|
|
|
return result; |
|
} |
|
|
|
rotate(rx: number, ry: number, rz: number): DOMMatrixReadOnly { |
|
webidl.assertBranded(this); |
|
|
|
// Convert angles from degrees to radians |
|
rx *= Math.PI / 180, ry *= Math.PI / 180, rz *= Math.PI / 180; |
|
|
|
const cx = Math.cos(rx), sx = Math.sin(rx); |
|
const cy = Math.cos(ry), sy = Math.sin(ry); |
|
const cz = Math.cos(rz), sz = Math.sin(rz); |
|
|
|
// deno-fmt-ignore |
|
const rotx = new this.#ctor([ |
|
0x1, 0x0, 0x0, 0x0, |
|
0x0, +cx, -sx, 0x0, |
|
0x0, +sx, +cx, 0x0, |
|
0x0, 0x0, 0x0, 0x1, |
|
]); |
|
|
|
// deno-fmt-ignore |
|
const roty = new this.#ctor([ |
|
+cy, 0x0, +sy, 0x0, |
|
0x0, 0x1, 0x0, 0x0, |
|
-sy, 0x0, +cy, 0x0, |
|
0x0, 0x0, 0x0, 0x1, |
|
]); |
|
|
|
// deno-fmt-ignore |
|
const rotz = new this.#ctor([ |
|
+cz, -sz, 0x0, 0x0, |
|
+sz, +cz, 0x0, 0x0, |
|
0x0, 0x0, 0x1, 0x0, |
|
0x0, 0x0, 0x0, 0x1, |
|
]); |
|
|
|
return this.multiply(rotz).multiply(roty).multiply(rotx); |
|
} |
|
|
|
rotateAxisAngle( |
|
x: number, |
|
y: number, |
|
z: number, |
|
angle: number, |
|
): DOMMatrixReadOnly { |
|
webidl.assertBranded(this); |
|
|
|
angle *= Math.PI / 180; // degrees -> radians |
|
|
|
const c = Math.cos(angle); |
|
const s = Math.sin(angle); |
|
const t = 1 - c; |
|
|
|
// deno-fmt-ignore |
|
const rotateAxisAngleMatrix = new this.#ctor([ |
|
t * x * x + c, |
|
t * x * y - s * z, |
|
t * x * z + s * y, 0x0, |
|
t * x * y + s * z, |
|
t * y * y + c, |
|
t * y * z - s * x, 0x0, |
|
t * x * z - s * y, |
|
t * y * z + s * x, |
|
t * z * z + c, 0x0, |
|
0x0, 0x0, 0x0, 0x1, |
|
]); |
|
|
|
return this.multiply(rotateAxisAngleMatrix); |
|
} |
|
|
|
rotateFromVector(x: number, y: number): DOMMatrixReadOnly { |
|
webidl.assertBranded(this); |
|
const angle = Math.atan2(y, x) * 180 / Math.PI; |
|
return this.rotate(0, 0, angle); |
|
} |
|
|
|
scale(sx: number, sy: number, sz: number): DOMMatrixReadOnly { |
|
webidl.assertBranded(this); |
|
|
|
// deno-fmt-ignore |
|
const scaleMatrix = new this.#ctor([ |
|
+sx, 0x0, 0x0, 0x0, |
|
0x0, +sy, 0x0, 0x0, |
|
0x0, 0x0, +sz, 0x0, |
|
0x0, 0x0, 0x0, 0x1, |
|
]); |
|
|
|
return this.multiply(scaleMatrix); |
|
} |
|
|
|
scale3d(scale: number): DOMMatrixReadOnly { |
|
webidl.assertBranded(this); |
|
|
|
return this.scale(scale, scale, scale); |
|
} |
|
|
|
scaleNonUniform(sx: number, sy: number, sz = 1): DOMMatrixReadOnly { |
|
webidl.assertBranded(this); |
|
|
|
return this.scale(sx, sy, sz); |
|
} |
|
|
|
skewX(angle: number): DOMMatrixReadOnly { |
|
webidl.assertBranded(this); |
|
|
|
angle *= Math.PI / 180; // degrees -> radians |
|
const A = Math.tan(angle); |
|
|
|
// deno-fmt-ignore |
|
const skewXMatrix = new this.#ctor([ |
|
1, 0, 0, 0, |
|
A, 1, 0, 0, |
|
0, 0, 1, 0, |
|
0, 0, 0, 1, |
|
]); |
|
|
|
return this.multiply(skewXMatrix); |
|
} |
|
|
|
skewY(angle: number): DOMMatrixReadOnly { |
|
webidl.assertBranded(this); |
|
|
|
angle *= Math.PI / 180; // degrees -> radians |
|
const A = Math.tan(angle); |
|
|
|
// deno-fmt-ignore |
|
const skewYMatrix = new this.#ctor([ |
|
1, A, 0, 0, |
|
0, 1, 0, 0, |
|
0, 0, 1, 0, |
|
0, 0, 0, 1, |
|
]); |
|
|
|
return this.multiply(skewYMatrix); |
|
} |
|
|
|
translate(tx: number, ty: number, tz: number): DOMMatrixReadOnly { |
|
webidl.assertBranded(this); |
|
// deno-fmt-ignore |
|
const translateMatrix = new this.#ctor([ |
|
1, 0, 0, tx, |
|
0, 1, 0, ty, |
|
0, 0, 1, tz, |
|
0, 0, 0, 1, |
|
]); |
|
return this.multiply(translateMatrix); |
|
} |
|
|
|
toFloat32Array(): Float32Array { |
|
webidl.assertBranded(this); |
|
return new Float32Array(this.#buffer); |
|
} |
|
|
|
toFloat64Array(): Float64Array { |
|
webidl.assertBranded(this); |
|
return new Float64Array(this.#buffer); |
|
} |
|
|
|
toJSON(): Record<string, number> { |
|
webidl.assertBranded(this); |
|
const { a, b, c, d, e, f } = this; |
|
const o: Record<string, number> = { a, b, c, d, e, f }; |
|
for (let i = 0; i < 4; i++) { |
|
for (let j = 0; j < 4; j++) { |
|
const k = `m${i + 1}${j + 1}`; |
|
o[k] = this.#data[i * 4 + j]; |
|
} |
|
} |
|
return o; |
|
} |
|
|
|
transformPoint( |
|
point: { x: number; y: number; z?: number }, |
|
): { x: number; y: number; z: number } { |
|
webidl.assertBranded(this); |
|
|
|
let { x, y, z = 0 } = point ?? {}; |
|
const { a, b, c, d, e, f } = this; |
|
|
|
x = (a * x) + (c * y) + (e * z) + this.#data[12]; |
|
y = (b * x) + (d * y) + (f * z) + this.#data[13]; |
|
z = (this.#data[10] * z) + this.#data[14]; |
|
|
|
return { x, y, z }; |
|
} |
|
|
|
toString(): string { |
|
webidl.assertBranded(this); |
|
|
|
return `matrix(${this.a}, ${this.b}, ${this.c}, ${this.d}, ${this.e}, ${this.f})`; |
|
} |
|
|
|
[Symbol.for("nodejs.util.inspect.custom")]( |
|
depth: number | null, |
|
// deno-lint-ignore no-explicit-any |
|
options: any, |
|
inspect: (value: unknown, options: unknown) => string, |
|
): string { |
|
webidl.assertBranded(this); |
|
|
|
options = { |
|
...options ?? {}, |
|
colors: true, |
|
depth: depth ?? options?.depth ?? 2, |
|
getters: true, |
|
sorted: true, |
|
}; |
|
|
|
return `${this.#ctor.name} ${inspect(this.toJSON(), options)}`; |
|
} |
|
|
|
static { |
|
_.setDOMMatrixBuffer = (matrix, buffer) => { |
|
if (ArrayBuffer.isView(buffer)) buffer = buffer.buffer; |
|
matrix.#buffer = buffer; |
|
return matrix; |
|
}; |
|
|
|
_.setDOMMatrixData = (matrix, data) => { |
|
if (data instanceof Float32Array) data = new Float64Array(data.buffer); |
|
matrix.#data = data; |
|
matrix.#buffer = data.buffer; |
|
matrix.#view = new DataView( |
|
matrix.#buffer, |
|
matrix.#data.byteOffset, |
|
matrix.#data.byteLength, |
|
); |
|
return matrix; |
|
}; |
|
|
|
_.setDOMMatrixIndex = (matrix, index, value) => { |
|
matrix.#data[index] = value; |
|
return matrix; |
|
}; |
|
|
|
_.getDOMMatrixBuffer = (matrix) => matrix.#buffer; |
|
_.getDOMMatrixDataView = (matrix) => matrix.#view; |
|
_.getDOMMatrixData = (matrix) => matrix.#data; |
|
_.getDOMMatrixIndex = (matrix, index) => matrix.#data[index]; |
|
|
|
Object.defineProperties(this.prototype, { |
|
[Symbol.toStringTag]: { |
|
writable: false, |
|
enumerable: false, |
|
configurable: true, |
|
value: this.name, |
|
}, |
|
}); |
|
} |
|
} |
|
|
|
// #endregion Geometry |
|
|
|
// #region Events |
|
|
|
// #endregion Events |
|
|
|
export type XPathNSResolver = ((prefix: string | null) => string | null) | { |
|
lookupNamespaceURI(prefix: string | null): string | null; |
|
}; |
|
|
|
// #region HTML |
|
|
|
export class HTMLElement extends Element { |
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, HTMLElement.prototype); |
|
} |
|
|
|
#style?: CSSStyleDeclaration; |
|
|
|
get style(): CSSStyleDeclaration { |
|
return this.#style ?? null!; |
|
} |
|
|
|
set style(style: string | CSSStyleDeclaration) { |
|
// TODO: implement |
|
this.#style = style as any; |
|
} |
|
|
|
get tabIndex(): number { |
|
let tabIndex: string | number | undefined = this.getAttribute("tabindex"); |
|
if (tabIndex == null || tabIndex === "") { |
|
tabIndex = -1; |
|
this.setAttribute("tabindex", tabIndex + ""); |
|
} |
|
return +tabIndex; |
|
} |
|
|
|
set tabIndex(tabIndex: string | number | undefined) { |
|
if (tabIndex == null) { |
|
this.removeAttribute("tabindex"); |
|
} else { |
|
this.setAttribute("tabindex", tabIndex + ""); |
|
} |
|
} |
|
|
|
get title(): string { |
|
return this.getAttribute("title") ?? ""; |
|
} |
|
|
|
set title(title: string) { |
|
this.setAttribute("title", title); |
|
} |
|
|
|
get lang(): string { |
|
return this.getAttribute("lang") ?? ""; |
|
} |
|
|
|
set lang(lang: string) { |
|
this.setAttribute("lang", lang); |
|
} |
|
|
|
get translate(): boolean { |
|
return this.getAttribute("translate") !== "no"; |
|
} |
|
|
|
set translate(translate: "yes" | "no" | boolean) { |
|
this.setAttribute( |
|
"translate", |
|
translate === true || translate === "yes" ? "yes" : "no", |
|
); |
|
} |
|
} |
|
|
|
export class HTMLAnchorElement extends HTMLElement { |
|
readonly nodeName = "A"; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, HTMLAnchorElement.prototype); |
|
} |
|
|
|
get alt(): string { |
|
return this.getAttribute("alt"); |
|
} |
|
|
|
set alt(alt: string | undefined) { |
|
if (alt) this.setAttribute("alt", alt); |
|
else this.removeAttribute("alt"); |
|
} |
|
|
|
get download(): string { |
|
return this.getAttribute("download"); |
|
} |
|
|
|
set download(download: string | undefined) { |
|
if (download) this.setAttribute("download", download); |
|
else this.removeAttribute("download"); |
|
} |
|
|
|
get href(): string { |
|
return this.getAttribute("href"); |
|
} |
|
|
|
set href(href: string | undefined) { |
|
if (href) this.setAttribute("href", href); |
|
else this.removeAttribute("href"); |
|
} |
|
|
|
get hreflang(): string { |
|
return this.getAttribute("hreflang"); |
|
} |
|
|
|
set hreflang(hreflang: string | undefined) { |
|
if (hreflang) this.setAttribute("hreflang", hreflang); |
|
else this.removeAttribute("hreflang"); |
|
} |
|
|
|
get media(): string { |
|
return this.getAttribute("media"); |
|
} |
|
|
|
set media(media: string | undefined) { |
|
if (media) this.setAttribute("media", media); |
|
else this.removeAttribute("media"); |
|
} |
|
|
|
get referrerPolicy(): string { |
|
return this.getAttribute("referrerPolicy"); |
|
} |
|
|
|
set referrerPolicy(referrerPolicy: string | undefined) { |
|
if (referrerPolicy) this.setAttribute("referrerPolicy", referrerPolicy); |
|
else this.removeAttribute("referrerPolicy"); |
|
} |
|
|
|
get rel(): string { |
|
return this.getAttribute("rel"); |
|
} |
|
|
|
set rel(rel: string | undefined) { |
|
if (rel) this.setAttribute("rel", rel); |
|
else this.removeAttribute("rel"); |
|
} |
|
|
|
get target(): string { |
|
return this.getAttribute("target"); |
|
} |
|
|
|
set target(target: string | undefined) { |
|
if (target) this.setAttribute("target", target); |
|
else this.removeAttribute("target"); |
|
} |
|
} |
|
|
|
export class HTMLAreaElement extends HTMLElement { |
|
readonly nodeName = "AREA"; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, HTMLAreaElement.prototype); |
|
} |
|
|
|
get alt(): string | undefined { |
|
return this.getAttribute("alt"); |
|
} |
|
|
|
set alt(alt: string | undefined) { |
|
if (alt) this.setAttribute("alt", alt); |
|
else this.removeAttribute("alt"); |
|
} |
|
|
|
get coords(): string | undefined { |
|
return this.getAttribute("coords"); |
|
} |
|
|
|
set coords(coords: string | undefined) { |
|
if (coords) this.setAttribute("coords", coords); |
|
else this.removeAttribute("coords"); |
|
} |
|
|
|
get download(): string | undefined { |
|
return this.getAttribute("download"); |
|
} |
|
|
|
set download(download: string | undefined) { |
|
if (download) this.setAttribute("download", download); |
|
else this.removeAttribute("download"); |
|
} |
|
|
|
get href(): string | undefined { |
|
return this.getAttribute("href"); |
|
} |
|
|
|
set href(href: string | undefined) { |
|
if (href) this.setAttribute("href", href); |
|
else this.removeAttribute("href"); |
|
} |
|
|
|
get hreflang(): string | undefined { |
|
return this.getAttribute("hreflang"); |
|
} |
|
|
|
set hreflang(hreflang: string | undefined) { |
|
if (hreflang) this.setAttribute("hreflang", hreflang); |
|
else this.removeAttribute("hreflang"); |
|
} |
|
|
|
get media(): string | undefined { |
|
return this.getAttribute("media"); |
|
} |
|
|
|
set media(media: string | undefined) { |
|
if (media) this.setAttribute("media", media); |
|
else this.removeAttribute("media"); |
|
} |
|
|
|
get ping(): string | undefined { |
|
return this.getAttribute("ping"); |
|
} |
|
|
|
set ping(ping: string | undefined) { |
|
if (ping) this.setAttribute("ping", ping); |
|
else this.removeAttribute("ping"); |
|
} |
|
|
|
get referrerPolicy(): string | undefined { |
|
return this.getAttribute("referrerPolicy"); |
|
} |
|
|
|
set referrerPolicy(referrerPolicy: string | undefined) { |
|
if (referrerPolicy) this.setAttribute("referrerPolicy", referrerPolicy); |
|
else this.removeAttribute("referrerPolicy"); |
|
} |
|
|
|
get rel(): string | undefined { |
|
return this.getAttribute("rel"); |
|
} |
|
|
|
set rel(rel: string | undefined) { |
|
if (rel) this.setAttribute("rel", rel); |
|
else this.removeAttribute("rel"); |
|
} |
|
} |
|
|
|
export class HTMLOptionElement extends HTMLElement { |
|
readonly nodeName = "OPTION"; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, HTMLOptionElement.prototype); |
|
} |
|
|
|
#selected = false; |
|
#form: HTMLFormElement | null = null; |
|
|
|
get selected(): boolean { |
|
return this.#selected; |
|
} |
|
|
|
set selected(value: boolean) { |
|
this.#selected = value; |
|
} |
|
|
|
get form(): HTMLFormElement | null { |
|
return this.#form; |
|
} |
|
|
|
set form(value: HTMLFormElement | null) { |
|
this.#form = value; |
|
} |
|
|
|
get text(): string { |
|
return this.textContent ?? ""; |
|
} |
|
|
|
set text(value: string) { |
|
this.textContent = value; |
|
} |
|
|
|
get index(): number { |
|
return indexOf(this.parentNode?.childNodes ?? [], this); |
|
} |
|
|
|
get value(): string { |
|
return this.getAttribute("value") ?? ""; |
|
} |
|
|
|
set value(value: string) { |
|
this.setAttribute("value", value); |
|
} |
|
} |
|
|
|
export class HTMLHtmlElement extends HTMLElement { |
|
readonly nodeName = "HTML"; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, HTMLHtmlElement.prototype); |
|
} |
|
|
|
get version(): string { |
|
return this.getAttribute("version") ?? ""; |
|
} |
|
|
|
set version(value: string) { |
|
this.setAttribute("version", value); |
|
} |
|
|
|
get manifest(): string { |
|
return this.getAttribute("manifest") ?? ""; |
|
} |
|
|
|
set manifest(value: string) { |
|
this.setAttribute("manifest", value); |
|
} |
|
|
|
get xmlns(): string { |
|
return this.getAttribute("xmlns") ?? ""; |
|
} |
|
|
|
set xmlns(value: string) { |
|
this.setAttribute("xmlns", value); |
|
} |
|
} |
|
|
|
export class HTMLBodyElement extends HTMLElement { |
|
readonly nodeName = "BODY"; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, HTMLBodyElement.prototype); |
|
} |
|
|
|
get text(): string { |
|
return this.textContent ?? ""; |
|
} |
|
|
|
set text(value: string) { |
|
this.textContent = value; |
|
} |
|
|
|
get link(): string { |
|
return this.getAttribute("link") ?? ""; |
|
} |
|
|
|
set link(value: string) { |
|
this.setAttribute("link", value); |
|
} |
|
|
|
get vLink(): string { |
|
return this.getAttribute("vlink") ?? ""; |
|
} |
|
|
|
set vLink(value: string) { |
|
this.setAttribute("vlink", value); |
|
} |
|
|
|
get aLink(): string { |
|
return this.getAttribute("alink") ?? ""; |
|
} |
|
|
|
set aLink(value: string) { |
|
this.setAttribute("alink", value); |
|
} |
|
|
|
get bgColor(): string { |
|
return this.getAttribute("bgcolor") ?? ""; |
|
} |
|
|
|
set bgColor(value: string) { |
|
this.setAttribute("bgcolor", value); |
|
} |
|
|
|
get background(): string { |
|
return this.getAttribute("background") ?? ""; |
|
} |
|
|
|
set background(value: string) { |
|
this.setAttribute("background", value); |
|
} |
|
} |
|
|
|
export class HTMLHeadElement extends HTMLElement { |
|
readonly nodeName = "HEAD"; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, HTMLHeadElement.prototype); |
|
} |
|
|
|
get profile(): string { |
|
return this.getAttribute("profile") ?? ""; |
|
} |
|
|
|
set profile(value: string) { |
|
this.setAttribute("profile", value); |
|
} |
|
} |
|
|
|
export class HTMLTitleElement extends HTMLElement { |
|
readonly nodeName = "TITLE"; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, HTMLTitleElement.prototype); |
|
} |
|
|
|
get text(): string { |
|
return this.textContent ?? ""; |
|
} |
|
|
|
set text(value: string) { |
|
this.textContent = value; |
|
} |
|
} |
|
|
|
export class HTMLDivElement extends HTMLElement { |
|
readonly nodeName = "DIV"; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, HTMLDivElement.prototype); |
|
} |
|
|
|
get text(): string { |
|
return this.textContent ?? ""; |
|
} |
|
|
|
set text(value: string) { |
|
this.textContent = value; |
|
} |
|
|
|
get align(): string { |
|
return this.getAttribute("align") ?? ""; |
|
} |
|
|
|
set align(value: string) { |
|
this.setAttribute("align", value); |
|
} |
|
|
|
get noWrap(): boolean { |
|
return this.getAttribute("nowrap") !== null; |
|
} |
|
|
|
set noWrap(value: boolean) { |
|
if (value) this.setAttribute("nowrap", ""); |
|
else this.removeAttribute("nowrap"); |
|
} |
|
} |
|
|
|
export class HTMLImageElement extends HTMLElement { |
|
readonly nodeName = "IMG"; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, HTMLImageElement.prototype); |
|
} |
|
|
|
get alt(): string { |
|
return this.getAttribute("alt") ?? ""; |
|
} |
|
|
|
set alt(value: string) { |
|
this.setAttribute("alt", value); |
|
} |
|
|
|
get src(): string { |
|
return this.getAttribute("src") ?? ""; |
|
} |
|
|
|
set src(value: string) { |
|
this.setAttribute("src", value); |
|
} |
|
|
|
get srcset(): string { |
|
return this.getAttribute("srcset") ?? ""; |
|
} |
|
|
|
set srcset(value: string) { |
|
this.setAttribute("srcset", value); |
|
} |
|
|
|
get sizes(): string { |
|
return this.getAttribute("sizes") ?? ""; |
|
} |
|
|
|
set sizes(value: string) { |
|
this.setAttribute("sizes", value); |
|
} |
|
|
|
get crossOrigin(): string { |
|
return this.getAttribute("crossOrigin") ?? ""; |
|
} |
|
|
|
set crossOrigin(value: string) { |
|
this.setAttribute("crossOrigin", value); |
|
} |
|
|
|
get type(): string { |
|
return this.getAttribute("type") ?? ""; |
|
} |
|
|
|
set type(value: string) { |
|
this.setAttribute("type", value); |
|
} |
|
|
|
get width(): number | undefined { |
|
const width = this.getAttribute("width"); |
|
return width ? +width : undefined; |
|
} |
|
|
|
set width(value: number) { |
|
this.setAttribute("width", value + ""); |
|
} |
|
|
|
get height(): number | undefined { |
|
const height = this.getAttribute("height"); |
|
return height ? +height : undefined; |
|
} |
|
|
|
set height(value: number) { |
|
this.setAttribute("height", value + ""); |
|
} |
|
|
|
#naturalWidth = 0; |
|
#naturalHeight = 0; |
|
|
|
get naturalWidth(): number { |
|
return this.#naturalWidth; |
|
} |
|
|
|
get naturalHeight(): number { |
|
return this.#naturalHeight; |
|
} |
|
} |
|
|
|
export class HTMLFormElement extends HTMLElement { |
|
readonly nodeName = "FORM"; |
|
|
|
#formData: FormData | undefined; |
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, HTMLFormElement.prototype); |
|
} |
|
|
|
get formdata(): FormData { |
|
return this.#formData ??= new FormData(this); |
|
} |
|
|
|
get elements(): HTMLFormControlsCollection { |
|
return new HTMLFormControlsCollection(this); |
|
} |
|
|
|
get length(): number { |
|
return this.elements.length; |
|
} |
|
|
|
get name(): string { |
|
return this.getAttribute("name") ?? ""; |
|
} |
|
|
|
set name(value: string) { |
|
this.setAttribute("name", value); |
|
} |
|
|
|
get acceptCharset(): string { |
|
return this.getAttribute("acceptCharset") ?? ""; |
|
} |
|
|
|
set acceptCharset(value: string) { |
|
this.setAttribute("acceptCharset", value); |
|
} |
|
|
|
get action(): string { |
|
return this.getAttribute("action") ?? ""; |
|
} |
|
|
|
set action(value: string) { |
|
this.setAttribute("action", value); |
|
} |
|
|
|
get autocomplete(): string { |
|
return this.getAttribute("autocomplete") ?? ""; |
|
} |
|
|
|
set autocomplete(value: string) { |
|
this.setAttribute("autocomplete", value); |
|
} |
|
|
|
get enctype(): string { |
|
return this.getAttribute("enctype") ?? ""; |
|
} |
|
|
|
set enctype(value: string) { |
|
this.setAttribute("enctype", value); |
|
} |
|
|
|
get encoding(): string { |
|
return this.getAttribute("encoding") ?? ""; |
|
} |
|
|
|
set encoding(value: string) { |
|
this.setAttribute("encoding", value); |
|
} |
|
|
|
get method(): string { |
|
return this.getAttribute("method") ?? ""; |
|
} |
|
|
|
set method(value: string) { |
|
this.setAttribute("method", value); |
|
} |
|
|
|
get noValidate(): boolean { |
|
return this.getAttribute("noValidate") !== null; |
|
} |
|
|
|
set noValidate(value: boolean) { |
|
if (value) this.setAttribute("noValidate", ""); |
|
else this.removeAttribute("noValidate"); |
|
} |
|
|
|
get target(): string { |
|
return this.getAttribute("target") ?? ""; |
|
} |
|
|
|
set target(value: string) { |
|
this.setAttribute("target", value); |
|
} |
|
|
|
async reset(): Promise<void> { |
|
const action = this.action, method = this.method; |
|
if (!action || !method) return; |
|
const event = new Event("reset", { cancelable: true }); |
|
if (this.dispatchEvent(event)) { |
|
const res = await fetch(action, { method }).catch( |
|
(e) => e.name !== "AbortError" && Promise.reject(e), |
|
); |
|
} |
|
return; |
|
} |
|
|
|
async submit(): Promise<void> { |
|
const action = this.action, method = this.method, target = this.target; |
|
if (!action || !method) return; |
|
const formdata = this.formdata; |
|
|
|
const event = new Event("submit", { cancelable: true }); |
|
if (this.dispatchEvent(event)) { |
|
const res = await fetch(action, { method, body: formdata }).catch( |
|
(e) => e.name !== "AbortError" && Promise.reject(e), |
|
); |
|
await this.reset(); |
|
} |
|
|
|
if (target) { |
|
const frame = this.ownerDocument?.querySelector(target); |
|
if (frame instanceof HTMLFrameElement) { |
|
frame.src = action; |
|
} |
|
} |
|
|
|
return; |
|
} |
|
} |
|
|
|
export class HTMLTableElement extends HTMLElement { |
|
readonly nodeName = "TABLE"; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, HTMLTableElement.prototype); |
|
} |
|
|
|
get caption(): HTMLTableCaptionElement | null { |
|
return this.querySelector("caption"); |
|
} |
|
|
|
get tHead(): HTMLTableSectionElement | null { |
|
return this.querySelector("thead"); |
|
} |
|
|
|
get tFoot(): HTMLTableSectionElement | null { |
|
return this.querySelector("tfoot"); |
|
} |
|
|
|
get tBodies(): HTMLCollection { |
|
return createHTMLCollection(this.querySelectorAll("tbody")); |
|
} |
|
|
|
createTHead(): HTMLTableSectionElement { |
|
const tHead = this.tHead ?? this.insertBefore( |
|
this.ownerDocument!.createElement("thead"), |
|
this.firstChild, |
|
); |
|
|
|
return tHead; |
|
} |
|
|
|
deleteTHead(): void { |
|
const tHead = this.tHead; |
|
if (tHead) this.removeChild(tHead); |
|
} |
|
|
|
createTFoot(): HTMLTableSectionElement { |
|
let tFoot = this.tFoot; |
|
if (!tFoot) { |
|
tFoot = this.ownerDocument!.createElement("tfoot"); |
|
this.appendChild(tFoot); |
|
} |
|
return tFoot; |
|
} |
|
} |
|
|
|
export class HTMLTableCaptionElement extends HTMLElement { |
|
readonly nodeName = "CAPTION"; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, HTMLTableCaptionElement.prototype); |
|
} |
|
|
|
get align(): string { |
|
return this.getAttribute("align") ?? ""; |
|
} |
|
|
|
set align(value: string) { |
|
this.setAttribute("align", value); |
|
} |
|
} |
|
|
|
export class HTMLTableSectionElement extends HTMLElement { |
|
readonly nodeName: string = "TBODY"; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, HTMLTableSectionElement.prototype); |
|
} |
|
|
|
get rows(): HTMLCollection { |
|
return createHTMLCollection(this.querySelectorAll("tr")); |
|
} |
|
|
|
insertRow(index?: number): HTMLTableRowElement { |
|
const row = this.ownerDocument?.createElement("tr"); |
|
if (row) { |
|
if (typeof index === "number" && index !== -1) { |
|
const rows = this.rows; |
|
if (index >= 0 && index < rows.length) { |
|
this.insertBefore(row, rows[index]); |
|
} else { |
|
this.appendChild(row); |
|
} |
|
} else { |
|
this.appendChild(row); |
|
} |
|
} |
|
return row ?? null!; |
|
} |
|
|
|
deleteRow(index: number): void { |
|
const rows = this.rows; |
|
if (index >= 0 && index < rows.length) { |
|
this.removeChild(rows[index]!); |
|
} |
|
} |
|
|
|
get align(): string { |
|
return this.getAttribute("align") ?? ""; |
|
} |
|
|
|
set align(value: string) { |
|
this.setAttribute("align", value); |
|
} |
|
|
|
get ch(): string { |
|
return this.getAttribute("char") ?? ""; |
|
} |
|
|
|
set ch(value: string) { |
|
this.setAttribute("char", value); |
|
} |
|
|
|
get cellpadding(): string { |
|
return this.getAttribute("cellpadding") ?? ""; |
|
} |
|
|
|
set cellpadding(value: string) { |
|
this.setAttribute("cellpadding", value); |
|
} |
|
|
|
get cellspacing(): string { |
|
return this.getAttribute("cellspacing") ?? ""; |
|
} |
|
|
|
set cellspacing(value: string) { |
|
this.setAttribute("cellspacing", value); |
|
} |
|
|
|
get rowspan(): number { |
|
return +this.getAttribute("rowspan"); |
|
} |
|
|
|
set rowspan(value: number) { |
|
this.setAttribute("rowspan", value + ""); |
|
} |
|
|
|
get valign(): string { |
|
return this.getAttribute("valign") ?? ""; |
|
} |
|
|
|
set valign(value: string) { |
|
this.setAttribute("valign", value); |
|
} |
|
} |
|
|
|
export class HTMLTableRowElement extends HTMLElement { |
|
readonly nodeName = "TR"; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, HTMLTableRowElement.prototype); |
|
} |
|
|
|
get cells(): HTMLCollection { |
|
return createHTMLCollection(this.querySelectorAll("td, th")); |
|
} |
|
|
|
insertCell(index?: number): HTMLTableCellElement { |
|
const cell = this.ownerDocument?.createElement("td"); |
|
if (cell) { |
|
if (typeof index === "number" && index !== -1) { |
|
const cells = this.cells; |
|
if (index >= 0 && index < cells.length) { |
|
this.insertBefore(cell, cells[index]); |
|
} else { |
|
this.appendChild(cell); |
|
} |
|
} else { |
|
this.appendChild(cell); |
|
} |
|
} |
|
return cell ?? null!; |
|
} |
|
|
|
deleteCell(index: number): void { |
|
const cells = this.cells; |
|
if (index >= 0 && index < cells.length) { |
|
this.removeChild(cells[index]!); |
|
} |
|
} |
|
|
|
get align(): string { |
|
return this.getAttribute("align") ?? ""; |
|
} |
|
|
|
set align(value: string) { |
|
this.setAttribute("align", value); |
|
} |
|
|
|
get rowIndex(): number { |
|
return indexOf(this.parentNode?.childNodes ?? [], this); |
|
} |
|
|
|
get sectionRowIndex(): number { |
|
return indexOf(this.parentNode?.childNodes ?? [], this); |
|
} |
|
|
|
get ch(): string { |
|
return this.getAttribute("char") ?? ""; |
|
} |
|
|
|
set ch(value: string) { |
|
this.setAttribute("char", value); |
|
} |
|
|
|
get chOff(): string { |
|
return this.getAttribute("charoff") ?? ""; |
|
} |
|
|
|
set chOff(value: string) { |
|
this.setAttribute("charoff", value); |
|
} |
|
|
|
get vAlign(): string { |
|
return this.getAttribute("valign") ?? ""; |
|
} |
|
|
|
set vAlign(value: string) { |
|
this.setAttribute("valign", value); |
|
} |
|
} |
|
|
|
export class HTMLTableCellElement extends HTMLElement { |
|
readonly nodeName = "TD"; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, HTMLTableCellElement.prototype); |
|
} |
|
|
|
get colSpan(): number { |
|
return +this.getAttribute("colspan"); |
|
} |
|
|
|
set colSpan(value: number) { |
|
this.setAttribute("colspan", value + ""); |
|
} |
|
|
|
get rowSpan(): number { |
|
return +this.getAttribute("rowspan"); |
|
} |
|
|
|
set rowSpan(value: number) { |
|
this.setAttribute("rowspan", value + ""); |
|
} |
|
|
|
get headers(): string { |
|
return this.getAttribute("headers") ?? ""; |
|
} |
|
|
|
set headers(value: string) { |
|
this.setAttribute("headers", value); |
|
} |
|
|
|
get align(): string { |
|
return this.getAttribute("align") ?? ""; |
|
} |
|
|
|
set align(value: string) { |
|
this.setAttribute("align", value); |
|
} |
|
|
|
get ch(): string { |
|
return this.getAttribute("char") ?? ""; |
|
} |
|
|
|
set ch(value: string) { |
|
this.setAttribute("char", value); |
|
} |
|
|
|
get chOff(): string { |
|
return this.getAttribute("charoff") ?? ""; |
|
} |
|
|
|
set chOff(value: string) { |
|
this.setAttribute("charoff", value); |
|
} |
|
|
|
get vAlign(): string { |
|
return this.getAttribute("valign") ?? ""; |
|
} |
|
|
|
set vAlign(value: string) { |
|
this.setAttribute("valign", value); |
|
} |
|
|
|
get noWrap(): boolean { |
|
return this.getAttribute("nowrap") !== null; |
|
} |
|
|
|
set noWrap(value: boolean) { |
|
if (value) this.setAttribute("nowrap", ""); |
|
else this.removeAttribute("nowrap"); |
|
} |
|
|
|
get width(): string { |
|
return this.getAttribute("width") ?? ""; |
|
} |
|
|
|
set width(value: string) { |
|
this.setAttribute("width", value); |
|
} |
|
|
|
get height(): string { |
|
return this.getAttribute("height") ?? ""; |
|
} |
|
|
|
set height(value: string) { |
|
this.setAttribute("height", value); |
|
} |
|
|
|
get axis(): string { |
|
return this.getAttribute("axis") ?? ""; |
|
} |
|
|
|
set axis(value: string) { |
|
this.setAttribute("axis", value); |
|
} |
|
|
|
get scope(): string { |
|
return this.getAttribute("scope") ?? ""; |
|
} |
|
|
|
set scope(value: string) { |
|
this.setAttribute("scope", value); |
|
} |
|
|
|
get abbr(): string { |
|
return this.getAttribute("abbr") ?? ""; |
|
} |
|
|
|
set abbr(value: string) { |
|
this.setAttribute("abbr", value); |
|
} |
|
} |
|
|
|
export class HTMLInputElement extends HTMLElement { |
|
} |
|
|
|
export class HTMLTextAreaElement extends HTMLElement { |
|
readonly nodeName = "TEXTAREA"; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, HTMLTextAreaElement.prototype); |
|
} |
|
|
|
get text(): string { |
|
return this.value; |
|
} |
|
|
|
set text(value: string) { |
|
this.value = value; |
|
} |
|
|
|
get defaultValue(): string { |
|
return this.getAttribute("defaultValue") ?? ""; |
|
} |
|
|
|
set defaultValue(value: string) { |
|
this.setAttribute("defaultValue", value); |
|
} |
|
|
|
get value(): string { |
|
return this.getAttribute("value") ?? this.defaultValue; |
|
} |
|
|
|
set value(value: string) { |
|
this.setAttribute("value", value); |
|
} |
|
|
|
get cols(): number { |
|
return +this.getAttribute("cols"); |
|
} |
|
|
|
set cols(value: number) { |
|
this.setAttribute("cols", value + ""); |
|
} |
|
|
|
get rows(): number { |
|
return +this.getAttribute("rows"); |
|
} |
|
|
|
set rows(value: number) { |
|
this.setAttribute("rows", value + ""); |
|
} |
|
|
|
get wrap(): string { |
|
return this.getAttribute("wrap") ?? ""; |
|
} |
|
|
|
set wrap(value: string) { |
|
this.setAttribute("wrap", value); |
|
} |
|
|
|
get placeholder(): string { |
|
return this.getAttribute("placeholder") ?? ""; |
|
} |
|
|
|
set placeholder(value: string) { |
|
this.setAttribute("placeholder", value); |
|
} |
|
|
|
get readOnly(): boolean { |
|
return this.getAttribute("readOnly") !== null; |
|
} |
|
|
|
set readOnly(value: boolean) { |
|
if (value) this.setAttribute("readOnly", "readOnly"); |
|
else this.removeAttribute("readOnly"); |
|
} |
|
|
|
get disabled(): boolean { |
|
return this.getAttribute("disabled") !== null; |
|
} |
|
|
|
set disabled(value: boolean) { |
|
if (value) this.setAttribute("disabled", "disabled"); |
|
else this.removeAttribute("disabled"); |
|
} |
|
|
|
get autofocus(): boolean { |
|
return this.getAttribute("autofocus") !== null; |
|
} |
|
|
|
set autofocus(value: boolean) { |
|
if (value) this.setAttribute("autofocus", "autofocus"); |
|
else this.removeAttribute("autofocus"); |
|
} |
|
|
|
get required(): boolean { |
|
return this.getAttribute("required") !== null; |
|
} |
|
|
|
set required(value: boolean) { |
|
if (value) this.setAttribute("required", "required"); |
|
else this.removeAttribute("required"); |
|
} |
|
|
|
get maxLength(): number { |
|
return +this.getAttribute("maxLength"); |
|
} |
|
|
|
set maxLength(value: number) { |
|
this.setAttribute("maxLength", value + ""); |
|
} |
|
|
|
get minLength(): number { |
|
return +this.getAttribute("minLength"); |
|
} |
|
|
|
set minLength(value: number) { |
|
this.setAttribute("minLength", value + ""); |
|
} |
|
|
|
get name(): string { |
|
return this.getAttribute("name") ?? ""; |
|
} |
|
|
|
set name(value: string) { |
|
this.setAttribute("name", value); |
|
} |
|
|
|
#form: HTMLFormElement | null = null; |
|
|
|
get form(): HTMLFormElement | null { |
|
return this.#form; |
|
} |
|
|
|
set form(value: HTMLFormElement | null) { |
|
this.#form = value; |
|
} |
|
} |
|
|
|
export class HTMLButtonElement extends HTMLElement { |
|
readonly nodeName = "BUTTON"; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, HTMLButtonElement.prototype); |
|
} |
|
|
|
get text(): string { |
|
return this.textContent ?? ""; |
|
} |
|
|
|
set text(value: string) { |
|
this.textContent = value; |
|
} |
|
|
|
get value(): string { |
|
return this.getAttribute("value") ?? ""; |
|
} |
|
|
|
set value(value: string) { |
|
this.setAttribute("value", value); |
|
} |
|
|
|
get type(): string { |
|
return this.getAttribute("type") ?? ""; |
|
} |
|
|
|
set type(value: string) { |
|
this.setAttribute("type", value); |
|
} |
|
|
|
get name(): string { |
|
return this.getAttribute("name") ?? ""; |
|
} |
|
|
|
set name(value: string) { |
|
this.setAttribute("name", value); |
|
} |
|
|
|
get disabled(): boolean { |
|
return this.getAttribute("disabled") !== null; |
|
} |
|
|
|
set disabled(value: boolean) { |
|
if (value) this.setAttribute("disabled", "disabled"); |
|
else this.removeAttribute("disabled"); |
|
} |
|
|
|
get autofocus(): boolean { |
|
return this.getAttribute("autofocus") !== null; |
|
} |
|
|
|
set autofocus(value: boolean) { |
|
if (value) this.setAttribute("autofocus", "autofocus"); |
|
else this.removeAttribute("autofocus"); |
|
} |
|
|
|
#form: HTMLFormElement | null = null; |
|
|
|
get form(): HTMLFormElement | null { |
|
return this.#form; |
|
} |
|
|
|
set form(value: HTMLFormElement | null) { |
|
this.#form = value; |
|
} |
|
} |
|
|
|
export class HTMLDataListElement extends HTMLElement { |
|
readonly nodeName = "DATALIST"; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, HTMLDataListElement.prototype); |
|
} |
|
|
|
get options(): HTMLCollection { |
|
return createHTMLCollection(this.querySelectorAll("option")); |
|
} |
|
|
|
get text(): string { |
|
return this.textContent ?? ""; |
|
} |
|
|
|
set text(value: string) { |
|
this.textContent = value; |
|
} |
|
|
|
get id(): string { |
|
return this.getAttribute("id") ?? ""; |
|
} |
|
|
|
set id(value: string) { |
|
this.setAttribute("id", value); |
|
} |
|
|
|
get label(): string { |
|
return this.getAttribute("label") ?? ""; |
|
} |
|
|
|
set label(value: string) { |
|
this.setAttribute("label", value); |
|
} |
|
|
|
get name(): string { |
|
return this.getAttribute("name") ?? ""; |
|
} |
|
|
|
set name(value: string) { |
|
this.setAttribute("name", value); |
|
} |
|
|
|
get disabled(): boolean { |
|
return this.getAttribute("disabled") !== null; |
|
} |
|
|
|
set disabled(value: boolean) { |
|
if (value) this.setAttribute("disabled", "disabled"); |
|
else this.removeAttribute("disabled"); |
|
} |
|
|
|
get autofocus(): boolean { |
|
return this.getAttribute("autofocus") !== null; |
|
} |
|
|
|
set autofocus(value: boolean) { |
|
if (value) this.setAttribute("autofocus", "autofocus"); |
|
else this.removeAttribute("autofocus"); |
|
} |
|
|
|
get required(): boolean { |
|
return this.getAttribute("required") !== null; |
|
} |
|
|
|
set required(value: boolean) { |
|
if (value) this.setAttribute("required", "required"); |
|
else this.removeAttribute("required"); |
|
} |
|
|
|
get size(): number { |
|
return +this.getAttribute("size"); |
|
} |
|
|
|
set size(value: number) { |
|
this.setAttribute("size", value + ""); |
|
} |
|
|
|
get optionsLength(): number { |
|
return this.options.length; |
|
} |
|
|
|
get selectedIndex(): number { |
|
return indexOf(this.options, this.selectedOption); |
|
} |
|
|
|
set selectedIndex(index: number) { |
|
const options = this.options; |
|
if (index >= 0 && index < options.length) { |
|
this.selectedOption = options[index]! as HTMLOptionElement; |
|
} |
|
} |
|
|
|
get selectedOption(): HTMLOptionElement | null { |
|
return this.querySelector<HTMLOptionElement>("option[selected]") ?? null; |
|
} |
|
|
|
set selectedOption(option: HTMLOptionElement | null) { |
|
const selectedOption = this.selectedOption; |
|
if (selectedOption) selectedOption.selected = false; |
|
if (option) option.selected = true; |
|
} |
|
|
|
get value(): string { |
|
return this.selectedOption?.value ?? ""; |
|
} |
|
|
|
set value(value: string) { |
|
const options = this.options; |
|
for (let i = 0; i < options.length; i++) { |
|
const option = options[i] as HTMLOptionElement; |
|
if (option.value === value) { |
|
this.selectedOption = option; |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
|
|
export class HTMLStyleElement extends HTMLElement { |
|
readonly nodeName = "STYLE"; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, HTMLStyleElement.prototype); |
|
} |
|
|
|
get media(): string { |
|
return this.getAttribute("media") ?? ""; |
|
} |
|
|
|
set media(value: string) { |
|
this.setAttribute("media", value); |
|
} |
|
|
|
get type(): string { |
|
return this.getAttribute("type") ?? ""; |
|
} |
|
|
|
set type(value: string) { |
|
this.setAttribute("type", value); |
|
} |
|
|
|
get disabled(): boolean { |
|
return this.getAttribute("disabled") !== null; |
|
} |
|
|
|
set disabled(value: boolean) { |
|
if (value) this.setAttribute("disabled", "disabled"); |
|
else this.removeAttribute("disabled"); |
|
} |
|
|
|
get scoped(): boolean { |
|
return this.getAttribute("scoped") !== null; |
|
} |
|
|
|
set scoped(value: boolean) { |
|
if (value) this.setAttribute("scoped", "scoped"); |
|
else this.removeAttribute("scoped"); |
|
} |
|
} |
|
|
|
export class HTMLScriptElement extends HTMLElement { |
|
readonly nodeName = "SCRIPT"; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, HTMLScriptElement.prototype); |
|
} |
|
|
|
get text(): string { |
|
return this.textContent ?? ""; |
|
} |
|
|
|
set text(value: string) { |
|
this.textContent = value; |
|
} |
|
|
|
get src(): string { |
|
return this.getAttribute("src") ?? ""; |
|
} |
|
|
|
set src(value: string) { |
|
this.setAttribute("src", value); |
|
} |
|
|
|
get type(): string { |
|
return this.getAttribute("type") ?? ""; |
|
} |
|
|
|
set type(value: string) { |
|
this.setAttribute("type", value); |
|
} |
|
|
|
get charset(): string { |
|
return this.getAttribute("charset") ?? ""; |
|
} |
|
|
|
set charset(value: string) { |
|
this.setAttribute("charset", value); |
|
} |
|
|
|
get async(): boolean { |
|
return this.getAttribute("async") !== null; |
|
} |
|
|
|
set async(value: boolean) { |
|
if (value) this.setAttribute("async", "async"); |
|
else this.removeAttribute("async"); |
|
} |
|
|
|
get defer(): boolean { |
|
return this.getAttribute("defer") !== null; |
|
} |
|
|
|
set defer(value: boolean) { |
|
if (value) this.setAttribute("defer", "defer"); |
|
else this.removeAttribute("defer"); |
|
} |
|
|
|
get crossOrigin(): string { |
|
return this.getAttribute("crossOrigin") ?? ""; |
|
} |
|
|
|
set crossOrigin(value: string) { |
|
this.setAttribute("crossOrigin", value); |
|
} |
|
|
|
get nonce(): string { |
|
return this.getAttribute("nonce") ?? ""; |
|
} |
|
|
|
set nonce(value: string) { |
|
this.setAttribute("nonce", value); |
|
} |
|
|
|
get noModule(): boolean { |
|
return this.getAttribute("noModule") !== null; |
|
} |
|
|
|
set noModule(value: boolean) { |
|
if (value) this.setAttribute("noModule", "noModule"); |
|
else this.removeAttribute("noModule"); |
|
} |
|
|
|
get integrity(): string { |
|
return this.getAttribute("integrity") ?? ""; |
|
} |
|
|
|
set integrity(value: string) { |
|
this.setAttribute("integrity", value); |
|
} |
|
|
|
get event(): string { |
|
return this.getAttribute("event") ?? ""; |
|
} |
|
|
|
set event(value: string) { |
|
this.setAttribute("event", value); |
|
} |
|
} |
|
|
|
export interface HTMLElementTagNameMap { |
|
"a": HTMLAnchorElement; |
|
"area": HTMLAreaElement; |
|
"body": HTMLBodyElement; |
|
"div": HTMLDivElement; |
|
"head": HTMLHeadElement; |
|
"html": HTMLHtmlElement; |
|
"img": HTMLImageElement; |
|
"option": HTMLOptionElement; |
|
"title": HTMLTitleElement; |
|
"table": HTMLTableElement; |
|
"caption": HTMLTableCaptionElement; |
|
"tbody": HTMLTableSectionElement; |
|
"thead": HTMLTableSectionElement; |
|
"tfoot": HTMLTableSectionElement; |
|
"tr": HTMLTableRowElement; |
|
"td": HTMLTableCellElement; |
|
"th": HTMLTableCellElement; |
|
"form": HTMLFormElement; |
|
"input": HTMLInputElement; |
|
"textarea": HTMLTextAreaElement; |
|
"button": HTMLButtonElement; |
|
"dl": HTMLDataListElement; |
|
"style": HTMLStyleElement; |
|
} |
|
|
|
export abstract class HTMLElementTagNameMap { |
|
[key: string]: HTMLElement; |
|
static readonly head = HTMLHeadElement; |
|
static readonly title = HTMLTitleElement; |
|
static readonly body = HTMLBodyElement; |
|
static readonly area = HTMLAreaElement; |
|
static readonly a = HTMLAnchorElement; |
|
static readonly div = HTMLDivElement; |
|
static readonly html = HTMLHtmlElement; |
|
static readonly img = HTMLImageElement; |
|
static readonly option = HTMLOptionElement; |
|
static readonly table = HTMLTableElement; |
|
static readonly caption = HTMLTableCaptionElement; |
|
static readonly tbody = HTMLTableSectionElement; |
|
static readonly thead = HTMLTableSectionElement; |
|
static readonly tfoot = HTMLTableSectionElement; |
|
static readonly tr = HTMLTableRowElement; |
|
static readonly td = HTMLTableCellElement; |
|
static readonly th = HTMLTableCellElement; |
|
static readonly form = HTMLFormElement; |
|
static readonly input = HTMLInputElement; |
|
static readonly textarea = HTMLTextAreaElement; |
|
static readonly button = HTMLButtonElement; |
|
static readonly dl = HTMLDataListElement; |
|
static readonly style = HTMLStyleElement; |
|
} |
|
|
|
// #endregion HTML |
|
|
|
// #region SVG |
|
|
|
export class SVGElement extends Element { |
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, SVGElement.prototype); |
|
} |
|
|
|
get ownerSVGElement(): SVGSVGElement | null { |
|
return this.ownerDocument?.querySelector("svg") ?? null; |
|
} |
|
|
|
get viewportElement(): SVGElement | null { |
|
return this.ownerSVGElement ?? null; |
|
} |
|
|
|
get x(): number { |
|
return +this.getAttribute("x"); |
|
} |
|
|
|
set x(value: number) { |
|
this.setAttribute("x", value + ""); |
|
} |
|
|
|
get y(): number { |
|
return +this.getAttribute("y"); |
|
} |
|
|
|
set y(value: number) { |
|
this.setAttribute("y", value + ""); |
|
} |
|
|
|
get width(): number { |
|
return +this.getAttribute("width"); |
|
} |
|
|
|
set width(value: number) { |
|
this.setAttribute("width", value + ""); |
|
} |
|
|
|
get height(): number { |
|
return +this.getAttribute("height"); |
|
} |
|
|
|
set height(value: number) { |
|
this.setAttribute("height", value + ""); |
|
} |
|
|
|
get fill(): string { |
|
return this.getAttribute("fill") ?? ""; |
|
} |
|
|
|
set fill(value: string) { |
|
this.setAttribute("fill", value); |
|
} |
|
|
|
get stroke(): string { |
|
return this.getAttribute("stroke") ?? ""; |
|
} |
|
|
|
set stroke(value: string) { |
|
this.setAttribute("stroke", value); |
|
} |
|
|
|
get strokeWidth(): number { |
|
return +this.getAttribute("stroke-width"); |
|
} |
|
|
|
set strokeWidth(value: number) { |
|
this.setAttribute("stroke-width", value + ""); |
|
} |
|
|
|
get strokeDashArray(): string { |
|
return this.getAttribute("stroke-dasharray") ?? ""; |
|
} |
|
|
|
set strokeDashArray(value: string) { |
|
this.setAttribute("stroke-dasharray", value); |
|
} |
|
|
|
get strokeDashOffset(): number { |
|
return +this.getAttribute("stroke-dashoffset"); |
|
} |
|
|
|
set strokeDashOffset(value: number) { |
|
this.setAttribute("stroke-dashoffset", value + ""); |
|
} |
|
|
|
get strokeOpacity(): number { |
|
return +this.getAttribute("stroke-opacity"); |
|
} |
|
|
|
set strokeOpacity(value: number) { |
|
this.setAttribute("stroke-opacity", value + ""); |
|
} |
|
|
|
get strokeLineCap(): string { |
|
return this.getAttribute("stroke-linecap") ?? ""; |
|
} |
|
|
|
set strokeLineCap(value: string) { |
|
this.setAttribute("stroke-linecap", value); |
|
} |
|
|
|
get strokeLineJoin(): string { |
|
return this.getAttribute("stroke-linejoin") ?? ""; |
|
} |
|
|
|
set strokeLineJoin(value: string) { |
|
this.setAttribute("stroke-linejoin", value); |
|
} |
|
|
|
get strokeMiterLimit(): number { |
|
return +this.getAttribute("stroke-miterlimit"); |
|
} |
|
|
|
set strokeMiterLimit(value: number) { |
|
this.setAttribute("stroke-miterlimit", value + ""); |
|
} |
|
|
|
get fillOpacity(): number { |
|
return +this.getAttribute("fill-opacity"); |
|
} |
|
|
|
set fillOpacity(value: number) { |
|
this.setAttribute("fill-opacity", value + ""); |
|
} |
|
|
|
get fillRule(): string { |
|
return this.getAttribute("fill-rule") ?? ""; |
|
} |
|
|
|
set fillRule(value: string) { |
|
this.setAttribute("fill-rule", value); |
|
} |
|
|
|
get transform(): string { |
|
return this.getAttribute("transform") ?? ""; |
|
} |
|
|
|
set transform(value: string) { |
|
this.setAttribute("transform", value); |
|
} |
|
|
|
get preserveAspectRatio(): string { |
|
return this.getAttribute("preserveAspectRatio") ?? ""; |
|
} |
|
|
|
set preserveAspectRatio(value: string) { |
|
this.setAttribute("preserveAspectRatio", value); |
|
} |
|
|
|
get mask(): string { |
|
return this.getAttribute("mask") ?? ""; |
|
} |
|
|
|
set mask(value: string) { |
|
this.setAttribute("mask", value); |
|
} |
|
|
|
get opacity(): number { |
|
return +this.getAttribute("opacity"); |
|
} |
|
|
|
set opacity(value: number) { |
|
this.setAttribute("opacity", value + ""); |
|
} |
|
|
|
get visibility(): string { |
|
return this.getAttribute("visibility") ?? ""; |
|
} |
|
|
|
set visibility(value: string) { |
|
this.setAttribute("visibility", value); |
|
} |
|
|
|
get clipPath(): string { |
|
return this.getAttribute("clip-path") ?? ""; |
|
} |
|
|
|
set clipPath(value: string) { |
|
this.setAttribute("clip-path", value); |
|
} |
|
|
|
get clipRule(): string { |
|
return this.getAttribute("clip-rule") ?? ""; |
|
} |
|
|
|
set clipRule(value: string) { |
|
this.setAttribute("clip-rule", value); |
|
} |
|
|
|
get filter(): string { |
|
return this.getAttribute("filter") ?? ""; |
|
} |
|
|
|
set filter(value: string) { |
|
this.setAttribute("filter", value); |
|
} |
|
|
|
get marker(): string { |
|
return this.getAttribute("marker") ?? ""; |
|
} |
|
|
|
set marker(value: string) { |
|
this.setAttribute("marker", value); |
|
} |
|
} |
|
|
|
export class SVGSVGElement extends SVGElement { |
|
readonly nodeName = "SVG"; |
|
readonly nodeType: Node.ELEMENT_NODE = Node.ELEMENT_NODE; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, SVGSVGElement.prototype); |
|
} |
|
|
|
get viewBox(): string { |
|
return this.getAttribute("viewBox") ?? ""; |
|
} |
|
|
|
set viewBox(value: string) { |
|
this.setAttribute("viewBox", value); |
|
} |
|
|
|
get preserveAspectRatio(): string { |
|
return this.getAttribute("preserveAspectRatio") ?? ""; |
|
} |
|
|
|
set preserveAspectRatio(value: string) { |
|
this.setAttribute("preserveAspectRatio", value); |
|
} |
|
} |
|
|
|
export class SVGRectElement extends SVGElement { |
|
readonly nodeName = "RECT"; |
|
readonly nodeType: Node.ELEMENT_NODE = Node.ELEMENT_NODE; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, SVGRectElement.prototype); |
|
} |
|
|
|
get x(): number { |
|
return +this.getAttribute("x"); |
|
} |
|
|
|
set x(value: number) { |
|
this.setAttribute("x", value + ""); |
|
} |
|
|
|
get y(): number { |
|
return +this.getAttribute("y"); |
|
} |
|
|
|
set y(value: number) { |
|
this.setAttribute("y", value + ""); |
|
} |
|
|
|
get rx(): number { |
|
return +this.getAttribute("rx"); |
|
} |
|
|
|
set rx(value: number) { |
|
this.setAttribute("rx", value + ""); |
|
} |
|
|
|
get ry(): number { |
|
return +this.getAttribute("ry"); |
|
} |
|
|
|
set ry(value: number) { |
|
this.setAttribute("ry", value + ""); |
|
} |
|
} |
|
|
|
export class SVGDefsElement extends SVGElement { |
|
readonly nodeName = "DEFS"; |
|
readonly nodeType: Node.ELEMENT_NODE = Node.ELEMENT_NODE; |
|
} |
|
|
|
export class SVGUseElement extends SVGElement { |
|
readonly nodeName = "USE"; |
|
readonly nodeType: Node.ELEMENT_NODE = Node.ELEMENT_NODE; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, SVGUseElement.prototype); |
|
} |
|
|
|
get href(): string { |
|
return this.getAttribute("href") ?? ""; |
|
} |
|
|
|
set href(value: string) { |
|
this.setAttribute("href", value); |
|
} |
|
} |
|
|
|
export class SVGGElement extends SVGElement { |
|
readonly nodeName = "G"; |
|
readonly nodeType: Node.ELEMENT_NODE = Node.ELEMENT_NODE; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, SVGGElement.prototype); |
|
} |
|
|
|
get transform(): string { |
|
return this.getAttribute("transform") ?? ""; |
|
} |
|
|
|
set transform(value: string) { |
|
this.setAttribute("transform", value); |
|
} |
|
|
|
get requiredExtensions(): string { |
|
return this.getAttribute("requiredExtensions") ?? ""; |
|
} |
|
|
|
set requiredExtensions(value: string) { |
|
this.setAttribute("requiredExtensions", value); |
|
} |
|
|
|
get requiredFeatures(): string { |
|
return this.getAttribute("requiredFeatures") ?? ""; |
|
} |
|
|
|
set requiredFeatures(value: string) { |
|
this.setAttribute("requiredFeatures", value); |
|
} |
|
|
|
get systemLanguage(): string { |
|
return this.getAttribute("systemLanguage") ?? ""; |
|
} |
|
|
|
set systemLanguage(value: string) { |
|
this.setAttribute("systemLanguage", value); |
|
} |
|
|
|
get requiredFormats(): string { |
|
return this.getAttribute("requiredFormats") ?? ""; |
|
} |
|
|
|
set requiredFormats(value: string) { |
|
this.setAttribute("requiredFormats", value); |
|
} |
|
|
|
get requiredFonts(): string { |
|
return this.getAttribute("requiredFonts") ?? ""; |
|
} |
|
|
|
set requiredFonts(value: string) { |
|
this.setAttribute("requiredFonts", value); |
|
} |
|
|
|
get requiredFontSizes(): string { |
|
return this.getAttribute("requiredFontSizes") ?? ""; |
|
} |
|
|
|
set requiredFontSizes(value: string) { |
|
this.setAttribute("requiredFontSizes", value); |
|
} |
|
} |
|
|
|
export class SVGPathElement extends SVGElement { |
|
readonly nodeName = "PATH"; |
|
readonly nodeType: Node.ELEMENT_NODE = Node.ELEMENT_NODE; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, SVGPathElement.prototype); |
|
} |
|
|
|
get d(): string { |
|
return this.getAttribute("d") ?? ""; |
|
} |
|
|
|
set d(value: string) { |
|
this.setAttribute("d", value); |
|
} |
|
} |
|
|
|
export class SVGCircleElement extends SVGElement { |
|
readonly nodeName = "CIRCLE"; |
|
readonly nodeType: Node.ELEMENT_NODE = Node.ELEMENT_NODE; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, SVGCircleElement.prototype); |
|
} |
|
|
|
get cx(): number { |
|
return +this.getAttribute("cx"); |
|
} |
|
|
|
set cx(value: number) { |
|
this.setAttribute("cx", value + ""); |
|
} |
|
|
|
get cy(): number { |
|
return +this.getAttribute("cy"); |
|
} |
|
|
|
set cy(value: number) { |
|
this.setAttribute("cy", value + ""); |
|
} |
|
|
|
get r(): number { |
|
return +this.getAttribute("r"); |
|
} |
|
|
|
set r(value: number) { |
|
this.setAttribute("r", value + ""); |
|
} |
|
} |
|
|
|
export class SVGEllipseElement extends SVGElement { |
|
readonly nodeName = "ELLIPSE"; |
|
readonly nodeType: Node.ELEMENT_NODE = Node.ELEMENT_NODE; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, SVGEllipseElement.prototype); |
|
} |
|
|
|
get cx(): number { |
|
return +this.getAttribute("cx"); |
|
} |
|
|
|
set cx(value: number) { |
|
this.setAttribute("cx", value + ""); |
|
} |
|
|
|
get cy(): number { |
|
return +this.getAttribute("cy"); |
|
} |
|
|
|
set cy(value: number) { |
|
this.setAttribute("cy", value + ""); |
|
} |
|
|
|
get rx(): number { |
|
return +this.getAttribute("rx"); |
|
} |
|
|
|
set rx(value: number) { |
|
this.setAttribute("rx", value + ""); |
|
} |
|
|
|
get ry(): number { |
|
return +this.getAttribute("ry"); |
|
} |
|
|
|
set ry(value: number) { |
|
this.setAttribute("ry", value + ""); |
|
} |
|
} |
|
|
|
export class SVGLineElement extends SVGElement { |
|
readonly nodeName = "LINE"; |
|
readonly nodeType: Node.ELEMENT_NODE = Node.ELEMENT_NODE; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, SVGLineElement.prototype); |
|
} |
|
|
|
get x1(): number { |
|
return +this.getAttribute("x1"); |
|
} |
|
|
|
set x1(value: number) { |
|
this.setAttribute("x1", value + ""); |
|
} |
|
|
|
get y1(): number { |
|
return +this.getAttribute("y1"); |
|
} |
|
|
|
set y1(value: number) { |
|
this.setAttribute("y1", value + ""); |
|
} |
|
|
|
get x2(): number { |
|
return +this.getAttribute("x2"); |
|
} |
|
|
|
set x2(value: number) { |
|
this.setAttribute("x2", value + ""); |
|
} |
|
|
|
get y2(): number { |
|
return +this.getAttribute("y2"); |
|
} |
|
|
|
set y2(value: number) { |
|
this.setAttribute("y2", value + ""); |
|
} |
|
} |
|
|
|
export class SVGPolylineElement extends SVGElement { |
|
readonly nodeName = "POLYLINE"; |
|
readonly nodeType: Node.ELEMENT_NODE = Node.ELEMENT_NODE; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, SVGPolylineElement.prototype); |
|
} |
|
|
|
get points(): string { |
|
return this.getAttribute("points") ?? ""; |
|
} |
|
|
|
set points(value: string) { |
|
this.setAttribute("points", value); |
|
} |
|
} |
|
|
|
export class SVGPolygonElement extends SVGElement { |
|
readonly nodeName = "POLYGON"; |
|
readonly nodeType: Node.ELEMENT_NODE = Node.ELEMENT_NODE; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, SVGPolygonElement.prototype); |
|
} |
|
|
|
get points(): string { |
|
return this.getAttribute("points") ?? ""; |
|
} |
|
|
|
set points(value: string) { |
|
this.setAttribute("points", value); |
|
} |
|
} |
|
|
|
export class SVGTextElement extends SVGElement { |
|
readonly nodeName = "TEXT"; |
|
readonly nodeType: Node.ELEMENT_NODE = Node.ELEMENT_NODE; |
|
|
|
constructor() { |
|
super(); |
|
Object.setPrototypeOf(this, SVGTextElement.prototype); |
|
} |
|
|
|
get x(): number { |
|
return +this.getAttribute("x"); |
|
} |
|
|
|
set x(value: number) { |
|
this.setAttribute("x", value + ""); |
|
} |
|
|
|
get y(): number { |
|
return +this.getAttribute("y"); |
|
} |
|
|
|
set y(value: number) { |
|
this.setAttribute("y", value + ""); |
|
} |
|
|
|
get dx(): number { |
|
return +this.getAttribute("dx"); |
|
} |
|
|
|
set dx(value: number) { |
|
this.setAttribute("dx", value + ""); |
|
} |
|
|
|
get dy(): number { |
|
return +this.getAttribute("dy"); |
|
} |
|
|
|
set dy(value: number) { |
|
this.setAttribute("dy", value + ""); |
|
} |
|
|
|
get rotate(): number { |
|
return +this.getAttribute("rotate"); |
|
} |
|
|
|
set rotate(value: number) { |
|
this.setAttribute("rotate", value + ""); |
|
} |
|
|
|
get textLength(): number { |
|
return +this.getAttribute("textLength"); |
|
} |
|
|
|
set textLength(value: number) { |
|
this.setAttribute("textLength", value + ""); |
|
} |
|
|
|
get lengthAdjust(): string { |
|
return this.getAttribute("lengthAdjust") ?? ""; |
|
} |
|
|
|
set lengthAdjust(value: string) { |
|
this.setAttribute("lengthAdjust", value); |
|
} |
|
} |
|
|
|
export interface SVGElementTagNameMap { |
|
"svg": SVGSVGElement; |
|
"rect": SVGRectElement; |
|
"defs": SVGDefsElement; |
|
"use": SVGUseElement; |
|
"g": SVGGElement; |
|
"path": SVGPathElement; |
|
"circle": SVGCircleElement; |
|
"ellipse": SVGEllipseElement; |
|
"line": SVGLineElement; |
|
"polyline": SVGPolylineElement; |
|
"polygon": SVGPolygonElement; |
|
"text": SVGTextElement; |
|
} |
|
|
|
// #endregion SVG |
|
|
|
// #region Serialization |
|
|
|
export type DOMParserSupportedType = |
|
| "text/html" |
|
| "text/xml" |
|
| "application/xml" |
|
| "application/xhtml+xml" |
|
| "image/svg+xml"; |
|
|
|
export class DOMParser { |
|
#parseFromString = (document: Document, string: string) => { |
|
// const parser = /<\?([^?]+)\?>|<((?:\w+:)?[\w-.]+)|\s*((?:\w+:)?[\w-.]+)=(?:"|')([^"']+)(?:"|')|(\s*\/>|<\/(?:\w+:)?[\w-.]+>)|<!--([^-]*)-->|([^</>]+)|>/y; |
|
const parser = |
|
/<\?(?<XML_PROCESSING_INSTRUCTION>[^?]+(?=\?>))\?>|<(?<XML_ELEMENT_START>(?:(?<XML_ELEMENT_NS>\w+):)?[\w\-.]+)|\s*(?<XML_ATTRIBUTE_NAME>(?<![>][\w ]*)[@]?(?:(?<XML_ATTRIBUTE_NS>\w+):)?[\w\-.]+)(?:=["']?(?<XML_ATTRIBUTE_VALUE>(?<=['"])[^"']+(?=["'])|(?<==)\S+(?=\b\s+|>))['"]?|(?![\w ]*[<])|)|(?<XML_ELEMENT_END>\s*(?:\/>)|<\/(?:\w+:)?[\w\-\.]+>)|<!--\s*(?<XML_COMMENT>.*?)\s*-->|(?<XML_WHITESPACE>\s+)|(?<XML_TEXT>(?<!<[\w ]*)[^</>]+(?![\w ]*>))|(?:<!\[CDATA\[)(?<XML_CDATA>(?!]]>)[\S\s]*?(?=]]>))(?:]]>)|>/yg; |
|
|
|
const XML_PROCESSING_INSTRUCTION = "XML_PROCESSING_INSTRUCTION"; |
|
const XML_ELEMENT_START = "XML_ELEMENT_START"; |
|
const XML_ATTRIBUTE_NAME = "XML_ATTRIBUTE_NAME"; |
|
const XML_ATTRIBUTE_VALUE = "XML_ATTRIBUTE_VALUE"; |
|
const XML_ELEMENT_END = "XML_ELEMENT_END"; |
|
const XML_COMMENT = "XML_COMMENT"; |
|
const XML_TEXT = "XML_TEXT"; |
|
const XML_CDATA = "XML_CDATA"; |
|
const XML_WHITESPACE = "XML_WHITESPACE"; |
|
const XML_ELEMENT_NS = "XML_ELEMENT_NS"; |
|
const XML_ATTRIBUTE_NS = "XML_ATTRIBUTE_NS"; |
|
|
|
interface Indices extends RegExpIndicesArray { |
|
readonly groups: { |
|
readonly [key: string]: [start: number, end: number]; |
|
}; |
|
} |
|
|
|
interface Match extends RegExpExecArray { |
|
readonly groups: { |
|
readonly [key: string]: string; |
|
}; |
|
readonly indices: Indices; |
|
} |
|
|
|
interface Handler { |
|
(document: Node, match: Match): Node | void | undefined | null; |
|
} |
|
|
|
type Handlers = { readonly [key: string]: Handler }; |
|
|
|
const getDocument = (node: Node): Document => { |
|
if (isDocument(node)) return node; |
|
if (isDocument(node.ownerDocument)) return node.ownerDocument; |
|
throw new TypeError("Invalid document"); |
|
}; |
|
|
|
const handlers = { |
|
[XML_PROCESSING_INSTRUCTION](document, { groups: $0 }) { |
|
const { [XML_PROCESSING_INSTRUCTION]: value } = $0; |
|
if (value) { |
|
const [target, ...data] = value.split(/\s+/); |
|
const doc = document instanceof Document |
|
? document |
|
: document.ownerDocument; |
|
const docEl = doc!.documentElement; |
|
if (target === "xml") { |
|
const [version, encoding, standalone] = data; |
|
if (version) docEl!.setAttribute("version", version); |
|
if (encoding) docEl!.setAttribute("encoding", encoding); |
|
if (standalone) docEl!.setAttribute("standalone", standalone); |
|
} else { |
|
const node = doc!.createProcessingInstruction( |
|
target, |
|
data.join(" "), |
|
); |
|
document.appendChild(node); |
|
} |
|
} |
|
}, |
|
|
|
// XML_ELEMENT_START |
|
[XML_ELEMENT_START](document, { groups: $0 = {} }) { |
|
if ($0[XML_ELEMENT_START]) { |
|
const { [XML_ELEMENT_NS]: ns, [XML_ELEMENT_START]: name } = $0; |
|
if (ns) { |
|
const el = document.ownerDocument!.createElementNS( |
|
document.lookupNamespaceURI(ns) ?? "", |
|
name, |
|
); |
|
document.appendChild(el); |
|
return el; |
|
} |
|
const el = document.ownerDocument!.createElement(name); |
|
document.appendChild(el); |
|
return el; |
|
} |
|
}, |
|
|
|
// XML_ATTRIBUTE_NAME |
|
[XML_ATTRIBUTE_NAME](parentNode, { groups: $0 = {} }) { |
|
if ( |
|
!("setAttribute" in parentNode && "setAttributeNS" in parentNode) || |
|
!( |
|
typeof parentNode.setAttribute === "function" && |
|
typeof parentNode.setAttributeNS === "function" |
|
) |
|
) { |
|
return; |
|
} |
|
const { |
|
[XML_ATTRIBUTE_NS]: ns, |
|
[XML_ATTRIBUTE_NAME]: name, |
|
[XML_ATTRIBUTE_VALUE]: value, |
|
} = $0; |
|
if (ns) { |
|
parentNode.setAttributeNS(ns, name, value); |
|
} else { |
|
parentNode.setAttribute(name, value); |
|
} |
|
}, |
|
|
|
// XML_ELEMENT_END |
|
[XML_ELEMENT_END](document) { |
|
return document.parentNode; |
|
}, |
|
|
|
// XML_COMMENT |
|
[XML_COMMENT](document, { groups: $0 = {} }) { |
|
const text = $0[XML_COMMENT]?.trim(); // ignore xml:space |
|
if (text.length) document.appendChild(new Comment(text)); |
|
}, |
|
|
|
// XML_TEXT |
|
[XML_TEXT](document, { groups: $0 = {} }) { |
|
const text = $0[XML_TEXT].trim(); // ignore xml:space |
|
if (text.length) document.appendChild(new Text(text)); |
|
}, |
|
|
|
// XML_CDATA |
|
[XML_CDATA](document, { groups: $0 = {} }) { |
|
const text = $0[XML_CDATA] ?? ""; // abide by xml:space rules |
|
if (text.length) document.appendChild(new CDATASection(text)); |
|
}, |
|
} satisfies Handlers; |
|
|
|
parser.lastIndex = 0; |
|
let match = parser.exec(string) as Match; |
|
do { |
|
let newCurrent; |
|
const test = ([group, handler]: [string, Handler?], _: number) => ( |
|
(handler && match!.groups[group]) |
|
? (newCurrent = handler(document, match! as Match), false) |
|
: true |
|
); |
|
const check = Object.entries(handlers).every(test); |
|
|
|
document = !check && newCurrent ? newCurrent : document; |
|
match = parser.exec(string) as Match; |
|
} while (match); |
|
}; |
|
|
|
public parseFromString(string: string, type: DOMParserSupportedType) { |
|
let namespaceURI: string | null = null; |
|
if ( |
|
type === "image/svg+xml" || type === "text/xml" || |
|
type === "application/xml" |
|
) { |
|
namespaceURI = "http://www.w3.org/2000/svg"; |
|
} else if (type === "application/xhtml+xml") { |
|
namespaceURI = "http://www.w3.org/1999/xhtml"; |
|
} else { |
|
namespaceURI = null; |
|
} |
|
|
|
const qualifiedName = type === "text/html" |
|
? "html" |
|
: type === "image/svg+xml" |
|
? "svg" |
|
: "xml"; |
|
|
|
const document: Document | undefined = implementation.createDocument( |
|
namespaceURI ?? "", |
|
qualifiedName ?? "", |
|
implementation.createDocumentType(qualifiedName, "", ""), |
|
); |
|
|
|
this.#parseFromString(document!, string); |
|
return document; |
|
} |
|
|
|
get [webidl.brand](): webidl.brand { |
|
return webidl.brand; |
|
} |
|
} |
|
|
|
/** |
|
* @constructor XMLSerializer |
|
* @class XMLSerializer |
|
*/ |
|
export class XMLSerializer { |
|
get [webidl.brand](): webidl.brand { |
|
return webidl.brand; |
|
} |
|
|
|
serializeToString(node: Element): string { |
|
const buf: string[] = []; |
|
this.#serializeToString(node, buf); |
|
return buf.join(""); |
|
} |
|
|
|
#serializeToString = ( |
|
node: AnyNode | null, |
|
buf: string[] = [], |
|
) => { |
|
if (node) { |
|
const htmlns = "http://www.w3.org/1999/xhtml"; |
|
if (isElement(node)) { |
|
const attrs = node.attributes; |
|
const len = attrs.length; |
|
let child = node.firstChild; |
|
const nodeName = node.tagName; |
|
const isHTML = htmlns === node.namespaceURI; |
|
buf.push("<", nodeName); |
|
for (let i = 0; i < len; i++) { |
|
this.#serializeToString(attrs.item(i)!, buf); |
|
} |
|
if ( |
|
child || isHTML && !/^(?:meta|link|img|br|hr|input)$/i.test(nodeName) |
|
) { |
|
buf.push(">"); |
|
//if is cdata child node |
|
if (isHTML && /^script$/i.test(nodeName)) { |
|
if (child) buf.push((child as Text).data); |
|
} else { |
|
while (child) { |
|
this.#serializeToString(child as Element, buf); |
|
child = child.nextSibling; |
|
} |
|
} |
|
buf.push("</", nodeName, ">"); |
|
} else { |
|
buf.push("/>"); |
|
} |
|
return buf.join(""); |
|
} else if (isDocument(node) || isDocumentFragment(node)) { |
|
let child = node.firstChild; |
|
while (child) { |
|
this.#serializeToString(child as Element, buf); |
|
child = child.nextSibling; |
|
} |
|
return buf.join(""); |
|
} else if (isAttr(node)) { |
|
return buf.push( |
|
" ", |
|
node.name, |
|
'="', |
|
node.value.replace(/[<>&"]/g, _xmlEncoder), |
|
'"', |
|
), |
|
buf.join(""); |
|
} else if (isText(node)) { |
|
return buf.push(node.data.replace(/[<>&]/g, _xmlEncoder)), buf.join(""); |
|
} else if (isCDATASection(node)) { |
|
return buf.push("<![CDATA[", node.data, "]]>"), buf.join(""); |
|
} else if (isComment(node)) { |
|
return buf.push("<!--", node.data, "-->"), buf.join(""); |
|
} else if (isDocumentType(node)) { |
|
const pubid = node.publicId; |
|
const sysid = node.systemId; |
|
buf.push("<!DOCTYPE ", node.name); |
|
if (pubid) { |
|
buf.push(' PUBLIC "', pubid); |
|
if (sysid && sysid != ".") { |
|
buf.push('" "', sysid); |
|
} |
|
buf.push('">'); |
|
} else if (sysid && sysid != ".") { |
|
buf.push(' SYSTEM "', sysid, '">'); |
|
} else { |
|
const sub = node.internalSubset; |
|
if (sub) buf.push(" [", sub, "]"); |
|
buf.push(">"); |
|
} |
|
return buf.join(""); |
|
} else if (isProcessingInstruction(node)) { |
|
return buf.push("<?", node.target, " ", node.data, "?>"), buf.join(""); |
|
} else if (isEntityReference(node)) { |
|
return buf.push("&", node.nodeName, ";"), buf.join(""); |
|
} else if (isEntity(node)) { |
|
buf.push("<!ENTITY ", node.nodeName); |
|
const pubid = node.publicId; |
|
if (pubid) { |
|
buf.push(' PUBLIC "', pubid); |
|
const sysid = node.systemId; |
|
if (sysid && sysid != ".") { |
|
buf.push('" "', sysid); |
|
} |
|
} else { |
|
const sysid = node.systemId; |
|
if (sysid && sysid != ".") { |
|
buf.push(' SYSTEM "', sysid); |
|
} |
|
} |
|
const notationName = node.notationName; |
|
if (notationName) { |
|
buf.push('" NDATA "', notationName); |
|
} |
|
buf.push('">'); |
|
return buf.join(""); |
|
} else if (isNotation(node)) { |
|
buf.push("<!NOTATION ", node.nodeName); |
|
const pubid = node.publicId; |
|
if (pubid) { |
|
buf.push(' PUBLIC "', pubid); |
|
const sysid = node.systemId; |
|
if (sysid && sysid != ".") { |
|
buf.push('" "', sysid); |
|
} |
|
} else { |
|
const sysid = node.systemId; |
|
if (sysid && sysid != ".") { |
|
buf.push(' SYSTEM "', sysid); |
|
} |
|
} |
|
buf.push('">'); |
|
return buf.join(""); |
|
} else { |
|
throw new Error("serializeToString invalid node type"); |
|
} |
|
} |
|
return buf.join(""); |
|
}; |
|
} |
|
|
|
// #endregion Serialization |
|
|
|
// #region helpers |
|
function assert(condition: unknown, message?: string): asserts condition { |
|
if (!condition) { |
|
const err = new Error(message ?? "Assertion failed: condition is falsy"); |
|
Error.captureStackTrace?.(err, assert); |
|
err.stack?.slice(); |
|
throw err; |
|
} |
|
} |
|
|
|
function _appendSingleChild(parent: Node, child: Node): Node { |
|
if (child.parentNode) child.parentNode.removeChild(child); |
|
if (parent.childNodes) push(parent.childNodes, child); |
|
child.parentNode = parent; |
|
return child; |
|
} |
|
|
|
function _xmlEncoder(c: string): string { |
|
switch (c) { |
|
case "<": |
|
return "<"; |
|
case ">": |
|
return ">"; |
|
case "&": |
|
return "&"; |
|
case "'": |
|
return "'"; |
|
case '"': |
|
return """; |
|
} |
|
return c; |
|
} |
|
|
|
function _insertBefore( |
|
parent: Node, |
|
newChildren: Node | Node[], |
|
refChild: Node | null, |
|
): Node { |
|
if (newChildren instanceof Node) newChildren = [newChildren]; |
|
if (parent.childNodes) { |
|
const index = indexOf(parent.childNodes, refChild); |
|
splice(parent.childNodes, index, 0, ...newChildren); |
|
for (const child of newChildren) child.parentNode = parent; |
|
} |
|
return parent; |
|
} |
|
|
|
function _querySelectorAll<TElement extends Element>( |
|
parentNode: Node | Element, |
|
predicate: (el: Element) => el is TElement, |
|
elements: TElement[], |
|
): TElement[]; |
|
function _querySelectorAll<TElement extends Element>( |
|
parentNode: Node | Element, |
|
predicate: (el: Element) => boolean, |
|
elements: TElement[], |
|
): TElement[]; |
|
function _querySelectorAll<TElement extends Element>( |
|
parentNode: Node | Element, |
|
predicate: (el: Element) => boolean, |
|
elements: TElement[], |
|
): TElement[] { |
|
const stack: Node[] = [parentNode]; |
|
while (stack.length) { |
|
const node = stack.pop(); |
|
if (node) { |
|
if (isElement(node)) { |
|
if (predicate(node)) push(elements, node); |
|
push(stack, ...node.children ?? []); |
|
} else if (isDocument(node) || isDocumentFragment(node)) { |
|
push(stack, ...node.childNodes ?? []); |
|
} |
|
} |
|
} |
|
return elements; |
|
} |
|
|
|
function _getElementById(node: Node | null, id: string): Element | null { |
|
if ( |
|
node && isElement(node) && |
|
node.attributes?.getNamedItem("id")?.value === id |
|
) { |
|
return node as Element; |
|
} |
|
const children = node?.childNodes; |
|
if (children) { |
|
for (let i = 0; i < children.length; i++) { |
|
const child = children.item(i); |
|
const result = _getElementById(child, id); |
|
if (result) { |
|
return result; |
|
} |
|
} |
|
} |
|
return null; |
|
} |
|
|
|
// #endregion helpers |
|
|
|
// #region type-guards |
|
|
|
function isNodeLike(node: unknown): node is Node { |
|
return node != null && typeof node === "object" && !Array.isArray(node) && |
|
"nodeType" in node && typeof node.nodeType === "number" && |
|
!isNaN(node.nodeType) && node.nodeType > 0 && node.nodeType < 13; |
|
} |
|
|
|
function isNode(node: unknown): node is Node { |
|
return isNodeLike(node) && node instanceof Node; // 🦆🦆 |
|
} |
|
|
|
function isElement(it: unknown): it is Element { |
|
return isNode(it) && it.nodeType === ELEMENT_NODE; |
|
} |
|
|
|
function isAttr(it: unknown): it is Attr { |
|
return isNode(it) && it.nodeType === ATTRIBUTE_NODE; |
|
} |
|
|
|
function isText(it: unknown): it is Text { |
|
return isNode(it) && it.nodeType === TEXT_NODE; |
|
} |
|
|
|
function isCDATASection(it: unknown): it is CDATASection { |
|
return isNode(it) && it.nodeType === CDATA_SECTION_NODE; |
|
} |
|
|
|
function isEntityReference(it: unknown): it is EntityReference { |
|
return isNode(it) && it.nodeType === ENTITY_REFERENCE_NODE; |
|
} |
|
|
|
function isEntity(it: unknown): it is Entity { |
|
return isNode(it) && it.nodeType === ENTITY_NODE; |
|
} |
|
|
|
function isProcessingInstruction(it: unknown): it is ProcessingInstruction { |
|
return isNode(it) && it.nodeType === PROCESSING_INSTRUCTION_NODE; |
|
} |
|
|
|
function isComment(it: unknown): it is Comment { |
|
return isNode(it) && it.nodeType === COMMENT_NODE; |
|
} |
|
|
|
function isDocument(it: unknown): it is Document { |
|
return isNode(it) && it.nodeType === DOCUMENT_NODE; |
|
} |
|
|
|
function isDocumentType(it: unknown): it is DocumentType { |
|
return isNode(it) && it.nodeType === DOCUMENT_TYPE_NODE; |
|
} |
|
|
|
function isDocumentFragment(it: unknown): it is DocumentFragment { |
|
return isNode(it) && it.nodeType === DOCUMENT_FRAGMENT_NODE; |
|
} |
|
|
|
function isNotation(it: unknown): it is Notation { |
|
return isNode(it) && it.nodeType === NOTATION_NODE; |
|
} |
|
|
|
// #endregion type-guards |
|
|
|
// #region Globals |
|
|
|
const implementation = new DOMImplementation(); |
|
|
|
// #endregion Globals |
|
|
|
export default { |
|
DOMImplementation, |
|
Node, |
|
Element, |
|
Attr, |
|
Comment, |
|
CharacterData, |
|
CDATASection, |
|
Text, |
|
ProcessingInstruction, |
|
Entity, |
|
EntityReference, |
|
Document, |
|
DocumentType, |
|
DocumentFragment, |
|
Notation, |
|
HTMLElement, |
|
HTMLBodyElement, |
|
HTMLHeadElement, |
|
HTMLHtmlElement, |
|
HTMLAnchorElement, |
|
HTMLAreaElement, |
|
HTMLOptionElement, |
|
TreeWalker, |
|
NodeFilter, |
|
NodeList, |
|
NamedNodeMap, |
|
DOMTokenList, |
|
DOMStringMap, |
|
HTMLCollection, |
|
HTMLFormControlsCollection, |
|
HTMLSelectOptionsCollection, |
|
SVGElement, |
|
SVGGElement, |
|
SVGSVGElement, |
|
SVGRectElement, |
|
SVGDefsElement, |
|
SVGUseElement, |
|
SVGPathElement, |
|
SVGCircleElement, |
|
SVGEllipseElement, |
|
SVGLineElement, |
|
SVGPolylineElement, |
|
SVGPolygonElement, |
|
SVGTextElement, |
|
DOMParser, |
|
XMLSerializer, |
|
} as const; |