diff --git a/API.md b/API.md
index 5f09844..fff8bfe 100644
--- a/API.md
+++ b/API.md
@@ -7,14 +7,22 @@
- [CDataNode](#classescdatanodemd)
- [Document](#classesdocumentmd)
+- [DocumentFragment](#classesdocumentfragmentmd)
- [Element](#classeselementmd)
+- [HierarchyError](#classeshierarchyerrormd)
+- [NamespaceError](#classesnamespaceerrormd)
- [Node](#classesnodemd)
+- [NotFoundError](#classesnotfounderrormd)
+- [ParserError](#classesparsererrormd)
- [TextNode](#classestextnodemd)
+- [XMLError](#classesxmlerrormd)
## Type Aliases
+- [CreateChildArgument](#type-aliasescreatechildargumentmd)
- [JsonMLAttr](#type-aliasesjsonmlattrmd)
- [JsonMLElement](#type-aliasesjsonmlelementmd)
+- [XMLAttr](#type-aliasesxmlattrmd)
## Variables
@@ -30,10 +38,15 @@
- [NOTATION\_NODE](#variablesnotation_nodemd)
- [PROCESSING\_INSTRUCTION\_NODE](#variablesprocessing_instruction_nodemd)
- [TEXT\_NODE](#variablestext_nodemd)
+- [XML\_DECLARATION](#variablesxml_declarationmd)
## Functions
+- [escapeXML](#functionsescapexmlmd)
+- [isElement](#functionsiselementmd)
- [parseXML](#functionsparsexmlmd)
+- [prettyPrint](#functionsprettyprintmd)
+- [simplePrint](#functionssimpleprintmd)
@@ -77,11 +90,51 @@ 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
+### 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
+
+[`Document`](#classesdocumentmd).[`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
+
+[`Document`](#classesdocumentmd).[`lastChild`](#lastchild)
+
+***
+
### preserveSpace
#### Get Signature
@@ -125,20 +178,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 +207,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 | Description |
+| ------ | ------ | ------ |
+| `child` | [`Node`](#classesnodemd) | The child node to be removed. |
+
+#### Returns
+
+[`Node`](#classesnodemd) \| `undefined`
+
+The removed child node.
+
+#### Inherited from
+
+[`Node`](#classesnodemd).[`removeChild`](#removechild)
+
+***
+
### toString()
```ts
@@ -223,6 +341,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 +424,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,465 +453,1865 @@ The same node that was passed in.
***
-### getElementsByTagName()
+### attachNS()
```ts
-getElementsByTagName(tagName: string): Element[];
+attachNS(namespaceURI: string, prefix?: string): (name: string, attr?: XMLAttr | null, ...children: (
+ | CreateChildArgument
+ | CreateChildArgument[])[]) => Element;
```
-Return all descendant elements that have the specified tag name.
+Attach a namespace to the document.
#### Parameters
-| Parameter | Type | Description |
-| ------ | ------ | ------ |
-| `tagName` | `string` | The tag name to filter by. |
+| 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
-[`Element`](#classeselementmd)[]
+```ts
+(
+ name: string,
+ attr?: XMLAttr | null, ...
+ children: (
+ | CreateChildArgument
+ | CreateChildArgument[])[]): Element;
+```
-The elements by tag name.
+##### Parameters
+
+| Parameter | Type |
+| ------ | ------ |
+| `name` | `string` |
+| `attr?` | [`XMLAttr`](#type-aliasesxmlattrmd) \| `null` |
+| ...`children?` | ( \| [`CreateChildArgument`](#type-aliasescreatechildargumentmd) \| [`CreateChildArgument`](#type-aliasescreatechildargumentmd)[])[] |
+
+##### Returns
+
+[`Element`](#classeselementmd)
***
-### querySelector()
+### createElement()
```ts
-querySelector(selector: string): Element | null;
+createElement(
+ qualifiedName: string,
+ attr: XMLAttr | null | undefined, ...
+ children: (
+ | CreateChildArgument
+ | CreateChildArgument[])[]): Element;
```
-Return the first descendant element that match a specified CSS selector.
+Create a new element node.
#### Parameters
| Parameter | Type | Description |
| ------ | ------ | ------ |
-| `selector` | `string` | The CSS selector to filter by. |
+| `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) \| `null`
+[`Element`](#classeselementmd)
-The elements by tag name.
+A new Element instance.
***
-### querySelectorAll()
+### createElementNS()
```ts
-querySelectorAll(selector: string): Element[];
+createElementNS(
+ namespaceURI: string,
+ qualifiedName: string,
+ attr: XMLAttr | null | undefined, ...
+ children: (
+ | CreateChildArgument
+ | CreateChildArgument[])[]): Element;
```
-Return all descendant elements that match a specified CSS selector.
+Create a new element node associated with a given namespace.
#### Parameters
| Parameter | Type | Description |
| ------ | ------ | ------ |
-| `selector` | `string` | The CSS selector to filter by. |
+| `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)[]
+[`Element`](#classeselementmd)
-The elements by tag name.
+A new Element instance.
***
-### toJS()
+### getElementsByTagName()
```ts
-toJS(): [] | JsonMLElement;
+getElementsByTagName(tagName: string): Element[];
```
-Returns a simple object representation of the node and its descendants.
+Return all descendant elements that have the specified tag name.
+
+#### Parameters
+
+| Parameter | Type | Description |
+| ------ | ------ | ------ |
+| `tagName` | `string` | The tag name to filter by. |
#### Returns
-\[\] \| [`JsonMLElement`](#type-aliasesjsonmlelementmd)
+[`Element`](#classeselementmd)[]
-JsonML representation of the nodes and its subtree.
+The elements by tag name.
***
-### toString()
+### insertBefore()
```ts
-toString(): string;
+insertBefore(newNode: T, referenceNode: Node | null): T;
```
-Returns a string representation of the node.
-
-#### Returns
-
-`string`
+Inserts a node before a _reference node_ as a child of a specified _parent node_.
-A formatted XML source.
+#### Type Parameters
-#### Inherited from
+| Type Parameter |
+| ------ |
+| `T` *extends* [`Node`](#classesnodemd) \| [`DocumentFragment`](#classesdocumentfragmentmd) |
-[`Node`](#classesnodemd).[`toString`](#tostring)
+#### 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
-# Element
+`T`
-A class describing an Element.
+The added child (unless newNode is a DocumentFragment, in which case the empty DocumentFragment is returned).
-## Extends
+#### Inherited from
-- [`Node`](#classesnodemd)
+[`Node`](#classesnodemd).[`insertBefore`](#insertbefore)
-## Constructors
+***
-### Constructor
+### print()
```ts
-new Element(
- tagName: string,
- attr?: Record,
- closed?: boolean): Element;
+print(pretty?: boolean): string;
```
-Constructs a new Element instance.
+Print the document as a string.
#### Parameters
| Parameter | Type | Default value | Description |
| ------ | ------ | ------ | ------ |
-| `tagName` | `string` | `undefined` | The tag name of the node. |
-| `attr?` | `Record`\<`string`, `string`\> | `{}` | A collection of attributes to assign. |
-| `closed?` | `boolean` | `false` | Was the element "self-closed" when read. |
+| `pretty` | `boolean` | `false` | Apply automatic linebreaks and indentation to the output. |
#### Returns
-`Element`
-
-#### Overrides
-
-[`Node`](#classesnodemd).[`constructor`](#constructor)
-
-## Properties
-
-| Property | Type | Default value | Description | Overrides | Inherited from |
-| ------ | ------ | ------ | ------ | ------ | ------ |
-| `attr` | `Record`\<`string`, `string`\> | `undefined` | An object 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. | - | - |
-| `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. | - | - |
-
-## Accessors
-
-### children
-
-#### Get Signature
-
-```ts
-get children(): Element[];
-```
-
-A list containing all child Elements of the current Element.
-
-##### Returns
+`string`
-`Element`[]
+The document as an XML string.
***
-### preserveSpace
-
-#### Get Signature
+### querySelector()
```ts
-get preserveSpace(): boolean;
+querySelector(selector: string): Element | null;
```
-True if xml:space has been set to true for this node or any of its ancestors.
+Return the first descendant element that match a specified CSS selector.
-##### Returns
+#### Parameters
-`boolean`
+| Parameter | Type | Description |
+| ------ | ------ | ------ |
+| `selector` | `string` | The CSS selector to filter by. |
-#### Overrides
+#### Returns
-[`Node`](#classesnodemd).[`preserveSpace`](#preservespace)
+[`Element`](#classeselementmd) \| `null`
-***
+The elements by tag name.
-### textContent
+***
-#### Get Signature
+### querySelectorAll()
```ts
-get textContent(): string;
+querySelectorAll(selector: string): Element[];
```
-The text content of this node (and its children).
+Return all descendant elements that match a specified CSS selector.
-##### Returns
+#### Parameters
-`string`
+| Parameter | Type | Description |
+| ------ | ------ | ------ |
+| `selector` | `string` | The CSS selector to filter by. |
-#### Inherited from
+#### Returns
-[`Node`](#classesnodemd).[`textContent`](#textcontent)
+[`Element`](#classeselementmd)[]
-## Methods
+The elements by tag name.
-### appendChild()
+***
+
+### removeChild()
```ts
-appendChild(node: Node): Node;
+removeChild(child: Node): Node | undefined;
```
-Appends a child node into the current one.
+Removes a child node from the DOM and returns the removed node.
#### Parameters
| Parameter | Type | Description |
| ------ | ------ | ------ |
-| `node` | [`Node`](#classesnodemd) | The new child node |
+| `child` | [`Node`](#classesnodemd) | The child node to be removed. |
#### Returns
-[`Node`](#classesnodemd)
+[`Node`](#classesnodemd) \| `undefined`
-The same node that was passed in.
+The removed child node.
#### Inherited from
-[`Node`](#classesnodemd).[`appendChild`](#appendchild)
+[`Node`](#classesnodemd).[`removeChild`](#removechild)
+
+***
+
+### toJS()
+
+```ts
+toJS(): JsonMLElement | [];
+```
+
+Returns a simple object representation of the node and its descendants.
+
+#### Returns
+
+[`JsonMLElement`](#type-aliasesjsonmlelementmd) \| \[\]
+
+JsonML representation of the nodes and its subtree.
+
+***
+
+### toString()
+
+```ts
+toString(): string;
+```
+
+Returns a string representation of the node.
+
+#### Returns
+
+`string`
+
+A formatted XML source.
+
+#### Inherited from
+
+[`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
+
+A class describing an Element.
+
+## Extends
+
+- [`Node`](#classesnodemd)
+
+## Constructors
+
+### Constructor
+
+```ts
+new Element(
+ tagName: string,
+ attr?: XMLAttr | null,
+ closed?: boolean): Element;
+```
+
+Constructs a new Element instance.
+
+#### Parameters
+
+| Parameter | Type | Default value | Description |
+| ------ | ------ | ------ | ------ |
+| `tagName` | `string` | `undefined` | The tag name of the node. |
+| `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
+
+`Element`
+
+#### Overrides
+
+[`Node`](#classesnodemd).[`constructor`](#constructor)
+
+## Properties
+
+| Property | Type | Default value | Description | Overrides | Inherited from |
+| ------ | ------ | ------ | ------ | ------ | ------ |
+| `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. | - | - |
+| `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) |
+| `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
+
+### children
+
+#### Get Signature
+
+```ts
+get children(): Element[];
+```
+
+A list containing all child Elements of the current Element.
+
+##### Returns
+
+`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
+
+[`Document`](#classesdocumentmd).[`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`
+
+***
+
+### 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
+
+```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
+
+[`Document`](#classesdocumentmd).[`lastChild`](#lastchild)
+
+***
+
+### preserveSpace
+
+#### Get Signature
+
+```ts
+get preserveSpace(): boolean;
+```
+
+True if xml:space has been set to true for this node or any of its ancestors.
+
+##### Returns
+
+`boolean`
+
+#### Overrides
+
+[`Node`](#classesnodemd).[`preserveSpace`](#preservespace)
+
+***
+
+### tagName
+
+#### Get Signature
+
+```ts
+get tagName(): string;
+```
+
+##### Returns
+
+`string`
+
+***
+
+### textContent
+
+#### Get Signature
+
+```ts
+get textContent(): string;
+```
+
+The text content of this node (and its children).
+
+##### Returns
+
+`string`
+
+#### Inherited from
+
+[`Node`](#classesnodemd).[`textContent`](#textcontent)
+
+## 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: 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)
+
+***
+
+### 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
-getAttribute(name: string): string | null;
+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 |
+| ------ | ------ | ------ |
+| `name` | `string` | The attribute name to test for. |
+
+#### Returns
+
+`boolean`
+
+True if the attribute is present.
+
+***
+
+### hasAttributes()
+
+```ts
+hasAttributes(): boolean;
+```
+
+#### Returns
+
+`boolean`
+
+***
+
+### 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)
+
+***
+
+### prepend()
+
+```ts
+prepend(...nodes: (
+ | CreateChildArgument
+ | CreateChildArgument[])[]): void;
+```
+
+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 |
+| ------ | ------ |
+| ...`nodes` | ( \| [`CreateChildArgument`](#type-aliasescreatechildargumentmd) \| [`CreateChildArgument`](#type-aliasescreatechildargumentmd)[])[] |
+
+#### Returns
+
+`void`
+
+***
+
+### 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
+querySelector(selector: string): Element | null;
+```
+
+Return the first descendant element that match a specified CSS selector.
+
+#### Parameters
+
+| Parameter | Type | Description |
+| ------ | ------ | ------ |
+| `selector` | `string` | The CSS selector to filter by. |
+
+#### Returns
+
+`Element` \| `null`
+
+The elements by tag name.
+
+***
+
+### querySelectorAll()
+
+```ts
+querySelectorAll(selector: string): Element[];
+```
+
+Return all descendant elements that match a specified CSS selector.
+
+#### Parameters
+
+| Parameter | Type | Description |
+| ------ | ------ | ------ |
+| `selector` | `string` | The CSS selector to filter by. |
+
+#### Returns
+
+`Element`[]
+
+The elements by tag name.
+
+***
+
+### removeAttribute()
+
+```ts
+removeAttribute(name: string): void;
+```
+
+Remove an attribute off the element.
+
+#### Parameters
+
+| Parameter | Type | Description |
+| ------ | ------ | ------ |
+| `name` | `string` | The attribute name to remove. |
+
+#### Returns
+
+`void`
+
+***
+
+### removeChild()
+
+```ts
+removeChild(child: Node): Node | undefined;
+```
+
+Removes a child node from the DOM and returns the removed node.
+
+#### Parameters
+
+| Parameter | Type | Description |
+| ------ | ------ | ------ |
+| `child` | [`Node`](#classesnodemd) | The child node to be removed. |
+
+#### Returns
+
+[`Node`](#classesnodemd) \| `undefined`
+
+The removed child node.
+
+#### Inherited from
+
+[`Node`](#classesnodemd).[`removeChild`](#removechild)
+
+***
+
+### setAttribute()
+
+```ts
+setAttribute(name: string, value: string | number | boolean): void;
+```
+
+Sets an attribute on the element.
+
+#### Parameters
+
+| Parameter | Type | Description |
+| ------ | ------ | ------ |
+| `name` | `string` | The attribute name to read. |
+| `value` | `string` \| `number` \| `boolean` | The value to set |
+
+#### Returns
+
+`void`
+
+***
+
+### 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
+toJS(): JsonMLElement;
+```
+
+Returns a simple object representation of the node and its descendants.
+
+#### Returns
+
+[`JsonMLElement`](#type-aliasesjsonmlelementmd)
+
+JsonML representation of the nodes and its subtree.
+
+***
+
+### toString()
+
+```ts
+toString(): string;
+```
+
+Returns a string representation of the node.
+
+#### Returns
+
+`string`
+
+A formatted XML source.
+
+#### Inherited from
+
+[`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
+
+A class describing a Node.
+
+## Extended by
+
+- [`Element`](#classeselementmd)
+- [`Document`](#classesdocumentmd)
+- [`TextNode`](#classestextnodemd)
+- [`CDataNode`](#classescdatanodemd)
+
+## Constructors
+
+### Constructor
+
+```ts
+new Node(): Node;
+```
+
+#### Returns
+
+`Node`
+
+## Properties
+
+| Property | Type | Default value | Description |
+| ------ | ------ | ------ | ------ |
+| `childNodes` | `Node`[] | `[]` | The node's immediate children. |
+| `nodeName` | `string` | `'#node'` | A node type string identifier. |
+| `nodeType` | `number` | `0` | A numerical node type identifier. |
+| `parentNode` | `Node` \| `null` | `null` | The node's parent node. |
+
+## Accessors
+
+### firstChild
+
+#### Get Signature
+
+```ts
+get firstChild(): Node | null;
```
-Read an attribute from the element.
+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
+
+```ts
+get preserveSpace(): boolean;
+```
+
+True if xml:space has been set to true for this node or any of its ancestors.
+
+##### Returns
+
+`boolean`
+
+***
+
+### textContent
+
+#### Get Signature
+
+```ts
+get textContent(): string;
+```
+
+The text content of this node (and its children).
+
+##### Returns
+
+`string`
+
+## Methods
+
+### appendChild()
+
+```ts
+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 |
| ------ | ------ | ------ |
-| `name` | `string` | The attribute name to read. |
+| `node` | `T` | The new child node |
#### Returns
-`string` \| `null`
+`T`
-The attribute.
+The same node that was passed in.
***
-### getElementsByTagName()
+### insertBefore()
```ts
-getElementsByTagName(tagName: string): Element[];
+insertBefore(newNode: T, referenceNode: Node | null): T;
```
-Return all descendant elements that have the specified tag name.
+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 |
| ------ | ------ | ------ |
-| `tagName` | `string` | The tag name to filter by. |
+| `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
-`Element`[]
+`T`
-The elements by tag name.
+The added child (unless newNode is a DocumentFragment, in which case the empty DocumentFragment is returned).
***
-### hasAttribute()
+### removeChild()
```ts
-hasAttribute(name: string): boolean;
+removeChild(child: Node): Node | undefined;
```
-Test if an attribute exists on the element.
+Removes a child node from the DOM and returns the removed node.
#### Parameters
| Parameter | Type | Description |
| ------ | ------ | ------ |
-| `name` | `string` | The attribute name to test for. |
+| `child` | `Node` | The child node to be removed. |
#### Returns
-`boolean`
+`Node` \| `undefined`
-True if the attribute is present.
+The removed child node.
***
-### querySelector()
+### toString()
```ts
-querySelector(selector: string): Element | null;
+toString(): string;
```
-Return the first descendant element that match a specified CSS selector.
+Returns a string representation of the node.
+
+#### Returns
+
+`string`
+
+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 | Description |
-| ------ | ------ | ------ |
-| `selector` | `string` | The CSS selector to filter by. |
+| Parameter | Type |
+| ------ | ------ |
+| `message` | `string` |
#### Returns
-`Element` \| `null`
+`NotFoundError`
-The elements by tag name.
+#### Overrides
-***
+[`XMLError`](#classesxmlerrormd).[`constructor`](#constructor)
-### querySelectorAll()
+## 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
-querySelectorAll(selector: string): Element[];
+static captureStackTrace(targetObject: object, constructorOpt?: Function): void;
```
-Return all descendant elements that match a specified CSS selector.
+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 | Description |
-| ------ | ------ | ------ |
-| `selector` | `string` | The CSS selector to filter by. |
+| Parameter | Type |
+| ------ | ------ |
+| `targetObject` | `object` |
+| `constructorOpt?` | `Function` |
#### Returns
-`Element`[]
+`void`
-The elements by tag name.
+#### Inherited from
+
+[`XMLError`](#classesxmlerrormd).[`captureStackTrace`](#capturestacktrace)
***
-### removeAttribute()
+### prepareStackTrace()
```ts
-removeAttribute(name: string): void;
+static prepareStackTrace(err: Error, stackTraces: CallSite[]): any;
```
-Remove an attribute off the element.
+#### 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 | Description |
-| ------ | ------ | ------ |
-| `name` | `string` | The attribute name to remove. |
+| 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)
+
***
-### setAttribute()
+### prepareStackTrace()
```ts
-setAttribute(name: string, value: string): void;
+static prepareStackTrace(err: Error, stackTraces: CallSite[]): any;
```
-Sets an attribute on the element.
+#### 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
+
+A class describing a TextNode.
+
+## Extends
+
+- [`Node`](#classesnodemd)
+
+## Constructors
+
+### Constructor
+
+```ts
+new TextNode(value?: any): TextNode;
+```
+
+Constructs a new TextNode instance.
#### Parameters
| Parameter | Type | Description |
| ------ | ------ | ------ |
-| `name` | `string` | The attribute name to read. |
-| `value` | `string` | The value to set |
+| `value?` | `any` | The data for the node. |
#### Returns
-`void`
+`TextNode`
+
+#### Overrides
+
+[`Node`](#classesnodemd).[`constructor`](#constructor)
+
+## Properties
+
+| 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) |
+
+## Accessors
-***
+### firstChild
-### toJS()
+#### Get Signature
```ts
-toJS(): JsonMLElement;
+get firstChild(): Node | null;
```
-Returns a simple object representation of the node and its descendants.
+Returns the node's first child in the tree, or null if the node has no children.
-#### Returns
+##### Returns
-[`JsonMLElement`](#type-aliasesjsonmlelementmd)
+[`Node`](#classesnodemd) \| `null`
-JsonML representation of the nodes and its subtree.
+#### Inherited from
+
+[`Document`](#classesdocumentmd).[`firstChild`](#firstchild)
***
-### toString()
+### lastChild
+
+#### Get Signature
```ts
-toString(): string;
+get lastChild(): Node | null;
```
-Returns a string representation of the node.
-
-#### Returns
+Returns the node's last child in the tree, or null if the node has no children.
-`string`
+##### Returns
-A formatted XML source.
+[`Node`](#classesnodemd) \| `null`
#### Inherited from
-[`Node`](#classesnodemd).[`toString`](#tostring)
-
+[`Document`](#classesdocumentmd).[`lastChild`](#lastchild)
-
+***
-# Node
+### nodeValue
-A class describing a Node.
+#### Get Signature
-## Extended by
+```ts
+get nodeValue(): string;
+```
-- [`Element`](#classeselementmd)
-- [`Document`](#classesdocumentmd)
-- [`TextNode`](#classestextnodemd)
-- [`CDataNode`](#classescdatanodemd)
+##### Returns
-## Constructors
+`string`
-### Constructor
+#### Set Signature
```ts
-new Node(): Node;
+set nodeValue(value: string): void;
```
-#### Returns
+##### Parameters
-`Node`
+| Parameter | Type |
+| ------ | ------ |
+| `value` | `string` |
-## Properties
+##### Returns
-| Property | Type | Default value | Description |
-| ------ | ------ | ------ | ------ |
-| `childNodes` | `Node`[] | `[]` | The node's immediate children. |
-| `nodeName` | `string` | `'#node'` | A node type string identifier. |
-| `nodeType` | `number` | `0` | A numerical node type identifier. |
-| `parentNode` | `Node` \| `null` | `null` | The node's parent node. |
+`void`
-## Accessors
+***
### preserveSpace
@@ -763,6 +2327,10 @@ True if xml:space has been set to true for this node or any of its ancestors.
`boolean`
+#### Inherited from
+
+[`Node`](#classesnodemd).[`preserveSpace`](#preservespace)
+
***
### textContent
@@ -779,28 +2347,101 @@ The text content of this node (and its children).
`string`
+#### Overrides
+
+[`Node`](#classesnodemd).[`textContent`](#textcontent)
+
## Methods
### 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` | The new child node |
+| `node` | `T` | The new child node |
#### Returns
-`Node`
+`T`
The same node that was passed in.
+#### Inherited from
+
+[`Node`](#classesnodemd).[`appendChild`](#appendchild)
+
+***
+
+### 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 | Description |
+| ------ | ------ | ------ |
+| `child` | [`Node`](#classesnodemd) | The child node to be removed. |
+
+#### Returns
+
+[`Node`](#classesnodemd) \| `undefined`
+
+The removed child node.
+
+#### Inherited from
+
+[`Node`](#classesnodemd).[`removeChild`](#removechild)
+
***
### toString()
@@ -817,136 +2458,202 @@ Returns a string representation of the node.
A formatted XML source.
+#### Inherited from
+
+[`Node`](#classesnodemd).[`toString`](#tostring)
-
-# TextNode
+
-A class describing a TextNode.
+# XMLError
+
+Base class for all errors thrown by this library. Catching `XMLError`
+will catch any of the library's specific error subclasses.
## Extends
-- [`Node`](#classesnodemd)
+- `Error`
+
+## Extended by
+
+- [`ParserError`](#classesparsererrormd)
+- [`NamespaceError`](#classesnamespaceerrormd)
+- [`HierarchyError`](#classeshierarchyerrormd)
+- [`NotFoundError`](#classesnotfounderrormd)
## Constructors
### Constructor
```ts
-new TextNode(value?: string): TextNode;
+new XMLError(message: string): XMLError;
```
-Constructs a new TextNode instance.
-
#### Parameters
-| Parameter | Type | Description |
-| ------ | ------ | ------ |
-| `value?` | `string` | The data for the node |
+| Parameter | Type |
+| ------ | ------ |
+| `message` | `string` |
#### Returns
-`TextNode`
+`XMLError`
#### Overrides
-[`Node`](#classesnodemd).[`constructor`](#constructor)
+```ts
+Error.constructor
+```
## Properties
-| Property | Type | Default value | Description | Inherited from |
+| Property | Modifier | Type | Description | Inherited from |
| ------ | ------ | ------ | ------ | ------ |
-| `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) |
-| `value` | `string` | `undefined` | The node's data value. | - |
-
-## Accessors
+| `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` |
-### preserveSpace
+## Methods
-#### Get Signature
+### captureStackTrace()
```ts
-get preserveSpace(): boolean;
+static captureStackTrace(targetObject: object, constructorOpt?: Function): void;
```
-True if xml:space has been set to true for this node or any of its ancestors.
+Creates a `.stack` property on `targetObject`, which when accessed returns
+a string representing the location in the code at which
+`Error.captureStackTrace()` was called.
-##### Returns
+```js
+const myObject = {};
+Error.captureStackTrace(myObject);
+myObject.stack; // Similar to `new Error().stack`
+```
-`boolean`
+The first line of the trace will be prefixed with
+`${myObject.name}: ${myObject.message}`.
-#### Inherited from
+The optional `constructorOpt` argument accepts a function. If given, all frames
+above `constructorOpt`, including `constructorOpt`, will be omitted from the
+generated stack trace.
-[`Node`](#classesnodemd).[`preserveSpace`](#preservespace)
+The `constructorOpt` argument is useful for hiding implementation
+details of error generation from the user. For instance:
-***
+```js
+function a() {
+ b();
+}
-### textContent
+function b() {
+ c();
+}
-#### Get Signature
+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;
-```ts
-get textContent(): string;
+ // 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();
```
-The text content of this node (and its children).
+#### Parameters
-##### Returns
+| Parameter | Type |
+| ------ | ------ |
+| `targetObject` | `object` |
+| `constructorOpt?` | `Function` |
-`string`
+#### Returns
-#### Overrides
+`void`
-[`Node`](#classesnodemd).[`textContent`](#textcontent)
+#### Inherited from
-## Methods
+```ts
+Error.captureStackTrace
+```
-### appendChild()
+***
+
+### prepareStackTrace()
```ts
-appendChild(node: Node): Node;
+static prepareStackTrace(err: Error, stackTraces: CallSite[]): any;
```
-Appends a child node into the current one.
-
#### Parameters
-| Parameter | Type | Description |
-| ------ | ------ | ------ |
-| `node` | [`Node`](#classesnodemd) | The new child node |
+| Parameter | Type |
+| ------ | ------ |
+| `err` | `Error` |
+| `stackTraces` | `CallSite`[] |
#### Returns
-[`Node`](#classesnodemd)
+`any`
-The same node that was passed in.
+#### See
+
+https://v8.dev/docs/stack-trace-api#customizing-stack-traces
#### Inherited from
-[`Node`](#classesnodemd).[`appendChild`](#appendchild)
+```ts
+Error.prepareStackTrace
+```
-***
-### toString()
+
+
+# escapeXML()
```ts
-toString(): string;
+function escapeXML(s: string): string;
```
-Returns a string representation of the node.
+Escape XML entities in a string.
-#### Returns
+## Parameters
+
+| Parameter | Type | Description |
+| ------ | ------ | ------ |
+| `s` | `string` | Unescaped string |
+
+## Returns
`string`
-A formatted XML source.
+Escaped string
-#### Inherited from
-[`Node`](#classesnodemd).[`toString`](#tostring)
+
+
+# isElement()
+
+```ts
+function isElement(d: unknown): d is Element;
+```
+
+## Parameters
+
+| Parameter | Type |
+| ------ | ------ |
+| `d` | `unknown` |
+
+## Returns
+
+`d is Element`
@@ -957,6 +2664,7 @@ A formatted XML source.
function parseXML(source: string, options?: {
emptyDoc?: boolean;
laxAttr?: boolean;
+ ns?: boolean;
}): Document;
```
@@ -967,9 +2675,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 +2687,79 @@ 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;
+```
+
+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 | 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.
+
+
+
+
+# simplePrint()
+
+```ts
+function simplePrint(node:
+ | 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 | Description |
+| ------ | ------ | ------ |
+| `node` | \| [`Node`](#classesnodemd) \| [`DocumentFragment`](#classesdocumentfragmentmd) | The node to serialize. |
+
+## Returns
+
+`string`
+
+The serialized XML string.
+
+
+
+
+# CreateChildArgument
+
+```ts
+type CreateChildArgument =
+ | Node
+ | DocumentFragment
+ | string
+ | boolean
+ | number
+ | null
+ | undefined;
+```
+
+
# JsonMLAttr
@@ -1001,6 +2783,15 @@ type JsonMLElement =
```
+
+
+# XMLAttr
+
+```ts
+type XMLAttr = Record;
+```
+
+
# ATTRIBUTE\_NODE
@@ -1131,3 +2922,14 @@ const TEXT_NODE: number = 3;
```
A text node identifier
+
+
+
+
+# XML\_DECLARATION
+
+```ts
+const XML_DECLARATION: "" = '';
+```
+
+XML declaration string
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: {...},
}
]
}
```
-
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/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..ad72f95 100644
--- a/lib/Document.ts
+++ b/lib/Document.ts
@@ -2,10 +2,17 @@ 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';
+import { HierarchyError, NamespaceError } from './errors.js';
/**
* This class describes an XML document.
@@ -14,6 +21,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 +34,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 */
+ private _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 +71,58 @@ 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);
+ 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,
+ attr: XMLAttr | null | undefined,
+ ...children: (CreateChildArgument | CreateChildArgument[])[]
+ ): Element => {
+ const ns = this.namespaces.get(namespaceURI);
+ if (ns == null) {
+ throw new NamespaceError('Unknown namespace ' + namespaceURI);
+ }
+ const element = new Element(ns ? ns + ':' + qualifiedName : qualifiedName);
+ element.setAttrValues(attr ?? null);
+ for (const child of children) {
+ element.append(child);
+ }
+ return element;
+ };
+
/**
* Return all descendant elements that have the specified tag name.
*
@@ -45,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, []);
}
@@ -58,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;
}
@@ -71,18 +157,32 @@ 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);
}
// 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 HierarchyError('A document must have only one child element.');
+ }
+ let root: Element;
+ if (node instanceof DocumentFragment) {
+ if (!(node.childNodes[0] instanceof Element)) {
+ throw new HierarchyError('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 HierarchyError('Document root node must be an Element');
+ }
+ appendChild(this, root);
+ this.root = root;
+ this._updateNS();
return node;
}
@@ -94,4 +194,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 HierarchyError('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..7425c86 100644
--- a/lib/Element.ts
+++ b/lib/Element.ts
@@ -1,12 +1,17 @@
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';
-
-// eslint-disable-next-line @typescript-eslint/unbound-method
-const hasOwnProperty = Object.prototype.hasOwnProperty;
+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';
+import { createNamedNodeMap, type NamedNodeMap } from './NamedNodeMap.ts';
+import { Attr } from './Attr.ts';
+import { splitTagName } from './splitTagName.ts';
/**
* A class describing an Element.
@@ -14,49 +19,60 @@ 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.
*
* @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) {
+ 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.assign(Object.create(null), attr);
+
+ 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) {
@@ -72,6 +88,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.
*
@@ -79,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;
}
/**
@@ -88,8 +116,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.attributes.setNamedItem(new Attr(name, value));
}
/**
@@ -99,7 +127,23 @@ 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;
+ }
+
+ /**
+ * 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);
+ }
+ }
+ }
}
/**
@@ -108,7 +152,75 @@ 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 {
+ 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);
+ }
+ }
+ }
+
+ /**
+ * 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.prefix ??= this.prefix;
+ elm.setAttrValues(attr ?? null);
+ this.appendChild(elm);
+ for (const child of children) {
+ elm.append(child);
+ }
+ return elm;
}
/**
@@ -119,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, []);
@@ -133,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;
}
@@ -146,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);
}
@@ -159,4 +271,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/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/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
new file mode 100644
index 0000000..ae00fba
--- /dev/null
+++ b/lib/NSMap.ts
@@ -0,0 +1,46 @@
+import { NamespaceError } from './errors.js';
+
+export class NSMap {
+ private uriToPres: Record = {};
+ private uriToPre: Record = {};
+ private preToUri: Record = {};
+
+ get (nsURI: string): string | undefined {
+ return this.uriToPre[nsURI];
+ }
+
+ getByPrefix (nsPrefix: string): string | undefined {
+ return this.preToUri[nsPrefix];
+ }
+
+ list (): [string, string][] {
+ 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) {
+ // A prefix can only point to one URI — collisions are an error.
+ if ((nsPrefix in this.preToUri) && (this.preToUri[nsPrefix] !== nsURI)) {
+ 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)) {
+ return;
+ }
+ if (!(nsURI in this.uriToPres)) {
+ this.uriToPres[nsURI] = [];
+ }
+ 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;
+ }
+ }
+}
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..e8c783b
--- /dev/null
+++ b/lib/NamedNodeMap.ts
@@ -0,0 +1,93 @@
+import type { Attr } from './Attr.ts';
+import { NotFoundError } from './errors.js';
+
+/**
+ * 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];
+ }
+ throw new NotFoundError(`No attribute 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/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..fa2328e 100644
--- a/lib/Node.ts
+++ b/lib/Node.ts
@@ -1,4 +1,6 @@
import { appendChild } from './appendChild.js';
+import { DocumentFragment } from './DocumentFragment.ts';
+import { NotFoundError } from './errors.js';
import { prettyPrint } from './prettyPrint.js';
/**
@@ -14,23 +16,82 @@ 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.');
+ throw new TypeError('1 argument required, but 0 present.');
}
- if (!(node instanceof Node)) {
- throw new Error('Cannot appendChild: Child is not a node');
+ if (!(node instanceof Node) && !(node instanceof DocumentFragment)) {
+ throw new TypeError('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 child 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) {
+ 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) {
+ 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..8c24e8c 100644
--- a/lib/TextNode.ts
+++ b/lib/TextNode.ts
@@ -8,22 +8,31 @@ 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.
- * @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.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/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..2cd9199 100644
--- a/lib/appendChild.ts
+++ b/lib/appendChild.ts
@@ -1,11 +1,26 @@
-import { Node } from './Node.js';
+import { DocumentFragment } from './DocumentFragment.ts';
+import type { Node } from './Node.js';
+import { HierarchyError } from './errors.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 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
+ 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..549749d 100644
--- a/lib/constants.ts
+++ b/lib/constants.ts
@@ -33,3 +33,6 @@ 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/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/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/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/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/index.ts b/lib/index.ts
index 390e19e..753a28e 100644
--- a/lib/index.ts
+++ b/lib/index.ts
@@ -4,6 +4,20 @@ 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 {
+ 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';
export {
ELEMENT_NODE,
@@ -19,3 +33,5 @@ export {
DOCUMENT_FRAGMENT_NODE,
NOTATION_NODE
} from './constants.js';
+
+export { XML_DECLARATION } from './constants.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 d21144f..5650f12 100644
--- a/lib/parser.ts
+++ b/lib/parser.ts
@@ -7,10 +7,12 @@ 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,
- laxAttr: false
+ laxAttr: false,
+ ns: false
};
const NON_ELEMENT = new Element('#');
@@ -238,6 +240,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 +248,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 +295,20 @@ export function parseXML (
while (m);
}
+ function checkNS (elm: Element) {
+ for (const attr of elm.attributes) {
+ if (attr.localName === 'xmlns') {
+ doc.attachNS(attr.value, '');
+ }
+ if (attr.prefix === 'xmlns') {
+ doc.attachNS(attr.value, attr.localName);
+ }
+ }
+ if (elm.prefix && !doc.namespaces.getByPrefix(elm.prefix)) {
+ throw new NamespaceError('Unknown namespace prefix ' + elm.prefix);
+ }
+ }
+
// BOM
if (xml.charCodeAt(pos) === 65279) {
pos++;
@@ -303,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;
});
@@ -316,11 +335,12 @@ 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;
});
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;
@@ -347,11 +367,13 @@ export function parseXML (
return true;
}
const msg = `Expected ${current?.fullName}> got ${t}> in line ${posToLine(pos, xml)}`;
- throw new Error(msg);
+ throw new ParserError(msg);
})
||
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;
@@ -365,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);
@@ -376,15 +398,14 @@ 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 ${root.tagName}> got EOF`);
+ throw new ParserError(`Expected ${root.localName}> got EOF`);
}
- const doc = new Document();
if (root !== NON_ELEMENT) {
doc.appendChild(root);
}
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 2f8e036..2efaecc 100644
--- a/lib/prettyPrint.ts
+++ b/lib/prettyPrint.ts
@@ -1,5 +1,5 @@
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';
@@ -9,32 +9,49 @@ 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) {
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 {
+/**
+ * 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);
+ }
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 +60,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..94083c2
--- /dev/null
+++ b/lib/simplePrint.ts
@@ -0,0 +1,51 @@
+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 { HierarchyError } from './errors.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('');
+ }
+ if (node.nodeType === DOCUMENT_NODE) {
+ const root = (node as Document).root;
+ if (!root) throw new HierarchyError('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).data);
+ }
+ 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)) {
+ for (const attr of node.attributes) {
+ attrList += ` ${attr.fullName}="${escape(attr.value)}"`;
+ }
+ }
+ return children
+ ? `<${tagName}${attrList}>${children}${tagName}>`
+ : `<${tagName}${attrList} />`;
+ }
+ return '';
+}
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/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);
}
diff --git a/package-lock.json b/package-lock.json
index 0b201a3..e1ed8a9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,16 +1,17 @@
{
"name": "@borgar/simple-xml",
- "version": "2.2.2",
+ "version": "3.0.0-rc.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@borgar/simple-xml",
- "version": "2.2.2",
+ "version": "3.0.0-rc.2",
"license": "MIT",
"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",
@@ -877,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"
],
@@ -891,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"
],
@@ -905,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"
],
@@ -919,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"
],
@@ -933,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"
],
@@ -947,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"
],
@@ -961,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": [
@@ -975,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": [
@@ -989,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": [
@@ -1003,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": [
@@ -1017,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": [
@@ -1031,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": [
@@ -1045,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": [
@@ -1059,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": [
@@ -1073,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": [
@@ -1087,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": [
@@ -1101,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"
],
@@ -1129,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"
],
@@ -1143,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"
],
@@ -1157,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"
],
@@ -1171,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"
],
@@ -1262,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": {
@@ -1383,6 +1465,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",
@@ -1586,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": {
@@ -1596,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"
@@ -1788,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": {
@@ -1971,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": {
@@ -3179,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"
},
@@ -3418,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": {
@@ -3428,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"
@@ -4438,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"
},
@@ -4980,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"
},
@@ -5070,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": [
{
@@ -5381,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"
},
@@ -5425,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": [
{
@@ -5445,7 +5539,7 @@
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.11",
+ "nanoid": "^3.3.12",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -5846,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": {
@@ -5862,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"
}
},
@@ -6570,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": {
@@ -6954,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": {
@@ -6964,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"
@@ -6979,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",
@@ -7064,10 +7148,18 @@
}
},
"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",
+ "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",
@@ -7199,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": {
@@ -7292,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": {
@@ -7383,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": {
@@ -7664,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",
diff --git a/package.json b/package.json
index ff68e93..50f6485 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@borgar/simple-xml",
- "version": "2.2.2",
+ "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",
@@ -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"
@@ -41,7 +45,7 @@
},
"repository": {
"type": "git",
- "url": "git@github.com:borgar/simple-xml.git"
+ "url": "git+ssh://git@github.com/borgar/simple-xml.git"
},
"keywords": [
"xml",
@@ -55,6 +59,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/domQuery-dedup.spec.ts b/test/domQuery-dedup.spec.ts
index ff5caec..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/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..56324ee 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/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);
});
});
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
+ }
}