Skip to content

Instantly share code, notes, and snippets.

@nberlette
Last active November 27, 2023 00:28
Show Gist options
  • Save nberlette/bd33d134bd811282aaa6c179ac3d96d8 to your computer and use it in GitHub Desktop.
Save nberlette/bd33d134bd811282aaa6c179ac3d96d8 to your computer and use it in GitHub Desktop.
`DUM`: like a DOM implementation for Deno... but dumber.

DUM: like a DOM implementation for Deno... but dumber.

Usage

Polyfill the global namespace

import "https://gist.githubusercontent.com/nberlette/bd33d134bd811282aaa6c179ac3d96d8/raw/global.ts";

Import individual APIs

import { Document, Element, Text } from "https://gist.githubusercontent.com/nberlette/bd33d134bd811282aaa6c179ac3d96d8/raw/dom.ts";

Import the whole thing

import * as DUM from "https://gist.githubusercontent.com/nberlette/bd33d134bd811282aaa6c179ac3d96d8/raw/dom.ts";

const { Document, Element, Text } = DUM;

console.log(new Text("Hello, world!"));
// => Text { data: "Hello, world!" }

API

DOMImplementation

interface DOMImplementation {
  createDocument(
    namespaceURI: string | null,
    qualifiedNameStr: string,
    doctype: DocumentType | null,
  ): Document;
  createDocumentType(
    qualifiedNameStr: string,
    publicId: string,
    systemId: string,
  ): DocumentType;
  createHTMLDocument(title?: string): Document;
}

Document

