From a90e58a15d11224dfb863da3676e85675b00773d Mon Sep 17 00:00:00 2001 From: Borgar Date: Sun, 29 Mar 2026 16:39:36 +0000 Subject: [PATCH 01/14] Version 3 - Add DocumentFragment, a tag-less container node. - Add slightly nicer NS handling. - Add output handling - Add some more DOM convenience methods --- lib/CreateChildArgument.ts | 4 + lib/Document.ts | 128 ++++- lib/DocumentFragment.ts | 43 ++ lib/Element.ts | 83 +++- lib/NSMap.ts | 27 ++ lib/Node.spec.ts | 319 +++++++++++++ lib/Node.ts | 65 ++- lib/TextNode.ts | 7 +- lib/XMLAttr.ts | 1 + lib/appendChild.ts | 32 +- lib/constants.ts | 2 + lib/escape.ts | 5 +- lib/index.ts | 7 + lib/parser.ts | 23 +- lib/prettyPrint.ts | 12 +- lib/simplePrint.ts | 44 ++ test/domQuery-dedup.spec.ts | 6 +- test/namespaces.spec.ts | 44 ++ test/parse-data.spec.ts | 905 ++++++++++++++++++------------------ tsconfig.json | 47 +- 20 files changed, 1294 insertions(+), 510 deletions(-) create mode 100644 lib/CreateChildArgument.ts create mode 100644 lib/DocumentFragment.ts create mode 100644 lib/NSMap.ts create mode 100644 lib/Node.spec.ts create mode 100644 lib/XMLAttr.ts create mode 100644 lib/simplePrint.ts create mode 100644 test/namespaces.spec.ts diff --git a/lib/CreateChildArgument.ts b/lib/CreateChildArgument.ts new file mode 100644 index 0000000..4ed45e4 --- /dev/null +++ b/lib/CreateChildArgument.ts @@ -0,0 +1,4 @@ +import type { DocumentFragment } from './DocumentFragment.ts'; +import type { Node } from './Node.ts'; + +export type CreateChildArgument = Node | DocumentFragment | string | boolean | number | null | undefined; diff --git a/lib/Document.ts b/lib/Document.ts index 94b3bc9..ce57609 100644 --- a/lib/Document.ts +++ b/lib/Document.ts @@ -2,10 +2,16 @@ import { JsonML, type JsonMLElement } from './JsonML.js'; import { Node } from './Node.js'; import { Element } from './Element.js'; import { appendChild } from './appendChild.js'; -import { DOCUMENT_NODE } from './constants.js'; +import { DOCUMENT_NODE, XML_DECLARATION } from './constants.js'; import { domQuery } from './domQuery/index.js'; import { findAll } from './findAll.js'; import { isElement } from './isElement.ts'; +import { DocumentFragment } from './DocumentFragment.ts'; +import { NSMap } from './NSMap.ts'; +import { prettyPrint } from './prettyPrint.ts'; +import { simplePrint } from './simplePrint.ts'; +import type { CreateChildArgument } from './CreateChildArgument.ts'; +import type { XMLAttr } from './XMLAttr.ts'; /** * This class describes an XML document. @@ -14,6 +20,8 @@ import { isElement } from './isElement.ts'; */ export class Document extends Node { root: Element | null = null; + /** @ignore */ + namespaces = new NSMap(); /** * Constructs a new Document instance. @@ -25,6 +33,31 @@ export class Document extends Node { this.nodeType = DOCUMENT_NODE; } + /** + * Attach a namespace to the document. + * + * @param namespaceURI The namespace URI to attach. + * @param [prefix] Prefix to use on elements belonging to the namespace. + */ + attachNS (namespaceURI: string, prefix = ''): (name: string, attr?: XMLAttr | null, ...children: (CreateChildArgument | CreateChildArgument[])[]) => Element { + this.namespaces.add(namespaceURI, prefix); + this._updateNS(); + + // Return a new create function bound to the namespace + return this.createElementNS.bind(this, namespaceURI); + } + + /** @ignore */ + _updateNS () { + // ensure that namespaces exist on the root node + if (this.root) { + for (const [ namespaceURI, prefix ] of this.namespaces.list()) { + const key = 'xmlns' + (prefix ? ':' + prefix : ''); + this.root.setAttribute(key, namespaceURI); + } + } + } + // overwrites super get textContent () { return this.root ? this.root.textContent : ''; @@ -37,6 +70,60 @@ export class Document extends Node { return this.childNodes.filter(isElement); } + /** + * Create a new element node. + * + * @param qualifiedName The local tagName of the element. + * @param attr A record of attributes to assign to the new element. + * If the value is null or undefined, the attribute will be omitted. + * @param children Nodes to insert as children. + * Strings will be converted to TextNodes and arrays will be flattened. + * @returns A new Element instance. + */ + createElement = ( + qualifiedName: string, + attr: XMLAttr | null | undefined, + ...children: (CreateChildArgument | CreateChildArgument[])[] + ): Element => { + const element = new Element(qualifiedName); + if (attr) { + for (const [ key, val ] of Object.entries(attr)) { + if (val != null) { + element.setAttribute(key, String(val)); + } + } + } + for (const child of children) { + element.append(child); + } + return element; + }; + + createElementNS = ( + namespaceURI: string, + qualifiedName: string, + attr: XMLAttr | null | undefined, + ...children: (CreateChildArgument | CreateChildArgument[])[] + ): Element => { + const ns = this.namespaces.get(namespaceURI); + if (!ns) { + throw new Error('Unknown namespace ' + namespaceURI); + } + const element = new Element(ns + ':' + qualifiedName); + // can this not be solved by Element(name, attr) ... does the same thing internally, right? + if (attr) { + for (const [ key, val ] of Object.entries(attr)) { + if (val != null) { + element.setAttribute(key, String(val)); + } + } + } + for (const child of children) { + element.append(child); + } + return element; + }; + /** * Return all descendant elements that have the specified tag name. * @@ -77,12 +164,26 @@ export class Document extends Node { } // overwrites super - appendChild (node: Element): Element { - if (this.root) { - throw new Error('A document may only have one child/root element.'); + appendChild (node: T): T { + if (this.root || (node instanceof DocumentFragment && node.childNodes.length > 1)) { + throw new Error('A document must have only one child element.'); + } + let root: Element; + if (node instanceof DocumentFragment) { + if (!(node.childNodes[0] instanceof Element)) { + throw new Error('Document root node must be an Element'); + } + root = node.childNodes[0]; + } + else if (node instanceof Element) { + root = node; } - appendChild(this, node); - this.root = node; + else { + throw new Error('Document root node must be an Element'); + } + appendChild(this, root); + this.root = root; + this._updateNS(); return node; } @@ -94,4 +195,19 @@ export class Document extends Node { toJS (): JsonMLElement | [] { return this.root ? JsonML(this.root) : []; } + + /** + * Print the document as a string. + * + * @param pretty Apply automatic linebreaks and indentation to the output. + * @returns The document as an XML string. + */ + print (pretty = false): string { + if (!(this.root instanceof Element)) { + throw new Error('root element is missing'); + } + return `${XML_DECLARATION}\n` + ( + pretty ? prettyPrint(this.root) : simplePrint(this.root) + ); + } } diff --git a/lib/DocumentFragment.ts b/lib/DocumentFragment.ts new file mode 100644 index 0000000..e92aba5 --- /dev/null +++ b/lib/DocumentFragment.ts @@ -0,0 +1,43 @@ +import { appendChild } from './appendChild.ts'; +import { DOCUMENT_FRAGMENT_NODE } from './constants.ts'; +import type { Node } from './Node.ts'; +import { prettyPrint } from './prettyPrint.ts'; +import { simplePrint } from './simplePrint.ts'; + +/** + * A class describing a DocumentFragment. + */ +export class DocumentFragment { + /** The immediate children contained in the fragment. */ + childNodes: Node[] = []; + /** A numerical node type identifier. */ + nodeType: number = DOCUMENT_FRAGMENT_NODE; + + /** + * Appends a child node into the document fragment. + * + * @param node The new child node + * @returns The same node that was passed in. + */ + appendChild (node: T): T { + appendChild(this, node); + return node; + } + + /** @ignore */ + toString (): string { + return '#document-fragment'; + } + + /** + * Print the document as a string. + * + * @param pretty Apply automatic linebreaks and indentation to the output. + * @returns The document as an XML string. + */ + print (pretty = false): string { + return pretty + ? prettyPrint(this) + : simplePrint(this); + } +} diff --git a/lib/Element.ts b/lib/Element.ts index b788fa7..580033e 100644 --- a/lib/Element.ts +++ b/lib/Element.ts @@ -1,9 +1,13 @@ import { Node } from './Node.js'; -import { ELEMENT_NODE } from './constants.js'; +import { ELEMENT_NODE, XML_DECLARATION } from './constants.js'; import { JsonML, type JsonMLElement } from './JsonML.js'; import { domQuery } from './domQuery/index.js'; import { findAll } from './findAll.js'; import { isElement } from './isElement.ts'; +import { TextNode } from './TextNode.ts'; +import type { CreateChildArgument } from './CreateChildArgument.ts'; +import { prettyPrint } from './prettyPrint.ts'; +import { simplePrint } from './simplePrint.ts'; // eslint-disable-next-line @typescript-eslint/unbound-method const hasOwnProperty = Object.prototype.hasOwnProperty; @@ -31,7 +35,7 @@ export class Element extends Node { * Constructs a new Element instance. * * @param tagName The tag name of the node. - * @param [attr={}] A collection of attributes to assign. + * @param [attr={}] A collection of attributes to assign. Values of null or undefined will be ignored. * @param [closed=false] Was the element "self-closed" when read. */ constructor (tagName: string, attr: Record = {}, closed: boolean = false) { @@ -46,7 +50,12 @@ export class Element extends Node { this.fullName = tagName; this.closed = !!closed; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - this.attr = Object.assign(Object.create(null), attr); + this.attr = Object.create(null); + for (const [ k, v ] of Object.entries(attr)) { + if (v != null) { + this.setAttribute(k, v); + } + } // inherited instance props from Node this.nodeName = this.tagName.toUpperCase(); @@ -72,6 +81,18 @@ export class Element extends Node { return this.childNodes.filter(isElement); } + /** + * Returns an element's first child Element, or null if there are no child elements + */ + get firstElementChild (): Element | null { + for (const child of this.childNodes) { + if (isElement(child)) { + return child; + } + } + return null; + } + /** * Read an attribute from the element. * @@ -88,8 +109,8 @@ export class Element extends Node { * @param name The attribute name to read. * @param value The value to set */ - setAttribute (name: string, value: string) { - this.attr[name] = value; + setAttribute (name: string, value: string | number | boolean) { + this.attr[name] = String(value); } /** @@ -111,6 +132,46 @@ export class Element extends Node { delete this.attr[name]; } + get className (): string { + return this.getAttribute('class') ?? ''; + } + + set className (val: unknown) { + this.setAttribute('class', String(val)); + } + + /** + * Inserts a set of Node objects or strings after the last child of the Element. + * Strings are inserted as equivalent Text nodes. + */ + append (...nodes: (CreateChildArgument | CreateChildArgument[])[]): void { + const flatNodes = nodes.flat(); + for (const n of flatNodes) { + if (typeof n === 'string' || typeof n === 'number' || typeof n === 'boolean') { + this.appendChild(new TextNode(n)); + } + else if (n) { + this.appendChild(n); + } + } + } + + /** + * Insert a set of Node objects or strings before the first child of the Element. + * Strings are inserted as equivalent Text nodes. + */ + prepend (...nodes: (CreateChildArgument | CreateChildArgument[])[]): void { + const flatNodes = nodes.flat(); + for (const n of flatNodes) { + if (typeof n === 'string' || typeof n === 'number' || typeof n === 'boolean') { + this.insertBefore(new TextNode(n), this.firstChild); + } + else if (n) { + this.insertBefore(n, this.firstChild); + } + } + } + /** * Return all descendant elements that have the specified tag name. * @@ -159,4 +220,16 @@ export class Element extends Node { toJS (): JsonMLElement { return JsonML(this); } + + /** + * Print the document as a string. + * + * @param pretty Apply automatic linebreaks and indentation to the output. + * @returns The document as an XML string. + */ + print (pretty = false): string { + return `${XML_DECLARATION}\n` + ( + pretty ? prettyPrint(this) : simplePrint(this) + ); + } } diff --git a/lib/NSMap.ts b/lib/NSMap.ts new file mode 100644 index 0000000..08d52bc --- /dev/null +++ b/lib/NSMap.ts @@ -0,0 +1,27 @@ +export class NSMap { + uriToPre: Record = {}; + preToUri: Record = {}; + + get (nsURI: string): string | undefined { + return this.uriToPre[nsURI]; + } + + getByPrefix (nsPrefix: string): string | undefined { + return this.preToUri[nsPrefix]; + } + + list (): [string, string][] { + return Array.from(Object.entries(this.uriToPre)); + } + + add (nsURI: string, nsPrefix: string) { + if ((nsURI in this.uriToPre) && (this.uriToPre[nsURI] !== nsPrefix)) { + throw new Error(nsURI + ' allready has a different prefix'); + } + if ((nsPrefix in this.preToUri) && (this.preToUri[nsPrefix] !== nsPrefix)) { + throw new Error(nsPrefix + ' allready has a different URI'); + } + this.uriToPre[nsURI] = nsPrefix; + this.preToUri[nsPrefix] = nsURI; + } +} diff --git a/lib/Node.spec.ts b/lib/Node.spec.ts new file mode 100644 index 0000000..496f920 --- /dev/null +++ b/lib/Node.spec.ts @@ -0,0 +1,319 @@ +import { describe, it, expect } from 'vitest'; +import { Node } from './Node.ts'; +import { Element } from './Element.ts'; +import { TextNode } from './TextNode.ts'; +import { CDataNode } from './CDataNode.ts'; +import { DocumentFragment } from './DocumentFragment.ts'; + +describe('Node', () => { + describe('constructor / default properties', () => { + it('has expected default property values', () => { + const node = new Node(); + expect(node.childNodes).toEqual([]); + expect(node.nodeName).toBe('#node'); + expect(node.nodeType).toBe(0); + expect(node.parentNode).toBeNull(); + }); + }); + + describe('appendChild', () => { + it('appends a child node and sets parentNode', () => { + const parent = new Node(); + const child = new Node(); + parent.appendChild(child); + expect(parent.childNodes).toContain(child); + expect(child.parentNode).toBe(parent); + }); + + it('returns the appended node', () => { + const parent = new Node(); + const child = new Node(); + const result = parent.appendChild(child); + expect(result).toBe(child); + }); + + it('throws when called with no argument, null or undefined', () => { + const parent = new Node(); + // @ts-expect-error - testing runtime error + expect(() => parent.appendChild()).toThrow('1 argument required'); + // @ts-expect-error - testing runtime error + expect(() => parent.appendChild(null)).toThrow(); + // @ts-expect-error - testing runtime error + expect(() => parent.appendChild(undefined)).toThrow(); + }); + + it('throws when called with a non-node value', () => { + const parent = new Node(); + // @ts-expect-error - testing runtime error + expect(() => parent.appendChild('hello')).toThrow('Cannot appendChild'); + // @ts-expect-error - testing runtime error + expect(() => parent.appendChild(42)).toThrow('Cannot appendChild'); + // @ts-expect-error - testing runtime error + expect(() => parent.appendChild({})).toThrow('Cannot appendChild'); + }); + + it('moves a child from one parent to another', () => { + const parent1 = new Node(); + const parent2 = new Node(); + const child = new Node(); + parent1.appendChild(child); + expect(parent1.childNodes).toContain(child); + expect(child.parentNode).toBe(parent1); + parent2.appendChild(child); + expect(parent1.childNodes).not.toContain(child); + expect(parent2.childNodes).toContain(child); + expect(child.parentNode).toBe(parent2); + }); + + it('appends multiple children in order', () => { + const parent = new Node(); + const a = parent.appendChild(new Node()); + const b = parent.appendChild(new Node()); + const c = parent.appendChild(new Node()); + expect(parent.childNodes).toEqual([ a, b, c ]); + }); + + it('appends DocumentFragment children in order', () => { + const frag = new DocumentFragment(); + const a = frag.appendChild(new Node()); + const b = frag.appendChild(new Node()); + const c = frag.appendChild(new Node()); + expect(frag.childNodes).toEqual([ a, b, c ]); + + const parent = new Node(); + parent.appendChild(frag); + expect(parent.childNodes).toEqual([ a, b, c ]); + }); + + it('re-appending an existing child moves it to the end', () => { + const parent = new Node(); + const a = new Node(); + const b = new Node(); + parent.appendChild(a); + parent.appendChild(b); + expect(parent.childNodes).toEqual([ a, b ]); + + parent.appendChild(a); + expect(parent.childNodes).toEqual([ b, a ]); + }); + }); + + describe('insertBefore', () => { + it('inserts a node before a reference node', () => { + const parent = new Node(); + const a = new Node(); + const b = new Node(); + const c = new Node(); + parent.appendChild(a); + parent.appendChild(c); + + parent.insertBefore(b, c); + expect(b.parentNode).toBe(parent); + expect(parent.childNodes.filter(d => d === b).length).toBe(1); + }); + + it('inserts DocumentFragment children in order', () => { + const frag = new DocumentFragment(); + const a = frag.appendChild(new Node()); + const b = frag.appendChild(new Node()); + const c = frag.appendChild(new Node()); + expect(frag.childNodes).toEqual([ a, b, c ]); + + const parent = new Node(); + const z = new Node(); + parent.appendChild(z); // parent now only contains z + + parent.insertBefore(frag, z); + expect(parent.childNodes).toEqual([ a, b, c, z ]); + }); + + it('falls back to appendChild when referenceNode is null', () => { + const parent = new Node(); + const child = new Node(); + parent.insertBefore(child, null); + expect(parent.childNodes).toContain(child); + expect(child.parentNode).toBe(parent); + }); + + it('moves node from previous parent when inserting', () => { + const oldParent = new Node(); + const newParent = new Node(); + const ref = new Node(); + const child = new Node(); + + oldParent.appendChild(child); + newParent.appendChild(ref); + + newParent.insertBefore(child, ref); + expect(oldParent.childNodes).not.toContain(child); + expect(child.parentNode).toBe(newParent); + }); + }); + + describe('removeChild', () => { + it('removes a child and returns it', () => { + const parent = new Node(); + const child = new Node(); + parent.appendChild(child); + const removed = parent.removeChild(child); + expect(removed).toBe(child); + expect(parent.childNodes).not.toContain(child); + expect(child.parentNode).toBeNull(); + }); + + it('throws when no argument is provided', () => { + const parent = new Node(); + // @ts-expect-error - testing runtime error + expect(() => parent.removeChild()).toThrow('not of type'); + // @ts-expect-error - testing runtime error + expect(() => parent.removeChild(null)).toThrow('not of type'); + // @ts-expect-error - testing runtime error + expect(() => parent.removeChild(undefined)).toThrow('not of type'); + }); + + it('throws when the node is not a child', () => { + const parent = new Node(); + const orphan = new Node(); + expect(() => parent.removeChild(orphan)).toThrow( + 'The node to be removed is not a child of this node.' + ); + }); + + it('removes the correct child when there are multiple', () => { + const parent = new Node(); + const a = new Node(); + const b = new Node(); + const c = new Node(); + parent.appendChild(a); + parent.appendChild(b); + parent.appendChild(c); + + parent.removeChild(b); + expect(parent.childNodes).toEqual([ a, c ]); + expect(b.parentNode).toBeNull(); + }); + + it('allows removing and re-adding a child', () => { + const parent = new Node(); + const child = new Node(); + parent.appendChild(child); + parent.removeChild(child); + expect(parent.childNodes.length).toBe(0); + expect(child.parentNode).toBeNull(); + + parent.appendChild(child); + expect(parent.childNodes).toEqual([ child ]); + expect(child.parentNode).toBe(parent); + }); + }); + + describe('preserveSpace', () => { + it('returns false by default on a root node', () => { + const node = new Node(); + expect(node.preserveSpace).toBe(false); + }); + + it('inherits preserveSpace from parent', () => { + // Use an Element with xml:space="preserve" as parent + const parent = new Element('root', { 'xml:space': 'preserve' }); + const child = new Node(); + parent.appendChild(child); + expect(child.preserveSpace).toBe(true); + }); + + it('returns false when parent does not preserve space', () => { + const parent = new Node(); + const child = new Node(); + parent.appendChild(child); + expect(child.preserveSpace).toBe(false); + }); + + it('propagates through multiple levels of ancestry', () => { + const grandparent = new Element('root', { 'xml:space': 'preserve' }); + const parent = new Node(); + const child = new Node(); + grandparent.appendChild(parent); + parent.appendChild(child); + expect(child.preserveSpace).toBe(true); + }); + }); + + describe('textContent', () => { + it('returns empty string for a node with no children', () => { + const node = new Node(); + expect(node.textContent).toBe(''); + }); + + it('returns text from TextNode children', () => { + const parent = new Node(); + const text = new TextNode('hello world'); + parent.appendChild(text); + expect(parent.textContent).toBe('hello world'); + }); + + it('concatenates text from multiple TextNode children', () => { + // A whitespace-only TextNode returns '' when preserveSpace is false, + // so 'foo' + '' + 'bar' = 'foobar' + const parent = new Node(); + parent.appendChild(new TextNode('foo')); + parent.appendChild(new TextNode(' ')); + parent.appendChild(new TextNode('bar')); + expect(parent.textContent).toBe('foobar'); + }); + + it('concatenates text including spaces when preserveSpace is true', () => { + const parent = new Element('div', { 'xml:space': 'preserve' }); + parent.appendChild(new TextNode('foo')); + parent.appendChild(new TextNode(' ')); + parent.appendChild(new TextNode('bar')); + expect(parent.textContent).toBe('foo bar'); + }); + + it('collects text content from nested children recursively', () => { + const root = new Element('root'); + const child = new Element('child'); + const text = new TextNode('deep'); + root.appendChild(child); + child.appendChild(text); + expect(root.textContent).toBe('deep'); + }); + + it('includes CDataNode values', () => { + const parent = new Node(); + parent.appendChild(new TextNode('before')); + parent.appendChild(new CDataNode(' cdata ')); + parent.appendChild(new TextNode('after')); + expect(parent.textContent).toBe('before cdata after'); + }); + + it('returns empty string for whitespace-only TextNode children (no preserveSpace)', () => { + const parent = new Element('div'); + parent.appendChild(new TextNode(' ')); + // TextNode with only whitespace returns '' when preserveSpace is false + expect(parent.textContent).toBe(''); + }); + + it('preserves whitespace-only TextNode when preserveSpace is true', () => { + const parent = new Element('div', { 'xml:space': 'preserve' }); + parent.appendChild(new TextNode(' ')); + expect(parent.textContent).toBe(' '); + }); + }); + + describe('toString', () => { + it('returns a string', () => { + const node = new Node(); + expect(typeof node.toString()).toBe('string'); + }); + + it('renders child elements to XML', () => { + const root = new Element('root'); + const child = new Element('child'); + root.appendChild(child); + const str = root.toString(); + expect(str).toContain(''); + expect(str).toContain(''); + }); + }); +}); diff --git a/lib/Node.ts b/lib/Node.ts index 4d6438b..6062825 100644 --- a/lib/Node.ts +++ b/lib/Node.ts @@ -1,4 +1,5 @@ import { appendChild } from './appendChild.js'; +import { DocumentFragment } from './DocumentFragment.ts'; import { prettyPrint } from './prettyPrint.js'; /** @@ -14,23 +15,83 @@ export class Node { /** The node's parent node. */ parentNode: Node | null = null; + /** + * Returns the node's first child in the tree, or null if the node has no children. + */ + get firstChild (): Node | null { + return this.childNodes.at(0) ?? null; + } + + /** + * Returns the node's last child in the tree, or null if the node has no children. + */ + get lastChild (): Node | null { + return this.childNodes.at(-1) ?? null; + } + /** * Appends a child node into the current one. * * @param node The new child node * @returns The same node that was passed in. */ - appendChild (node: Node): Node { + appendChild (node: T): T { if (!node) { throw new Error('1 argument required, but 0 present.'); } - if (!(node instanceof Node)) { + if (!(node instanceof Node) && !(node instanceof DocumentFragment)) { throw new Error('Cannot appendChild: Child is not a node'); } appendChild(this, node); return node; } + /** + * Inserts a node before a _reference node_ as a child of a specified _parent node_. + * + * @param newNode The node to be inserted. + * @param referenceNode The node before which newNode is inserted. If this is null, then newNode is inserted at the end of node's child nodes. + * @returns The added child (unless newNode is a DocumentFragment, in which case the empty DocumentFragment is returned). + */ + insertBefore (newNode: T, referenceNode: Node | null): T { + if (referenceNode) { + const index = this.childNodes.indexOf(referenceNode); + if (index > -1) { + const insertNodes = newNode instanceof Node ? [ newNode ] : newNode.childNodes; + // update parentage for all the new nodes + for (const node of insertNodes) { + node.parentNode?.removeChild(node); + node.parentNode = this; + } + // insert the new nodes + this.childNodes.splice(index, 0, ...insertNodes); + } + return newNode; + } + return this.appendChild(newNode); + } + + /** + * Removes a child node from the DOM and returns the removed node. + * @param node The child node to be removed. + * @returns The removed child node. + */ + removeChild (child: Node) { + if (!child) { + throw new TypeError('parameter 1 is not of type \'Node\''); + } + const index = this.childNodes.indexOf(child); + if (index === -1) { + // DOMException + throw new Error('The node to be removed is not a child of this node.'); + } + const node = this.childNodes.splice(index, 1).at(0); + if (node) { + node.parentNode = null; + } + return node; + } + /** * True if xml:space has been set to true for this node or any of its ancestors. */ diff --git a/lib/TextNode.ts b/lib/TextNode.ts index 5cd7848..cc86cae 100644 --- a/lib/TextNode.ts +++ b/lib/TextNode.ts @@ -12,13 +12,14 @@ export class TextNode extends Node { /** * Constructs a new TextNode instance. - * @param {string} [value] The data for the node + * + * @param [value] The data for the node. */ - constructor (value: string) { + constructor (value: any) { super(); this.nodeName = '#text'; this.nodeType = TEXT_NODE; - this.value = value || ''; + this.value = String(value); } // overwrites super diff --git a/lib/XMLAttr.ts b/lib/XMLAttr.ts new file mode 100644 index 0000000..f69dbdf --- /dev/null +++ b/lib/XMLAttr.ts @@ -0,0 +1 @@ +export type XMLAttr = Record; diff --git a/lib/appendChild.ts b/lib/appendChild.ts index 78e8bb3..83f3c2a 100644 --- a/lib/appendChild.ts +++ b/lib/appendChild.ts @@ -1,11 +1,25 @@ -import { Node } from './Node.js'; +import { DocumentFragment } from './DocumentFragment.ts'; +import type { Node } from './Node.js'; -export function appendChild (parent: Node, child: Node) { - // if node is attached to a parent, first detach it - if (child.parentNode) { - const p = child.parentNode; - p.childNodes = p.childNodes.filter(d => d !== child); - } - child.parentNode = parent; - parent.childNodes.push(child); +export function appendChild (parent: Node | DocumentFragment, child: Node | DocumentFragment) { + if (child instanceof DocumentFragment) { + // perform an append operation for every child in the fragment + for (const d of child.childNodes) { + appendChild(parent, d); + } + } + else if (parent === child) { + // XXX: there should really be a more elaborate tests here to determine that child does not contain parent + throw new Error('The new child element contains the parent.'); + } + else if (parent instanceof DocumentFragment) { + // appending to a fragment does not mess with the node's parentage + parent.childNodes.push(child); + } + else { + // if node is attached to a parent, first detach it + child.parentNode?.removeChild(child); + child.parentNode = parent; + parent.childNodes.push(child); + } } diff --git a/lib/constants.ts b/lib/constants.ts index b90e410..2ba3228 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -33,3 +33,5 @@ export const DOCUMENT_FRAGMENT_NODE: number = 11; /** A documentation node identifier */ export const NOTATION_NODE: number = 12; + +export const XML_DECLARATION = ''; diff --git a/lib/escape.ts b/lib/escape.ts index cd6d597..5ea8b7b 100644 --- a/lib/escape.ts +++ b/lib/escape.ts @@ -9,9 +9,10 @@ const entities: Record = { }; /** - * @ignore + * Escape XML entities in a string. + * * @param {string} s Unescaped string - * @returns {string} XML escaped string + * @returns {string} Escaped string */ export function escape (s: string): string { // eslint-disable-next-line no-control-regex diff --git a/lib/index.ts b/lib/index.ts index 390e19e..f6280d4 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -4,6 +4,13 @@ export { Element } from './Element.js'; export { Document } from './Document.js'; export { TextNode } from './TextNode.js'; export { CDataNode } from './CDataNode.js'; +export { DocumentFragment } from './DocumentFragment.ts'; +export { escape as escapeXML } from './escape.ts'; +export { isElement } from './isElement.ts'; +export { prettyPrint } from './prettyPrint.ts'; +export { simplePrint } from './simplePrint.ts'; +export type { CreateChildArgument } from './CreateChildArgument.js'; +export type { XMLAttr } from './XMLAttr.js'; export type { JsonMLElement, JsonMLAttr } from './JsonML.js'; export { ELEMENT_NODE, diff --git a/lib/parser.ts b/lib/parser.ts index d21144f..575d0b1 100644 --- a/lib/parser.ts +++ b/lib/parser.ts @@ -238,6 +238,7 @@ function posToLine (pos: number, src: string): number { * @param [options={}] Parsing options. * @param [options.emptyDoc=false] Permit "rootless" documents. * @param [options.laxAttr=false] Permit unquoted attributes (``). + * @param [options.ns=false] Validate xmlns and element namespaces as they are parsed. * @returns A DOM representing the XML node tree. */ export function parseXML ( @@ -245,11 +246,13 @@ export function parseXML ( options: { emptyDoc?: boolean; laxAttr?: boolean; + ns?: boolean; } = DEFAULTOPTIONS ): Document { // 2.11: before parsing, translate both the two-character sequence // \r\n and any \r that is not followed by \n to a single \n const xml = removeCR(source); + const doc = new Document(); let pos = 0; let root = NON_ELEMENT; @@ -290,6 +293,20 @@ export function parseXML ( while (m); } + function checkNS (elm: Element) { + for (const key in elm.attr) { + if (key === 'xmlns') { + doc.attachNS(elm.attr[key], ''); + } + if (key.startsWith('xmlns:')) { + doc.attachNS(elm.attr[key], key.slice(6)); + } + } + if (elm.ns && !doc.namespaces.getByPrefix(elm.ns)) { + throw new Error('Unknown namespace prefix ' + elm.ns); + } + } + // BOM if (xml.charCodeAt(pos) === 65279) { pos++; @@ -316,6 +333,7 @@ export function parseXML ( // root tag maybeMatchFn(fnTag, (_, t, a, c) => { root = new Element(t, parseAttr(a, options.laxAttr), !!c); + if (options.ns) checkNS(root); return true; }); @@ -351,7 +369,9 @@ export function parseXML ( }) || maybeMatchFn(fnTag, (_, t, a, c) => { - const elm = new Element(t, parseAttr(a, options.laxAttr), !!c); + const attr = parseAttr(a, options.laxAttr); + const elm = new Element(t, attr, !!c); + if (options.ns) checkNS(elm); current?.appendChild(elm); if (!elm.closed) { current = elm; @@ -384,7 +404,6 @@ export function parseXML ( throw new Error(`Expected got EOF`); } - const doc = new Document(); if (root !== NON_ELEMENT) { doc.appendChild(root); } diff --git a/lib/prettyPrint.ts b/lib/prettyPrint.ts index 2f8e036..75a96bb 100644 --- a/lib/prettyPrint.ts +++ b/lib/prettyPrint.ts @@ -1,5 +1,6 @@ import type { CDataNode } from './CDataNode.ts'; import type { Document } from './Document.ts'; +import { DocumentFragment } from './DocumentFragment.ts'; import type { Node } from './Node.js'; import type { TextNode } from './TextNode.ts'; import { CDATA_SECTION_NODE, DOCUMENT_NODE, ELEMENT_NODE, TEXT_NODE } from './constants.js'; @@ -25,16 +26,19 @@ function printCData (node: CDataNode) { return `/g, ']]>')}]]>`; } -function printDocument (node: Node): string { +function printDocument (node: Node | DocumentFragment): string { return node.childNodes .map(n => prettyPrint(n)) .join('\n'); } -export function prettyPrint (node: Node, indent: string = ''): string { +export function prettyPrint (node: Node | DocumentFragment, indent: string = ''): string { + if (node instanceof DocumentFragment) { + return printDocument(node); + } const { preserveSpace } = node; if (node.nodeType === DOCUMENT_NODE) { - return printDocument(node as Document); + return printDocument(node); } else if (node.nodeType === CDATA_SECTION_NODE) { return printCData(node as CDataNode); @@ -43,7 +47,7 @@ export function prettyPrint (node: Node, indent: string = ''): string { return printTextNode(node as TextNode); } else if (isElement(node)) { - const tagName = node.tagName; + const tagName = node.fullName; const { childNodes } = node; let children = ''; diff --git a/lib/simplePrint.ts b/lib/simplePrint.ts new file mode 100644 index 0000000..9229214 --- /dev/null +++ b/lib/simplePrint.ts @@ -0,0 +1,44 @@ +import type { CDataNode } from './CDataNode.ts'; +import type { Document } from './Document.ts'; +import { DocumentFragment } from './DocumentFragment.ts'; +import type { Node } from './Node.js'; +import type { TextNode } from './TextNode.ts'; +import { CDATA_SECTION_NODE, DOCUMENT_NODE, TEXT_NODE } from './constants.js'; +import { escape } from './escape.js'; +import { isElement } from './isElement.ts'; + +export function simplePrint (node: Node | DocumentFragment): string { + if (node instanceof DocumentFragment) { + return node.childNodes.map(n => simplePrint(n)).join(''); + } + if (node.nodeType === DOCUMENT_NODE) { + const root = (node as Document).root; + if (!root) throw new Error('root element is missing'); + return simplePrint(root); + } + else if (node.nodeType === CDATA_SECTION_NODE) { + return `/g, ']]>')}]]>`; + } + else if (node.nodeType === TEXT_NODE) { + return escape((node as TextNode).value); + } + else if (isElement(node)) { + const tagName = node.fullName; + const { childNodes } = node; + let children = ''; + for (const n of childNodes) { + children += simplePrint(n); + } + let attrList = ''; + if (isElement(node)) { + const attr = node.attr; + for (const [ key, val ] of Object.entries(attr)) { + attrList += ` ${escape(key)}="${escape(val)}"`; + } + } + return children + ? `<${tagName}${attrList}>${children}` + : `<${tagName}${attrList} />`; + } + return ''; +} diff --git a/test/domQuery-dedup.spec.ts b/test/domQuery-dedup.spec.ts index ff5caec..d2b1503 100644 --- a/test/domQuery-dedup.spec.ts +++ b/test/domQuery-dedup.spec.ts @@ -5,7 +5,7 @@ describe('domQuery dedup fix', () => { it('preserves document order for single-group selectors', () => { const doc = parseXML(''); const result = doc.root!.querySelectorAll('*'); - expect(result.map(e => e.tagName)).toEqual(['x', 'y', 'z']); + expect(result.map(e => e.tagName)).toEqual([ 'x', 'y', 'z' ]); }); it('preserves document order for multi-group (comma) selectors', () => { @@ -13,7 +13,7 @@ describe('domQuery dedup fix', () => { // Even if groups are listed out of document order, results should // be in document order (via the getElementsByTagName tree walk). const result = doc.root!.querySelectorAll('c, a'); - expect(result.map(e => e.tagName)).toEqual(['a', 'c']); + expect(result.map(e => e.tagName)).toEqual([ 'a', 'c' ]); }); it('deduplicates descendant combinator results', () => { @@ -21,7 +21,7 @@ describe('domQuery dedup fix', () => { const doc = parseXML(''); // "a c" matches through both 's; should find exactly one const result = doc.root!.querySelectorAll('a c'); - expect(result.map(e => e.tagName)).toEqual(['c']); + expect(result.map(e => e.tagName)).toEqual([ 'c' ]); }); it('deduplicates multi-group (comma) selector results', () => { diff --git a/test/namespaces.spec.ts b/test/namespaces.spec.ts new file mode 100644 index 0000000..edf0370 --- /dev/null +++ b/test/namespaces.spec.ts @@ -0,0 +1,44 @@ +import { describe, it, expect } from 'vitest'; +import { parseXML } from '../lib/index.ts'; + +const ooxml = ` + + + + + + + + + + + + + + + + +`; + +describe('namespace validation', () => { + it('simple tag', () => { + const doc = parseXML(ooxml, { ns: true }); + expect(doc.namespaces.list()).toStrictEqual([ + [ 'http://schemas.openxmlformats.org/spreadsheetml/2006/main', '' ], + [ 'http://schemas.openxmlformats.org/markup-compatibility/2006', 'mc' ], + [ 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac', 'x14ac' ], + [ 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/main', 'x14' ], + [ 'http://schemas.microsoft.com/office/spreadsheetml/2010/11/main', 'x15' ] + ]); + }); +}); + +// test: +// - missing decl +// - decl collisions (reused prefix) +// - decl collisions (reused uri) diff --git a/test/parse-data.spec.ts b/test/parse-data.spec.ts index d21256f..a132d80 100644 --- a/test/parse-data.spec.ts +++ b/test/parse-data.spec.ts @@ -1,4 +1,3 @@ -/* eslint-disable quotes, quote-props, indent */ import { describe, it, expect } from 'vitest'; import fs from 'fs'; import { parseXML } from '../lib/index.js'; @@ -23,20 +22,20 @@ describe('parse-data', () => { it('whitespace.xml', () => { const fileName = 'test/data/whitespace.xml'; const expected = [ - "node", - [ "a", - { "xml:space": "preserve" }, - " ", - [ "b", " ", [ "c", " c " ], " b " ], - " " ], - [ "a", - { "xml:space": "default" }, - [ "b", [ "c", " c " ], " b " ] ] + 'node', + [ 'a', + { 'xml:space': 'preserve' }, + ' ', + [ 'b', ' ', [ 'c', ' c ' ], ' b ' ], + ' ' ], + [ 'a', + { 'xml:space': 'default' }, + [ 'b', [ 'c', ' c ' ], ' b ' ] ] ]; const src = fs.readFileSync(fileName, 'utf8'); const dom = parseXML(src); expect(dom.toJS()).toEqual(expected); - expect(dom.root.textContent).toBe(" c b c b "); + expect(dom.root.textContent).toBe(' c b c b '); }); it('refs.xml', () => { @@ -88,24 +87,24 @@ describe('parse-data', () => { it('truncation.xml', () => { const fileName = 'test/data/truncation.xml'; const expected = [ - "mesh", - { "name": "mesh_root" }, - "\n\tsome text\n\tsomeothertext\n\tsome more text\n\t", - [ "node", - { "attr1": "value1", - "attr2": "value2" } ], - [ "node", - { "attr1": "value2" }, - [ "汉语", - { "名字": "name", - "价值": "value" }, - "世界有很多语言𤭢" ], - [ "innernode" ] ], - [ "氏名", - [ "氏", - "山田" ], - [ "名", - "太郎" ] ] + 'mesh', + { name: 'mesh_root' }, + '\n\tsome text\n\tsomeothertext\n\tsome more text\n\t', + [ 'node', + { attr1: 'value1', + attr2: 'value2' } ], + [ 'node', + { attr1: 'value2' }, + [ '汉语', + { 名字: 'name', + 价值: 'value' }, + '世界有很多语言𤭢' ], + [ 'innernode' ] ], + [ '氏名', + [ '氏', + '山田' ], + [ '名', + '太郎' ] ] ]; const src = fs.readFileSync(fileName, 'utf8'); const dom = parseXML(src); @@ -127,112 +126,112 @@ describe('parse-data', () => { it('utftest_utf8.xml', () => { const fileName = 'test/data/utftest_utf8.xml'; - const expected = [ "週報", - [ "English", - { "name": "name", - "value": "value" }, - "The world has many languages" ], - [ "Russian", - { "name": "название(имя)", - "value": "ценность" }, - "Мир имеет много языков" ], - [ "Spanish", - { "name": "el nombre", - "value": "el valor" }, - "el mundo tiene muchos idiomas" ], - [ "SimplifiedChinese", - { "name": "名字", - "value": "价值" }, - "世界有很多语言" ], - [ "Русский", - { "название": "name", - "ценность": "value" }, - "<имеет>" ], - [ "汉语", - { "名字": "name", - "价值": "value" }, - "世界有很多语言𤭢" ], - [ "Heavy", - "\"Mëtæl!\"" ], - [ "ä", - "Umlaut Element" ], - [ "年月週", - [ "年度", - "1997" ], - [ "月度", - "1" ], - [ "週", - "1" ] ], - [ "氏名", - [ "氏", - "山田" ], - [ "名", - "太郎" ] ], - [ "業務報告リスト", - [ "業務報告", - [ "業務名", - "XMLエディターの作成" ], - [ "業務コード", - "X3355-23" ], - [ "工数管理", - [ "見積もり工数", - "1600" ], - [ "実績工数", - "320" ], - [ "当月見積もり工数", - "160" ], - [ "当月実績工数", - "24" ] ], - [ "予定項目リスト", - [ "予定項目", - [ "P", - "XMLエディターの基本仕様の作成" ] ] ], - [ "実施事項リスト", - [ "実施事項", - [ "P", - "XMLエディターの基本仕様の作成" ] ], - [ "実施事項", - [ "P", - "競合他社製品の機能調査" ] ] ], - [ "上長への要請事項リスト", - [ "上長への要請事項", - [ "P", - "特になし" ] ] ], - [ "問題点対策", - [ "P", - "XMLとは何かわからない。" ] ] ], - [ "業務報告", - [ "業務名", - "検索エンジンの開発" ], - [ "業務コード", - "S8821-76" ], - [ "工数管理", - [ "見積もり工数", - "120" ], - [ "実績工数", - "6" ], - [ "当月見積もり工数", - "32" ], - [ "当月実績工数", - "2" ] ], - [ "予定項目リスト", - [ "予定項目", - [ "P", - [ "A", - { "href": "http://www.goo.ne.jp" }, - "goo" ], - "の機能を調べてみる" ] ] ], - [ "実施事項リスト", - [ "実施事項", - [ "P", - "更に、どういう検索エンジンがあるか調査する" ] ] ], - [ "上長への要請事項リスト", - [ "上長への要請事項", - [ "P", - "開発をするのはめんどうなので、Yahoo!を買収して下さい。" ] ] ], - [ "問題点対策", - [ "P", - "検索エンジンで車を走らせることができない。(要調査)" ] ] ] ] ]; + const expected = [ '週報', + [ 'English', + { name: 'name', + value: 'value' }, + 'The world has many languages' ], + [ 'Russian', + { name: 'название(имя)', + value: 'ценность' }, + 'Мир имеет много языков' ], + [ 'Spanish', + { name: 'el nombre', + value: 'el valor' }, + 'el mundo tiene muchos idiomas' ], + [ 'SimplifiedChinese', + { name: '名字', + value: '价值' }, + '世界有很多语言' ], + [ 'Русский', + { название: 'name', + ценность: 'value' }, + '<имеет>' ], + [ '汉语', + { 名字: 'name', + 价值: 'value' }, + '世界有很多语言𤭢' ], + [ 'Heavy', + '"Mëtæl!"' ], + [ 'ä', + 'Umlaut Element' ], + [ '年月週', + [ '年度', + '1997' ], + [ '月度', + '1' ], + [ '週', + '1' ] ], + [ '氏名', + [ '氏', + '山田' ], + [ '名', + '太郎' ] ], + [ '業務報告リスト', + [ '業務報告', + [ '業務名', + 'XMLエディターの作成' ], + [ '業務コード', + 'X3355-23' ], + [ '工数管理', + [ '見積もり工数', + '1600' ], + [ '実績工数', + '320' ], + [ '当月見積もり工数', + '160' ], + [ '当月実績工数', + '24' ] ], + [ '予定項目リスト', + [ '予定項目', + [ 'P', + 'XMLエディターの基本仕様の作成' ] ] ], + [ '実施事項リスト', + [ '実施事項', + [ 'P', + 'XMLエディターの基本仕様の作成' ] ], + [ '実施事項', + [ 'P', + '競合他社製品の機能調査' ] ] ], + [ '上長への要請事項リスト', + [ '上長への要請事項', + [ 'P', + '特になし' ] ] ], + [ '問題点対策', + [ 'P', + 'XMLとは何かわからない。' ] ] ], + [ '業務報告', + [ '業務名', + '検索エンジンの開発' ], + [ '業務コード', + 'S8821-76' ], + [ '工数管理', + [ '見積もり工数', + '120' ], + [ '実績工数', + '6' ], + [ '当月見積もり工数', + '32' ], + [ '当月実績工数', + '2' ] ], + [ '予定項目リスト', + [ '予定項目', + [ 'P', + [ 'A', + { href: 'http://www.goo.ne.jp' }, + 'goo' ], + 'の機能を調べてみる' ] ] ], + [ '実施事項リスト', + [ '実施事項', + [ 'P', + '更に、どういう検索エンジンがあるか調査する' ] ] ], + [ '上長への要請事項リスト', + [ '上長への要請事項', + [ 'P', + '開発をするのはめんどうなので、Yahoo!を買収して下さい。' ] ] ], + [ '問題点対策', + [ 'P', + '検索エンジンで車を走らせることができない。(要調査)' ] ] ] ] ]; const src = fs.readFileSync(fileName, 'utf8'); const dom = parseXML(src); @@ -241,112 +240,112 @@ describe('parse-data', () => { it('utftest_utf8_bom.xml', () => { const fileName = 'test/data/utftest_utf8_bom.xml'; - const expected = [ "週報", - [ "English", - { "name": "name", - "value": "value" }, - "The world has many languages" ], - [ "Russian", - { "name": "название(имя)", - "value": "ценность" }, - "Мир имеет много языков" ], - [ "Spanish", - { "name": "el nombre", - "value": "el valor" }, - "el mundo tiene muchos idiomas" ], - [ "SimplifiedChinese", - { "name": "名字", - "value": "价值" }, - "世界有很多语言" ], - [ "Русский", - { "название": "name", - "ценность": "value" }, - "<имеет>" ], - [ "汉语", - { "名字": "name", - "价值": "value" }, - "世界有很多语言𤭢" ], - [ "Heavy", - "\"Mëtæl!\"" ], - [ "ä", - "Umlaut Element" ], - [ "年月週", - [ "年度", - "1997" ], - [ "月度", - "1" ], - [ "週", - "1" ] ], - [ "氏名", - [ "氏", - "山田" ], - [ "名", - "太郎" ] ], - [ "業務報告リスト", - [ "業務報告", - [ "業務名", - "XMLエディターの作成" ], - [ "業務コード", - "X3355-23" ], - [ "工数管理", - [ "見積もり工数", - "1600" ], - [ "実績工数", - "320" ], - [ "当月見積もり工数", - "160" ], - [ "当月実績工数", - "24" ] ], - [ "予定項目リスト", - [ "予定項目", - [ "P", - "XMLエディターの基本仕様の作成" ] ] ], - [ "実施事項リスト", - [ "実施事項", - [ "P", - "XMLエディターの基本仕様の作成" ] ], - [ "実施事項", - [ "P", - "競合他社製品の機能調査" ] ] ], - [ "上長への要請事項リスト", - [ "上長への要請事項", - [ "P", - "特になし" ] ] ], - [ "問題点対策", - [ "P", - "XMLとは何かわからない。" ] ] ], - [ "業務報告", - [ "業務名", - "検索エンジンの開発" ], - [ "業務コード", - "S8821-76" ], - [ "工数管理", - [ "見積もり工数", - "120" ], - [ "実績工数", - "6" ], - [ "当月見積もり工数", - "32" ], - [ "当月実績工数", - "2" ] ], - [ "予定項目リスト", - [ "予定項目", - [ "P", - [ "A", - { "href": "http://www.goo.ne.jp" }, - "goo" ], - "の機能を調べてみる" ] ] ], - [ "実施事項リスト", - [ "実施事項", - [ "P", - "更に、どういう検索エンジンがあるか調査する" ] ] ], - [ "上長への要請事項リスト", - [ "上長への要請事項", - [ "P", - "開発をするのはめんどうなので、Yahoo!を買収して下さい。" ] ] ], - [ "問題点対策", - [ "P", - "検索エンジンで車を走らせることができない。(要調査)" ] ] ] ] ]; + const expected = [ '週報', + [ 'English', + { name: 'name', + value: 'value' }, + 'The world has many languages' ], + [ 'Russian', + { name: 'название(имя)', + value: 'ценность' }, + 'Мир имеет много языков' ], + [ 'Spanish', + { name: 'el nombre', + value: 'el valor' }, + 'el mundo tiene muchos idiomas' ], + [ 'SimplifiedChinese', + { name: '名字', + value: '价值' }, + '世界有很多语言' ], + [ 'Русский', + { название: 'name', + ценность: 'value' }, + '<имеет>' ], + [ '汉语', + { 名字: 'name', + 价值: 'value' }, + '世界有很多语言𤭢' ], + [ 'Heavy', + '"Mëtæl!"' ], + [ 'ä', + 'Umlaut Element' ], + [ '年月週', + [ '年度', + '1997' ], + [ '月度', + '1' ], + [ '週', + '1' ] ], + [ '氏名', + [ '氏', + '山田' ], + [ '名', + '太郎' ] ], + [ '業務報告リスト', + [ '業務報告', + [ '業務名', + 'XMLエディターの作成' ], + [ '業務コード', + 'X3355-23' ], + [ '工数管理', + [ '見積もり工数', + '1600' ], + [ '実績工数', + '320' ], + [ '当月見積もり工数', + '160' ], + [ '当月実績工数', + '24' ] ], + [ '予定項目リスト', + [ '予定項目', + [ 'P', + 'XMLエディターの基本仕様の作成' ] ] ], + [ '実施事項リスト', + [ '実施事項', + [ 'P', + 'XMLエディターの基本仕様の作成' ] ], + [ '実施事項', + [ 'P', + '競合他社製品の機能調査' ] ] ], + [ '上長への要請事項リスト', + [ '上長への要請事項', + [ 'P', + '特になし' ] ] ], + [ '問題点対策', + [ 'P', + 'XMLとは何かわからない。' ] ] ], + [ '業務報告', + [ '業務名', + '検索エンジンの開発' ], + [ '業務コード', + 'S8821-76' ], + [ '工数管理', + [ '見積もり工数', + '120' ], + [ '実績工数', + '6' ], + [ '当月見積もり工数', + '32' ], + [ '当月実績工数', + '2' ] ], + [ '予定項目リスト', + [ '予定項目', + [ 'P', + [ 'A', + { href: 'http://www.goo.ne.jp' }, + 'goo' ], + 'の機能を調べてみる' ] ] ], + [ '実施事項リスト', + [ '実施事項', + [ 'P', + '更に、どういう検索エンジンがあるか調査する' ] ] ], + [ '上長への要請事項リスト', + [ '上長への要請事項', + [ 'P', + '開発をするのはめんどうなので、Yahoo!を買収して下さい。' ] ] ], + [ '問題点対策', + [ 'P', + '検索エンジンで車を走らせることができない。(要調査)' ] ] ] ] ]; const src = fs.readFileSync(fileName, 'utf8'); const dom = parseXML(src); expect(dom.toJS()).toEqual(expected); @@ -354,112 +353,112 @@ describe('parse-data', () => { it('utftest_utf8_clean.xml', () => { const fileName = 'test/data/utftest_utf8_clean.xml'; - const expected = [ "週報", - [ "English", - { "name": "name", - "value": "value" }, - "The world has many languages" ], - [ "Russian", - { "name": "название(имя)", - "value": "ценность" }, - "Мир имеет много языков" ], - [ "Spanish", - { "name": "el nombre", - "value": "el valor" }, - "el mundo tiene muchos idiomas" ], - [ "SimplifiedChinese", - { "name": "名字", - "value": "价值" }, - "世界有很多语言" ], - [ "Русский", - { "название": "name", - "ценность": "value" }, - "<имеет>" ], - [ "汉语", - { "名字": "name", - "价值": "value" }, - "世界有很多语言𤭢" ], - [ "Heavy", - "quot;Mëtæl!quot;" ], - [ "ä", - "Umlaut Element" ], - [ "年月週", - [ "年度", - "1997" ], - [ "月度", - "1" ], - [ "週", - "1" ] ], - [ "氏名", - [ "氏", - "山田" ], - [ "名", - "太郎" ] ], - [ "業務報告リスト", - [ "業務報告", - [ "業務名", - "XMLエディターの作成" ], - [ "業務コード", - "X3355-23" ], - [ "工数管理", - [ "見積もり工数", - "1600" ], - [ "実績工数", - "320" ], - [ "当月見積もり工数", - "160" ], - [ "当月実績工数", - "24" ] ], - [ "予定項目リスト", - [ "予定項目", - [ "P", - "XMLエディターの基本仕様の作成" ] ] ], - [ "実施事項リスト", - [ "実施事項", - [ "P", - "XMLエディターの基本仕様の作成" ] ], - [ "実施事項", - [ "P", - "競合他社製品の機能調査" ] ] ], - [ "上長への要請事項リスト", - [ "上長への要請事項", - [ "P", - "特になし" ] ] ], - [ "問題点対策", - [ "P", - "XMLとは何かわからない。" ] ] ], - [ "業務報告", - [ "業務名", - "検索エンジンの開発" ], - [ "業務コード", - "S8821-76" ], - [ "工数管理", - [ "見積もり工数", - "120" ], - [ "実績工数", - "6" ], - [ "当月見積もり工数", - "32" ], - [ "当月実績工数", - "2" ] ], - [ "予定項目リスト", - [ "予定項目", - [ "P", - [ "A", - { "href": "http://www.goo.ne.jp" }, - "goo" ], - "の機能を調べてみる" ] ] ], - [ "実施事項リスト", - [ "実施事項", - [ "P", - "更に、どういう検索エンジンがあるか調査する" ] ] ], - [ "上長への要請事項リスト", - [ "上長への要請事項", - [ "P", - "開発をするのはめんどうなので、Yahoo!を買収して下さい。" ] ] ], - [ "問題点対策", - [ "P", - "検索エンジンで車を走らせることができない。(要調査)" ] ] ] ] ]; + const expected = [ '週報', + [ 'English', + { name: 'name', + value: 'value' }, + 'The world has many languages' ], + [ 'Russian', + { name: 'название(имя)', + value: 'ценность' }, + 'Мир имеет много языков' ], + [ 'Spanish', + { name: 'el nombre', + value: 'el valor' }, + 'el mundo tiene muchos idiomas' ], + [ 'SimplifiedChinese', + { name: '名字', + value: '价值' }, + '世界有很多语言' ], + [ 'Русский', + { название: 'name', + ценность: 'value' }, + '<имеет>' ], + [ '汉语', + { 名字: 'name', + 价值: 'value' }, + '世界有很多语言𤭢' ], + [ 'Heavy', + 'quot;Mëtæl!quot;' ], + [ 'ä', + 'Umlaut Element' ], + [ '年月週', + [ '年度', + '1997' ], + [ '月度', + '1' ], + [ '週', + '1' ] ], + [ '氏名', + [ '氏', + '山田' ], + [ '名', + '太郎' ] ], + [ '業務報告リスト', + [ '業務報告', + [ '業務名', + 'XMLエディターの作成' ], + [ '業務コード', + 'X3355-23' ], + [ '工数管理', + [ '見積もり工数', + '1600' ], + [ '実績工数', + '320' ], + [ '当月見積もり工数', + '160' ], + [ '当月実績工数', + '24' ] ], + [ '予定項目リスト', + [ '予定項目', + [ 'P', + 'XMLエディターの基本仕様の作成' ] ] ], + [ '実施事項リスト', + [ '実施事項', + [ 'P', + 'XMLエディターの基本仕様の作成' ] ], + [ '実施事項', + [ 'P', + '競合他社製品の機能調査' ] ] ], + [ '上長への要請事項リスト', + [ '上長への要請事項', + [ 'P', + '特になし' ] ] ], + [ '問題点対策', + [ 'P', + 'XMLとは何かわからない。' ] ] ], + [ '業務報告', + [ '業務名', + '検索エンジンの開発' ], + [ '業務コード', + 'S8821-76' ], + [ '工数管理', + [ '見積もり工数', + '120' ], + [ '実績工数', + '6' ], + [ '当月見積もり工数', + '32' ], + [ '当月実績工数', + '2' ] ], + [ '予定項目リスト', + [ '予定項目', + [ 'P', + [ 'A', + { href: 'http://www.goo.ne.jp' }, + 'goo' ], + 'の機能を調べてみる' ] ] ], + [ '実施事項リスト', + [ '実施事項', + [ 'P', + '更に、どういう検索エンジンがあるか調査する' ] ] ], + [ '上長への要請事項リスト', + [ '上長への要請事項', + [ 'P', + '開発をするのはめんどうなので、Yahoo!を買収して下さい。' ] ] ], + [ '問題点対策', + [ 'P', + '検索エンジンで車を走らせることができない。(要調査)' ] ] ] ] ]; const src = fs.readFileSync(fileName, 'utf8'); const dom = parseXML(src); expect(dom.toJS()).toEqual(expected); @@ -467,112 +466,112 @@ describe('parse-data', () => { it('utftest_utf8_nodecl.xml', () => { const fileName = 'test/data/utftest_utf8_nodecl.xml'; - const expected = [ "週報", - [ "English", - { "name": "name", - "value": "value" }, - "The world has many languages" ], - [ "Russian", - { "name": "название(имя)", - "value": "ценность" }, - "Мир имеет много языков" ], - [ "Spanish", - { "name": "el nombre", - "value": "el valor" }, - "el mundo tiene muchos idiomas" ], - [ "SimplifiedChinese", - { "name": "名字", - "value": "价值" }, - "世界有很多语言" ], - [ "Русский", - { "название": "name", - "ценность": "value" }, - "<имеет>" ], - [ "汉语", - { "名字": "name", - "价值": "value" }, - "世界有很多语言𤭢" ], - [ "Heavy", - "\"Mëtæl!\"" ], - [ "ä", - "Umlaut Element" ], - [ "年月週", - [ "年度", - "1997" ], - [ "月度", - "1" ], - [ "週", - "1" ] ], - [ "氏名", - [ "氏", - "山田" ], - [ "名", - "太郎" ] ], - [ "業務報告リスト", - [ "業務報告", - [ "業務名", - "XMLエディターの作成" ], - [ "業務コード", - "X3355-23" ], - [ "工数管理", - [ "見積もり工数", - "1600" ], - [ "実績工数", - "320" ], - [ "当月見積もり工数", - "160" ], - [ "当月実績工数", - "24" ] ], - [ "予定項目リスト", - [ "予定項目", - [ "P", - "XMLエディターの基本仕様の作成" ] ] ], - [ "実施事項リスト", - [ "実施事項", - [ "P", - "XMLエディターの基本仕様の作成" ] ], - [ "実施事項", - [ "P", - "競合他社製品の機能調査" ] ] ], - [ "上長への要請事項リスト", - [ "上長への要請事項", - [ "P", - "特になし" ] ] ], - [ "問題点対策", - [ "P", - "XMLとは何かわからない。" ] ] ], - [ "業務報告", - [ "業務名", - "検索エンジンの開発" ], - [ "業務コード", - "S8821-76" ], - [ "工数管理", - [ "見積もり工数", - "120" ], - [ "実績工数", - "6" ], - [ "当月見積もり工数", - "32" ], - [ "当月実績工数", - "2" ] ], - [ "予定項目リスト", - [ "予定項目", - [ "P", - [ "A", - { "href": "http://www.goo.ne.jp" }, - "goo" ], - "の機能を調べてみる" ] ] ], - [ "実施事項リスト", - [ "実施事項", - [ "P", - "更に、どういう検索エンジンがあるか調査する" ] ] ], - [ "上長への要請事項リスト", - [ "上長への要請事項", - [ "P", - "開発をするのはめんどうなので、Yahoo!を買収して下さい。" ] ] ], - [ "問題点対策", - [ "P", - "検索エンジンで車を走らせることができない。(要調査)" ] ] ] ] ]; + const expected = [ '週報', + [ 'English', + { name: 'name', + value: 'value' }, + 'The world has many languages' ], + [ 'Russian', + { name: 'название(имя)', + value: 'ценность' }, + 'Мир имеет много языков' ], + [ 'Spanish', + { name: 'el nombre', + value: 'el valor' }, + 'el mundo tiene muchos idiomas' ], + [ 'SimplifiedChinese', + { name: '名字', + value: '价值' }, + '世界有很多语言' ], + [ 'Русский', + { название: 'name', + ценность: 'value' }, + '<имеет>' ], + [ '汉语', + { 名字: 'name', + 价值: 'value' }, + '世界有很多语言𤭢' ], + [ 'Heavy', + '"Mëtæl!"' ], + [ 'ä', + 'Umlaut Element' ], + [ '年月週', + [ '年度', + '1997' ], + [ '月度', + '1' ], + [ '週', + '1' ] ], + [ '氏名', + [ '氏', + '山田' ], + [ '名', + '太郎' ] ], + [ '業務報告リスト', + [ '業務報告', + [ '業務名', + 'XMLエディターの作成' ], + [ '業務コード', + 'X3355-23' ], + [ '工数管理', + [ '見積もり工数', + '1600' ], + [ '実績工数', + '320' ], + [ '当月見積もり工数', + '160' ], + [ '当月実績工数', + '24' ] ], + [ '予定項目リスト', + [ '予定項目', + [ 'P', + 'XMLエディターの基本仕様の作成' ] ] ], + [ '実施事項リスト', + [ '実施事項', + [ 'P', + 'XMLエディターの基本仕様の作成' ] ], + [ '実施事項', + [ 'P', + '競合他社製品の機能調査' ] ] ], + [ '上長への要請事項リスト', + [ '上長への要請事項', + [ 'P', + '特になし' ] ] ], + [ '問題点対策', + [ 'P', + 'XMLとは何かわからない。' ] ] ], + [ '業務報告', + [ '業務名', + '検索エンジンの開発' ], + [ '業務コード', + 'S8821-76' ], + [ '工数管理', + [ '見積もり工数', + '120' ], + [ '実績工数', + '6' ], + [ '当月見積もり工数', + '32' ], + [ '当月実績工数', + '2' ] ], + [ '予定項目リスト', + [ '予定項目', + [ 'P', + [ 'A', + { href: 'http://www.goo.ne.jp' }, + 'goo' ], + 'の機能を調べてみる' ] ] ], + [ '実施事項リスト', + [ '実施事項', + [ 'P', + '更に、どういう検索エンジンがあるか調査する' ] ] ], + [ '上長への要請事項リスト', + [ '上長への要請事項', + [ 'P', + '開発をするのはめんどうなので、Yahoo!を買収して下さい。' ] ] ], + [ '問題点対策', + [ 'P', + '検索エンジンで車を走らせることができない。(要調査)' ] ] ] ] ]; const src = fs.readFileSync(fileName, 'utf8'); const dom = parseXML(src); expect(dom.toJS()).toEqual(expected); diff --git a/tsconfig.json b/tsconfig.json index 05367b8..025a118 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,23 +1,28 @@ { - "include": ["lib/", "eslint.config.js"], - "compilerOptions": { - "rootDir": ".", - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "allowSyntheticDefaultImports": true, - "allowImportingTsExtensions": true, - "forceConsistentCasingInFileNames": true, - "verbatimModuleSyntax": true, - "erasableSyntaxOnly": true, - "strict": true, - "allowJs": true, - "checkJs": true, - "stripInternal": true, - "noEmitOnError": true, - "noErrorTruncation": true, - "outDir": "types", - "declarationMap": false, - "skipLibCheck": true - } + "include": [ + "lib/", + "test/", + "eslint.config.js" + ], + "compilerOptions": { + "rootDir": ".", + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "allowSyntheticDefaultImports": true, + "allowImportingTsExtensions": true, + "forceConsistentCasingInFileNames": true, + "verbatimModuleSyntax": true, + "erasableSyntaxOnly": true, + "strict": true, + "allowJs": true, + "checkJs": true, + "noEmit": true, + "stripInternal": true, + "noEmitOnError": true, + "noErrorTruncation": true, + "outDir": "types", + "declarationMap": false, + "skipLibCheck": true + } } From 2776dcb6bb76b1c443906c538a521ca4eb335d39 Mon Sep 17 00:00:00 2001 From: Borgar Date: Sun, 29 Mar 2026 16:43:43 +0000 Subject: [PATCH 02/14] 2.2.3-rc.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0b201a3..1187386 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@borgar/simple-xml", - "version": "2.2.2", + "version": "2.2.3-rc.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@borgar/simple-xml", - "version": "2.2.2", + "version": "2.2.3-rc.0", "license": "MIT", "devDependencies": { "@borgar/eslint-config": "~4.0.1", diff --git a/package.json b/package.json index ff68e93..2bf26e0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@borgar/simple-xml", - "version": "2.2.2", + "version": "2.2.3-rc.0", "description": "A reasonably fast, simple and pure-JS XML parser with no dependencies", "type": "module", "source": "lib/index.ts", From 559baba80bc3d25c988a3cd899d2e9a02fa362d4 Mon Sep 17 00:00:00 2001 From: Borgar Date: Sun, 29 Mar 2026 16:46:47 +0000 Subject: [PATCH 03/14] Tag the release version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1187386..cf25650 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@borgar/simple-xml", - "version": "2.2.3-rc.0", + "version": "3.0.0-rc.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@borgar/simple-xml", - "version": "2.2.3-rc.0", + "version": "3.0.0-rc.0", "license": "MIT", "devDependencies": { "@borgar/eslint-config": "~4.0.1", diff --git a/package.json b/package.json index 2bf26e0..e4e57a5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@borgar/simple-xml", - "version": "2.2.3-rc.0", + "version": "3.0.0-rc.0", "description": "A reasonably fast, simple and pure-JS XML parser with no dependencies", "type": "module", "source": "lib/index.ts", From e968d391f78c64d09f61a7b0006ff490365aedf8 Mon Sep 17 00:00:00 2001 From: Borgar Date: Sun, 29 Mar 2026 17:00:48 +0000 Subject: [PATCH 04/14] Deal with test errors --- lib/prettyPrint.ts | 1 - package-lock.json | 18 +++ package.json | 1 + test/parse-data.spec.ts | 4 +- test/querySelectorAll.spec.ts | 236 +++++++++++++++++----------------- 5 files changed, 141 insertions(+), 119 deletions(-) diff --git a/lib/prettyPrint.ts b/lib/prettyPrint.ts index 75a96bb..7634473 100644 --- a/lib/prettyPrint.ts +++ b/lib/prettyPrint.ts @@ -1,5 +1,4 @@ import type { CDataNode } from './CDataNode.ts'; -import type { Document } from './Document.ts'; import { DocumentFragment } from './DocumentFragment.ts'; import type { Node } from './Node.js'; import type { TextNode } from './TextNode.ts'; diff --git a/package-lock.json b/package-lock.json index cf25650..a108611 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "devDependencies": { "@borgar/eslint-config": "~4.0.1", "@eslint/js": "~9.38.0", + "@types/node": "~25.5.0", "concat-md": "~0.5.1", "eslint": "~9.38.0", "jsdoc": "~4.0.5", @@ -1383,6 +1384,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", @@ -7069,6 +7080,13 @@ "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", "dev": true }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, "node_modules/unified": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz", diff --git a/package.json b/package.json index e4e57a5..63688f4 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "devDependencies": { "@borgar/eslint-config": "~4.0.1", "@eslint/js": "~9.38.0", + "@types/node": "~25.5.0", "concat-md": "~0.5.1", "eslint": "~9.38.0", "jsdoc": "~4.0.5", diff --git a/test/parse-data.spec.ts b/test/parse-data.spec.ts index a132d80..fa7505c 100644 --- a/test/parse-data.spec.ts +++ b/test/parse-data.spec.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { describe, it, expect } from 'vitest'; import fs from 'fs'; import { parseXML } from '../lib/index.js'; @@ -35,7 +37,7 @@ describe('parse-data', () => { const src = fs.readFileSync(fileName, 'utf8'); const dom = parseXML(src); expect(dom.toJS()).toEqual(expected); - expect(dom.root.textContent).toBe(' c b c b '); + expect(dom.root!.textContent).toBe(' c b c b '); }); it('refs.xml', () => { diff --git a/test/querySelectorAll.spec.ts b/test/querySelectorAll.spec.ts index 0a086fc..d1feb27 100644 --- a/test/querySelectorAll.spec.ts +++ b/test/querySelectorAll.spec.ts @@ -1,472 +1,474 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import fs from 'fs'; import { describe, it, expect } from 'vitest'; import { parseXML } from '../lib/index.js'; const src = fs.readFileSync('test/data/css-selectors.xml', 'utf8'); -const dom = parseXML(src); +const domRoot = parseXML(src).root!; describe('querySelectorAll', () => { it('a.url.fn', () => { - expect(dom.root.querySelectorAll('a.url.fn').length).toBe(2); + expect(domRoot.querySelectorAll('a.url.fn').length).toBe(2); }); it('a.url, a.fn', () => { - expect(dom.root.querySelectorAll('a.url, a.fn').length).toBe(2); + expect(domRoot.querySelectorAll('a.url, a.fn').length).toBe(2); }); it('li[class]:nth-of-type(2n+1)', () => { - expect(dom.root.querySelectorAll('li[class]:nth-of-type(2n+1)').length).toBe(18); + expect(domRoot.querySelectorAll('li[class]:nth-of-type(2n+1)').length).toBe(18); }); it('*[class]:nth-of-type(2n+1)', () => { - expect(dom.root.querySelectorAll('*[class]:nth-of-type(2n+1)').length).toBe(160); + expect(domRoot.querySelectorAll('*[class]:nth-of-type(2n+1)').length).toBe(160); }); it('li:nth-of-type(2n+1)', () => { - expect(dom.root.querySelectorAll('li:nth-of-type(2n+1)').length).toBe(55); + expect(domRoot.querySelectorAll('li:nth-of-type(2n+1)').length).toBe(55); }); it('li:nth-of-type(2n+2)', () => { - expect(dom.root.querySelectorAll('li:nth-of-type(2n+2)').length).toBe(44); + expect(domRoot.querySelectorAll('li:nth-of-type(2n+2)').length).toBe(44); }); it('li:nth-of-type(2n+3)', () => { - expect(dom.root.querySelectorAll('li:nth-of-type(2n+3)').length).toBe(31); + expect(domRoot.querySelectorAll('li:nth-of-type(2n+3)').length).toBe(31); }); it('li:nth-of-type(2n+4)', () => { - expect(dom.root.querySelectorAll('li:nth-of-type(2n+4)').length).toBe(23); + expect(domRoot.querySelectorAll('li:nth-of-type(2n+4)').length).toBe(23); }); it('li:nth-last-of-type(2n+1)', () => { - expect(dom.root.querySelectorAll('li:nth-last-of-type(2n+1)').length).toBe(55); + expect(domRoot.querySelectorAll('li:nth-last-of-type(2n+1)').length).toBe(55); }); it('li:nth-last-of-type(2n+2)', () => { - expect(dom.root.querySelectorAll('li:nth-last-of-type(2n+2)').length).toBe(44); + expect(domRoot.querySelectorAll('li:nth-last-of-type(2n+2)').length).toBe(44); }); it('li:nth-last-of-type(2n+3)', () => { - expect(dom.root.querySelectorAll('li:nth-last-of-type(2n+3)').length).toBe(31); + expect(domRoot.querySelectorAll('li:nth-last-of-type(2n+3)').length).toBe(31); }); it('li:nth-last-of-type(2n+4)', () => { - expect(dom.root.querySelectorAll('li:nth-last-of-type(2n+4)').length).toBe(23); + expect(domRoot.querySelectorAll('li:nth-last-of-type(2n+4)').length).toBe(23); }); it('li:nth-of-type(1n)', () => { - expect(dom.root.querySelectorAll('li:nth-of-type(1n)').length).toBe(99); + expect(domRoot.querySelectorAll('li:nth-of-type(1n)').length).toBe(99); }); it('li:nth-of-type(2n)', () => { - expect(dom.root.querySelectorAll('li:nth-of-type(2n)').length).toBe(44); + expect(domRoot.querySelectorAll('li:nth-of-type(2n)').length).toBe(44); }); it('li:nth-of-type(3n)', () => { - expect(dom.root.querySelectorAll('li:nth-of-type(3n)').length).toBe(26); + expect(domRoot.querySelectorAll('li:nth-of-type(3n)').length).toBe(26); }); it('li:nth-of-type(4n)', () => { - expect(dom.root.querySelectorAll('li:nth-of-type(4n)').length).toBe(15); + expect(domRoot.querySelectorAll('li:nth-of-type(4n)').length).toBe(15); }); it('li:nth-last-of-type(1n)', () => { - expect(dom.root.querySelectorAll('li:nth-last-of-type(1n)').length).toBe(99); + expect(domRoot.querySelectorAll('li:nth-last-of-type(1n)').length).toBe(99); }); it('li:nth-last-of-type(2n)', () => { - expect(dom.root.querySelectorAll('li:nth-last-of-type(2n)').length).toBe(44); + expect(domRoot.querySelectorAll('li:nth-last-of-type(2n)').length).toBe(44); }); it('li:nth-last-of-type(3n)', () => { - expect(dom.root.querySelectorAll('li:nth-last-of-type(3n)').length).toBe(26); + expect(domRoot.querySelectorAll('li:nth-last-of-type(3n)').length).toBe(26); }); it('li:nth-last-of-type(4n)', () => { - expect(dom.root.querySelectorAll('li:nth-last-of-type(4n)').length).toBe(15); + expect(domRoot.querySelectorAll('li:nth-last-of-type(4n)').length).toBe(15); }); it('li:nth-of-type(1)', () => { - expect(dom.root.querySelectorAll('li:nth-of-type(1)').length).toBe(24); + expect(domRoot.querySelectorAll('li:nth-of-type(1)').length).toBe(24); }); it('li:nth-of-type(2)', () => { - expect(dom.root.querySelectorAll('li:nth-of-type(2)').length).toBe(21); + expect(domRoot.querySelectorAll('li:nth-of-type(2)').length).toBe(21); }); it('li:nth-of-type(3)', () => { - expect(dom.root.querySelectorAll('li:nth-of-type(3)').length).toBe(15); + expect(domRoot.querySelectorAll('li:nth-of-type(3)').length).toBe(15); }); it('li:nth-of-type(4)', () => { - expect(dom.root.querySelectorAll('li:nth-of-type(4)').length).toBe(9); + expect(domRoot.querySelectorAll('li:nth-of-type(4)').length).toBe(9); }); it('li:nth-last-of-type(1)', () => { - expect(dom.root.querySelectorAll('li:nth-last-of-type(1)').length).toBe(24); + expect(domRoot.querySelectorAll('li:nth-last-of-type(1)').length).toBe(24); }); it('li:nth-last-of-type(2)', () => { - expect(dom.root.querySelectorAll('li:nth-last-of-type(2)').length).toBe(21); + expect(domRoot.querySelectorAll('li:nth-last-of-type(2)').length).toBe(21); }); it('li:nth-last-of-type(3)', () => { - expect(dom.root.querySelectorAll('li:nth-last-of-type(3)').length).toBe(15); + expect(domRoot.querySelectorAll('li:nth-last-of-type(3)').length).toBe(15); }); it('li:nth-last-of-type(4)', () => { - expect(dom.root.querySelectorAll('li:nth-last-of-type(4)').length).toBe(9); + expect(domRoot.querySelectorAll('li:nth-last-of-type(4)').length).toBe(9); }); it('div:nth-child(2n+1)', () => { - expect(dom.root.querySelectorAll('div:nth-child(2n+1)').length).toBe(26); + expect(domRoot.querySelectorAll('div:nth-child(2n+1)').length).toBe(26); }); it('div:nth-child(2n+2)', () => { - expect(dom.root.querySelectorAll('div:nth-child(2n+2)').length).toBe(25); + expect(domRoot.querySelectorAll('div:nth-child(2n+2)').length).toBe(25); }); it('div:nth-child(2n+3)', () => { - expect(dom.root.querySelectorAll('div:nth-child(2n+3)').length).toBe(25); + expect(domRoot.querySelectorAll('div:nth-child(2n+3)').length).toBe(25); }); it('div:nth-child(2n+4)', () => { - expect(dom.root.querySelectorAll('div:nth-child(2n+4)').length).toBe(25); + expect(domRoot.querySelectorAll('div:nth-child(2n+4)').length).toBe(25); }); it('div:nth-last-child(2n+1)', () => { - expect(dom.root.querySelectorAll('div:nth-last-child(2n+1)').length).toBe(24); + expect(domRoot.querySelectorAll('div:nth-last-child(2n+1)').length).toBe(24); }); it('div:nth-last-child(2n+2)', () => { - expect(dom.root.querySelectorAll('div:nth-last-child(2n+2)').length).toBe(27); + expect(domRoot.querySelectorAll('div:nth-last-child(2n+2)').length).toBe(27); }); it('div:nth-last-child(2n+3)', () => { - expect(dom.root.querySelectorAll('div:nth-last-child(2n+3)').length).toBe(23); + expect(domRoot.querySelectorAll('div:nth-last-child(2n+3)').length).toBe(23); }); it('div:nth-last-child(2n+4)', () => { - expect(dom.root.querySelectorAll('div:nth-last-child(2n+4)').length).toBe(27); + expect(domRoot.querySelectorAll('div:nth-last-child(2n+4)').length).toBe(27); }); it('div:nth-child(1n)', () => { - expect(dom.root.querySelectorAll('div:nth-child(1n)').length).toBe(51); + expect(domRoot.querySelectorAll('div:nth-child(1n)').length).toBe(51); }); it('div:nth-child(2n)', () => { - expect(dom.root.querySelectorAll('div:nth-child(2n)').length).toBe(25); + expect(domRoot.querySelectorAll('div:nth-child(2n)').length).toBe(25); }); it('div:nth-child(3n)', () => { - expect(dom.root.querySelectorAll('div:nth-child(3n)').length).toBe(18); + expect(domRoot.querySelectorAll('div:nth-child(3n)').length).toBe(18); }); it('div:nth-child(4n)', () => { - expect(dom.root.querySelectorAll('div:nth-child(4n)').length).toBe(15); + expect(domRoot.querySelectorAll('div:nth-child(4n)').length).toBe(15); }); it('div:nth-last-child(1n)', () => { - expect(dom.root.querySelectorAll('div:nth-last-child(1n)').length).toBe(51); + expect(domRoot.querySelectorAll('div:nth-last-child(1n)').length).toBe(51); }); it('div:nth-last-child(2n)', () => { - expect(dom.root.querySelectorAll('div:nth-last-child(2n)').length).toBe(27); + expect(domRoot.querySelectorAll('div:nth-last-child(2n)').length).toBe(27); }); it('div:nth-last-child(3n)', () => { - expect(dom.root.querySelectorAll('div:nth-last-child(3n)').length).toBe(16); + expect(domRoot.querySelectorAll('div:nth-last-child(3n)').length).toBe(16); }); it('div:nth-last-child(4n)', () => { - expect(dom.root.querySelectorAll('div:nth-last-child(4n)').length).toBe(17); + expect(domRoot.querySelectorAll('div:nth-last-child(4n)').length).toBe(17); }); it('div:nth-child(1)', () => { - expect(dom.root.querySelectorAll('div:nth-child(1)').length).toBe(1); + expect(domRoot.querySelectorAll('div:nth-child(1)').length).toBe(1); }); it('div:nth-child(2)', () => { - expect(dom.root.querySelectorAll('div:nth-child(2)').length).toBe(0); + expect(domRoot.querySelectorAll('div:nth-child(2)').length).toBe(0); }); it('div:nth-child(3)', () => { - expect(dom.root.querySelectorAll('div:nth-child(3)').length).toBe(0); + expect(domRoot.querySelectorAll('div:nth-child(3)').length).toBe(0); }); it('div:nth-child(4)', () => { - expect(dom.root.querySelectorAll('div:nth-child(4)').length).toBe(2); + expect(domRoot.querySelectorAll('div:nth-child(4)').length).toBe(2); }); it('div:nth-last-child(1)', () => { - expect(dom.root.querySelectorAll('div:nth-last-child(1)').length).toBe(1); + expect(domRoot.querySelectorAll('div:nth-last-child(1)').length).toBe(1); }); it('div:nth-last-child(2)', () => { - expect(dom.root.querySelectorAll('div:nth-last-child(2)').length).toBe(0); + expect(domRoot.querySelectorAll('div:nth-last-child(2)').length).toBe(0); }); it('div:nth-last-child(3)', () => { - expect(dom.root.querySelectorAll('div:nth-last-child(3)').length).toBe(0); + expect(domRoot.querySelectorAll('div:nth-last-child(3)').length).toBe(0); }); it('div:nth-last-child(4)', () => { - expect(dom.root.querySelectorAll('div:nth-last-child(4)').length).toBe(1); + expect(domRoot.querySelectorAll('div:nth-last-child(4)').length).toBe(1); }); it('div:nth-child(even)', () => { - expect(dom.root.querySelectorAll('div:nth-child(even)').length).toBe(25); + expect(domRoot.querySelectorAll('div:nth-child(even)').length).toBe(25); }); it('div:nth-child(odd)', () => { - expect(dom.root.querySelectorAll('div:nth-child(odd)').length).toBe(26); + expect(domRoot.querySelectorAll('div:nth-child(odd)').length).toBe(26); }); it('div:nth-child(n)', () => { - expect(dom.root.querySelectorAll('div:nth-child(n)').length).toBe(51); + expect(domRoot.querySelectorAll('div:nth-child(n)').length).toBe(51); }); it('div:nth-last-child(even)', () => { - expect(dom.root.querySelectorAll('div:nth-last-child(even)').length).toBe(27); + expect(domRoot.querySelectorAll('div:nth-last-child(even)').length).toBe(27); }); it('div:nth-last-child(odd)', () => { - expect(dom.root.querySelectorAll('div:nth-last-child(odd)').length).toBe(24); + expect(domRoot.querySelectorAll('div:nth-last-child(odd)').length).toBe(24); }); it('div:nth-last-child(n)', () => { - expect(dom.root.querySelectorAll('div:nth-last-child(n)').length).toBe(51); + expect(domRoot.querySelectorAll('div:nth-last-child(n)').length).toBe(51); }); it('div:first-of-type', () => { - expect(dom.root.querySelectorAll('div:first-of-type').length).toBe(3); + expect(domRoot.querySelectorAll('div:first-of-type').length).toBe(3); }); it('div:last-of-type', () => { - expect(dom.root.querySelectorAll('div:last-of-type').length).toBe(3); + expect(domRoot.querySelectorAll('div:last-of-type').length).toBe(3); }); it('div:only-of-type', () => { - expect(dom.root.querySelectorAll('div:only-of-type').length).toBe(2); + expect(domRoot.querySelectorAll('div:only-of-type').length).toBe(2); }); it('div:nth-of-type(even)', () => { - expect(dom.root.querySelectorAll('div:nth-of-type(even)').length).toBe(24); + expect(domRoot.querySelectorAll('div:nth-of-type(even)').length).toBe(24); }); it('div:nth-of-type(2n)', () => { - expect(dom.root.querySelectorAll('div:nth-of-type(2n)').length).toBe(24); + expect(domRoot.querySelectorAll('div:nth-of-type(2n)').length).toBe(24); }); it('div:nth-of-type(odd)', () => { - expect(dom.root.querySelectorAll('div:nth-of-type(odd)').length).toBe(27); + expect(domRoot.querySelectorAll('div:nth-of-type(odd)').length).toBe(27); }); it('div:nth-of-type(2n+1)', () => { - expect(dom.root.querySelectorAll('div:nth-of-type(2n+1)').length).toBe(27); + expect(domRoot.querySelectorAll('div:nth-of-type(2n+1)').length).toBe(27); }); it('div:nth-of-type(n)', () => { - expect(dom.root.querySelectorAll('div:nth-of-type(n)').length).toBe(51); + expect(domRoot.querySelectorAll('div:nth-of-type(n)').length).toBe(51); }); it('div:nth-last-of-type(even)', () => { - expect(dom.root.querySelectorAll('div:nth-last-of-type(even)').length).toBe(24); + expect(domRoot.querySelectorAll('div:nth-last-of-type(even)').length).toBe(24); }); it('div:nth-last-of-type(2n)', () => { - expect(dom.root.querySelectorAll('div:nth-last-of-type(2n)').length).toBe(24); + expect(domRoot.querySelectorAll('div:nth-last-of-type(2n)').length).toBe(24); }); it('div:nth-last-of-type(odd)', () => { - expect(dom.root.querySelectorAll('div:nth-last-of-type(odd)').length).toBe(27); + expect(domRoot.querySelectorAll('div:nth-last-of-type(odd)').length).toBe(27); }); it('div:nth-last-of-type(2n+1)', () => { - expect(dom.root.querySelectorAll('div:nth-last-of-type(2n+1)').length).toBe(27); + expect(domRoot.querySelectorAll('div:nth-last-of-type(2n+1)').length).toBe(27); }); it('div:nth-last-of-type(n)', () => { - expect(dom.root.querySelectorAll('div:nth-last-of-type(n)').length).toBe(51); + expect(domRoot.querySelectorAll('div:nth-last-of-type(n)').length).toBe(51); }); it('label[for]', () => { - expect(dom.root.querySelectorAll('label[for]').length).toBe(0); + expect(domRoot.querySelectorAll('label[for]').length).toBe(0); }); it('*', () => { - expect(dom.root.querySelectorAll('*').length).toBe(1778); + expect(domRoot.querySelectorAll('*').length).toBe(1778); }); it('body', () => { - expect(dom.root.querySelectorAll('body').length).toBe(0); + expect(domRoot.querySelectorAll('body').length).toBe(0); }); it('div', () => { - expect(dom.root.querySelectorAll('div').length).toBe(51); + expect(domRoot.querySelectorAll('div').length).toBe(51); }); it('body div', () => { - expect(dom.root.querySelectorAll('body div').length).toBe(0); + expect(domRoot.querySelectorAll('body div').length).toBe(0); }); it('div div', () => { - expect(dom.root.querySelectorAll('div div').length).toBe(2); + expect(domRoot.querySelectorAll('div div').length).toBe(2); }); it('div div div', () => { - expect(dom.root.querySelectorAll('div div div').length).toBe(0); + expect(domRoot.querySelectorAll('div div div').length).toBe(0); }); it('div p', () => { - expect(dom.root.querySelectorAll('div p').length).toBe(140); + expect(domRoot.querySelectorAll('div p').length).toBe(140); }); it('div > p', () => { - expect(dom.root.querySelectorAll('div > p').length).toBe(134); + expect(domRoot.querySelectorAll('div > p').length).toBe(134); }); it('div + p', () => { - expect(dom.root.querySelectorAll('div + p').length).toBe(22); + expect(domRoot.querySelectorAll('div + p').length).toBe(22); }); it('div ~ p', () => { - expect(dom.root.querySelectorAll('div ~ p').length).toBe(183); + expect(domRoot.querySelectorAll('div ~ p').length).toBe(183); }); it('div.example ~ p', () => { - expect(dom.root.querySelectorAll('div.example ~ p').length).toBe(152); + expect(domRoot.querySelectorAll('div.example ~ p').length).toBe(152); }); it('div[class^=exa][class$=mple]', () => { - expect(dom.root.querySelectorAll('div[class^=exa][class$=mple]').length).toBe(43); + expect(domRoot.querySelectorAll('div[class^=exa][class$=mple]').length).toBe(43); }); it('div p a', () => { - expect(dom.root.querySelectorAll('div p a').length).toBe(12); + expect(domRoot.querySelectorAll('div p a').length).toBe(12); }); it('div, p, a', () => { - expect(dom.root.querySelectorAll('div, p, a').length).toBe(671); + expect(domRoot.querySelectorAll('div, p, a').length).toBe(671); }); it('.note', () => { - expect(dom.root.querySelectorAll('.note').length).toBe(14); + expect(domRoot.querySelectorAll('.note').length).toBe(14); }); it('div.example', () => { - expect(dom.root.querySelectorAll('div.example').length).toBe(43); + expect(domRoot.querySelectorAll('div.example').length).toBe(43); }); it('ul .tocline2', () => { - expect(dom.root.querySelectorAll('ul .tocline2').length).toBe(12); + expect(domRoot.querySelectorAll('ul .tocline2').length).toBe(12); }); it('div.example, div.note', () => { - expect(dom.root.querySelectorAll('div.example, div.note').length).toBe(44); + expect(domRoot.querySelectorAll('div.example, div.note').length).toBe(44); }); it('#title', () => { - expect(dom.root.querySelectorAll('#title').length).toBe(1); + expect(domRoot.querySelectorAll('#title').length).toBe(1); }); it('h1#title', () => { - expect(dom.root.querySelectorAll('h1#title').length).toBe(1); + expect(domRoot.querySelectorAll('h1#title').length).toBe(1); }); it('div #title', () => { - expect(dom.root.querySelectorAll('div #title').length).toBe(1); + expect(domRoot.querySelectorAll('div #title').length).toBe(1); }); it('ul.toc li.tocline2', () => { - expect(dom.root.querySelectorAll('ul.toc li.tocline2').length).toBe(12); + expect(domRoot.querySelectorAll('ul.toc li.tocline2').length).toBe(12); }); it('ul.toc > li.tocline2', () => { - expect(dom.root.querySelectorAll('ul.toc > li.tocline2').length).toBe(12); + expect(domRoot.querySelectorAll('ul.toc > li.tocline2').length).toBe(12); }); it('h1#title + div > p', () => { - expect(dom.root.querySelectorAll('h1#title + div > p').length).toBe(0); + expect(domRoot.querySelectorAll('h1#title + div > p').length).toBe(0); }); it('h1[id]:contains(Selectors)', () => { - expect(dom.root.querySelectorAll('h1[id]:contains(Selectors)').length).toBe(1); + expect(domRoot.querySelectorAll('h1[id]:contains(Selectors)').length).toBe(1); }); it('a[href][lang][class]', () => { - expect(dom.root.querySelectorAll('a[href][lang][class]').length).toBe(1); + expect(domRoot.querySelectorAll('a[href][lang][class]').length).toBe(1); }); it('div[class]', () => { - expect(dom.root.querySelectorAll('div[class]').length).toBe(51); + expect(domRoot.querySelectorAll('div[class]').length).toBe(51); }); it('div[class=example]', () => { - expect(dom.root.querySelectorAll('div[class=example]').length).toBe(43); + expect(domRoot.querySelectorAll('div[class=example]').length).toBe(43); }); it('div[class^=exa]', () => { - expect(dom.root.querySelectorAll('div[class^=exa]').length).toBe(43); + expect(domRoot.querySelectorAll('div[class^=exa]').length).toBe(43); }); it('div[class$=mple]', () => { - expect(dom.root.querySelectorAll('div[class$=mple]').length).toBe(43); + expect(domRoot.querySelectorAll('div[class$=mple]').length).toBe(43); }); it('div[class*=e]', () => { - expect(dom.root.querySelectorAll('div[class*=e]').length).toBe(50); + expect(domRoot.querySelectorAll('div[class*=e]').length).toBe(50); }); it('div[class|=dialog]', () => { - expect(dom.root.querySelectorAll('div[class|=dialog]').length).toBe(0); + expect(domRoot.querySelectorAll('div[class|=dialog]').length).toBe(0); }); it('div[class!=made_up]', () => { - expect(dom.root.querySelectorAll('div[class!=made_up]').length).toBe(51); + expect(domRoot.querySelectorAll('div[class!=made_up]').length).toBe(51); }); it('div[class~=example]', () => { - expect(dom.root.querySelectorAll('div[class~=example]').length).toBe(43); + expect(domRoot.querySelectorAll('div[class~=example]').length).toBe(43); }); it('div:not(.example)', () => { - expect(dom.root.querySelectorAll('div:not(.example)').length).toBe(8); + expect(domRoot.querySelectorAll('div:not(.example)').length).toBe(8); }); it('p:contains(selectors)', () => { - expect(dom.root.querySelectorAll('p:contains(selectors)').length).toBe(54); + expect(domRoot.querySelectorAll('p:contains(selectors)').length).toBe(54); }); it('p:nth-child(even)', () => { - expect(dom.root.querySelectorAll('p:nth-child(even)').length).toBe(158); + expect(domRoot.querySelectorAll('p:nth-child(even)').length).toBe(158); }); it('p:nth-child(2n)', () => { - expect(dom.root.querySelectorAll('p:nth-child(2n)').length).toBe(158); + expect(domRoot.querySelectorAll('p:nth-child(2n)').length).toBe(158); }); it('p:nth-child(odd)', () => { - expect(dom.root.querySelectorAll('p:nth-child(odd)').length).toBe(166); + expect(domRoot.querySelectorAll('p:nth-child(odd)').length).toBe(166); }); it('p:nth-child(2n+1)', () => { - expect(dom.root.querySelectorAll('p:nth-child(2n+1)').length).toBe(166); + expect(domRoot.querySelectorAll('p:nth-child(2n+1)').length).toBe(166); }); it('p:nth-child(n)', () => { - expect(dom.root.querySelectorAll('p:nth-child(n)').length).toBe(324); + expect(domRoot.querySelectorAll('p:nth-child(n)').length).toBe(324); }); it('p:only-child', () => { - expect(dom.root.querySelectorAll('p:only-child').length).toBe(3); + expect(domRoot.querySelectorAll('p:only-child').length).toBe(3); }); it('p:last-child', () => { - expect(dom.root.querySelectorAll('p:last-child').length).toBe(19); + expect(domRoot.querySelectorAll('p:last-child').length).toBe(19); }); it('p:first-child', () => { - expect(dom.root.querySelectorAll('p:first-child').length).toBe(54); + expect(domRoot.querySelectorAll('p:first-child').length).toBe(54); }); }); From aa0a2f2bb5a7d6f34689f4ff54121b37cb2e5c25 Mon Sep 17 00:00:00 2001 From: Borgar Date: Sun, 29 Mar 2026 17:02:54 +0000 Subject: [PATCH 05/14] Rebuild docs & fix package repo url --- API.md | 1042 +++++++++++++++++++++++++++++++++++++++++++++++--- package.json | 2 +- 2 files changed, 991 insertions(+), 53 deletions(-) diff --git a/API.md b/API.md index 5f09844..e65fb92 100644 --- a/API.md +++ b/API.md @@ -7,14 +7,17 @@ - [CDataNode](#classescdatanodemd) - [Document](#classesdocumentmd) +- [DocumentFragment](#classesdocumentfragmentmd) - [Element](#classeselementmd) - [Node](#classesnodemd) - [TextNode](#classestextnodemd) ## Type Aliases +- [CreateChildArgument](#type-aliasescreatechildargumentmd) - [JsonMLAttr](#type-aliasesjsonmlattrmd) - [JsonMLElement](#type-aliasesjsonmlelementmd) +- [XMLAttr](#type-aliasesxmlattrmd) ## Variables @@ -33,7 +36,11 @@ ## Functions +- [escapeXML](#functionsescapexmlmd) +- [isElement](#functionsiselementmd) - [parseXML](#functionsparsexmlmd) +- [prettyPrint](#functionsprettyprintmd) +- [simplePrint](#functionssimpleprintmd) @@ -82,6 +89,46 @@ Constructs a new CDataNode instance. ## Accessors +### firstChild + +#### Get Signature + +```ts +get firstChild(): Node | null; +``` + +Returns the node's first child in the tree, or null if the node has no children. + +##### Returns + +[`Node`](#classesnodemd) \| `null` + +#### Inherited from + +[`Node`](#classesnodemd).[`firstChild`](#firstchild) + +*** + +### lastChild + +#### Get Signature + +```ts +get lastChild(): Node | null; +``` + +Returns the node's last child in the tree, or null if the node has no children. + +##### Returns + +[`Node`](#classesnodemd) \| `null` + +#### Inherited from + +[`Node`](#classesnodemd).[`lastChild`](#lastchild) + +*** + ### preserveSpace #### Get Signature @@ -125,20 +172,26 @@ The text content of this node (and its children). ### appendChild() ```ts -appendChild(node: Node): Node; +appendChild(node: T): T; ``` Appends a child node into the current one. +#### Type Parameters + +| Type Parameter | +| ------ | +| `T` *extends* [`Node`](#classesnodemd) \| [`DocumentFragment`](#classesdocumentfragmentmd) | + #### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | -| `node` | [`Node`](#classesnodemd) | The new child node | +| `node` | `T` | The new child node | #### Returns -[`Node`](#classesnodemd) +`T` The same node that was passed in. @@ -148,6 +201,65 @@ The same node that was passed in. *** +### insertBefore() + +```ts +insertBefore(newNode: T, referenceNode: Node | null): T; +``` + +Inserts a node before a _reference node_ as a child of a specified _parent node_. + +#### Type Parameters + +| Type Parameter | +| ------ | +| `T` *extends* [`Node`](#classesnodemd) \| [`DocumentFragment`](#classesdocumentfragmentmd) | + +#### Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `newNode` | `T` | The node to be inserted. | +| `referenceNode` | [`Node`](#classesnodemd) \| `null` | The node before which newNode is inserted. If this is null, then newNode is inserted at the end of node's child nodes. | + +#### Returns + +`T` + +The added child (unless newNode is a DocumentFragment, in which case the empty DocumentFragment is returned). + +#### Inherited from + +[`Node`](#classesnodemd).[`insertBefore`](#insertbefore) + +*** + +### removeChild() + +```ts +removeChild(child: Node): Node | undefined; +``` + +Removes a child node from the DOM and returns the removed node. + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `child` | [`Node`](#classesnodemd) | + +#### Returns + +[`Node`](#classesnodemd) \| `undefined` + +The removed child node. + +#### Inherited from + +[`Node`](#classesnodemd).[`removeChild`](#removechild) + +*** + ### toString() ```ts @@ -223,6 +335,46 @@ A list containing all child Elements of the current Element. *** +### firstChild + +#### Get Signature + +```ts +get firstChild(): Node | null; +``` + +Returns the node's first child in the tree, or null if the node has no children. + +##### Returns + +[`Node`](#classesnodemd) \| `null` + +#### Inherited from + +[`Node`](#classesnodemd).[`firstChild`](#firstchild) + +*** + +### lastChild + +#### Get Signature + +```ts +get lastChild(): Node | null; +``` + +Returns the node's last child in the tree, or null if the node has no children. + +##### Returns + +[`Node`](#classesnodemd) \| `null` + +#### Inherited from + +[`Node`](#classesnodemd).[`lastChild`](#lastchild) + +*** + ### preserveSpace #### Get Signature @@ -266,20 +418,26 @@ The text content of this node (and its children). ### appendChild() ```ts -appendChild(node: Element): Element; +appendChild(node: T): T; ``` Appends a child node into the current one. +#### Type Parameters + +| Type Parameter | +| ------ | +| `T` *extends* [`Node`](#classesnodemd) \| [`DocumentFragment`](#classesdocumentfragmentmd) | + #### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | -| `node` | [`Element`](#classeselementmd) | The new child node | +| `node` | `T` | The new child node | #### Returns -[`Element`](#classeselementmd) +`T` The same node that was passed in. @@ -289,6 +447,104 @@ The same node that was passed in. *** +### attachNS() + +```ts +attachNS(namespaceURI: string, prefix?: string): (name: string, attr?: XMLAttr | null, ...children: ( + | CreateChildArgument + | CreateChildArgument[])[]) => Element; +``` + +Attach a namespace to the document. + +#### Parameters + +| Parameter | Type | Default value | Description | +| ------ | ------ | ------ | ------ | +| `namespaceURI` | `string` | `undefined` | The namespace URI to attach. | +| `prefix?` | `string` | `''` | Prefix to use on elements belonging to the namespace. | + +#### Returns + +```ts +( + name: string, + attr?: XMLAttr | null, ... + children: ( + | CreateChildArgument + | CreateChildArgument[])[]): Element; +``` + +##### Parameters + +| Parameter | Type | +| ------ | ------ | +| `name` | `string` | +| `attr?` | [`XMLAttr`](#type-aliasesxmlattrmd) \| `null` | +| ...`children?` | ( \| [`CreateChildArgument`](#type-aliasescreatechildargumentmd) \| [`CreateChildArgument`](#type-aliasescreatechildargumentmd)[])[] | + +##### Returns + +[`Element`](#classeselementmd) + +*** + +### createElement() + +```ts +createElement( + qualifiedName: string, + attr: XMLAttr | null | undefined, ... + children: ( + | CreateChildArgument + | CreateChildArgument[])[]): Element; +``` + +Create a new element node. + +#### Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `qualifiedName` | `string` | The local tagName of the element. | +| `attr` | [`XMLAttr`](#type-aliasesxmlattrmd) \| `null` \| `undefined` | A record of attributes to assign to the new element. If the value is null or undefined, the attribute will be omitted. | +| ...`children` | ( \| [`CreateChildArgument`](#type-aliasescreatechildargumentmd) \| [`CreateChildArgument`](#type-aliasescreatechildargumentmd)[])[] | Nodes to insert as children. Strings will be converted to TextNodes and arrays will be flattened. | + +#### Returns + +[`Element`](#classeselementmd) + +A new Element instance. + +*** + +### createElementNS() + +```ts +createElementNS( + namespaceURI: string, + qualifiedName: string, + attr: XMLAttr | null | undefined, ... + children: ( + | CreateChildArgument + | CreateChildArgument[])[]): Element; +``` + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `namespaceURI` | `string` | +| `qualifiedName` | `string` | +| `attr` | [`XMLAttr`](#type-aliasesxmlattrmd) \| `null` \| `undefined` | +| ...`children` | ( \| [`CreateChildArgument`](#type-aliasescreatechildargumentmd) \| [`CreateChildArgument`](#type-aliasescreatechildargumentmd)[])[] | + +#### Returns + +[`Element`](#classeselementmd) + +*** + ### getElementsByTagName() ```ts @@ -311,6 +567,61 @@ The elements by tag name. *** +### insertBefore() + +```ts +insertBefore(newNode: T, referenceNode: Node | null): T; +``` + +Inserts a node before a _reference node_ as a child of a specified _parent node_. + +#### Type Parameters + +| Type Parameter | +| ------ | +| `T` *extends* [`Node`](#classesnodemd) \| [`DocumentFragment`](#classesdocumentfragmentmd) | + +#### Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `newNode` | `T` | The node to be inserted. | +| `referenceNode` | [`Node`](#classesnodemd) \| `null` | The node before which newNode is inserted. If this is null, then newNode is inserted at the end of node's child nodes. | + +#### Returns + +`T` + +The added child (unless newNode is a DocumentFragment, in which case the empty DocumentFragment is returned). + +#### Inherited from + +[`Node`](#classesnodemd).[`insertBefore`](#insertbefore) + +*** + +### print() + +```ts +print(pretty?: boolean): string; +``` + +Print the document as a string. + +#### Parameters + +| Parameter | Type | Default value | Description | +| ------ | ------ | ------ | ------ | +| `pretty` | `boolean` | `false` | Apply automatic linebreaks and indentation to the output. | + +#### Returns + +`string` + +The document as an XML string. + +*** + ### querySelector() ```ts @@ -355,17 +666,43 @@ The elements by tag name. *** +### removeChild() + +```ts +removeChild(child: Node): Node | undefined; +``` + +Removes a child node from the DOM and returns the removed node. + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `child` | [`Node`](#classesnodemd) | + +#### Returns + +[`Node`](#classesnodemd) \| `undefined` + +The removed child node. + +#### Inherited from + +[`Node`](#classesnodemd).[`removeChild`](#removechild) + +*** + ### toJS() ```ts -toJS(): [] | JsonMLElement; +toJS(): JsonMLElement | []; ``` Returns a simple object representation of the node and its descendants. #### Returns -\[\] \| [`JsonMLElement`](#type-aliasesjsonmlelementmd) +[`JsonMLElement`](#type-aliasesjsonmlelementmd) \| \[\] JsonML representation of the nodes and its subtree. @@ -390,6 +727,82 @@ A formatted XML source. [`Node`](#classesnodemd).[`toString`](#tostring) + + +# DocumentFragment + +A class describing a DocumentFragment. + +## Constructors + +### Constructor + +```ts +new DocumentFragment(): DocumentFragment; +``` + +#### Returns + +`DocumentFragment` + +## Properties + +| Property | Type | Default value | Description | +| ------ | ------ | ------ | ------ | +| `childNodes` | [`Node`](#classesnodemd)[] | `[]` | The immediate children contained in the fragment. | +| `nodeType` | `number` | `DOCUMENT_FRAGMENT_NODE` | A numerical node type identifier. | + +## Methods + +### appendChild() + +```ts +appendChild(node: T): T; +``` + +Appends a child node into the document fragment. + +#### Type Parameters + +| Type Parameter | +| ------ | +| `T` *extends* [`Node`](#classesnodemd) \| `DocumentFragment` | + +#### Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `node` | `T` | The new child node | + +#### Returns + +`T` + +The same node that was passed in. + +*** + +### print() + +```ts +print(pretty?: boolean): string; +``` + +Print the document as a string. + +#### Parameters + +| Parameter | Type | Default value | Description | +| ------ | ------ | ------ | ------ | +| `pretty` | `boolean` | `false` | Apply automatic linebreaks and indentation to the output. | + +#### Returns + +`string` + +The document as an XML string. + + # Element @@ -418,7 +831,7 @@ Constructs a new Element instance. | Parameter | Type | Default value | Description | | ------ | ------ | ------ | ------ | | `tagName` | `string` | `undefined` | The tag name of the node. | -| `attr?` | `Record`\<`string`, `string`\> | `{}` | A collection of attributes to assign. | +| `attr?` | `Record`\<`string`, `string`\> | `{}` | A collection of attributes to assign. Values of null or undefined will be ignored. | | `closed?` | `boolean` | `false` | Was the element "self-closed" when read. | #### Returns @@ -461,6 +874,92 @@ A list containing all child Elements of the current Element. *** +### className + +#### Get Signature + +```ts +get className(): string; +``` + +##### Returns + +`string` + +#### Set Signature + +```ts +set className(val: unknown): void; +``` + +##### Parameters + +| Parameter | Type | +| ------ | ------ | +| `val` | `unknown` | + +##### Returns + +`void` + +*** + +### firstChild + +#### Get Signature + +```ts +get firstChild(): Node | null; +``` + +Returns the node's first child in the tree, or null if the node has no children. + +##### Returns + +[`Node`](#classesnodemd) \| `null` + +#### Inherited from + +[`Node`](#classesnodemd).[`firstChild`](#firstchild) + +*** + +### firstElementChild + +#### Get Signature + +```ts +get firstElementChild(): Element | null; +``` + +Returns an element's first child Element, or null if there are no child elements + +##### Returns + +`Element` \| `null` + +*** + +### lastChild + +#### Get Signature + +```ts +get lastChild(): Node | null; +``` + +Returns the node's last child in the tree, or null if the node has no children. + +##### Returns + +[`Node`](#classesnodemd) \| `null` + +#### Inherited from + +[`Node`](#classesnodemd).[`lastChild`](#lastchild) + +*** + ### preserveSpace #### Get Signature @@ -501,95 +1000,202 @@ The text content of this node (and its children). ## Methods +### append() + +```ts +append(...nodes: ( + | CreateChildArgument + | CreateChildArgument[])[]): void; +``` + +Inserts a set of Node objects or strings after the last child of the Element. +Strings are inserted as equivalent Text nodes. + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| ...`nodes` | ( \| [`CreateChildArgument`](#type-aliasescreatechildargumentmd) \| [`CreateChildArgument`](#type-aliasescreatechildargumentmd)[])[] | + +#### Returns + +`void` + +*** + ### appendChild() ```ts -appendChild(node: Node): Node; +appendChild(node: T): T; ``` Appends a child node into the current one. +#### Type Parameters + +| Type Parameter | +| ------ | +| `T` *extends* [`Node`](#classesnodemd) \| [`DocumentFragment`](#classesdocumentfragmentmd) | + +#### Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `node` | `T` | The new child node | + +#### Returns + +`T` + +The same node that was passed in. + +#### Inherited from + +[`Node`](#classesnodemd).[`appendChild`](#appendchild) + +*** + +### getAttribute() + +```ts +getAttribute(name: string): string | null; +``` + +Read an attribute from the element. + +#### Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `name` | `string` | The attribute name to read. | + +#### Returns + +`string` \| `null` + +The attribute. + +*** + +### getElementsByTagName() + +```ts +getElementsByTagName(tagName: string): Element[]; +``` + +Return all descendant elements that have the specified tag name. + +#### Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `tagName` | `string` | The tag name to filter by. | + +#### Returns + +`Element`[] + +The elements by tag name. + +*** + +### hasAttribute() + +```ts +hasAttribute(name: string): boolean; +``` + +Test if an attribute exists on the element. + #### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | -| `node` | [`Node`](#classesnodemd) | The new child node | +| `name` | `string` | The attribute name to test for. | #### Returns -[`Node`](#classesnodemd) - -The same node that was passed in. - -#### Inherited from +`boolean` -[`Node`](#classesnodemd).[`appendChild`](#appendchild) +True if the attribute is present. *** -### getAttribute() +### insertBefore() ```ts -getAttribute(name: string): string | null; +insertBefore(newNode: T, referenceNode: Node | null): T; ``` -Read an attribute from the element. +Inserts a node before a _reference node_ as a child of a specified _parent node_. + +#### Type Parameters + +| Type Parameter | +| ------ | +| `T` *extends* [`Node`](#classesnodemd) \| [`DocumentFragment`](#classesdocumentfragmentmd) | #### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | -| `name` | `string` | The attribute name to read. | +| `newNode` | `T` | The node to be inserted. | +| `referenceNode` | [`Node`](#classesnodemd) \| `null` | The node before which newNode is inserted. If this is null, then newNode is inserted at the end of node's child nodes. | #### Returns -`string` \| `null` +`T` -The attribute. +The added child (unless newNode is a DocumentFragment, in which case the empty DocumentFragment is returned). + +#### Inherited from + +[`Node`](#classesnodemd).[`insertBefore`](#insertbefore) *** -### getElementsByTagName() +### prepend() ```ts -getElementsByTagName(tagName: string): Element[]; +prepend(...nodes: ( + | CreateChildArgument + | CreateChildArgument[])[]): void; ``` -Return all descendant elements that have the specified tag name. +Insert a set of Node objects or strings before the first child of the Element. +Strings are inserted as equivalent Text nodes. #### Parameters -| Parameter | Type | Description | -| ------ | ------ | ------ | -| `tagName` | `string` | The tag name to filter by. | +| Parameter | Type | +| ------ | ------ | +| ...`nodes` | ( \| [`CreateChildArgument`](#type-aliasescreatechildargumentmd) \| [`CreateChildArgument`](#type-aliasescreatechildargumentmd)[])[] | #### Returns -`Element`[] - -The elements by tag name. +`void` *** -### hasAttribute() +### print() ```ts -hasAttribute(name: string): boolean; +print(pretty?: boolean): string; ``` -Test if an attribute exists on the element. +Print the document as a string. #### Parameters -| Parameter | Type | Description | -| ------ | ------ | ------ | -| `name` | `string` | The attribute name to test for. | +| Parameter | Type | Default value | Description | +| ------ | ------ | ------ | ------ | +| `pretty` | `boolean` | `false` | Apply automatic linebreaks and indentation to the output. | #### Returns -`boolean` +`string` -True if the attribute is present. +The document as an XML string. *** @@ -657,10 +1263,36 @@ Remove an attribute off the element. *** +### removeChild() + +```ts +removeChild(child: Node): Node | undefined; +``` + +Removes a child node from the DOM and returns the removed node. + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `child` | [`Node`](#classesnodemd) | + +#### Returns + +[`Node`](#classesnodemd) \| `undefined` + +The removed child node. + +#### Inherited from + +[`Node`](#classesnodemd).[`removeChild`](#removechild) + +*** + ### setAttribute() ```ts -setAttribute(name: string, value: string): void; +setAttribute(name: string, value: string | number | boolean): void; ``` Sets an attribute on the element. @@ -670,7 +1302,7 @@ Sets an attribute on the element. | Parameter | Type | Description | | ------ | ------ | ------ | | `name` | `string` | The attribute name to read. | -| `value` | `string` | The value to set | +| `value` | `string` \| `number` \| `boolean` | The value to set | #### Returns @@ -749,6 +1381,38 @@ new Node(): Node; ## Accessors +### firstChild + +#### Get Signature + +```ts +get firstChild(): Node | null; +``` + +Returns the node's first child in the tree, or null if the node has no children. + +##### Returns + +`Node` \| `null` + +*** + +### lastChild + +#### Get Signature + +```ts +get lastChild(): Node | null; +``` + +Returns the node's last child in the tree, or null if the node has no children. + +##### Returns + +`Node` \| `null` + +*** + ### preserveSpace #### Get Signature @@ -784,25 +1448,82 @@ The text content of this node (and its children). ### appendChild() ```ts -appendChild(node: Node): Node; +appendChild(node: T): T; ``` Appends a child node into the current one. +#### Type Parameters + +| Type Parameter | +| ------ | +| `T` *extends* `Node` \| [`DocumentFragment`](#classesdocumentfragmentmd) | + #### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | -| `node` | `Node` | The new child node | +| `node` | `T` | The new child node | #### Returns -`Node` +`T` The same node that was passed in. *** +### insertBefore() + +```ts +insertBefore(newNode: T, referenceNode: Node | null): T; +``` + +Inserts a node before a _reference node_ as a child of a specified _parent node_. + +#### Type Parameters + +| Type Parameter | +| ------ | +| `T` *extends* `Node` \| [`DocumentFragment`](#classesdocumentfragmentmd) | + +#### Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `newNode` | `T` | The node to be inserted. | +| `referenceNode` | `Node` \| `null` | The node before which newNode is inserted. If this is null, then newNode is inserted at the end of node's child nodes. | + +#### Returns + +`T` + +The added child (unless newNode is a DocumentFragment, in which case the empty DocumentFragment is returned). + +*** + +### removeChild() + +```ts +removeChild(child: Node): Node | undefined; +``` + +Removes a child node from the DOM and returns the removed node. + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `child` | `Node` | + +#### Returns + +`Node` \| `undefined` + +The removed child node. + +*** + ### toString() ```ts @@ -833,7 +1554,7 @@ A class describing a TextNode. ### Constructor ```ts -new TextNode(value?: string): TextNode; +new TextNode(value?: any): TextNode; ``` Constructs a new TextNode instance. @@ -842,7 +1563,7 @@ Constructs a new TextNode instance. | Parameter | Type | Description | | ------ | ------ | ------ | -| `value?` | `string` | The data for the node | +| `value?` | `any` | The data for the node. | #### Returns @@ -859,11 +1580,51 @@ Constructs a new TextNode instance. | `childNodes` | [`Node`](#classesnodemd)[] | `[]` | The node's immediate children. | [`Node`](#classesnodemd).[`childNodes`](#childnodes) | | `nodeName` | `string` | `'#node'` | A node type string identifier. | [`Node`](#classesnodemd).[`nodeName`](#nodename) | | `nodeType` | `number` | `0` | A numerical node type identifier. | [`Node`](#classesnodemd).[`nodeType`](#nodetype) | -| `parentNode` | [`Node`](#classesnodemd) \| `null` | `null` | The node's parent node. | [`Node`](#classesnodemd).[`parentNode`](#parentnode) | +| `parentNode` | [`Node`](#classesnodemd) \| `null` | `null` | The node's parent node. | [`Document`](#classesdocumentmd).[`parentNode`](#parentnode) | | `value` | `string` | `undefined` | The node's data value. | - | ## Accessors +### firstChild + +#### Get Signature + +```ts +get firstChild(): Node | null; +``` + +Returns the node's first child in the tree, or null if the node has no children. + +##### Returns + +[`Node`](#classesnodemd) \| `null` + +#### Inherited from + +[`CDataNode`](#classescdatanodemd).[`firstChild`](#firstchild) + +*** + +### lastChild + +#### Get Signature + +```ts +get lastChild(): Node | null; +``` + +Returns the node's last child in the tree, or null if the node has no children. + +##### Returns + +[`Node`](#classesnodemd) \| `null` + +#### Inherited from + +[`CDataNode`](#classescdatanodemd).[`lastChild`](#lastchild) + +*** + ### preserveSpace #### Get Signature @@ -907,20 +1668,26 @@ The text content of this node (and its children). ### appendChild() ```ts -appendChild(node: Node): Node; +appendChild(node: T): T; ``` Appends a child node into the current one. +#### Type Parameters + +| Type Parameter | +| ------ | +| `T` *extends* [`Node`](#classesnodemd) \| [`DocumentFragment`](#classesdocumentfragmentmd) | + #### Parameters | Parameter | Type | Description | | ------ | ------ | ------ | -| `node` | [`Node`](#classesnodemd) | The new child node | +| `node` | `T` | The new child node | #### Returns -[`Node`](#classesnodemd) +`T` The same node that was passed in. @@ -930,6 +1697,65 @@ The same node that was passed in. *** +### insertBefore() + +```ts +insertBefore(newNode: T, referenceNode: Node | null): T; +``` + +Inserts a node before a _reference node_ as a child of a specified _parent node_. + +#### Type Parameters + +| Type Parameter | +| ------ | +| `T` *extends* [`Node`](#classesnodemd) \| [`DocumentFragment`](#classesdocumentfragmentmd) | + +#### Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `newNode` | `T` | The node to be inserted. | +| `referenceNode` | [`Node`](#classesnodemd) \| `null` | The node before which newNode is inserted. If this is null, then newNode is inserted at the end of node's child nodes. | + +#### Returns + +`T` + +The added child (unless newNode is a DocumentFragment, in which case the empty DocumentFragment is returned). + +#### Inherited from + +[`Node`](#classesnodemd).[`insertBefore`](#insertbefore) + +*** + +### removeChild() + +```ts +removeChild(child: Node): Node | undefined; +``` + +Removes a child node from the DOM and returns the removed node. + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `child` | [`Node`](#classesnodemd) | + +#### Returns + +[`Node`](#classesnodemd) \| `undefined` + +The removed child node. + +#### Inherited from + +[`Node`](#classesnodemd).[`removeChild`](#removechild) + +*** + ### toString() ```ts @@ -949,6 +1775,48 @@ A formatted XML source. [`Node`](#classesnodemd).[`toString`](#tostring) + + +# escapeXML() + +```ts +function escapeXML(s: string): string; +``` + +Escape XML entities in a string. + +## Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `s` | `string` | Unescaped string | + +## Returns + +`string` + +Escaped string + + + + +# isElement() + +```ts +function isElement(d: unknown): d is Element; +``` + +## Parameters + +| Parameter | Type | +| ------ | ------ | +| `d` | `unknown` | + +## Returns + +`d is Element` + + # parseXML() @@ -957,6 +1825,7 @@ A formatted XML source. function parseXML(source: string, options?: { emptyDoc?: boolean; laxAttr?: boolean; + ns?: boolean; }): Document; ``` @@ -967,9 +1836,10 @@ Parse an XML source and return a Node tree. | Parameter | Type | Default value | Description | | ------ | ------ | ------ | ------ | | `source` | `string` | `undefined` | The XML source to parse. | -| `options?` | \{ `emptyDoc?`: `boolean`; `laxAttr?`: `boolean`; \} | `DEFAULTOPTIONS` | Parsing options. | +| `options?` | \{ `emptyDoc?`: `boolean`; `laxAttr?`: `boolean`; `ns?`: `boolean`; \} | `DEFAULTOPTIONS` | Parsing options. | | `options.emptyDoc?` | `boolean` | `undefined` | Permit "rootless" documents. | | `options.laxAttr?` | `boolean` | `undefined` | Permit unquoted attributes (``). | +| `options.ns?` | `boolean` | `undefined` | Validate xmlns and element namespaces as they are parsed. | ## Returns @@ -978,6 +1848,65 @@ Parse an XML source and return a Node tree. A DOM representing the XML node tree. + + +# prettyPrint() + +```ts +function prettyPrint(node: + | Node + | DocumentFragment, indent?: string): string; +``` + +## Parameters + +| Parameter | Type | Default value | +| ------ | ------ | ------ | +| `node` | \| [`Node`](#classesnodemd) \| [`DocumentFragment`](#classesdocumentfragmentmd) | `undefined` | +| `indent` | `string` | `''` | + +## Returns + +`string` + + + + +# simplePrint() + +```ts +function simplePrint(node: + | Node + | DocumentFragment): string; +``` + +## Parameters + +| Parameter | Type | +| ------ | ------ | +| `node` | \| [`Node`](#classesnodemd) \| [`DocumentFragment`](#classesdocumentfragmentmd) | + +## Returns + +`string` + + + + +# CreateChildArgument + +```ts +type CreateChildArgument = + | Node + | DocumentFragment + | string + | boolean + | number + | null + | undefined; +``` + + # JsonMLAttr @@ -1001,6 +1930,15 @@ type JsonMLElement = ``` + + +# XMLAttr + +```ts +type XMLAttr = Record; +``` + + # ATTRIBUTE\_NODE diff --git a/package.json b/package.json index 63688f4..a82b85f 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ }, "repository": { "type": "git", - "url": "git@github.com:borgar/simple-xml.git" + "url": "git+ssh://git@github.com/borgar/simple-xml.git" }, "keywords": [ "xml", From ed02a29ca5f1e095e45151bb9feea49371a42d17 Mon Sep 17 00:00:00 2001 From: Borgar Date: Mon, 6 Apr 2026 12:35:23 +0000 Subject: [PATCH 06/14] Fix a few issues in NS handling. --- lib/Document.ts | 6 +- lib/NSMap.spec.ts | 176 ++++++++++++++++++++++++++++++++++++++++++++++ lib/NSMap.ts | 33 ++++++--- 3 files changed, 204 insertions(+), 11 deletions(-) create mode 100644 lib/NSMap.spec.ts diff --git a/lib/Document.ts b/lib/Document.ts index ce57609..687138c 100644 --- a/lib/Document.ts +++ b/lib/Document.ts @@ -48,7 +48,7 @@ export class Document extends Node { } /** @ignore */ - _updateNS () { + private _updateNS () { // ensure that namespaces exist on the root node if (this.root) { for (const [ namespaceURI, prefix ] of this.namespaces.list()) { @@ -106,10 +106,10 @@ export class Document extends Node { ...children: (CreateChildArgument | CreateChildArgument[])[] ): Element => { const ns = this.namespaces.get(namespaceURI); - if (!ns) { + if (ns == null) { throw new Error('Unknown namespace ' + namespaceURI); } - const element = new Element(ns + ':' + qualifiedName); + const element = new Element(ns ? ns + ':' + qualifiedName : qualifiedName); // can this not be solved by Element(name, attr) ... does the same thing internally, right? if (attr) { for (const [ key, val ] of Object.entries(attr)) { diff --git a/lib/NSMap.spec.ts b/lib/NSMap.spec.ts new file mode 100644 index 0000000..f9b9e91 --- /dev/null +++ b/lib/NSMap.spec.ts @@ -0,0 +1,176 @@ +import { describe, it, expect } from 'vitest'; +import { NSMap } from './NSMap.ts'; + +describe('NSMap', () => { + describe('initial state', () => { + it('has no entries by default', () => { + const map = new NSMap(); + expect(map.list()).toEqual([]); + }); + + it('get returns undefined for unknown URI', () => { + const map = new NSMap(); + expect(map.get('http://example.com/ns')).toBeUndefined(); + }); + + it('getByPrefix returns undefined for unknown prefix', () => { + const map = new NSMap(); + expect(map.getByPrefix('ex')).toBeUndefined(); + }); + }); + + describe('add', () => { + it('registers a URI/prefix pair', () => { + const map = new NSMap(); + map.add('http://example.com/ns', 'ex'); + expect(map.get('http://example.com/ns')).toBe('ex'); + expect(map.getByPrefix('ex')).toBe('http://example.com/ns'); + }); + + it('registers a pair with an empty prefix', () => { + const map = new NSMap(); + map.add('http://example.com/default', ''); + expect(map.get('http://example.com/default')).toBe(''); + expect(map.getByPrefix('')).toBe('http://example.com/default'); + }); + + it('re-adding the same URI/prefix pair is idempotent', () => { + const map = new NSMap(); + map.add('http://example.com/ns', 'ex'); + expect(() => map.add('http://example.com/ns', 'ex')).not.toThrow(); + expect(map.list()).toHaveLength(1); + }); + + it('supports multiple prefixes for the same URI', () => { + const map = new NSMap(); + map.add('http://example.com/ns', 'ex'); + map.add('http://example.com/ns', 'alt'); + expect(map.getByPrefix('ex')).toBe('http://example.com/ns'); + expect(map.getByPrefix('alt')).toBe('http://example.com/ns'); + }); + + it('get returns a non-empty prefix when a URI has multiple', () => { + const map = new NSMap(); + map.add('http://example.com/ns', 'first'); + map.add('http://example.com/ns', 'second'); + expect(map.get('http://example.com/ns')).toBe('first'); + }); + + it('get prefers a named prefix over an empty one', () => { + const map = new NSMap(); + map.add('http://example.com/ns', ''); + map.add('http://example.com/ns', 'wx'); + expect(map.get('http://example.com/ns')).toBe('wx'); + }); + + it('get returns empty prefix when it is the only one', () => { + const map = new NSMap(); + map.add('http://example.com/ns', ''); + expect(map.get('http://example.com/ns')).toBe(''); + }); + + it('supports multiple distinct URI/prefix pairs', () => { + const map = new NSMap(); + map.add('http://example.com/a', 'a'); + map.add('http://example.com/b', 'b'); + map.add('http://example.com/c', 'c'); + expect(map.get('http://example.com/a')).toBe('a'); + expect(map.get('http://example.com/b')).toBe('b'); + expect(map.get('http://example.com/c')).toBe('c'); + expect(map.getByPrefix('a')).toBe('http://example.com/a'); + expect(map.getByPrefix('b')).toBe('http://example.com/b'); + expect(map.getByPrefix('c')).toBe('http://example.com/c'); + }); + + it('throws when adding a prefix that already has a different URI', () => { + const map = new NSMap(); + map.add('http://example.com/first', 'ex'); + expect(() => map.add('http://example.com/second', 'ex')).toThrow( + 'ex already has a different URI' + ); + }); + }); + + describe('get', () => { + it('returns the prefix for a registered URI', () => { + const map = new NSMap(); + map.add('urn:foo', 'foo'); + expect(map.get('urn:foo')).toBe('foo'); + }); + + it('returns undefined for an unregistered URI', () => { + const map = new NSMap(); + map.add('urn:foo', 'foo'); + expect(map.get('urn:bar')).toBeUndefined(); + }); + }); + + describe('getByPrefix', () => { + it('returns the URI for a registered prefix', () => { + const map = new NSMap(); + map.add('urn:foo', 'foo'); + expect(map.getByPrefix('foo')).toBe('urn:foo'); + }); + + it('returns undefined for an unregistered prefix', () => { + const map = new NSMap(); + map.add('urn:foo', 'foo'); + expect(map.getByPrefix('bar')).toBeUndefined(); + }); + }); + + describe('list', () => { + it('returns an empty array when no entries exist', () => { + const map = new NSMap(); + expect(map.list()).toEqual([]); + }); + + it('returns [uri, prefix] tuples for all entries', () => { + const map = new NSMap(); + map.add('http://a.com', 'a'); + map.add('http://b.com', 'b'); + const entries = map.list(); + expect(entries).toHaveLength(2); + expect(entries).toContainEqual([ 'http://a.com', 'a' ]); + expect(entries).toContainEqual([ 'http://b.com', 'b' ]); + }); + + it('returns one entry per prefix when a URI has multiple prefixes', () => { + const map = new NSMap(); + map.add('http://a.com', 'a1'); + map.add('http://a.com', 'a2'); + map.add('http://b.com', 'b'); + const entries = map.list(); + expect(entries).toHaveLength(3); + expect(entries).toContainEqual([ 'http://a.com', 'a1' ]); + expect(entries).toContainEqual([ 'http://a.com', 'a2' ]); + expect(entries).toContainEqual([ 'http://b.com', 'b' ]); + }); + + it('returns a new array each time (not a reference to internal state)', () => { + const map = new NSMap(); + map.add('http://a.com', 'a'); + const list1 = map.list(); + const list2 = map.list(); + expect(list1).toEqual(list2); + expect(list1).not.toBe(list2); + }); + }); + + describe('bi-directional consistency', () => { + it('every listed URI resolves to its prefix and vice-versa', () => { + const map = new NSMap(); + const pairs: [string, string][] = [ + [ 'http://www.w3.org/2000/svg', 'svg' ], + [ 'http://www.w3.org/1999/xlink', 'xlink' ], + [ 'http://www.w3.org/XML/1998/namespace', 'xml' ] + ]; + for (const [ uri, prefix ] of pairs) { + map.add(uri, prefix); + } + for (const [ uri, prefix ] of map.list()) { + expect(map.getByPrefix(prefix)).toBe(uri); + } + }); + }); +}); diff --git a/lib/NSMap.ts b/lib/NSMap.ts index 08d52bc..1a5d39a 100644 --- a/lib/NSMap.ts +++ b/lib/NSMap.ts @@ -1,6 +1,7 @@ export class NSMap { - uriToPre: Record = {}; - preToUri: Record = {}; + private uriToPres: Record = {}; + private uriToPre: Record = {}; + private preToUri: Record = {}; get (nsURI: string): string | undefined { return this.uriToPre[nsURI]; @@ -11,17 +12,33 @@ export class NSMap { } list (): [string, string][] { - return Array.from(Object.entries(this.uriToPre)); + const result: [string, string][] = []; + for (const [ uri, prefixes ] of Object.entries(this.uriToPres)) { + for (const prefix of prefixes) { + result.push([ uri, prefix ]); + } + } + return result; } add (nsURI: string, nsPrefix: string) { - if ((nsURI in this.uriToPre) && (this.uriToPre[nsURI] !== nsPrefix)) { - throw new Error(nsURI + ' allready has a different prefix'); + // A prefix can only point to one URI — collisions are an error. + if ((nsPrefix in this.preToUri) && (this.preToUri[nsPrefix] !== nsURI)) { + throw new Error(nsPrefix + ' already has a different URI'); + } + // Registering the same pair twice is a no-op. + if ((nsURI in this.uriToPres) && this.uriToPres[nsURI].includes(nsPrefix)) { + return; } - if ((nsPrefix in this.preToUri) && (this.preToUri[nsPrefix] !== nsPrefix)) { - throw new Error(nsPrefix + ' allready has a different URI'); + if (!(nsURI in this.uriToPres)) { + this.uriToPres[nsURI] = []; } - this.uriToPre[nsURI] = nsPrefix; + this.uriToPres[nsURI].push(nsPrefix); this.preToUri[nsPrefix] = nsURI; + // Keep uriToPre pointing at the best prefix for fast get() lookups: + // prefer any named prefix over the empty (default) one. + if (!(nsURI in this.uriToPre) || (this.uriToPre[nsURI] === '' && nsPrefix !== '')) { + this.uriToPre[nsURI] = nsPrefix; + } } } From b15adb97f7a16b412665aae3668b4ed7e23068a7 Mon Sep 17 00:00:00 2001 From: Borgar Date: Mon, 6 Apr 2026 12:36:45 +0000 Subject: [PATCH 07/14] Bump rc version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a108611..a720fd0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@borgar/simple-xml", - "version": "3.0.0-rc.0", + "version": "3.0.0-rc.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@borgar/simple-xml", - "version": "3.0.0-rc.0", + "version": "3.0.0-rc.1", "license": "MIT", "devDependencies": { "@borgar/eslint-config": "~4.0.1", diff --git a/package.json b/package.json index a82b85f..9c6029d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@borgar/simple-xml", - "version": "3.0.0-rc.0", + "version": "3.0.0-rc.1", "description": "A reasonably fast, simple and pure-JS XML parser with no dependencies", "type": "module", "source": "lib/index.ts", From 08ec7e22357725726aca254537ec80e861dd03a0 Mon Sep 17 00:00:00 2001 From: Borgar Date: Mon, 6 Apr 2026 12:39:41 +0000 Subject: [PATCH 08/14] Add files to remove extra crud from the package --- package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package.json b/package.json index 9c6029d..3c95f71 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,10 @@ "build": "npm run build:dist && npm run docs && echo 'Success!'", "release": "git diff --exit-code && tsup && npm publish && V=$(jq -r .version package.json) && git tag -a $V -m $V && git push origin $V && gh release create $V --generate-notes" }, + "files": [ + "*.md", + "dist" + ], "tsup": { "entry": [ "lib/index.ts" From f3e0216b7bc3e06c6f1eb0ecce6d07411adee56d Mon Sep 17 00:00:00 2001 From: Borgar Date: Fri, 22 May 2026 14:23:37 +0000 Subject: [PATCH 09/14] Relax attribute types and add some sugar methods I'm adding createChild & setAttrValues convenience functions. These are inspired by Simple SVG API (https://www.w3.org/Graphics/SVG/WG/wiki/Simple_SVG_API). CreateChild is lightly altered to better fit with the already established createElement method. I've not added a createChildNS because nodes are not connected to a DOM in simple-xml so there is no link from an element to the document/root namespace container. --- API.md | 142 +++++++++++++++++++++++++++++++++++---------- lib/Document.ts | 28 +++++---- lib/Element.ts | 54 ++++++++++++++--- lib/Node.ts | 2 +- lib/constants.ts | 1 + lib/index.ts | 2 + lib/prettyPrint.ts | 12 ++++ lib/simplePrint.ts | 7 +++ 8 files changed, 195 insertions(+), 53 deletions(-) diff --git a/API.md b/API.md index e65fb92..f45511f 100644 --- a/API.md +++ b/API.md @@ -33,6 +33,7 @@ - [NOTATION\_NODE](#variablesnotation_nodemd) - [PROCESSING\_INSTRUCTION\_NODE](#variablesprocessing_instruction_nodemd) - [TEXT\_NODE](#variablestext_nodemd) +- [XML\_DECLARATION](#variablesxml_declarationmd) ## Functions @@ -244,9 +245,9 @@ Removes a child node from the DOM and returns the removed node. #### Parameters -| Parameter | Type | -| ------ | ------ | -| `child` | [`Node`](#classesnodemd) | +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `child` | [`Node`](#classesnodemd) | The child node to be removed. | #### Returns @@ -530,19 +531,23 @@ createElementNS( | CreateChildArgument[])[]): Element; ``` +Create a new element node associated with a given namespace. + #### Parameters -| Parameter | Type | -| ------ | ------ | -| `namespaceURI` | `string` | -| `qualifiedName` | `string` | -| `attr` | [`XMLAttr`](#type-aliasesxmlattrmd) \| `null` \| `undefined` | -| ...`children` | ( \| [`CreateChildArgument`](#type-aliasescreatechildargumentmd) \| [`CreateChildArgument`](#type-aliasescreatechildargumentmd)[])[] | +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `namespaceURI` | `string` | The namespaceURI to associate with the element. | +| `qualifiedName` | `string` | The local tagName of the element. | +| `attr` | [`XMLAttr`](#type-aliasesxmlattrmd) \| `null` \| `undefined` | A record of attributes to assign to the new element. If the value is null or undefined, the attribute will be omitted. | +| ...`children` | ( \| [`CreateChildArgument`](#type-aliasescreatechildargumentmd) \| [`CreateChildArgument`](#type-aliasescreatechildargumentmd)[])[] | Nodes to insert as children. Strings will be converted to TextNodes and arrays will be flattened. | #### Returns [`Element`](#classeselementmd) +A new Element instance. + *** ### getElementsByTagName() @@ -676,9 +681,9 @@ Removes a child node from the DOM and returns the removed node. #### Parameters -| Parameter | Type | -| ------ | ------ | -| `child` | [`Node`](#classesnodemd) | +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `child` | [`Node`](#classesnodemd) | The child node to be removed. | #### Returns @@ -820,7 +825,7 @@ A class describing an Element. ```ts new Element( tagName: string, - attr?: Record, + attr?: XMLAttr | null, closed?: boolean): Element; ``` @@ -831,7 +836,7 @@ Constructs a new Element instance. | Parameter | Type | Default value | Description | | ------ | ------ | ------ | ------ | | `tagName` | `string` | `undefined` | The tag name of the node. | -| `attr?` | `Record`\<`string`, `string`\> | `{}` | A collection of attributes to assign. Values of null or undefined will be ignored. | +| `attr?` | [`XMLAttr`](#type-aliasesxmlattrmd) \| `null` | `undefined` | A collection of attributes to assign. Values of null or undefined will be ignored. | | `closed?` | `boolean` | `false` | Was the element "self-closed" when read. | #### Returns @@ -1055,6 +1060,38 @@ The same node that was passed in. *** +### createChild() + +```ts +createChild( + qualifiedName: string, + attr?: XMLAttr | null, ... + children: ( + | CreateChildArgument + | CreateChildArgument[])[]): Element; +``` + +This method creates an element and immediately inserts it as a child of the element on which the +method was called. + +The method implicitly creates the new element in the same namespace as the parent element. + +#### Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `qualifiedName` | `string` | The local tagName of the element. | +| `attr?` | [`XMLAttr`](#type-aliasesxmlattrmd) \| `null` | A record of attributes to assign to the new element. If the value is null or undefined, the attribute will be omitted. | +| ...`children?` | ( \| [`CreateChildArgument`](#type-aliasescreatechildargumentmd) \| [`CreateChildArgument`](#type-aliasescreatechildargumentmd)[])[] | Nodes to insert as children. Strings will be converted to TextNodes and arrays will be flattened. | + +#### Returns + +`Element` + +A new Element instance. + +*** + ### getAttribute() ```ts @@ -1273,9 +1310,9 @@ Removes a child node from the DOM and returns the removed node. #### Parameters -| Parameter | Type | -| ------ | ------ | -| `child` | [`Node`](#classesnodemd) | +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `child` | [`Node`](#classesnodemd) | The child node to be removed. | #### Returns @@ -1310,6 +1347,26 @@ Sets an attribute on the element. *** +### setAttrValues() + +```ts +setAttrValues(attr: XMLAttr | null): void; +``` + +Assign multiple attributes at once to the current elemeent. + +#### Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `attr` | [`XMLAttr`](#type-aliasesxmlattrmd) \| `null` | A record of attributes to assign to the element. If the value is null or undefined, the attribute will be omitted. | + +#### Returns + +`void` + +*** + ### toJS() ```ts @@ -1512,9 +1569,9 @@ Removes a child node from the DOM and returns the removed node. #### Parameters -| Parameter | Type | -| ------ | ------ | -| `child` | `Node` | +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `child` | `Node` | The child node to be removed. | #### Returns @@ -1740,9 +1797,9 @@ Removes a child node from the DOM and returns the removed node. #### Parameters -| Parameter | Type | -| ------ | ------ | -| `child` | [`Node`](#classesnodemd) | +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `child` | [`Node`](#classesnodemd) | The child node to be removed. | #### Returns @@ -1858,17 +1915,26 @@ function prettyPrint(node: | DocumentFragment, indent?: string): string; ``` +Serialize a node tree to an XML string with indentation and whitespace +formatting for readability. + +Element children are placed on separate indented lines, except when the +element preserves whitespace (`xml:space="preserve"`), contains only text +nodes, or contains a single CDATA section. + ## Parameters -| Parameter | Type | Default value | -| ------ | ------ | ------ | -| `node` | \| [`Node`](#classesnodemd) \| [`DocumentFragment`](#classesdocumentfragmentmd) | `undefined` | -| `indent` | `string` | `''` | +| Parameter | Type | Default value | Description | +| ------ | ------ | ------ | ------ | +| `node` | \| [`Node`](#classesnodemd) \| [`DocumentFragment`](#classesdocumentfragmentmd) | `undefined` | The node to serialize. | +| `indent?` | `string` | `''` | The indentation applied to the current depth. | ## Returns `string` +The formatted XML string. + @@ -1880,16 +1946,21 @@ function simplePrint(node: | DocumentFragment): string; ``` +Serialize a node tree to a compact XML string without added whitespace, +preserving the original child order and content verbatim. + ## Parameters -| Parameter | Type | -| ------ | ------ | -| `node` | \| [`Node`](#classesnodemd) \| [`DocumentFragment`](#classesdocumentfragmentmd) | +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `node` | \| [`Node`](#classesnodemd) \| [`DocumentFragment`](#classesdocumentfragmentmd) | The node to serialize. | ## Returns `string` +The serialized XML string. + @@ -2069,3 +2140,14 @@ const TEXT_NODE: number = 3; ``` A text node identifier + + + + +# XML\_DECLARATION + +```ts +const XML_DECLARATION: "" = ''; +``` + +XML declaration string diff --git a/lib/Document.ts b/lib/Document.ts index 687138c..9fe9b44 100644 --- a/lib/Document.ts +++ b/lib/Document.ts @@ -86,19 +86,24 @@ export class Document extends Node { ...children: (CreateChildArgument | CreateChildArgument[])[] ): Element => { const element = new Element(qualifiedName); - if (attr) { - for (const [ key, val ] of Object.entries(attr)) { - if (val != null) { - element.setAttribute(key, String(val)); - } - } - } + element.setAttrValues(attr ?? null); for (const child of children) { element.append(child); } return element; }; + /** + * Create a new element node associated with a given namespace. + * + * @param namespaceURI The namespaceURI to associate with the element. + * @param qualifiedName The local tagName of the element. + * @param attr A record of attributes to assign to the new element. + * If the value is null or undefined, the attribute will be omitted. + * @param children Nodes to insert as children. + * Strings will be converted to TextNodes and arrays will be flattened. + * @returns A new Element instance. + */ createElementNS = ( namespaceURI: string, qualifiedName: string, @@ -110,14 +115,7 @@ export class Document extends Node { throw new Error('Unknown namespace ' + namespaceURI); } const element = new Element(ns ? ns + ':' + qualifiedName : qualifiedName); - // can this not be solved by Element(name, attr) ... does the same thing internally, right? - if (attr) { - for (const [ key, val ] of Object.entries(attr)) { - if (val != null) { - element.setAttribute(key, String(val)); - } - } - } + element.setAttrValues(attr ?? null); for (const child of children) { element.append(child); } diff --git a/lib/Element.ts b/lib/Element.ts index 580033e..0d2ae30 100644 --- a/lib/Element.ts +++ b/lib/Element.ts @@ -8,6 +8,7 @@ import { TextNode } from './TextNode.ts'; import type { CreateChildArgument } from './CreateChildArgument.ts'; import { prettyPrint } from './prettyPrint.ts'; import { simplePrint } from './simplePrint.ts'; +import type { XMLAttr } from './XMLAttr.ts'; // eslint-disable-next-line @typescript-eslint/unbound-method const hasOwnProperty = Object.prototype.hasOwnProperty; @@ -38,7 +39,7 @@ export class Element extends Node { * @param [attr={}] A collection of attributes to assign. Values of null or undefined will be ignored. * @param [closed=false] Was the element "self-closed" when read. */ - constructor (tagName: string, attr: Record = {}, closed: boolean = false) { + constructor (tagName: string, attr?: XMLAttr | null, closed: boolean = false) { super(); let tagName_ = tagName; let ns: string | null = null; @@ -51,12 +52,7 @@ export class Element extends Node { this.closed = !!closed; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment this.attr = Object.create(null); - for (const [ k, v ] of Object.entries(attr)) { - if (v != null) { - this.setAttribute(k, v); - } - } - + this.setAttrValues(attr ?? null); // inherited instance props from Node this.nodeName = this.tagName.toUpperCase(); this.nodeType = ELEMENT_NODE; @@ -123,6 +119,22 @@ export class Element extends Node { return this.attr && hasOwnProperty.call(this.attr, name); } + /** + * Assign multiple attributes at once to the current elemeent. + * + * @param attr A record of attributes to assign to the element. + * If the value is null or undefined, the attribute will be omitted. + */ + setAttrValues (attr: XMLAttr | null) { + if (attr) { + for (const [ key, val ] of Object.entries(attr)) { + if (val != null) { + this.setAttribute(key, val); + } + } + } + } + /** * Remove an attribute off the element. * @@ -172,6 +184,34 @@ export class Element extends Node { } } + /** + * This method creates an element and immediately inserts it as a child of the element on which the + * method was called. + * + * The method implicitly creates the new element in the same namespace as the parent element. + * + * @param qualifiedName The local tagName of the element. + * @param attr A record of attributes to assign to the new element. + * If the value is null or undefined, the attribute will be omitted. + * @param children Nodes to insert as children. + * Strings will be converted to TextNodes and arrays will be flattened. + * @returns A new Element instance. + */ + createChild ( + qualifiedName: string, + attr?: XMLAttr | null, + ...children: (CreateChildArgument | CreateChildArgument[])[] + ): Element { + const elm = new Element(qualifiedName); + elm.ns ??= this.ns; + elm.setAttrValues(attr ?? null); + this.appendChild(elm); + for (const child of children) { + elm.append(child); + } + return elm; + } + /** * Return all descendant elements that have the specified tag name. * diff --git a/lib/Node.ts b/lib/Node.ts index 6062825..5a1fb47 100644 --- a/lib/Node.ts +++ b/lib/Node.ts @@ -73,7 +73,7 @@ export class Node { /** * Removes a child node from the DOM and returns the removed node. - * @param node The child node to be removed. + * @param child The child node to be removed. * @returns The removed child node. */ removeChild (child: Node) { diff --git a/lib/constants.ts b/lib/constants.ts index 2ba3228..549749d 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -34,4 +34,5 @@ export const DOCUMENT_FRAGMENT_NODE: number = 11; /** A documentation node identifier */ export const NOTATION_NODE: number = 12; +/** XML declaration string */ export const XML_DECLARATION = ''; diff --git a/lib/index.ts b/lib/index.ts index f6280d4..0fb99fc 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -26,3 +26,5 @@ export { DOCUMENT_FRAGMENT_NODE, NOTATION_NODE } from './constants.js'; + +export { XML_DECLARATION } from './constants.js'; diff --git a/lib/prettyPrint.ts b/lib/prettyPrint.ts index 7634473..918f0a5 100644 --- a/lib/prettyPrint.ts +++ b/lib/prettyPrint.ts @@ -31,6 +31,18 @@ function printDocument (node: Node | DocumentFragment): string { .join('\n'); } +/** + * Serialize a node tree to an XML string with indentation and whitespace + * formatting for readability. + * + * Element children are placed on separate indented lines, except when the + * element preserves whitespace (`xml:space="preserve"`), contains only text + * nodes, or contains a single CDATA section. + * + * @param {Node | DocumentFragment} node The node to serialize. + * @param {string} [indent=''] The indentation applied to the current depth. + * @returns {string} The formatted XML string. + */ export function prettyPrint (node: Node | DocumentFragment, indent: string = ''): string { if (node instanceof DocumentFragment) { return printDocument(node); diff --git a/lib/simplePrint.ts b/lib/simplePrint.ts index 9229214..70382cf 100644 --- a/lib/simplePrint.ts +++ b/lib/simplePrint.ts @@ -7,6 +7,13 @@ import { CDATA_SECTION_NODE, DOCUMENT_NODE, TEXT_NODE } from './constants.js'; import { escape } from './escape.js'; import { isElement } from './isElement.ts'; +/** + * Serialize a node tree to a compact XML string without added whitespace, + * preserving the original child order and content verbatim. + * + * @param {Node | DocumentFragment} node The node to serialize. + * @returns {string} The serialized XML string. + */ export function simplePrint (node: Node | DocumentFragment): string { if (node instanceof DocumentFragment) { return node.childNodes.map(n => simplePrint(n)).join(''); From 4f9bc030b38bdac52459ea1a1a699e10ae79d815 Mon Sep 17 00:00:00 2001 From: Borgar Date: Sat, 23 May 2026 17:44:03 +0000 Subject: [PATCH 10/14] Conform attributes & element name props to w3c to get better compatibility with other libs --- lib/Attr.ts | 47 +++++ lib/Element.ts | 65 ++++--- lib/JsonML.ts | 8 +- lib/NamedNodeMap.spec.ts | 303 ++++++++++++++++++++++++++++++ lib/NamedNodeMap.ts | 93 +++++++++ lib/TextNode.ts | 14 +- lib/domQuery/filters.ts | 12 +- lib/findAll.ts | 2 +- lib/parser.ts | 19 +- {test => lib}/prettyPrint.spec.ts | 2 +- lib/prettyPrint.ts | 10 +- lib/simplePrint.ts | 7 +- lib/splitTagName.ts | 6 + test/domQuery-dedup.spec.ts | 8 +- test/getElementsByTagName.spec.ts | 2 +- test/parse-data.spec.ts | 2 - 16 files changed, 536 insertions(+), 64 deletions(-) create mode 100644 lib/Attr.ts create mode 100644 lib/NamedNodeMap.spec.ts create mode 100644 lib/NamedNodeMap.ts rename {test => lib}/prettyPrint.spec.ts (97%) create mode 100644 lib/splitTagName.ts diff --git a/lib/Attr.ts b/lib/Attr.ts new file mode 100644 index 0000000..7f09c19 --- /dev/null +++ b/lib/Attr.ts @@ -0,0 +1,47 @@ +import { Node } from './Node.js'; +import { ATTRIBUTE_NODE } from './constants.js'; +import { splitTagName } from './splitTagName.ts'; + +/** + * A class describing an attribute. + * + * @augments Node + */ +export class Attr extends Node { + /** The namespace prefix of the element, or null' if no prefix is specified. */ + prefix: string | null; + /** The name of the tag for the given element, excluding any namespace prefix. */ + localName: string; + + /** The node's name. */ + name: string; + /** The node's data value. */ + value: string; + + /** + * Constructs a new Attr instance. + * + * @param name The name of the attribute. + * @param value The data of the attribute. + */ + constructor (name: string, value: any) { + super(); + + const [ prefix, localName ] = splitTagName(name); + this.prefix = prefix; + this.localName = localName; + this.name = localName; + this.nodeName = localName; + + this.value = String(value); + + this.nodeType = ATTRIBUTE_NODE; + } + + /** The full name of the tag for the given element, including a namespace prefix. */ + get fullName () { + return this.prefix + ? this.prefix + ':' + this.localName + : this.localName; + } +} diff --git a/lib/Element.ts b/lib/Element.ts index 0d2ae30..626db91 100644 --- a/lib/Element.ts +++ b/lib/Element.ts @@ -9,9 +9,9 @@ import type { CreateChildArgument } from './CreateChildArgument.ts'; import { prettyPrint } from './prettyPrint.ts'; import { simplePrint } from './simplePrint.ts'; import type { XMLAttr } from './XMLAttr.ts'; - -// eslint-disable-next-line @typescript-eslint/unbound-method -const hasOwnProperty = Object.prototype.hasOwnProperty; +import { createNamedNodeMap, type NamedNodeMap } from './NamedNodeMap.ts'; +import { Attr } from './Attr.ts'; +import { splitTagName } from './splitTagName.ts'; /** * A class describing an Element. @@ -19,18 +19,16 @@ const hasOwnProperty = Object.prototype.hasOwnProperty; * @augments Node */ export class Element extends Node { - /** The namespace prefix of the element, or null if no prefix is specified. */ - ns: string; + /** The namespace prefix of the element, or null' if no prefix is specified. */ + prefix: string | null; /** The name of the tag for the given element, excluding any namespace prefix. */ - tagName: string; - /** The full name of the tag for the given element, including a namespace prefix. */ - fullName: string; + localName: string; /** A state representing if the element was "self-closed" when read. */ closed: boolean; - /** An object of attributes assigned to this element. */ - attr: Record; /** The node's parent node. */ parentNode: Element | null = null; + /** A list of attributes assigned to this element. */ + attributes: NamedNodeMap; /** * Constructs a new Element instance. @@ -41,27 +39,40 @@ export class Element extends Node { */ constructor (tagName: string, attr?: XMLAttr | null, closed: boolean = false) { super(); - let tagName_ = tagName; - let ns: string | null = null; - if (tagName.includes(':')) { - [ ns, tagName_ ] = tagName.split(':'); - } - this.ns = ns || ''; - this.tagName = tagName_; - this.fullName = tagName; + + const [ prefix, localName ] = splitTagName(tagName); + this.prefix = prefix; + this.localName = localName; + this.closed = !!closed; - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - this.attr = Object.create(null); + + this.attributes = createNamedNodeMap(); this.setAttrValues(attr ?? null); + // inherited instance props from Node - this.nodeName = this.tagName.toUpperCase(); + this.nodeName = this.localName.toUpperCase(); this.nodeType = ELEMENT_NODE; this.childNodes = []; } + get tagName () { + return this.localName; + } + + /** The full name of the tag for the given element, including a namespace prefix. */ + get fullName () { + return this.prefix + ? this.prefix + ':' + this.localName + : this.localName; + } + + hasAttributes () { + return !!this.attributes.length; + } + // overwrites super get preserveSpace (): boolean { - if (this.attr?.['xml:space'] === 'preserve') { + if (this.getAttribute('xml:space') === 'preserve') { return true; } if (this.parentNode) { @@ -96,7 +107,7 @@ export class Element extends Node { * @returns The attribute. */ getAttribute (name: string): string | null { - return this.hasAttribute(name) ? this.attr[name] : null; + return this.attributes.getNamedItem(name)?.value ?? null; } /** @@ -106,7 +117,7 @@ export class Element extends Node { * @param value The value to set */ setAttribute (name: string, value: string | number | boolean) { - this.attr[name] = String(value); + this.attributes.setNamedItem(new Attr(name, value)); } /** @@ -116,7 +127,7 @@ export class Element extends Node { * @returns True if the attribute is present. */ hasAttribute (name: string): boolean { - return this.attr && hasOwnProperty.call(this.attr, name); + return this.attributes.getNamedItem(name) != null; } /** @@ -141,7 +152,7 @@ export class Element extends Node { * @param name The attribute name to remove. */ removeAttribute (name: string) { - delete this.attr[name]; + this.attributes.removeNamedItem(name); } get className (): string { @@ -203,7 +214,7 @@ export class Element extends Node { ...children: (CreateChildArgument | CreateChildArgument[])[] ): Element { const elm = new Element(qualifiedName); - elm.ns ??= this.ns; + elm.prefix ??= this.prefix; elm.setAttrValues(attr ?? null); this.appendChild(elm); for (const child of children) { diff --git a/lib/JsonML.ts b/lib/JsonML.ts index f10e739..57d8ec1 100644 --- a/lib/JsonML.ts +++ b/lib/JsonML.ts @@ -20,9 +20,13 @@ export function JsonML (node: Element): JsonMLElement { // name of the element const n: JsonMLElement = [ node.fullName ?? node.nodeName ]; // it's attributes as an object - if (node.attr && Object.keys(node.attr).length) { + if (node.attributes?.length) { + const attr: JsonMLAttr = {}; + for (const a of node.attributes) { + attr[a.fullName] = a.value; + } // @ts-expect-error -- TS has trouble figuring out how this is built - n.push(Object.assign({}, node.attr) as JsonMLAttr); + n.push(attr); } // it's content if (node.childNodes?.length) { diff --git a/lib/NamedNodeMap.spec.ts b/lib/NamedNodeMap.spec.ts new file mode 100644 index 0000000..3eca614 --- /dev/null +++ b/lib/NamedNodeMap.spec.ts @@ -0,0 +1,303 @@ +import { describe, it, expect } from 'vitest'; +import { Element } from '../lib/index.ts'; +import { Attr } from '../lib/Attr.ts'; + +describe('NamedNodeMap', () => { + describe('construction and identity', () => { + it('a new Element exposes an attributes NamedNodeMap', () => { + const el = new Element('foo'); + expect(el.attributes).toBeDefined(); + expect(el.attributes).not.toBeNull(); + }); + + it('the attributes map is empty for an element constructed with no attrs', () => { + const el = new Element('foo'); + expect(el.attributes.length).toBe(0); + }); + + it('returns the same NamedNodeMap instance on repeated reads', () => { + const el = new Element('foo'); + expect(el.attributes).toBe(el.attributes); + }); + + it('each element gets its own independent NamedNodeMap', () => { + const a = new Element('a'); + const b = new Element('b'); + a.setAttribute('x', '1'); + expect(b.attributes.length).toBe(0); + expect(a.attributes).not.toBe(b.attributes); + }); + }); + + describe('length', () => { + it('reflects the number of attributes assigned at construction time', () => { + const el = new Element('foo', { a: '1', b: '2', c: '3' }); + expect(el.attributes.length).toBe(3); + }); + + it('grows when new attributes are added via setAttribute', () => { + const el = new Element('foo'); + expect(el.attributes.length).toBe(0); + el.setAttribute('a', '1'); + expect(el.attributes.length).toBe(1); + el.setAttribute('b', '2'); + expect(el.attributes.length).toBe(2); + }); + + it('does not grow when an existing attribute is overwritten', () => { + const el = new Element('foo'); + el.setAttribute('a', '1'); + el.setAttribute('a', '2'); + expect(el.attributes.length).toBe(1); + }); + + it('shrinks when an attribute is removed via removeAttribute', () => { + const el = new Element('foo', { a: '1', b: '2' }); + el.removeAttribute('a'); + expect(el.attributes.length).toBe(1); + el.removeAttribute('b'); + expect(el.attributes.length).toBe(0); + }); + + it('is read-only (assigning to length has no effect)', () => { + const el = new Element('foo', { a: '1', b: '2' }); + try { + // @ts-expect-error - testing runtime immutability + el.attributes.length = 0; + } + catch { + // strict mode may throw; either way, length should not change + } + expect(el.attributes.length).toBe(2); + }); + }); + + describe('getNamedItem', () => { + it('returns an Attr object for an attribute that exists', () => { + const el = new Element('foo', { color: 'red' }); + const attr = el.attributes.getNamedItem('color'); + expect(attr).not.toBeNull(); + expect(attr?.value).toBe('red'); + }); + + it('returns null for an attribute that does not exist', () => { + const el = new Element('foo', { color: 'red' }); + expect(el.attributes.getNamedItem('missing')).toBeNull(); + }); + + it('is case-sensitive (XML semantics)', () => { + const el = new Element('foo', { color: 'red' }); + expect(el.attributes.getNamedItem('Color')).toBeNull(); + expect(el.attributes.getNamedItem('COLOR')).toBeNull(); + expect(el.attributes.getNamedItem('color')?.value).toBe('red'); + }); + + it('reflects updated values after setAttribute overwrites an existing attribute', () => { + const el = new Element('foo', { color: 'red' }); + el.setAttribute('color', 'blue'); + expect(el.attributes.getNamedItem('color')?.value).toBe('blue'); + }); + + it('returns null after the attribute has been removed', () => { + const el = new Element('foo', { color: 'red' }); + el.removeAttribute('color'); + expect(el.attributes.getNamedItem('color')).toBeNull(); + }); + }); + + describe('setNamedItem', () => { + it('adds a new Attr to the map and increases length', () => { + const el = new Element('foo'); + el.attributes.setNamedItem(new Attr('color', 'red')); + expect(el.attributes.length).toBe(1); + expect(el.attributes.getNamedItem('color')?.value).toBe('red'); + }); + + it('replaces an existing attribute with the same name', () => { + const el = new Element('foo', { color: 'red' }); + el.attributes.setNamedItem(new Attr('color', 'green')); + expect(el.attributes.length).toBe(1); + expect(el.attributes.getNamedItem('color')?.value).toBe('green'); + }); + + it('returns null when adding a brand-new attribute (per DOM spec)', () => { + const el = new Element('foo'); + const result = el.attributes.setNamedItem(new Attr('color', 'red')); + expect(result).toBeNull(); + }); + + it('returns the previously-stored Attr when replacing (per DOM spec)', () => { + const el = new Element('foo', { color: 'red' }); + const previous = el.attributes.getNamedItem('color'); + const result = el.attributes.setNamedItem(new Attr('color', 'green')); + expect(result).toBe(previous); + }); + + it('Element.setAttribute is observable through the NamedNodeMap', () => { + const el = new Element('foo'); + el.setAttribute('a', '1'); + expect(el.attributes.getNamedItem('a')?.value).toBe('1'); + }); + }); + + describe('removeNamedItem', () => { + it('removes the named attribute and decreases length', () => { + const el = new Element('foo', { a: '1', b: '2' }); + el.attributes.removeNamedItem('a'); + expect(el.attributes.length).toBe(1); + expect(el.attributes.getNamedItem('a')).toBeNull(); + expect(el.attributes.getNamedItem('b')?.value).toBe('2'); + }); + + it('returns the removed Attr (per DOM spec)', () => { + const el = new Element('foo', { color: 'red' }); + const target = el.attributes.getNamedItem('color'); + const removed = el.attributes.removeNamedItem('color'); + expect(removed).toBe(target); + }); + + it('throws (or otherwise signals failure) when removing a name that does not exist', () => { + const el = new Element('foo'); + // Per DOM spec a NotFoundError should be thrown; some implementations + // return null instead. Accept either, but never silently mutate state. + let threw = false; + let returned: unknown = undefined; + try { + returned = el.attributes.removeNamedItem('missing'); + } + catch { + threw = true; + } + expect(threw || returned === null).toBe(true); + expect(el.attributes.length).toBe(0); + }); + + it('Element.removeAttribute is observable through the NamedNodeMap', () => { + const el = new Element('foo', { a: '1' }); + el.removeAttribute('a'); + expect(el.attributes.length).toBe(0); + expect(el.attributes.getNamedItem('a')).toBeNull(); + }); + }); + + describe('item() and indexed access', () => { + it('item(i) returns the Attr at the given position', () => { + const el = new Element('foo', { a: '1', b: '2', c: '3' }); + const first = el.attributes.item(0); + const second = el.attributes.item(1); + const third = el.attributes.item(2); + expect(first?.name).toBe('a'); + expect(first?.value).toBe('1'); + expect(second?.name).toBe('b'); + expect(third?.name).toBe('c'); + }); + + it('item(i) returns null for out-of-range indices', () => { + const el = new Element('foo', { a: '1' }); + expect(el.attributes.item(1)).toBeNull(); + expect(el.attributes.item(99)).toBeNull(); + expect(el.attributes.item(-1)).toBeNull(); + }); + + it('preserves insertion order across constructor + setAttribute', () => { + const el = new Element('foo', { a: '1', b: '2' }); + el.setAttribute('c', '3'); + el.setAttribute('d', '4'); + const names: string[] = []; + for (let i = 0; i < el.attributes.length; i++) { + names.push(el.attributes.item(i).name); + } + expect(names).toEqual([ 'a', 'b', 'c', 'd' ]); + }); + + it('keeps an attribute in place when its value is overwritten', () => { + const el = new Element('foo', { a: '1', b: '2', c: '3' }); + el.setAttribute('b', '20'); + const names: string[] = []; + for (let i = 0; i < el.attributes.length; i++) { + names.push(el.attributes.item(i).name); + } + expect(names).toEqual([ 'a', 'b', 'c' ]); + expect(el.attributes.getNamedItem('b')?.value).toBe('20'); + }); + + it('closes the gap when an attribute is removed (no holes)', () => { + const el = new Element('foo', { a: '1', b: '2', c: '3' }); + el.removeAttribute('b'); + expect(el.attributes.length).toBe(2); + expect(el.attributes.item(0)?.name).toBe('a'); + expect(el.attributes.item(1)?.name).toBe('c'); + expect(el.attributes.item(2)).toBeNull(); + }); + }); + + describe('iteration', () => { + it('is iterable with for...of, yielding Attr objects', () => { + const el = new Element('foo', { a: '1', b: '2', c: '3' }); + const collected: [string, string][] = []; + for (const attr of el.attributes) { + collected.push([ attr.name, attr.value ]); + } + expect(collected).toEqual([ [ 'a', '1' ], [ 'b', '2' ], [ 'c', '3' ] ]); + }); + + it('supports the spread operator', () => { + const el = new Element('foo', { a: '1', b: '2' }); + const spread = [ ...el.attributes ]; + expect(spread).toHaveLength(2); + expect(spread[0].name).toBe('a'); + expect(spread[1].name).toBe('b'); + }); + + it('Array.from converts the map into an array of Attrs', () => { + const el = new Element('foo', { a: '1', b: '2' }); + const arr = Array.from(el.attributes); + expect(arr).toHaveLength(2); + expect(arr.map(a => a.name)).toEqual([ 'a', 'b' ]); + }); + }); + + describe('Attr objects exposed by the map', () => { + it('expose a name and value that match what was assigned', () => { + const el = new Element('foo', { color: 'red' }); + const attr = el.attributes.getNamedItem('color'); + expect(attr?.name).toBe('color'); + expect(attr?.value).toBe('red'); + }); + + it('reflect later updates to the same attribute name', () => { + const el = new Element('foo', { color: 'red' }); + const first = el.attributes.getNamedItem('color'); + el.setAttribute('color', 'blue'); + const second = el.attributes.getNamedItem('color'); + // After a replacement, the value reported should be 'blue' whether or + // not the same Attr instance is reused. + expect(second?.value).toBe('blue'); + expect(first === second ? first?.value : 'blue').toBe('blue'); + }); + }); + + describe('live behavior', () => { + it('changes made through Element are immediately visible on the map', () => { + const el = new Element('foo'); + const map = el.attributes; + expect(map.length).toBe(0); + el.setAttribute('a', '1'); + expect(map.length).toBe(1); + expect(map.getNamedItem('a')?.value).toBe('1'); + el.removeAttribute('a'); + expect(map.length).toBe(0); + expect(map.getNamedItem('a')).toBeNull(); + }); + + it('changes made through the map are visible on Element accessors', () => { + const el = new Element('foo'); + el.attributes.setNamedItem(new Attr('a', '1')); + expect(el.getAttribute('a')).toBe('1'); + expect(el.hasAttribute('a')).toBe(true); + el.attributes.removeNamedItem('a'); + expect(el.getAttribute('a')).toBeNull(); + expect(el.hasAttribute('a')).toBe(false); + }); + }); +}); diff --git a/lib/NamedNodeMap.ts b/lib/NamedNodeMap.ts new file mode 100644 index 0000000..0adfa48 --- /dev/null +++ b/lib/NamedNodeMap.ts @@ -0,0 +1,93 @@ +import type { Attr } from './Attr.ts'; + +/** + * Runtime class for {@link NamedNodeMap}. Kept un-exported so that the public + * `NamedNodeMap` type can be widened with an `Attr` index signature without + * the index signature having to be compatible with the class's typed members + * (`length`, `getNamedItem(): Attr | null`, etc.). + */ +class NamedNodeMapImpl { + #attr: Attr[]; + + constructor () { + this.#attr = []; + } + + item (index: number) { + return this.#attr[index] ?? null; + } + + getNamedItem (qualifiedName: string) { + return this.#attr.find(d => d.fullName === qualifiedName) ?? null; + } + + // Returns the old attribute if replaced, or null if the attribute is new. + setNamedItem (attr: Attr) { + const fn = attr.fullName; + const existing = this.#attr.findIndex(d => d.fullName === fn); + if (existing > -1) { + const old = this.#attr[existing]; + this.#attr[existing] = attr; + return old; + } + else { + this.#attr.push(attr); + } + return null; + } + + removeNamedItem (attrName: string) { + const existing = this.#attr.findIndex(d => d.nodeName === attrName); + if (existing > -1) { + const old = this.#attr.splice(existing, 1); + return old[0]; + } + // XXX: DOMException + throw new Error(`NotFoundError: Failed to execute 'removeNamedItem' on 'NamedNodeMap': No item with name '${attrName}' was found.`); + } + + get length () { + return Object.keys(this.#attr).length; + } + + *[Symbol.iterator] (): IterableIterator { + yield* this.#attr; + } +} + +const nameNodeMapProxy = { + get (target: NamedNodeMap, prop: string) { + if (prop in NamedNodeMapImpl.prototype) { + const value = Reflect.get(target, prop, target); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + return typeof value === 'function' ? (value as any).bind(target) : value; + } + const attr = target.getNamedItem(prop); + if (attr) { + return attr; + } + return null; + }, + + has (target: NamedNodeMap, key: string) { + if (key in target) { + return true; + } + // integers + const num = Number(key); + if (Number.isFinite(num) && !(num % 1)) { + return num < target.length; + } + // propnames + if (target.getNamedItem(key)) { + return true; + } + return false; + } +}; + +export type NamedNodeMap = NamedNodeMapImpl & Readonly>; + +export function createNamedNodeMap (): NamedNodeMap { + return new Proxy(new NamedNodeMapImpl(), nameNodeMapProxy) as NamedNodeMap; +} diff --git a/lib/TextNode.ts b/lib/TextNode.ts index cc86cae..8c24e8c 100644 --- a/lib/TextNode.ts +++ b/lib/TextNode.ts @@ -8,7 +8,7 @@ import { TEXT_NODE } from './constants.js'; */ export class TextNode extends Node { /** The node's data value. */ - value: string; + data: string; /** * Constructs a new TextNode instance. @@ -19,12 +19,20 @@ export class TextNode extends Node { super(); this.nodeName = '#text'; this.nodeType = TEXT_NODE; - this.value = String(value); + this.data = String(value); + } + + get nodeValue () { + return this.data; + } + + set nodeValue (value) { + this.data = String(value); } // overwrites super get textContent () { - const s = this.value; + const s = this.data; // pure whitespace nodes are ignored when xml:space="default" if (!/[^\r\n\t ]/.test(s)) { return this.preserveSpace ? s : ''; diff --git a/lib/domQuery/filters.ts b/lib/domQuery/filters.ts index 4494fef..fa877e7 100644 --- a/lib/domQuery/filters.ts +++ b/lib/domQuery/filters.ts @@ -14,7 +14,7 @@ export const FILTERS: Record = { const found: Element[] = []; for (const parent of elms) { parent.children.forEach(elm => { - if (nameMatch(tagName, elm.tagName)) { + if (nameMatch(tagName, elm.localName)) { found.push(elm); } }); @@ -30,7 +30,7 @@ export const FILTERS: Record = { const nodes = elm.parentNode.children; const idx = nodes.indexOf(elm); const prev = nodes.at(idx + 1); - if (prev && nameMatch(tagName, prev.tagName)) { + if (prev && nameMatch(tagName, prev.localName)) { found.push(prev); } } @@ -47,7 +47,7 @@ export const FILTERS: Record = { if (parent && !parents.includes(parent)) { parents.push(parent); const elmIdx = parent.childNodes.indexOf(elm); - const siblings = parent.children.filter(d => nameMatch(tagName, d.tagName)); + const siblings = parent.children.filter(d => nameMatch(tagName, d.localName)); for (let i = 0; i < siblings.length; i++) { const ref = siblings.at(i)!; if (ref.parentNode && elmIdx < ref.parentNode.childNodes.indexOf(ref)) { @@ -117,9 +117,9 @@ export const FILTERS: Record = { let siblings = [ elm ]; if (elm.parentNode) { siblings = elm.parentNode.children; - const tn = tagName === '*' ? elm.tagName : tagName; + const tn = tagName === '*' ? elm.localName : tagName; if (tn !== '*' && tn) { - siblings = siblings.filter(n => nameMatch(tn, n.tagName)); + siblings = siblings.filter(n => nameMatch(tn, n.localName)); } } const index = siblings.indexOf(elm) + 1; @@ -175,7 +175,7 @@ export const FILTERS: Record = { ), byTagName: (s, elms, not) => ( - elms.filter(d => xor(d.tagName === s, not)) + elms.filter(d => xor(d.localName === s, not)) ), attrEqual: (s, elms, not, name) => ( diff --git a/lib/findAll.ts b/lib/findAll.ts index 71904bb..42b2900 100644 --- a/lib/findAll.ts +++ b/lib/findAll.ts @@ -7,7 +7,7 @@ export function findAll (node: Element | Document, tagName: string, list: Elemen if (ch) { for (const c of ch) { if (isElement(c)) { - if (c.tagName === tagName || tagName === '*') { + if (c.localName === tagName || tagName === '*') { list.push(c); } // and its children... (traversal order) diff --git a/lib/parser.ts b/lib/parser.ts index 575d0b1..2663456 100644 --- a/lib/parser.ts +++ b/lib/parser.ts @@ -10,7 +10,8 @@ import { parseAttr } from './parseAttr.js'; const DEFAULTOPTIONS = { emptyDoc: false, - laxAttr: false + laxAttr: false, + ns: false }; const NON_ELEMENT = new Element('#'); @@ -294,16 +295,16 @@ export function parseXML ( } function checkNS (elm: Element) { - for (const key in elm.attr) { - if (key === 'xmlns') { - doc.attachNS(elm.attr[key], ''); + for (const attr of elm.attributes) { + if (attr.localName === 'xmlns') { + doc.attachNS(attr.value, ''); } - if (key.startsWith('xmlns:')) { - doc.attachNS(elm.attr[key], key.slice(6)); + if (attr.prefix === 'xmlns') { + doc.attachNS(attr.value, attr.localName); } } - if (elm.ns && !doc.namespaces.getByPrefix(elm.ns)) { - throw new Error('Unknown namespace prefix ' + elm.ns); + if (elm.prefix && !doc.namespaces.getByPrefix(elm.prefix)) { + throw new Error('Unknown namespace prefix ' + elm.prefix); } } @@ -401,7 +402,7 @@ export function parseXML ( // root should have been closed if (root !== NON_ELEMENT && !root.closed && current !== null) { - throw new Error(`Expected got EOF`); + throw new Error(`Expected got EOF`); } if (root !== NON_ELEMENT) { diff --git a/test/prettyPrint.spec.ts b/lib/prettyPrint.spec.ts similarity index 97% rename from test/prettyPrint.spec.ts rename to lib/prettyPrint.spec.ts index a906a44..a555089 100644 --- a/test/prettyPrint.spec.ts +++ b/lib/prettyPrint.spec.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { Element, parseXML, TextNode } from '../lib/index.js'; +import { Element, parseXML, TextNode } from './index.ts'; describe('prettyPrint', () => { it('simple tag', () => { diff --git a/lib/prettyPrint.ts b/lib/prettyPrint.ts index 918f0a5..2efaecc 100644 --- a/lib/prettyPrint.ts +++ b/lib/prettyPrint.ts @@ -9,16 +9,18 @@ import { isElement } from './isElement.ts'; function printAttributes (node: Node): string { let attrList = ''; if (isElement(node)) { - const attr = node.attr; - for (const [ key, val ] of Object.entries(attr)) { - attrList += ` ${key}="${escape(val)}"`; + for (const attr of node.attributes) { + attrList += ` ${attr.fullName}="${escape(attr.value)}"`; } + // for (const [ key, val ] of Object.entries(attr)) { + // attrList += ` ${key}="${escape(val)}"`; + // } } return attrList; } function printTextNode (node: TextNode): string { - return escape(node.value); + return escape(node.data); } function printCData (node: CDataNode) { diff --git a/lib/simplePrint.ts b/lib/simplePrint.ts index 70382cf..4805468 100644 --- a/lib/simplePrint.ts +++ b/lib/simplePrint.ts @@ -27,7 +27,7 @@ export function simplePrint (node: Node | DocumentFragment): string { return `/g, ']]>')}]]>`; } else if (node.nodeType === TEXT_NODE) { - return escape((node as TextNode).value); + return escape((node as TextNode).data); } else if (isElement(node)) { const tagName = node.fullName; @@ -38,9 +38,8 @@ export function simplePrint (node: Node | DocumentFragment): string { } let attrList = ''; if (isElement(node)) { - const attr = node.attr; - for (const [ key, val ] of Object.entries(attr)) { - attrList += ` ${escape(key)}="${escape(val)}"`; + for (const attr of node.attributes) { + attrList += ` ${attr.fullName}="${escape(attr.value)}"`; } } return children diff --git a/lib/splitTagName.ts b/lib/splitTagName.ts new file mode 100644 index 0000000..b94e2ad --- /dev/null +++ b/lib/splitTagName.ts @@ -0,0 +1,6 @@ +export function splitTagName (tagName: string): [ string, string ] | [ null, string ] { + if (tagName.includes(':')) { + return tagName.split(':').slice(0, 2) as [ string, string ]; + } + return [ null, tagName ]; +} diff --git a/test/domQuery-dedup.spec.ts b/test/domQuery-dedup.spec.ts index d2b1503..9358d4e 100644 --- a/test/domQuery-dedup.spec.ts +++ b/test/domQuery-dedup.spec.ts @@ -5,7 +5,7 @@ describe('domQuery dedup fix', () => { it('preserves document order for single-group selectors', () => { const doc = parseXML(''); const result = doc.root!.querySelectorAll('*'); - expect(result.map(e => e.tagName)).toEqual([ 'x', 'y', 'z' ]); + expect(result.map(e => e.localName)).toEqual([ 'x', 'y', 'z' ]); }); it('preserves document order for multi-group (comma) selectors', () => { @@ -13,7 +13,7 @@ describe('domQuery dedup fix', () => { // Even if groups are listed out of document order, results should // be in document order (via the getElementsByTagName tree walk). const result = doc.root!.querySelectorAll('c, a'); - expect(result.map(e => e.tagName)).toEqual([ 'a', 'c' ]); + expect(result.map(e => e.localName)).toEqual([ 'a', 'c' ]); }); it('deduplicates descendant combinator results', () => { @@ -21,7 +21,7 @@ describe('domQuery dedup fix', () => { const doc = parseXML(''); // "a c" matches through both 's; should find exactly one const result = doc.root!.querySelectorAll('a c'); - expect(result.map(e => e.tagName)).toEqual([ 'c' ]); + expect(result.map(e => e.localName)).toEqual([ 'c' ]); }); it('deduplicates multi-group (comma) selector results', () => { @@ -31,6 +31,6 @@ describe('domQuery dedup fix', () => { const doc = parseXML(''); const result = doc.root!.querySelectorAll('a c, b c'); expect(result.length).toBe(1); - expect(result[0].tagName).toBe('c'); + expect(result[0].localName).toBe('c'); }); }); diff --git a/test/getElementsByTagName.spec.ts b/test/getElementsByTagName.spec.ts index 6c48253..daf5771 100644 --- a/test/getElementsByTagName.spec.ts +++ b/test/getElementsByTagName.spec.ts @@ -35,7 +35,7 @@ describe('getElementByTagName', () => { `); - const m1 = dom.getElementsByTagName('d').map(d => +d.attr.o); + const m1 = dom.getElementsByTagName('d').map(d => +d.attributes.o.value); expect(m1).toEqual([ 1, 2, 3, 4, 5, 6, 7, 8 ]); }); }); diff --git a/test/parse-data.spec.ts b/test/parse-data.spec.ts index fa7505c..56324ee 100644 --- a/test/parse-data.spec.ts +++ b/test/parse-data.spec.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { describe, it, expect } from 'vitest'; import fs from 'fs'; import { parseXML } from '../lib/index.js'; From d17723c8ffc56abca358045add49078118c47c92 Mon Sep 17 00:00:00 2001 From: Borgar Date: Sat, 23 May 2026 17:54:05 +0000 Subject: [PATCH 11/14] Use error subclasses to allow instanceof filtering for specific catches --- lib/Document.ts | 17 +++++++------- lib/Element.ts | 6 ++--- lib/NSMap.ts | 4 +++- lib/NamedNodeMap.ts | 4 ++-- lib/Node.ts | 8 +++---- lib/appendChild.ts | 3 ++- lib/errors.ts | 57 +++++++++++++++++++++++++++++++++++++++++++++ lib/index.ts | 7 ++++++ lib/parseAttr.ts | 11 +++++---- lib/parser.ts | 15 ++++++------ lib/simplePrint.ts | 3 ++- lib/unquote.ts | 4 +++- 12 files changed, 106 insertions(+), 33 deletions(-) create mode 100644 lib/errors.ts diff --git a/lib/Document.ts b/lib/Document.ts index 9fe9b44..ad72f95 100644 --- a/lib/Document.ts +++ b/lib/Document.ts @@ -12,6 +12,7 @@ import { prettyPrint } from './prettyPrint.ts'; import { simplePrint } from './simplePrint.ts'; import type { CreateChildArgument } from './CreateChildArgument.ts'; import type { XMLAttr } from './XMLAttr.ts'; +import { HierarchyError, NamespaceError } from './errors.js'; /** * This class describes an XML document. @@ -112,7 +113,7 @@ export class Document extends Node { ): Element => { const ns = this.namespaces.get(namespaceURI); if (ns == null) { - throw new Error('Unknown namespace ' + namespaceURI); + throw new NamespaceError('Unknown namespace ' + namespaceURI); } const element = new Element(ns ? ns + ':' + qualifiedName : qualifiedName); element.setAttrValues(attr ?? null); @@ -130,7 +131,7 @@ export class Document extends Node { */ getElementsByTagName (tagName: string): Element[] { if (!tagName) { - throw new Error('1 argument required, but 0 present.'); + throw new TypeError('1 argument required, but 0 present.'); } return findAll(this, tagName, []); } @@ -143,7 +144,7 @@ export class Document extends Node { */ querySelector (selector: string): Element | null { if (!selector) { - throw new Error('1 argument required, but 0 present.'); + throw new TypeError('1 argument required, but 0 present.'); } return domQuery(this, selector)[0] || null; } @@ -156,7 +157,7 @@ export class Document extends Node { */ querySelectorAll (selector: string): Element[] { if (!selector) { - throw new Error('1 argument required, but 0 present.'); + throw new TypeError('1 argument required, but 0 present.'); } return domQuery(this, selector); } @@ -164,12 +165,12 @@ export class Document extends Node { // overwrites super appendChild (node: T): T { if (this.root || (node instanceof DocumentFragment && node.childNodes.length > 1)) { - throw new Error('A document must have only one child element.'); + throw new HierarchyError('A document must have only one child element.'); } let root: Element; if (node instanceof DocumentFragment) { if (!(node.childNodes[0] instanceof Element)) { - throw new Error('Document root node must be an Element'); + throw new HierarchyError('Document root node must be an Element'); } root = node.childNodes[0]; } @@ -177,7 +178,7 @@ export class Document extends Node { root = node; } else { - throw new Error('Document root node must be an Element'); + throw new HierarchyError('Document root node must be an Element'); } appendChild(this, root); this.root = root; @@ -202,7 +203,7 @@ export class Document extends Node { */ print (pretty = false): string { if (!(this.root instanceof Element)) { - throw new Error('root element is missing'); + throw new HierarchyError('root element is missing'); } return `${XML_DECLARATION}\n` + ( pretty ? prettyPrint(this.root) : simplePrint(this.root) diff --git a/lib/Element.ts b/lib/Element.ts index 626db91..7425c86 100644 --- a/lib/Element.ts +++ b/lib/Element.ts @@ -231,7 +231,7 @@ export class Element extends Node { */ getElementsByTagName (tagName: string): Element[] { if (!tagName) { - throw new Error('1 argument required, but 0 present.'); + throw new TypeError('1 argument required, but 0 present.'); } // @ts-ignore return findAll(this, tagName, []); @@ -245,7 +245,7 @@ export class Element extends Node { */ querySelector (selector: string): Element | null { if (!selector) { - throw new Error('1 argument required, but 0 present.'); + throw new TypeError('1 argument required, but 0 present.'); } return domQuery(this, selector)[0] || null; } @@ -258,7 +258,7 @@ export class Element extends Node { */ querySelectorAll (selector: string): Element[] { if (!selector) { - throw new Error('1 argument required, but 0 present.'); + throw new TypeError('1 argument required, but 0 present.'); } return domQuery(this, selector); } diff --git a/lib/NSMap.ts b/lib/NSMap.ts index 1a5d39a..ae00fba 100644 --- a/lib/NSMap.ts +++ b/lib/NSMap.ts @@ -1,3 +1,5 @@ +import { NamespaceError } from './errors.js'; + export class NSMap { private uriToPres: Record = {}; private uriToPre: Record = {}; @@ -24,7 +26,7 @@ export class NSMap { add (nsURI: string, nsPrefix: string) { // A prefix can only point to one URI — collisions are an error. if ((nsPrefix in this.preToUri) && (this.preToUri[nsPrefix] !== nsURI)) { - throw new Error(nsPrefix + ' already has a different URI'); + throw new NamespaceError(nsPrefix + ' already has a different URI'); } // Registering the same pair twice is a no-op. if ((nsURI in this.uriToPres) && this.uriToPres[nsURI].includes(nsPrefix)) { diff --git a/lib/NamedNodeMap.ts b/lib/NamedNodeMap.ts index 0adfa48..e8c783b 100644 --- a/lib/NamedNodeMap.ts +++ b/lib/NamedNodeMap.ts @@ -1,4 +1,5 @@ import type { Attr } from './Attr.ts'; +import { NotFoundError } from './errors.js'; /** * Runtime class for {@link NamedNodeMap}. Kept un-exported so that the public @@ -42,8 +43,7 @@ class NamedNodeMapImpl { const old = this.#attr.splice(existing, 1); return old[0]; } - // XXX: DOMException - throw new Error(`NotFoundError: Failed to execute 'removeNamedItem' on 'NamedNodeMap': No item with name '${attrName}' was found.`); + throw new NotFoundError(`No attribute with name '${attrName}' was found.`); } get length () { diff --git a/lib/Node.ts b/lib/Node.ts index 5a1fb47..fa2328e 100644 --- a/lib/Node.ts +++ b/lib/Node.ts @@ -1,5 +1,6 @@ import { appendChild } from './appendChild.js'; import { DocumentFragment } from './DocumentFragment.ts'; +import { NotFoundError } from './errors.js'; import { prettyPrint } from './prettyPrint.js'; /** @@ -37,10 +38,10 @@ export class Node { */ appendChild (node: T): T { if (!node) { - throw new Error('1 argument required, but 0 present.'); + throw new TypeError('1 argument required, but 0 present.'); } if (!(node instanceof Node) && !(node instanceof DocumentFragment)) { - throw new Error('Cannot appendChild: Child is not a node'); + throw new TypeError('Cannot appendChild: Child is not a node'); } appendChild(this, node); return node; @@ -82,8 +83,7 @@ export class Node { } const index = this.childNodes.indexOf(child); if (index === -1) { - // DOMException - throw new Error('The node to be removed is not a child of this node.'); + throw new NotFoundError('The node to be removed is not a child of this node.'); } const node = this.childNodes.splice(index, 1).at(0); if (node) { diff --git a/lib/appendChild.ts b/lib/appendChild.ts index 83f3c2a..2cd9199 100644 --- a/lib/appendChild.ts +++ b/lib/appendChild.ts @@ -1,5 +1,6 @@ import { DocumentFragment } from './DocumentFragment.ts'; import type { Node } from './Node.js'; +import { HierarchyError } from './errors.js'; export function appendChild (parent: Node | DocumentFragment, child: Node | DocumentFragment) { if (child instanceof DocumentFragment) { @@ -10,7 +11,7 @@ export function appendChild (parent: Node | DocumentFragment, child: Node | Docu } else if (parent === child) { // XXX: there should really be a more elaborate tests here to determine that child does not contain parent - throw new Error('The new child element contains the parent.'); + throw new HierarchyError('The new child element contains the parent.'); } else if (parent instanceof DocumentFragment) { // appending to a fragment does not mess with the node's parentage diff --git a/lib/errors.ts b/lib/errors.ts new file mode 100644 index 0000000..92d88d8 --- /dev/null +++ b/lib/errors.ts @@ -0,0 +1,57 @@ +/** + * Base class for all errors thrown by this library. Catching `XMLError` + * will catch any of the library's specific error subclasses. + */ +export class XMLError extends Error { + constructor (message: string) { + super(message); + this.name = 'XMLError'; + } +} + +/** + * Thrown when XML input cannot be parsed: malformed attributes, missing + * declarations, premature EOF, content outside the root element, etc. + */ +export class ParserError extends XMLError { + constructor (message: string) { + super(message); + this.name = 'ParserError'; + } +} + +/** + * Thrown for namespace-related problems: an unknown prefix, a prefix + * re-bound to a different URI, or a lookup for a URI that hasn't been + * declared. + */ +export class NamespaceError extends XMLError { + constructor (message: string) { + super(message); + this.name = 'NamespaceError'; + } +} + +/** + * Thrown when an operation would violate the structure of the document + * tree: e.g. inserting an ancestor as a descendant, giving a Document + * more than one root, or requiring a root that isn't present. + */ +export class HierarchyError extends XMLError { + constructor (message: string) { + super(message); + this.name = 'HierarchyError'; + } +} + +/** + * Thrown when an item referenced by name or identity cannot be found + * (e.g. removing a child that isn't a child, or a named attribute that + * isn't in the map). + */ +export class NotFoundError extends XMLError { + constructor (message: string) { + super(message); + this.name = 'NotFoundError'; + } +} diff --git a/lib/index.ts b/lib/index.ts index 0fb99fc..753a28e 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -9,6 +9,13 @@ export { escape as escapeXML } from './escape.ts'; export { isElement } from './isElement.ts'; export { prettyPrint } from './prettyPrint.ts'; export { simplePrint } from './simplePrint.ts'; +export { + XMLError, + ParserError, + NamespaceError, + HierarchyError, + NotFoundError +} from './errors.ts'; export type { CreateChildArgument } from './CreateChildArgument.js'; export type { XMLAttr } from './XMLAttr.js'; export type { JsonMLElement, JsonMLAttr } from './JsonML.js'; diff --git a/lib/parseAttr.ts b/lib/parseAttr.ts index b97bec8..5facfa5 100644 --- a/lib/parseAttr.ts +++ b/lib/parseAttr.ts @@ -1,4 +1,5 @@ import { unescape } from './unescape.js'; +import { ParserError } from './errors.js'; function isWS (ch: string): boolean { return ( @@ -64,7 +65,7 @@ export function parseAttr (s: string, laxAttr = false): Record { // start-tag or empty-element tag. // The replacement text of any entity referred to directly or // indirectly in an attribute value MUST NOT contain a <. - throw new Error('Attribute error: expected name'); + throw new ParserError('Attribute error: expected name'); } const nameEnd = i; @@ -79,7 +80,7 @@ export function parseAttr (s: string, laxAttr = false): Record { r[s.slice(nameStart, nameEnd)] = ''; continue; } - throw new Error('Attribute error: expected ='); + throw new ParserError('Attribute error: expected ='); } i += skipWS(s, i); @@ -98,7 +99,7 @@ export function parseAttr (s: string, laxAttr = false): Record { fnSeek = isWS; } else { - throw new Error('Attribute error: expected value'); + throw new ParserError('Attribute error: expected value'); } const startValue = i; @@ -118,7 +119,7 @@ export function parseAttr (s: string, laxAttr = false): Record { r[key] = unescape(s.slice(startValue, n)); break; } - throw new Error('Attribute error: unterminated value'); + throw new ParserError('Attribute error: unterminated value'); } r[s.slice(nameStart, nameEnd)] = unescape(s.slice(startValue, endValue)); @@ -126,7 +127,7 @@ export function parseAttr (s: string, laxAttr = false): Record { const j = i; i += skipWS(s, i); if (i === j && i < n) { - throw new Error('Attribute error: expected space'); + throw new ParserError('Attribute error: expected space'); } } while (i < n); diff --git a/lib/parser.ts b/lib/parser.ts index 2663456..5650f12 100644 --- a/lib/parser.ts +++ b/lib/parser.ts @@ -7,6 +7,7 @@ import { CDataNode } from './CDataNode.js'; import { unescape } from './unescape.js'; import { removeCR } from './removeCR.js'; import { parseAttr } from './parseAttr.js'; +import { NamespaceError, ParserError } from './errors.js'; const DEFAULTOPTIONS = { emptyDoc: false, @@ -304,7 +305,7 @@ export function parseXML ( } } if (elm.prefix && !doc.namespaces.getByPrefix(elm.prefix)) { - throw new Error('Unknown namespace prefix ' + elm.prefix); + throw new NamespaceError('Unknown namespace prefix ' + elm.prefix); } } @@ -321,7 +322,7 @@ export function parseXML ( // MUST: have version const attr = parseAttr(a, options.laxAttr); if (!attr.version) { - throw new Error('XML missing version'); + throw new ParserError('XML missing version'); } return false; }); @@ -339,7 +340,7 @@ export function parseXML ( }); if (root === NON_ELEMENT && !options.emptyDoc) { - throw new Error('no root tag found'); + throw new ParserError('no root tag found'); } let current: Element | null = root; @@ -366,7 +367,7 @@ export function parseXML ( return true; } const msg = `Expected got in line ${posToLine(pos, xml)}`; - throw new Error(msg); + throw new ParserError(msg); }) || maybeMatchFn(fnTag, (_, t, a, c) => { @@ -386,7 +387,7 @@ export function parseXML ( }) ); if (pos === lastPos) { - throw new Error('Parser error'); + throw new ParserError('Parser error'); } } while (some && current && pos < xml.length); @@ -397,12 +398,12 @@ export function parseXML ( // file should be done if (xml.slice(pos)) { - throw new Error('DATA outside root node'); + throw new ParserError('DATA outside root node'); } // root should have been closed if (root !== NON_ELEMENT && !root.closed && current !== null) { - throw new Error(`Expected got EOF`); + throw new ParserError(`Expected got EOF`); } if (root !== NON_ELEMENT) { diff --git a/lib/simplePrint.ts b/lib/simplePrint.ts index 4805468..94083c2 100644 --- a/lib/simplePrint.ts +++ b/lib/simplePrint.ts @@ -5,6 +5,7 @@ import type { Node } from './Node.js'; import type { TextNode } from './TextNode.ts'; import { CDATA_SECTION_NODE, DOCUMENT_NODE, TEXT_NODE } from './constants.js'; import { escape } from './escape.js'; +import { HierarchyError } from './errors.js'; import { isElement } from './isElement.ts'; /** @@ -20,7 +21,7 @@ export function simplePrint (node: Node | DocumentFragment): string { } if (node.nodeType === DOCUMENT_NODE) { const root = (node as Document).root; - if (!root) throw new Error('root element is missing'); + if (!root) throw new HierarchyError('root element is missing'); return simplePrint(root); } else if (node.nodeType === CDATA_SECTION_NODE) { diff --git a/lib/unquote.ts b/lib/unquote.ts index 5debe02..93e88cf 100644 --- a/lib/unquote.ts +++ b/lib/unquote.ts @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/prefer-string-starts-ends-with */ +import { ParserError } from './errors.js'; + export function unquote (s: string, laxValue = false): string { if (s && s.length > 1) { if (s[0] === '"' && s[s.length - 1] === '"') { @@ -12,5 +14,5 @@ export function unquote (s: string, laxValue = false): string { if (laxValue) { return s; } - throw new Error('Invalid attribute: ' + s); + throw new ParserError('Invalid attribute: ' + s); } From 4dc110fd8a454ae04cef32ae070cdfc95f272b90 Mon Sep 17 00:00:00 2001 From: Borgar Date: Sat, 23 May 2026 18:03:01 +0000 Subject: [PATCH 12/14] Update docs --- API.md | 808 +++++++++++++++++++++++++++++++++++++++++++++++++++++- README.md | 41 ++- 2 files changed, 822 insertions(+), 27 deletions(-) diff --git a/API.md b/API.md index f45511f..fff8bfe 100644 --- a/API.md +++ b/API.md @@ -9,8 +9,13 @@ - [Document](#classesdocumentmd) - [DocumentFragment](#classesdocumentfragmentmd) - [Element](#classeselementmd) +- [HierarchyError](#classeshierarchyerrormd) +- [NamespaceError](#classesnamespaceerrormd) - [Node](#classesnodemd) +- [NotFoundError](#classesnotfounderrormd) +- [ParserError](#classesparsererrormd) - [TextNode](#classestextnodemd) +- [XMLError](#classesxmlerrormd) ## Type Aliases @@ -85,7 +90,7 @@ Constructs a new CDataNode instance. | `childNodes` | [`Node`](#classesnodemd)[] | `[]` | The node's immediate children. | [`Node`](#classesnodemd).[`childNodes`](#childnodes) | | `nodeName` | `string` | `'#node'` | A node type string identifier. | [`Node`](#classesnodemd).[`nodeName`](#nodename) | | `nodeType` | `number` | `0` | A numerical node type identifier. | [`Node`](#classesnodemd).[`nodeType`](#nodetype) | -| `parentNode` | [`Node`](#classesnodemd) \| `null` | `null` | The node's parent node. | [`Node`](#classesnodemd).[`parentNode`](#parentnode) | +| `parentNode` | [`Node`](#classesnodemd) \| `null` | `null` | The node's parent node. | [`Document`](#classesdocumentmd).[`parentNode`](#parentnode) | | `value` | `string` | `undefined` | The nodes data value. | - | ## Accessors @@ -106,7 +111,7 @@ Returns the node's first child in the tree, or null if the node has no children. #### Inherited from -[`Node`](#classesnodemd).[`firstChild`](#firstchild) +[`Document`](#classesdocumentmd).[`firstChild`](#firstchild) *** @@ -126,7 +131,7 @@ Returns the node's last child in the tree, or null if the node has no children. #### Inherited from -[`Node`](#classesnodemd).[`lastChild`](#lastchild) +[`Document`](#classesdocumentmd).[`lastChild`](#lastchild) *** @@ -851,15 +856,14 @@ Constructs a new Element instance. | Property | Type | Default value | Description | Overrides | Inherited from | | ------ | ------ | ------ | ------ | ------ | ------ | -| `attr` | `Record`\<`string`, `string`\> | `undefined` | An object of attributes assigned to this element. | - | - | +| `attributes` | `NamedNodeMap` | `undefined` | A list of attributes assigned to this element. | - | - | | `childNodes` | [`Node`](#classesnodemd)[] | `[]` | The node's immediate children. | - | [`Node`](#classesnodemd).[`childNodes`](#childnodes) | | `closed` | `boolean` | `undefined` | A state representing if the element was "self-closed" when read. | - | - | -| `fullName` | `string` | `undefined` | The full name of the tag for the given element, including a namespace prefix. | - | - | +| `localName` | `string` | `undefined` | The name of the tag for the given element, excluding any namespace prefix. | - | - | | `nodeName` | `string` | `'#node'` | A node type string identifier. | - | [`Node`](#classesnodemd).[`nodeName`](#nodename) | | `nodeType` | `number` | `0` | A numerical node type identifier. | - | [`Node`](#classesnodemd).[`nodeType`](#nodetype) | -| `ns` | `string` | `undefined` | The namespace prefix of the element, or null if no prefix is specified. | - | - | -| `parentNode` | `Element` \| `null` | `null` | The node's parent node. | [`Node`](#classesnodemd).[`parentNode`](#parentnode) | - | -| `tagName` | `string` | `undefined` | The name of the tag for the given element, excluding any namespace prefix. | - | - | +| `parentNode` | `Element` \| `null` | `null` | The node's parent node. | [`Document`](#classesdocumentmd).[`parentNode`](#parentnode) | - | +| `prefix` | `string` \| `null` | `undefined` | The namespace prefix of the element, or null' if no prefix is specified. | - | - | ## Accessors @@ -925,7 +929,7 @@ Returns the node's first child in the tree, or null if the node has no children. #### Inherited from -[`Node`](#classesnodemd).[`firstChild`](#firstchild) +[`Document`](#classesdocumentmd).[`firstChild`](#firstchild) *** @@ -945,6 +949,22 @@ Returns an element's first child Element, or null if there are no child elements *** +### fullName + +#### Get Signature + +```ts +get fullName(): string; +``` + +The full name of the tag for the given element, including a namespace prefix. + +##### Returns + +`string` + +*** + ### lastChild #### Get Signature @@ -961,7 +981,7 @@ Returns the node's last child in the tree, or null if the node has no children. #### Inherited from -[`Node`](#classesnodemd).[`lastChild`](#lastchild) +[`Document`](#classesdocumentmd).[`lastChild`](#lastchild) *** @@ -985,6 +1005,20 @@ True if xml:space has been set to true for this node or any of its ancestors. *** +### tagName + +#### Get Signature + +```ts +get tagName(): string; +``` + +##### Returns + +`string` + +*** + ### textContent #### Get Signature @@ -1158,6 +1192,18 @@ True if the attribute is present. *** +### hasAttributes() + +```ts +hasAttributes(): boolean; +``` + +#### Returns + +`boolean` + +*** + ### insertBefore() ```ts @@ -1402,6 +1448,284 @@ A formatted XML source. [`Node`](#classesnodemd).[`toString`](#tostring) + + +# HierarchyError + +Thrown when an operation would violate the structure of the document +tree: e.g. inserting an ancestor as a descendant, giving a Document +more than one root, or requiring a root that isn't present. + +## Extends + +- [`XMLError`](#classesxmlerrormd) + +## Constructors + +### Constructor + +```ts +new HierarchyError(message: string): HierarchyError; +``` + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `message` | `string` | + +#### Returns + +`HierarchyError` + +#### Overrides + +[`XMLError`](#classesxmlerrormd).[`constructor`](#constructor) + +## Properties + +| Property | Modifier | Type | Description | Inherited from | +| ------ | ------ | ------ | ------ | ------ | +| `cause?` | `public` | `unknown` | - | [`XMLError`](#classesxmlerrormd).[`cause`](#cause) | +| `message` | `public` | `string` | - | [`XMLError`](#classesxmlerrormd).[`message`](#message) | +| `name` | `public` | `string` | - | [`XMLError`](#classesxmlerrormd).[`name`](#name) | +| `stack?` | `public` | `string` | - | [`XMLError`](#classesxmlerrormd).[`stack`](#stack) | +| `stackTraceLimit` | `static` | `number` | The `Error.stackTraceLimit` property specifies the number of stack frames collected by a stack trace (whether generated by `new Error().stack` or `Error.captureStackTrace(obj)`). The default value is `10` but may be set to any valid JavaScript number. Changes will affect any stack trace captured _after_ the value has been changed. If set to a non-number value, or set to a negative number, stack traces will not capture any frames. | [`XMLError`](#classesxmlerrormd).[`stackTraceLimit`](#stacktracelimit) | + +## Methods + +### captureStackTrace() + +```ts +static captureStackTrace(targetObject: object, constructorOpt?: Function): void; +``` + +Creates a `.stack` property on `targetObject`, which when accessed returns +a string representing the location in the code at which +`Error.captureStackTrace()` was called. + +```js +const myObject = {}; +Error.captureStackTrace(myObject); +myObject.stack; // Similar to `new Error().stack` +``` + +The first line of the trace will be prefixed with +`${myObject.name}: ${myObject.message}`. + +The optional `constructorOpt` argument accepts a function. If given, all frames +above `constructorOpt`, including `constructorOpt`, will be omitted from the +generated stack trace. + +The `constructorOpt` argument is useful for hiding implementation +details of error generation from the user. For instance: + +```js +function a() { + b(); +} + +function b() { + c(); +} + +function c() { + // Create an error without stack trace to avoid calculating the stack trace twice. + const { stackTraceLimit } = Error; + Error.stackTraceLimit = 0; + const error = new Error(); + Error.stackTraceLimit = stackTraceLimit; + + // Capture the stack trace above function b + Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace + throw error; +} + +a(); +``` + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `targetObject` | `object` | +| `constructorOpt?` | `Function` | + +#### Returns + +`void` + +#### Inherited from + +[`XMLError`](#classesxmlerrormd).[`captureStackTrace`](#capturestacktrace) + +*** + +### prepareStackTrace() + +```ts +static prepareStackTrace(err: Error, stackTraces: CallSite[]): any; +``` + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `err` | `Error` | +| `stackTraces` | `CallSite`[] | + +#### Returns + +`any` + +#### See + +https://v8.dev/docs/stack-trace-api#customizing-stack-traces + +#### Inherited from + +[`XMLError`](#classesxmlerrormd).[`prepareStackTrace`](#preparestacktrace) + + + + +# NamespaceError + +Thrown for namespace-related problems: an unknown prefix, a prefix +re-bound to a different URI, or a lookup for a URI that hasn't been +declared. + +## Extends + +- [`XMLError`](#classesxmlerrormd) + +## Constructors + +### Constructor + +```ts +new NamespaceError(message: string): NamespaceError; +``` + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `message` | `string` | + +#### Returns + +`NamespaceError` + +#### Overrides + +[`XMLError`](#classesxmlerrormd).[`constructor`](#constructor) + +## Properties + +| Property | Modifier | Type | Description | Inherited from | +| ------ | ------ | ------ | ------ | ------ | +| `cause?` | `public` | `unknown` | - | [`XMLError`](#classesxmlerrormd).[`cause`](#cause) | +| `message` | `public` | `string` | - | [`XMLError`](#classesxmlerrormd).[`message`](#message) | +| `name` | `public` | `string` | - | [`XMLError`](#classesxmlerrormd).[`name`](#name) | +| `stack?` | `public` | `string` | - | [`XMLError`](#classesxmlerrormd).[`stack`](#stack) | +| `stackTraceLimit` | `static` | `number` | The `Error.stackTraceLimit` property specifies the number of stack frames collected by a stack trace (whether generated by `new Error().stack` or `Error.captureStackTrace(obj)`). The default value is `10` but may be set to any valid JavaScript number. Changes will affect any stack trace captured _after_ the value has been changed. If set to a non-number value, or set to a negative number, stack traces will not capture any frames. | [`XMLError`](#classesxmlerrormd).[`stackTraceLimit`](#stacktracelimit) | + +## Methods + +### captureStackTrace() + +```ts +static captureStackTrace(targetObject: object, constructorOpt?: Function): void; +``` + +Creates a `.stack` property on `targetObject`, which when accessed returns +a string representing the location in the code at which +`Error.captureStackTrace()` was called. + +```js +const myObject = {}; +Error.captureStackTrace(myObject); +myObject.stack; // Similar to `new Error().stack` +``` + +The first line of the trace will be prefixed with +`${myObject.name}: ${myObject.message}`. + +The optional `constructorOpt` argument accepts a function. If given, all frames +above `constructorOpt`, including `constructorOpt`, will be omitted from the +generated stack trace. + +The `constructorOpt` argument is useful for hiding implementation +details of error generation from the user. For instance: + +```js +function a() { + b(); +} + +function b() { + c(); +} + +function c() { + // Create an error without stack trace to avoid calculating the stack trace twice. + const { stackTraceLimit } = Error; + Error.stackTraceLimit = 0; + const error = new Error(); + Error.stackTraceLimit = stackTraceLimit; + + // Capture the stack trace above function b + Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace + throw error; +} + +a(); +``` + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `targetObject` | `object` | +| `constructorOpt?` | `Function` | + +#### Returns + +`void` + +#### Inherited from + +[`XMLError`](#classesxmlerrormd).[`captureStackTrace`](#capturestacktrace) + +*** + +### prepareStackTrace() + +```ts +static prepareStackTrace(err: Error, stackTraces: CallSite[]): any; +``` + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `err` | `Error` | +| `stackTraces` | `CallSite`[] | + +#### Returns + +`any` + +#### See + +https://v8.dev/docs/stack-trace-api#customizing-stack-traces + +#### Inherited from + +[`XMLError`](#classesxmlerrormd).[`prepareStackTrace`](#preparestacktrace) + + # Node @@ -1596,6 +1920,283 @@ Returns a string representation of the node. A formatted XML source. + + +# NotFoundError + +Thrown when an item referenced by name or identity cannot be found +(e.g. removing a child that isn't a child, or a named attribute that +isn't in the map). + +## Extends + +- [`XMLError`](#classesxmlerrormd) + +## Constructors + +### Constructor + +```ts +new NotFoundError(message: string): NotFoundError; +``` + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `message` | `string` | + +#### Returns + +`NotFoundError` + +#### Overrides + +[`XMLError`](#classesxmlerrormd).[`constructor`](#constructor) + +## Properties + +| Property | Modifier | Type | Description | Inherited from | +| ------ | ------ | ------ | ------ | ------ | +| `cause?` | `public` | `unknown` | - | [`XMLError`](#classesxmlerrormd).[`cause`](#cause) | +| `message` | `public` | `string` | - | [`XMLError`](#classesxmlerrormd).[`message`](#message) | +| `name` | `public` | `string` | - | [`XMLError`](#classesxmlerrormd).[`name`](#name) | +| `stack?` | `public` | `string` | - | [`XMLError`](#classesxmlerrormd).[`stack`](#stack) | +| `stackTraceLimit` | `static` | `number` | The `Error.stackTraceLimit` property specifies the number of stack frames collected by a stack trace (whether generated by `new Error().stack` or `Error.captureStackTrace(obj)`). The default value is `10` but may be set to any valid JavaScript number. Changes will affect any stack trace captured _after_ the value has been changed. If set to a non-number value, or set to a negative number, stack traces will not capture any frames. | [`XMLError`](#classesxmlerrormd).[`stackTraceLimit`](#stacktracelimit) | + +## Methods + +### captureStackTrace() + +```ts +static captureStackTrace(targetObject: object, constructorOpt?: Function): void; +``` + +Creates a `.stack` property on `targetObject`, which when accessed returns +a string representing the location in the code at which +`Error.captureStackTrace()` was called. + +```js +const myObject = {}; +Error.captureStackTrace(myObject); +myObject.stack; // Similar to `new Error().stack` +``` + +The first line of the trace will be prefixed with +`${myObject.name}: ${myObject.message}`. + +The optional `constructorOpt` argument accepts a function. If given, all frames +above `constructorOpt`, including `constructorOpt`, will be omitted from the +generated stack trace. + +The `constructorOpt` argument is useful for hiding implementation +details of error generation from the user. For instance: + +```js +function a() { + b(); +} + +function b() { + c(); +} + +function c() { + // Create an error without stack trace to avoid calculating the stack trace twice. + const { stackTraceLimit } = Error; + Error.stackTraceLimit = 0; + const error = new Error(); + Error.stackTraceLimit = stackTraceLimit; + + // Capture the stack trace above function b + Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace + throw error; +} + +a(); +``` + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `targetObject` | `object` | +| `constructorOpt?` | `Function` | + +#### Returns + +`void` + +#### Inherited from + +[`XMLError`](#classesxmlerrormd).[`captureStackTrace`](#capturestacktrace) + +*** + +### prepareStackTrace() + +```ts +static prepareStackTrace(err: Error, stackTraces: CallSite[]): any; +``` + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `err` | `Error` | +| `stackTraces` | `CallSite`[] | + +#### Returns + +`any` + +#### See + +https://v8.dev/docs/stack-trace-api#customizing-stack-traces + +#### Inherited from + +[`XMLError`](#classesxmlerrormd).[`prepareStackTrace`](#preparestacktrace) + + + + +# ParserError + +Thrown when XML input cannot be parsed: malformed attributes, missing +declarations, premature EOF, content outside the root element, etc. + +## Extends + +- [`XMLError`](#classesxmlerrormd) + +## Constructors + +### Constructor + +```ts +new ParserError(message: string): ParserError; +``` + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `message` | `string` | + +#### Returns + +`ParserError` + +#### Overrides + +[`XMLError`](#classesxmlerrormd).[`constructor`](#constructor) + +## Properties + +| Property | Modifier | Type | Description | Inherited from | +| ------ | ------ | ------ | ------ | ------ | +| `cause?` | `public` | `unknown` | - | [`XMLError`](#classesxmlerrormd).[`cause`](#cause) | +| `message` | `public` | `string` | - | [`XMLError`](#classesxmlerrormd).[`message`](#message) | +| `name` | `public` | `string` | - | [`XMLError`](#classesxmlerrormd).[`name`](#name) | +| `stack?` | `public` | `string` | - | [`XMLError`](#classesxmlerrormd).[`stack`](#stack) | +| `stackTraceLimit` | `static` | `number` | The `Error.stackTraceLimit` property specifies the number of stack frames collected by a stack trace (whether generated by `new Error().stack` or `Error.captureStackTrace(obj)`). The default value is `10` but may be set to any valid JavaScript number. Changes will affect any stack trace captured _after_ the value has been changed. If set to a non-number value, or set to a negative number, stack traces will not capture any frames. | [`XMLError`](#classesxmlerrormd).[`stackTraceLimit`](#stacktracelimit) | + +## Methods + +### captureStackTrace() + +```ts +static captureStackTrace(targetObject: object, constructorOpt?: Function): void; +``` + +Creates a `.stack` property on `targetObject`, which when accessed returns +a string representing the location in the code at which +`Error.captureStackTrace()` was called. + +```js +const myObject = {}; +Error.captureStackTrace(myObject); +myObject.stack; // Similar to `new Error().stack` +``` + +The first line of the trace will be prefixed with +`${myObject.name}: ${myObject.message}`. + +The optional `constructorOpt` argument accepts a function. If given, all frames +above `constructorOpt`, including `constructorOpt`, will be omitted from the +generated stack trace. + +The `constructorOpt` argument is useful for hiding implementation +details of error generation from the user. For instance: + +```js +function a() { + b(); +} + +function b() { + c(); +} + +function c() { + // Create an error without stack trace to avoid calculating the stack trace twice. + const { stackTraceLimit } = Error; + Error.stackTraceLimit = 0; + const error = new Error(); + Error.stackTraceLimit = stackTraceLimit; + + // Capture the stack trace above function b + Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace + throw error; +} + +a(); +``` + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `targetObject` | `object` | +| `constructorOpt?` | `Function` | + +#### Returns + +`void` + +#### Inherited from + +[`XMLError`](#classesxmlerrormd).[`captureStackTrace`](#capturestacktrace) + +*** + +### prepareStackTrace() + +```ts +static prepareStackTrace(err: Error, stackTraces: CallSite[]): any; +``` + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `err` | `Error` | +| `stackTraces` | `CallSite`[] | + +#### Returns + +`any` + +#### See + +https://v8.dev/docs/stack-trace-api#customizing-stack-traces + +#### Inherited from + +[`XMLError`](#classesxmlerrormd).[`prepareStackTrace`](#preparestacktrace) + + # TextNode @@ -1635,10 +2236,10 @@ Constructs a new TextNode instance. | Property | Type | Default value | Description | Inherited from | | ------ | ------ | ------ | ------ | ------ | | `childNodes` | [`Node`](#classesnodemd)[] | `[]` | The node's immediate children. | [`Node`](#classesnodemd).[`childNodes`](#childnodes) | +| `data` | `string` | `undefined` | The node's data value. | - | | `nodeName` | `string` | `'#node'` | A node type string identifier. | [`Node`](#classesnodemd).[`nodeName`](#nodename) | | `nodeType` | `number` | `0` | A numerical node type identifier. | [`Node`](#classesnodemd).[`nodeType`](#nodetype) | | `parentNode` | [`Node`](#classesnodemd) \| `null` | `null` | The node's parent node. | [`Document`](#classesdocumentmd).[`parentNode`](#parentnode) | -| `value` | `string` | `undefined` | The node's data value. | - | ## Accessors @@ -1658,7 +2259,7 @@ Returns the node's first child in the tree, or null if the node has no children. #### Inherited from -[`CDataNode`](#classescdatanodemd).[`firstChild`](#firstchild) +[`Document`](#classesdocumentmd).[`firstChild`](#firstchild) *** @@ -1678,7 +2279,37 @@ Returns the node's last child in the tree, or null if the node has no children. #### Inherited from -[`CDataNode`](#classescdatanodemd).[`lastChild`](#lastchild) +[`Document`](#classesdocumentmd).[`lastChild`](#lastchild) + +*** + +### nodeValue + +#### Get Signature + +```ts +get nodeValue(): string; +``` + +##### Returns + +`string` + +#### Set Signature + +```ts +set nodeValue(value: string): void; +``` + +##### Parameters + +| Parameter | Type | +| ------ | ------ | +| `value` | `string` | + +##### Returns + +`void` *** @@ -1832,6 +2463,157 @@ A formatted XML source. [`Node`](#classesnodemd).[`toString`](#tostring) + + +# XMLError + +Base class for all errors thrown by this library. Catching `XMLError` +will catch any of the library's specific error subclasses. + +## Extends + +- `Error` + +## Extended by + +- [`ParserError`](#classesparsererrormd) +- [`NamespaceError`](#classesnamespaceerrormd) +- [`HierarchyError`](#classeshierarchyerrormd) +- [`NotFoundError`](#classesnotfounderrormd) + +## Constructors + +### Constructor + +```ts +new XMLError(message: string): XMLError; +``` + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `message` | `string` | + +#### Returns + +`XMLError` + +#### Overrides + +```ts +Error.constructor +``` + +## Properties + +| Property | Modifier | Type | Description | Inherited from | +| ------ | ------ | ------ | ------ | ------ | +| `cause?` | `public` | `unknown` | - | `Error.cause` | +| `message` | `public` | `string` | - | `Error.message` | +| `name` | `public` | `string` | - | `Error.name` | +| `stack?` | `public` | `string` | - | `Error.stack` | +| `stackTraceLimit` | `static` | `number` | The `Error.stackTraceLimit` property specifies the number of stack frames collected by a stack trace (whether generated by `new Error().stack` or `Error.captureStackTrace(obj)`). The default value is `10` but may be set to any valid JavaScript number. Changes will affect any stack trace captured _after_ the value has been changed. If set to a non-number value, or set to a negative number, stack traces will not capture any frames. | `Error.stackTraceLimit` | + +## Methods + +### captureStackTrace() + +```ts +static captureStackTrace(targetObject: object, constructorOpt?: Function): void; +``` + +Creates a `.stack` property on `targetObject`, which when accessed returns +a string representing the location in the code at which +`Error.captureStackTrace()` was called. + +```js +const myObject = {}; +Error.captureStackTrace(myObject); +myObject.stack; // Similar to `new Error().stack` +``` + +The first line of the trace will be prefixed with +`${myObject.name}: ${myObject.message}`. + +The optional `constructorOpt` argument accepts a function. If given, all frames +above `constructorOpt`, including `constructorOpt`, will be omitted from the +generated stack trace. + +The `constructorOpt` argument is useful for hiding implementation +details of error generation from the user. For instance: + +```js +function a() { + b(); +} + +function b() { + c(); +} + +function c() { + // Create an error without stack trace to avoid calculating the stack trace twice. + const { stackTraceLimit } = Error; + Error.stackTraceLimit = 0; + const error = new Error(); + Error.stackTraceLimit = stackTraceLimit; + + // Capture the stack trace above function b + Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace + throw error; +} + +a(); +``` + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `targetObject` | `object` | +| `constructorOpt?` | `Function` | + +#### Returns + +`void` + +#### Inherited from + +```ts +Error.captureStackTrace +``` + +*** + +### prepareStackTrace() + +```ts +static prepareStackTrace(err: Error, stackTraces: CallSite[]): any; +``` + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `err` | `Error` | +| `stackTraces` | `CallSite`[] | + +#### Returns + +`any` + +#### See + +https://v8.dev/docs/stack-trace-api#customizing-stack-traces + +#### Inherited from + +```ts +Error.prepareStackTrace +``` + + # escapeXML() diff --git a/README.md b/README.md index f3645e6..c02fb07 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ There are some limitations: - Comments are discarded. - Processing instructions are discarded. -- Namespace prefixes are preserved but otherwise ignored. +- Namespace prefixes are preserved on every node; full xmlns validation is opt-in via the `ns` parse option. - Doctypes are discarded. - Nested doctypes are not supported and will cause errors. - Nodes have a limited interface. @@ -34,10 +34,10 @@ $ npm i @borgar/simple-xml import { parseXML } from '@borgar/simple-xml'; const dom = parseXML('Lorem ipsum'); -console.log(dom.getElementsByTagName('node').textContent); +console.log(dom.getElementsByTagName('node')[0].textContent); ``` -The parse function accepts two arguments, the first is an XML source. The second is an options object. There are two options: +The parse function accepts two arguments, the first is an XML source. The second is an options object. There are three options: Allow normally forbidden unquoted attributes with `laxAttr`: @@ -51,7 +51,13 @@ Allow normally forbidden "rootless" documents with `emptyDoc`: parseXML('', { emptyDoc: true }); ``` -As well as a parse method, the package exports Node classes: Node, Element, Document, TextNode, and CDataNode. They are pretty much what you expect but with an incomplete or altered set of DOM API functions. +Validate `xmlns` declarations and element/attribute prefixes against them with `ns`. With this flag, an unknown prefix or a re-bound prefix throws a `NamespaceError`: + +```js +parseXML('', { ns: true }); +``` + +As well as a parse method, the package exports the node classes (`Node`, `Element`, `Document`, `DocumentFragment`, `TextNode`, `CDataNode`), serialization helpers (`prettyPrint`, `simplePrint`, `escapeXML`), the `isElement` type guard, and an error hierarchy rooted at `XMLError` (`ParserError`, `NamespaceError`, `HierarchyError`, `NotFoundError`). The node classes are pretty much what you'd expect but with an "incomplete" or altered set of DOM API functions. [See the API documentation.](API.md) @@ -68,20 +74,24 @@ As well as a parse method, the package exports Node classes: Node, Element, Docu * `.nodeType` A node type number (e.g. `Element.ELEMENT_NODE === 1`) -* `.ns` - A namespace identifier (`foo` in the case of ``) +* `.prefix` + The namespace prefix of the element, or `null` if it has none (`foo` in the case of ``). + +* `.localName` + The tag name with any namespace prefix stripped (`bar` in the case of ``). * `.fullName` - A full tag name with identifier (`foo:bar` in the case of ``) + The full tag name including the prefix (`foo:bar` in the case of ``). **Attributes** * `.getAttribute( attrName )` * `.setAttribute( attrName, attrValue )` * `.hasAttribute( attrName )` +* `.hasAttributes()` * `.removeAttribute( attrName )` - -Attributes are not stored as attribute nodes in a list, but rather they are a simple `{ name: value }` style object on the node. +* `.attributes` + A `NamedNodeMap` of `Attr` nodes — iterable, indexable by name (`el.attributes.foo` returns the `Attr`), and with DOM-style `getNamedItem` / `setNamedItem` / `removeNamedItem` methods. **Tree & traversal** @@ -105,6 +115,9 @@ Attributes are not stored as attribute nodes in a list, but rather they are a si * `.getElementsByTagName( tagName )` Lists all Elements in the target's subtree, traversal order, that have `.tagName` equal to the argument. Function is case-sensitive. +* `.querySelector( cssSelector )` + Returns the first Element in the target's subtree, traversal order, that matches the supplied CSS selector, or `null` if none match. + * `.querySelectorAll( cssSelector )` Lists all Elements in the target's subtree, traversal order, that match the supplied CSS selector. Function should be case-sensitive but may be case insensitive for some. @@ -123,19 +136,19 @@ It will parse to a document which has a `.root` node looking roughly like this: { nodeType: 1, nodeName: 'TAG', - ns: 'x', + prefix: 'x', + localName: 'tag', tagName: 'tag', fullName: 'x:tag', - attr: { 'foo': 'bar' } + attributes: NamedNodeMap { /* Attr { name: 'foo', value: 'bar' } */ }, parentNode: null, - childNodes = [ + childNodes: [ { nodeName: '#text', nodeType: 3, - value: 'Text content', + data: 'Text content', parentNode: {...}, } ] } ``` - From 619b50f35115f2b5072996c533c6f47c130c8b3a Mon Sep 17 00:00:00 2001 From: Borgar Date: Sat, 23 May 2026 18:07:59 +0000 Subject: [PATCH 13/14] Ran npm audit fix --- package-lock.json | 428 ++++++++++++++++++++++++++++------------------ 1 file changed, 259 insertions(+), 169 deletions(-) diff --git a/package-lock.json b/package-lock.json index a720fd0..01b0a21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -878,9 +878,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", - "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", "cpu": [ "arm" ], @@ -892,9 +892,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", - "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", "cpu": [ "arm64" ], @@ -906,9 +906,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", - "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", "cpu": [ "arm64" ], @@ -920,9 +920,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", - "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", "cpu": [ "x64" ], @@ -934,9 +934,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", - "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", "cpu": [ "arm64" ], @@ -948,9 +948,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", - "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", "cpu": [ "x64" ], @@ -962,13 +962,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", - "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -976,13 +979,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", - "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -990,13 +996,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", - "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1004,13 +1013,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", - "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1018,13 +1030,33 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", - "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", "cpu": [ "loong64" ], "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1032,13 +1064,33 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", - "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1046,13 +1098,16 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", - "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1060,13 +1115,16 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", - "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1074,13 +1132,16 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", - "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1088,13 +1149,16 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", - "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1102,23 +1166,40 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", - "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", - "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", "cpu": [ "arm64" ], @@ -1130,9 +1211,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", - "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", "cpu": [ "arm64" ], @@ -1144,9 +1225,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", - "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", "cpu": [ "ia32" ], @@ -1158,9 +1239,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", - "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", "cpu": [ "x64" ], @@ -1172,9 +1253,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", - "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", "cpu": [ "x64" ], @@ -1263,9 +1344,9 @@ } }, "node_modules/@stylistic/eslint-plugin/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -1597,9 +1678,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -1607,13 +1688,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -1799,9 +1880,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -1982,9 +2063,9 @@ "dev": true }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -3190,9 +3271,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -3429,9 +3510,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -3439,13 +3520,13 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -4449,9 +4530,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "dev": true, "license": "MIT" }, @@ -4991,10 +5072,11 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5081,9 +5163,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, "funding": [ { @@ -5392,10 +5474,11 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -5436,9 +5519,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -5456,7 +5539,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -5857,9 +5940,9 @@ } }, "node_modules/rollup": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", - "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", "dev": true, "license": "MIT", "dependencies": { @@ -5873,28 +5956,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.5", - "@rollup/rollup-android-arm64": "4.52.5", - "@rollup/rollup-darwin-arm64": "4.52.5", - "@rollup/rollup-darwin-x64": "4.52.5", - "@rollup/rollup-freebsd-arm64": "4.52.5", - "@rollup/rollup-freebsd-x64": "4.52.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", - "@rollup/rollup-linux-arm-musleabihf": "4.52.5", - "@rollup/rollup-linux-arm64-gnu": "4.52.5", - "@rollup/rollup-linux-arm64-musl": "4.52.5", - "@rollup/rollup-linux-loong64-gnu": "4.52.5", - "@rollup/rollup-linux-ppc64-gnu": "4.52.5", - "@rollup/rollup-linux-riscv64-gnu": "4.52.5", - "@rollup/rollup-linux-riscv64-musl": "4.52.5", - "@rollup/rollup-linux-s390x-gnu": "4.52.5", - "@rollup/rollup-linux-x64-gnu": "4.52.5", - "@rollup/rollup-linux-x64-musl": "4.52.5", - "@rollup/rollup-openharmony-arm64": "4.52.5", - "@rollup/rollup-win32-arm64-msvc": "4.52.5", - "@rollup/rollup-win32-ia32-msvc": "4.52.5", - "@rollup/rollup-win32-x64-gnu": "4.52.5", - "@rollup/rollup-win32-x64-msvc": "4.52.5", + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" } }, @@ -6581,9 +6667,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -6965,9 +7051,9 @@ } }, "node_modules/typedoc/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -6975,13 +7061,13 @@ } }, "node_modules/typedoc/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -6990,19 +7076,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/typedoc/node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "dev": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -7075,10 +7148,11 @@ } }, "node_modules/underscore": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", - "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", - "dev": true + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" }, "node_modules/undici-types": { "version": "7.18.2", @@ -7217,9 +7291,9 @@ } }, "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz", + "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", "dev": true, "license": "MIT", "dependencies": { @@ -7310,9 +7384,9 @@ } }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -7401,9 +7475,9 @@ } }, "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -7682,6 +7756,22 @@ "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", "dev": true }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs-parser": { "version": "20.2.9", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", From b55013218623698123cd8f1ca1aede17eb4f5a8a Mon Sep 17 00:00:00 2001 From: Borgar Date: Sat, 23 May 2026 18:08:40 +0000 Subject: [PATCH 14/14] Update version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 01b0a21..e1ed8a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@borgar/simple-xml", - "version": "3.0.0-rc.1", + "version": "3.0.0-rc.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@borgar/simple-xml", - "version": "3.0.0-rc.1", + "version": "3.0.0-rc.2", "license": "MIT", "devDependencies": { "@borgar/eslint-config": "~4.0.1", diff --git a/package.json b/package.json index 3c95f71..50f6485 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@borgar/simple-xml", - "version": "3.0.0-rc.1", + "version": "3.0.0-rc.2", "description": "A reasonably fast, simple and pure-JS XML parser with no dependencies", "type": "module", "source": "lib/index.ts",