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 got 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 got EOF`); + throw new ParserError(`Expected 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}${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 + } }