interface Document extends Node, NonElementParentNode, ParentNode {
  readonly URL: string;
  readonly documentElement: Element | null;
  readonly head: HTMLHeadElement;
  readonly body: HTMLElement;
  readonly title: string;
  readonly location: Location;
  readonly readyState: DocumentReadyState;
  readonly hidden: boolean;
  readonly visibilityState: VisibilityState;
  readonly cookie: string;
  readonly referrer: string;
  readonly lastModified: string;
  readonly designMode: string;
  readonly dir: string;
  readonly domain: string;
  readonly compatMode: string;
  readonly characterSet: string;
  readonly charset: string;
  readonly inputEncoding: string;
  readonly contentType: string;
  readonly doctype: DocumentType | null;
  readonly implementation: DOMImplementation;
  readonly activeElement: Element | null;
  readonly fullscreenElement: Element | null;
  readonly pointerLockElement: Element | null;
  readonly preferredStyleSheetSet: string;
  readonly selectedStyleSheetSet: string | null;
  readonly styleSheetSets: string[];
  readonly origin: string;
  readonly fonts: FontFaceSet;
  readonly images: HTMLCollectionOf<HTMLImageElement>;
  readonly embeds: HTMLCollectionOf<HTMLEmbedElement>;
  readonly plugins: HTMLCollectionOf<HTMLEmbedElement>;
  readonly links: HTMLCollectionOf<HTMLLinkElement>;
  readonly forms: HTMLCollectionOf<HTMLFormElement>;
  readonly anchors: HTMLCollectionOf<HTMLAnchorElement>;
  readonly lastStyleSheetSet: string | null;
  readonly scrollingElement: Element | null;
  readonly preferredStyleSheet: CSSStyleSheet | null;
  readonly selectedStyleSheet: CSSStyleSheet | null;
  readonly fullscreenEnabled: boolean;
  readonly pointerLockEnabled: boolean;
  readonly hiddenProperty: string;
  readonly visibilityStateProperty: string;
  readonly visibilityChangeEvent: string;
  readonly hiddenChangeEvent: string;
}
/// <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 "&lt;";
case ">":
return "&gt;";
case "&":
return "&amp;";
case "'":
return "&apos;";
case '"':
return "&quot;";
}
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;
// #region XMLFormatter
interface XMLFormatterOptions {
newLine?: EOL | `${EOL}`;
lineWidth?: number;
tabSize?: number;
useTabs?: boolean;
splitNS?: boolean;
finalNewLine?: boolean;
removeComments?: boolean;
verbose?: boolean;
debug?: boolean;
}
enum EOL {
CRLF = "\r\n",
CR = "\r",
LF = "\n",
}
/**
* Internal tool for minifying or formatting XML and HTML data.
*
* Based on pretty-data.
*
* @see https://github.com/vkiryukhin/pretty-data
*/
export class XMLFormatter {
static readonly options = {
useTabs: false,
splitNS: true,
tabSize: 4,
newLine: "\n",
finalNewLine: true,
removeComments: false,
verbose: false,
debug: false,
} satisfies XMLFormatter.Options;
static #default?: XMLFormatter;
static get default(): XMLFormatter {
return XMLFormatter.#default ??= new XMLFormatter(XMLFormatter.options);
}
static format(
xml: string,
options: XMLFormatter.Options = XMLFormatter.options,
): string {
return new XMLFormatter(options).format(xml);
}
static minify(
xml: string,
options: XMLFormatter.Options = XMLFormatter.options,
): string {
return new XMLFormatter(options).minify(xml);
}
static from(options: XMLFormatter.Options = XMLFormatter.options) {
return new XMLFormatter(options);
}
constructor(options: XMLFormatter.Options = {}) {
const opt = { ...XMLFormatter.options, ...options } as Required<
XMLFormatter.Options
>;
const {
newLine,
useTabs,
tabSize,
splitNS,
finalNewLine,
verbose,
debug,
removeComments,
} = opt;
Object.assign(this, {
newLine,
useTabs,
tabSize,
splitNS,
verbose,
debug,
finalNewLine,
removeComments,
});
}
#lineWidth = 80;
#newLine: EOL | `${EOL}` = EOL.LF;
#removeComments = false;
#splitNS = true;
#tabSize = 4;
#useTabs = false;
#verbose = false;
#debug = false;
#finalNewLine = true;
public get indent(): string {
return this.useTabs ? "\t" : " ".repeat(this.tabSize);
}
public get useTabs(): boolean {
return this.#useTabs;
}
public set useTabs(value: boolean) {
this.#useTabs = Boolean(value);
}
public get tabSize(): number {
return this.#tabSize;
}
public set tabSize(value: number) {
if (typeof value !== "number" || isNaN(value)) {
throw new TypeError("[XMLFormatter] 'tabSize' must be a number");
}
if (value < 0 || value > 8) {
throw new RangeError("[XMLFormatter] 'tabSize' must be between 0 and 8");
}
this.#tabSize = value;
}
public get splitNS(): boolean {
return this.#splitNS;
}
public set splitNS(value: boolean) {
this.#splitNS = Boolean(value);
}
public get removeComments(): boolean {
return this.#removeComments;
}
public set removeComments(value: boolean) {
this.#removeComments = Boolean(value);
}
public get lineWidth(): number {
return this.#lineWidth;
}
public set lineWidth(value: number) {
if (typeof value !== "number" || isNaN(value)) {
throw new TypeError("[XMLFormatter] 'lineWidth' must be a number");
}
if (value < 0 || value > 1000) {
throw new RangeError(
"[XMLFormatter] 'lineWidth' must be between 0 and 1000",
);
}
this.#lineWidth = value;
}
public get newLine(): EOL | `${EOL}` {
return this.#newLine;
}
public set newLine(value: EOL | `${EOL}`) {
if (![EOL.CRLF, EOL.CR, EOL.LF].includes(value as EOL)) {
throw new TypeError(
"[XMLFormatter] 'newLine' must be either '\\r\\n', '\\r', or '\\n'.",
);
}
this.#newLine = value;
}
public get finalNewLine(): boolean {
return this.#finalNewLine;
}
public set finalNewLine(value: boolean) {
this.#finalNewLine = Boolean(value);
}
public get verbose(): boolean {
return this.#verbose ??= false;
}
public set verbose(value: boolean) {
this.#verbose = Boolean(value);
}
public get debug(): boolean {
return this.#debug ??= false;
}
public set debug(value: boolean) {
this.#debug = Boolean(value);
}
public format(xml: string): string {
const DELIM = "~::~";
xml = this.minify(xml, false);
xml = xml.replace(/(<)/g, `${DELIM}$1`);
if (this.splitNS) xml = xml.replace(/xmlns([:=])/g, `${DELIM}xmlns$1`);
const parts = xml.split(DELIM);
if (this.debug) console.log(parts);
let inComment = false;
let level = 0;
let output = "";
for (let i = 0; i < parts.length; i++) {
// <!
if (parts[i].search(/<!/) > -1) {
output += this.#getIndent(level, parts[i]);
// end <!
inComment = !(
parts[i].search(/-->/) > -1 || parts[i].search(/\]>/) > -1 ||
parts[i].search(/!DOCTYPE/i) > -1
);
} // end <!
else if (parts[i].search(/-->/) > -1 || parts[i].search(/\]>/) > -1) {
output += parts[i];
inComment = false;
} // <elm></elm>
else if (
/^<(\w|:)/.test(parts[i - 1]) && /^<\/(\w|:)/.test(parts[i]) &&
/^<[\w:\-.,/]+/.exec(parts[i - 1])?.[0] ==
/^<\/[\w:\-.,]+/.exec(parts[i])?.[0]?.replace(/\//, "")
) {
output += parts[i];
if (!inComment) level--;
} // <elm>
else if (
parts[i].search(/<(\w|:)/) > -1 && parts[i].search(/<\//) == -1 &&
parts[i].search(/\/>/) == -1
) {
if (inComment) output += parts[i];
else output += this.#getIndent(level++, parts[i]);
} // <elm>...</elm>
else if (parts[i].search(/<(\w|:)/) > -1 && parts[i].search(/<\//) > -1) {
if (inComment) output += parts[i];
else output += this.#getIndent(level, parts[i]);
} // </elm>
else if (parts[i].search(/<\//) > -1) {
if (inComment) output += parts[i];
else output += this.#getIndent(--level, parts[i]);
} // <elm />
else if (
parts[i].search(/\/>/) > -1 &&
(!this.splitNS || parts[i].search(/xmlns[:=]/) == -1)
) {
if (inComment) output += parts[i];
else output += this.#getIndent(level, parts[i]);
} // xmlns />
else if (
parts[i].search(/\/>/) > -1 &&
parts[i].search(/xmlns[:=]/) > -1 &&
this.splitNS
) {
if (inComment) output += parts[i];
else output += this.#getIndent(level--, parts[i]);
} // <?xml ... ?>
else if (parts[i].search(/<\?/) > -1) {
output += this.#getIndent(level, parts[i]);
} // xmlns
else if (
this.splitNS &&
(parts[i].search(/xmlns\:/) > -1 || parts[i].search(/xmlns\=/) > -1)
) {
output += this.#getIndent(level, parts[i]);
} else {
output += parts[i];
}
}
// remove leading newline
if (output[0] == this.newLine) {
output = output.slice(1);
} else if (output.slice(0, 1) == this.newLine) {
output = output.slice(2);
}
// remove trailing newlines
output = output.replace(/[\r\n]+$/, "");
// add final newline, if desired
if (this.finalNewLine) output += this.newLine;
return output;
}
public minify(xml: string, removeComments = this.removeComments): string {
removeComments ??= false;
// all line breaks outside of CDATA elements
xml = this.#stripLineBreaks(xml);
// remove comments
if (removeComments) {
xml = xml.replace(
/\<![ \r\n\t]*(--([^\-]|[\r\n]|-[^\-])*--[ \r\n\t]*)\>/g,
"",
);
}
// insignificant whitespace between tags
xml = xml.replace(/>\s{0,}</g, "><");
// spaces between attributes
xml = xml.replace(/"\s+(?=[^\s]+=)/g, '" ');
// spaces between the last attribute and tag close (>)
xml = xml.replace(/"\s+(?=>)/g, '"');
// spaces between the last attribute and tag close (/>)
xml = xml.replace(/"\s+(?=\/>)/g, '" ');
// spaces between the node name and the first attribute
xml = xml.replace(/[^ <>="]\s+[^ <>="]+=/g, (m) => m.replace(/\s+/g, " "));
// final new line
xml = xml.replace(/\s+$/, "");
if (this.finalNewLine) xml += this.newLine;
return xml;
}
#getIndent(level: number, trailingValue = ""): string {
return `${this.newLine}${this.indent.repeat(level)}${trailingValue}`;
}
#stripLineBreaks(xml: string): string {
let output = "";
let inCdata = false, inComment = false;
let inTag = false, inTagName = false, inAttribute = false;
const reset = () => {
// deno-fmt-ignore
inTag = inCdata = inTagName = inComment = inAttribute = false;
};
for (let i = 0; i < xml.length; i++) {
const char = xml[i], prev = xml[i - 1], next = xml[i + 1];
if (
!inCdata && !inComment && !inTag && char == "!" &&
(xml.slice(i, 8) == "![CDATA[" || xml.slice(i, 3) == "!--")
) {
inCdata = true;
inComment = xml.slice(i, 3) == "!--";
} else if (
inCdata && !inComment && !inTagName && !inAttribute && (
(char == "]" && (xml.slice(i, 3) == "]]>")) ||
(char == "-" && (xml.slice(i, 3) == "-->"))
)
) {
reset();
} else if (char.search(/[\r\n]/g) > -1 && !inCdata && !inComment) {
if (
/\r/.test(char) && /\S|\r|\n/.test(prev) &&
/\S|\r|\n/.test(xml.charAt(i + this.newLine.length))
) {
output += char;
} else if (
/\n/.test(char) &&
/\S|\r|\n/.test(xml.charAt(i - this.newLine.length)) &&
/\S|\r|\n/.test(next)
) {
output += char;
}
continue;
}
output += char;
}
return output;
}
}
export declare namespace XMLFormatter {
export type { XMLFormatterOptions as Options };
}
// #endregion XMLFormatter
/// <reference lib="deno.window" />
import DOM from "./dom.ts";
import { assign } from "./utils.ts";
assign(globalThis, DOM);
assign(Window.prototype, DOM);
// import shiki from "npm:shiki-es";
// import {
// bold,
// italic,
// rgb24,
// underline,
// } from "https://deno.land/[email protected]/fmt/colors.ts";
export {
inspect,
type InspectOptions,
type InspectOptionsStylized,
} from "node:util";
export { XMLFormatter } from "./format.ts";
export const assign: <const T extends object, U extends object>(
target: T,
source: U,
) => asserts target is T & U = Object.assign;
const { bind, call, apply } = Function.prototype;
export function hasOwn<const T extends object, K extends PropertyKey>(
o: T,
k: K,
): o is {
[P in keyof (T & Record<K, unknown>)]: P extends keyof T ? T[P]
: (T & Record<K, unknown>)[P];
} {
return Object.hasOwn(o, k);
}
export const uncurryThis: <T, A extends readonly unknown[], R>(
fn: (this: T, ...args: A) => R,
_thisArg?: T,
) => (thisArg: T, ...args: A) => R = bind.bind(call);
export const uncurry: <T, A extends readonly unknown[], R>(
fn: (this: T, ...args: A) => R,
_thisArg?: T,
) => (...args: [thisArg: T, ...args: A]) => R = bind.bind(apply);
export const indexOf = uncurryThis(Array.prototype.indexOf) as (
<const T extends readonly unknown[]>(
array: T | ArrayLike<T[number]>,
searchElement: T[number],
fromIndex?: number,
) => number
);
export const slice = uncurryThis(Array.prototype.slice) as (
<const T extends readonly unknown[], N extends number, E extends number>(
array: T | ArrayLike<T[number]>,
start?: N,
end?: E,
) => T[number][]
);
export const splice = uncurryThis(Array.prototype.splice) as <
const T extends readonly unknown[],
N extends number,
E extends number,
>(
array: T | ArrayLike<T[number]>,
start: N,
deleteCount?: E,
...items: T
) => T[number][];
export const push = uncurryThis(Array.prototype.push, [] as ArrayLike<unknown>) as (
<T>(target: ArrayLike<T>, ...items: T[]) => number
);
export const pop = uncurryThis(Array.prototype.pop, [] as unknown[]) as (
<T>(array: ArrayLike<T>) => T | undefined
);
export const shift = uncurryThis(Array.prototype.shift, [] as unknown[]) as (
<T>(array: ArrayLike<T>) => T | undefined
);
export const unshift = uncurryThis(
Array.prototype.unshift,
[] as ArrayLike<unknown>,
) as <T>(target: ArrayLike<T>, ...items: T[]) => number;
// deno-lint-ignore no-namespace
export namespace Tuple {
export const Push = <
const A extends readonly unknown[],
const B extends readonly unknown[],
>(array: A, ...items: B): Tuple.Push<A, B> => {
push(array, ...items);
return array as unknown as Tuple.Push<A, B>;
};
export const Pop = uncurryThis(Array.prototype.pop, [] as unknown[]) as (
<const T extends readonly unknown[]>(array: T) => Tuple.Pop<T>
);
export const Shift = uncurryThis(Array.prototype.shift, [] as unknown[]) as (
<const T extends readonly unknown[]>(array: T) => Tuple.Shift<T>
);
export const Unshift = <
const A extends readonly unknown[],
const B extends readonly unknown[],
>(array: A, ...items: B): Tuple.Unshift<A, B> => {
unshift(array, ...items);
return array as unknown as Tuple.Unshift<A, B>;
};
export const Concat = uncurryThis(
Array.prototype.concat,
[] as readonly unknown[],
) as (
<
const A extends readonly unknown[],
const B extends readonly unknown[],
>(a: A, b: B) => Tuple.Concat<A, B>
);
export const Reverse = uncurryThis(Array.prototype.reverse) as (
<const T extends readonly unknown[]>(array: T) => Tuple.Reverse<T>
);
export const Slice = uncurryThis(Array.prototype.slice) as (
<const T extends readonly unknown[], N extends number, E extends number>(
array: T,
start?: N,
end?: E,
) => Tuple.Slice<T, N, E>
);
/** Build a tuple type of length `N` with element type `T` */
// deno-fmt-ignore
export type Of<N extends number, T = readonly [unknown]> =
| Internal.BuildTuple<N, T> extends infer R ? R : never;
// deno-fmt-ignore
export type Concat<
A extends readonly unknown[],
B extends readonly unknown[],
> = readonly [...A, ...B];
// deno-fmt-ignore
export type Exclude<A, T = never> =
| A extends readonly [infer B, ...infer C]
? [B] extends [T]
? Exclude<C, T>
: [B, ...Exclude<C, T>]
: [A] extends [T] ? never : A;
// deno-fmt-ignore
export type Extract<A, T> =
| A extends readonly [infer B, ...infer C]
? [B] extends [T]
? [B, ...Extract<C, T>]
: Extract<C, T>
: [A] extends [T] ? A : never;
// deno-fmt-ignore
export type Extrema<A extends readonly unknown[]> =
| A extends readonly [infer B, ...infer C]
? C extends [...unknown[], infer E]
? [first: B, last: E]
: [first: B, last: B]
: never;
export type From<A extends ArrayLike<unknown>> = A extends
ReadonlyArray<infer T> ? T[]
: A extends Readonly<ArrayLike<infer T>>
? A extends { readonly length: infer L extends number }
? Internal.BuildTuple<L, T>
: T[]
: unknown[];
// deno-fmt-ignore
export type Head<A extends readonly unknown[]> =
| A extends readonly [infer B, ...unknown[]] ? B : never;
// deno-fmt-ignore
export type Length<A extends readonly unknown[]> = A["length"];
// deno-fmt-ignore
export type Pop<A extends readonly unknown[]> =
| A extends readonly [...unknown[], infer B] ? B : never;
// deno-fmt-ignore
export type Push<A extends readonly unknown[], B = unknown> =
| [...A, ...B extends readonly unknown[] ? B : [B]];
// deno-fmt-ignore
export type Reverse<A extends readonly unknown[]> =
| A extends readonly [infer B, ...infer C] ? [...Reverse<C>, B] : [];
// deno-fmt-ignore
export type ReverseConcat<
A extends readonly unknown[],
B extends readonly unknown[]
> = Reverse<Concat<A, B>>;
// deno-fmt-ignore
export type Shift<A extends readonly unknown[]> =
| A extends readonly [infer B, ...unknown[]] ? B : never;
export type Slice<
A extends readonly unknown[],
Start extends number = 0,
End extends number = A["length"],
> = Internal.Slice<A, Start, End> extends infer R ? R : never;
// deno-fmt-ignore
export type Tail<A extends readonly unknown[]> =
| A extends readonly [unknown, ...infer B] ? B : never;
// deno-fmt-ignore
export type Unshift<A extends readonly unknown[], B = A[number]> =
| [...B extends readonly unknown[] ? B : [B], ...A];
// #region Internal
// deno-lint-ignore no-namespace
namespace Internal {
/** @internal */
export type BuildTuple<
N extends number,
T = unknown,
R extends readonly unknown[] = readonly [],
> = R["length"] extends N ? R
: BuildTuple<N, T, readonly [...R, T]>;
// if N is negative, convert it to its positive counterpart by the A
type ToPositive<N extends number, A extends readonly unknown[]> =
`${N}` extends `-${infer P extends number}` ? Slice<A, P>["length"]
: N;
// get the initial N items of A
type InitialN<
A extends readonly unknown[],
N extends number,
R extends readonly unknown[] = [],
> = R["length"] extends N | A["length"] ? R
: InitialN<A, N, [...R, A[R["length"]]]>;
export type Slice<
A extends readonly unknown[],
Start extends number = 0,
End extends number = A["length"],
> = InitialN<A, ToPositive<End, A>> extends
[...InitialN<A, ToPositive<Start, A>>, ...infer Rest] ? Rest
: [];
}
// #endregion Internal
}
// #endregion internal
// const $ = await shiki.getHighlighter({
// langs: ["html", "xml", "css"],
// themes: ["material-theme-ocean", "material-theme-palenight", "dracula"],
// });
// export function highlight(
// html: string,
// formatOptions: XMLFormatter.Options & { minify?: boolean } = {},
// ) {
// const { minify = false, ...options } = {
// useTabs: false,
// tabSize: 2,
// lineWidth: 75,
// removeComments: true,
// finalNewLine: true,
// ...formatOptions,
// };
// const fmt = XMLFormatter.from(options);
// let text = fmt.format(html);
// if (minify) text = fmt.minify(text, options.removeComments);
// const lang = "html", theme = "material-theme-ocean";
// const tokens = $.codeToThemedTokens(text, lang, theme, {
// includeExplanation: false,
// });
// const colorful = tokens.reduce((acc, groups) => {
// let line = "";
// for (const group of groups) {
// let { content } = group;
// const { color, fontStyle = shiki.FontStyle.None } = group;
// if (fontStyle & shiki.FontStyle.Bold) content = bold(content);
// if (fontStyle & shiki.FontStyle.Italic) content = italic(content);
// if (fontStyle & shiki.FontStyle.Underline) content = underline(content);
// content = rgb24(
// content,
// parseInt(color?.replaceAll("#", "") ?? "000000", 16),
// );
// line += content;
// }
// return (acc.push(line), acc);
// }, [""] as string[]).join("\n");
// return colorful.replaceAll(/[\r\n]+$/g, options.finalNewLine ? "\n" : "");
// }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment