diff --git a/lib/CreateChildArgument.ts b/lib/CreateChildArgument.ts new file mode 100644 index 0000000..4ed45e4 --- /dev/null +++ b/lib/CreateChildArgument.ts @@ -0,0 +1,4 @@ +import type { DocumentFragment } from './DocumentFragment.ts'; +import type { Node } from './Node.ts'; + +export type CreateChildArgument = Node | DocumentFragment | string | boolean | number | null | undefined; diff --git a/lib/Document.ts b/lib/Document.ts index 94b3bc9..ce57609 100644 --- a/lib/Document.ts +++ b/lib/Document.ts @@ -2,10 +2,16 @@ import { JsonML, type JsonMLElement } from './JsonML.js'; import { Node } from './Node.js'; import { Element } from './Element.js'; import { appendChild } from './appendChild.js'; -import { DOCUMENT_NODE } from './constants.js'; +import { DOCUMENT_NODE, XML_DECLARATION } from './constants.js'; import { domQuery } from './domQuery/index.js'; import { findAll } from './findAll.js'; import { isElement } from './isElement.ts'; +import { DocumentFragment } from './DocumentFragment.ts'; +import { NSMap } from './NSMap.ts'; +import { prettyPrint } from './prettyPrint.ts'; +import { simplePrint } from './simplePrint.ts'; +import type { CreateChildArgument } from './CreateChildArgument.ts'; +import type { XMLAttr } from './XMLAttr.ts'; /** * This class describes an XML document. @@ -14,6 +20,8 @@ import { isElement } from './isElement.ts'; */ export class Document extends Node { root: Element | null = null; + /** @ignore */ + namespaces = new NSMap(); /** * Constructs a new Document instance. @@ -25,6 +33,31 @@ export class Document extends Node { this.nodeType = DOCUMENT_NODE; } + /** + * Attach a namespace to the document. + * + * @param namespaceURI The namespace URI to attach. + * @param [prefix] Prefix to use on elements belonging to the namespace. + */ + attachNS (namespaceURI: string, prefix = ''): (name: string, attr?: XMLAttr | null, ...children: (CreateChildArgument | CreateChildArgument[])[]) => Element { + this.namespaces.add(namespaceURI, prefix); + this._updateNS(); + + // Return a new create function bound to the namespace + return this.createElementNS.bind(this, namespaceURI); + } + + /** @ignore */ + _updateNS () { + // ensure that namespaces exist on the root node + if (this.root) { + for (const [ namespaceURI, prefix ] of this.namespaces.list()) { + const key = 'xmlns' + (prefix ? ':' + prefix : ''); + this.root.setAttribute(key, namespaceURI); + } + } + } + // overwrites super get textContent () { return this.root ? this.root.textContent : ''; @@ -37,6 +70,60 @@ export class Document extends Node { return this.childNodes.filter(isElement); } + /** + * Create a new element node. + * + * @param qualifiedName The local tagName of the element. + * @param attr A record of attributes to assign to the new element. + * If the value is null or undefined, the attribute will be omitted. + * @param children Nodes to insert as children. + * Strings will be converted to TextNodes and arrays will be flattened. + * @returns A new Element instance. + */ + createElement = ( + qualifiedName: string, + attr: XMLAttr | null | undefined, + ...children: (CreateChildArgument | CreateChildArgument[])[] + ): Element => { + const element = new Element(qualifiedName); + if (attr) { + for (const [ key, val ] of Object.entries(attr)) { + if (val != null) { + element.setAttribute(key, String(val)); + } + } + } + for (const child of children) { + element.append(child); + } + return element; + }; + + createElementNS = ( + namespaceURI: string, + qualifiedName: string, + attr: XMLAttr | null | undefined, + ...children: (CreateChildArgument | CreateChildArgument[])[] + ): Element => { + const ns = this.namespaces.get(namespaceURI); + if (!ns) { + throw new Error('Unknown namespace ' + namespaceURI); + } + const element = new Element(ns + ':' + qualifiedName); + // can this not be solved by Element(name, attr) ... does the same thing internally, right? + if (attr) { + for (const [ key, val ] of Object.entries(attr)) { + if (val != null) { + element.setAttribute(key, String(val)); + } + } + } + for (const child of children) { + element.append(child); + } + return element; + }; + /** * Return all descendant elements that have the specified tag name. * @@ -77,12 +164,26 @@ export class Document extends Node { } // overwrites super - appendChild (node: Element): Element { - if (this.root) { - throw new Error('A document may only have one child/root element.'); + appendChild (node: T): T { + if (this.root || (node instanceof DocumentFragment && node.childNodes.length > 1)) { + throw new Error('A document must have only one child element.'); + } + let root: Element; + if (node instanceof DocumentFragment) { + if (!(node.childNodes[0] instanceof Element)) { + throw new Error('Document root node must be an Element'); + } + root = node.childNodes[0]; + } + else if (node instanceof Element) { + root = node; } - appendChild(this, node); - this.root = node; + else { + throw new Error('Document root node must be an Element'); + } + appendChild(this, root); + this.root = root; + this._updateNS(); return node; } @@ -94,4 +195,19 @@ export class Document extends Node { toJS (): JsonMLElement | [] { return this.root ? JsonML(this.root) : []; } + + /** + * Print the document as a string. + * + * @param pretty Apply automatic linebreaks and indentation to the output. + * @returns The document as an XML string. + */ + print (pretty = false): string { + if (!(this.root instanceof Element)) { + throw new Error('root element is missing'); + } + return `${XML_DECLARATION}\n` + ( + pretty ? prettyPrint(this.root) : simplePrint(this.root) + ); + } } diff --git a/lib/DocumentFragment.ts b/lib/DocumentFragment.ts new file mode 100644 index 0000000..e92aba5 --- /dev/null +++ b/lib/DocumentFragment.ts @@ -0,0 +1,43 @@ +import { appendChild } from './appendChild.ts'; +import { DOCUMENT_FRAGMENT_NODE } from './constants.ts'; +import type { Node } from './Node.ts'; +import { prettyPrint } from './prettyPrint.ts'; +import { simplePrint } from './simplePrint.ts'; + +/** + * A class describing a DocumentFragment. + */ +export class DocumentFragment { + /** The immediate children contained in the fragment. */ + childNodes: Node[] = []; + /** A numerical node type identifier. */ + nodeType: number = DOCUMENT_FRAGMENT_NODE; + + /** + * Appends a child node into the document fragment. + * + * @param node The new child node + * @returns The same node that was passed in. + */ + appendChild (node: T): T { + appendChild(this, node); + return node; + } + + /** @ignore */ + toString (): string { + return '#document-fragment'; + } + + /** + * Print the document as a string. + * + * @param pretty Apply automatic linebreaks and indentation to the output. + * @returns The document as an XML string. + */ + print (pretty = false): string { + return pretty + ? prettyPrint(this) + : simplePrint(this); + } +} diff --git a/lib/Element.ts b/lib/Element.ts index b788fa7..580033e 100644 --- a/lib/Element.ts +++ b/lib/Element.ts @@ -1,9 +1,13 @@ import { Node } from './Node.js'; -import { ELEMENT_NODE } from './constants.js'; +import { ELEMENT_NODE, XML_DECLARATION } from './constants.js'; import { JsonML, type JsonMLElement } from './JsonML.js'; import { domQuery } from './domQuery/index.js'; import { findAll } from './findAll.js'; import { isElement } from './isElement.ts'; +import { TextNode } from './TextNode.ts'; +import type { CreateChildArgument } from './CreateChildArgument.ts'; +import { prettyPrint } from './prettyPrint.ts'; +import { simplePrint } from './simplePrint.ts'; // eslint-disable-next-line @typescript-eslint/unbound-method const hasOwnProperty = Object.prototype.hasOwnProperty; @@ -31,7 +35,7 @@ export class Element extends Node { * Constructs a new Element instance. * * @param tagName The tag name of the node. - * @param [attr={}] A collection of attributes to assign. + * @param [attr={}] A collection of attributes to assign. Values of null or undefined will be ignored. * @param [closed=false] Was the element "self-closed" when read. */ constructor (tagName: string, attr: Record = {}, closed: boolean = false) { @@ -46,7 +50,12 @@ export class Element extends Node { this.fullName = tagName; this.closed = !!closed; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - this.attr = Object.assign(Object.create(null), attr); + this.attr = Object.create(null); + for (const [ k, v ] of Object.entries(attr)) { + if (v != null) { + this.setAttribute(k, v); + } + } // inherited instance props from Node this.nodeName = this.tagName.toUpperCase(); @@ -72,6 +81,18 @@ export class Element extends Node { return this.childNodes.filter(isElement); } + /** + * Returns an element's first child Element, or null if there are no child elements + */ + get firstElementChild (): Element | null { + for (const child of this.childNodes) { + if (isElement(child)) { + return child; + } + } + return null; + } + /** * Read an attribute from the element. * @@ -88,8 +109,8 @@ export class Element extends Node { * @param name The attribute name to read. * @param value The value to set */ - setAttribute (name: string, value: string) { - this.attr[name] = value; + setAttribute (name: string, value: string | number | boolean) { + this.attr[name] = String(value); } /** @@ -111,6 +132,46 @@ export class Element extends Node { delete this.attr[name]; } + get className (): string { + return this.getAttribute('class') ?? ''; + } + + set className (val: unknown) { + this.setAttribute('class', String(val)); + } + + /** + * Inserts a set of Node objects or strings after the last child of the Element. + * Strings are inserted as equivalent Text nodes. + */ + append (...nodes: (CreateChildArgument | CreateChildArgument[])[]): void { + const flatNodes = nodes.flat(); + for (const n of flatNodes) { + if (typeof n === 'string' || typeof n === 'number' || typeof n === 'boolean') { + this.appendChild(new TextNode(n)); + } + else if (n) { + this.appendChild(n); + } + } + } + + /** + * Insert a set of Node objects or strings before the first child of the Element. + * Strings are inserted as equivalent Text nodes. + */ + prepend (...nodes: (CreateChildArgument | CreateChildArgument[])[]): void { + const flatNodes = nodes.flat(); + for (const n of flatNodes) { + if (typeof n === 'string' || typeof n === 'number' || typeof n === 'boolean') { + this.insertBefore(new TextNode(n), this.firstChild); + } + else if (n) { + this.insertBefore(n, this.firstChild); + } + } + } + /** * Return all descendant elements that have the specified tag name. * @@ -159,4 +220,16 @@ export class Element extends Node { toJS (): JsonMLElement { return JsonML(this); } + + /** + * Print the document as a string. + * + * @param pretty Apply automatic linebreaks and indentation to the output. + * @returns The document as an XML string. + */ + print (pretty = false): string { + return `${XML_DECLARATION}\n` + ( + pretty ? prettyPrint(this) : simplePrint(this) + ); + } } diff --git a/lib/NSMap.ts b/lib/NSMap.ts new file mode 100644 index 0000000..08d52bc --- /dev/null +++ b/lib/NSMap.ts @@ -0,0 +1,27 @@ +export class NSMap { + uriToPre: Record = {}; + preToUri: Record = {}; + + get (nsURI: string): string | undefined { + return this.uriToPre[nsURI]; + } + + getByPrefix (nsPrefix: string): string | undefined { + return this.preToUri[nsPrefix]; + } + + list (): [string, string][] { + return Array.from(Object.entries(this.uriToPre)); + } + + add (nsURI: string, nsPrefix: string) { + if ((nsURI in this.uriToPre) && (this.uriToPre[nsURI] !== nsPrefix)) { + throw new Error(nsURI + ' allready has a different prefix'); + } + if ((nsPrefix in this.preToUri) && (this.preToUri[nsPrefix] !== nsPrefix)) { + throw new Error(nsPrefix + ' allready has a different URI'); + } + this.uriToPre[nsURI] = nsPrefix; + this.preToUri[nsPrefix] = nsURI; + } +} diff --git a/lib/Node.spec.ts b/lib/Node.spec.ts new file mode 100644 index 0000000..496f920 --- /dev/null +++ b/lib/Node.spec.ts @@ -0,0 +1,319 @@ +import { describe, it, expect } from 'vitest'; +import { Node } from './Node.ts'; +import { Element } from './Element.ts'; +import { TextNode } from './TextNode.ts'; +import { CDataNode } from './CDataNode.ts'; +import { DocumentFragment } from './DocumentFragment.ts'; + +describe('Node', () => { + describe('constructor / default properties', () => { + it('has expected default property values', () => { + const node = new Node(); + expect(node.childNodes).toEqual([]); + expect(node.nodeName).toBe('#node'); + expect(node.nodeType).toBe(0); + expect(node.parentNode).toBeNull(); + }); + }); + + describe('appendChild', () => { + it('appends a child node and sets parentNode', () => { + const parent = new Node(); + const child = new Node(); + parent.appendChild(child); + expect(parent.childNodes).toContain(child); + expect(child.parentNode).toBe(parent); + }); + + it('returns the appended node', () => { + const parent = new Node(); + const child = new Node(); + const result = parent.appendChild(child); + expect(result).toBe(child); + }); + + it('throws when called with no argument, null or undefined', () => { + const parent = new Node(); + // @ts-expect-error - testing runtime error + expect(() => parent.appendChild()).toThrow('1 argument required'); + // @ts-expect-error - testing runtime error + expect(() => parent.appendChild(null)).toThrow(); + // @ts-expect-error - testing runtime error + expect(() => parent.appendChild(undefined)).toThrow(); + }); + + it('throws when called with a non-node value', () => { + const parent = new Node(); + // @ts-expect-error - testing runtime error + expect(() => parent.appendChild('hello')).toThrow('Cannot appendChild'); + // @ts-expect-error - testing runtime error + expect(() => parent.appendChild(42)).toThrow('Cannot appendChild'); + // @ts-expect-error - testing runtime error + expect(() => parent.appendChild({})).toThrow('Cannot appendChild'); + }); + + it('moves a child from one parent to another', () => { + const parent1 = new Node(); + const parent2 = new Node(); + const child = new Node(); + parent1.appendChild(child); + expect(parent1.childNodes).toContain(child); + expect(child.parentNode).toBe(parent1); + parent2.appendChild(child); + expect(parent1.childNodes).not.toContain(child); + expect(parent2.childNodes).toContain(child); + expect(child.parentNode).toBe(parent2); + }); + + it('appends multiple children in order', () => { + const parent = new Node(); + const a = parent.appendChild(new Node()); + const b = parent.appendChild(new Node()); + const c = parent.appendChild(new Node()); + expect(parent.childNodes).toEqual([ a, b, c ]); + }); + + it('appends DocumentFragment children in order', () => { + const frag = new DocumentFragment(); + const a = frag.appendChild(new Node()); + const b = frag.appendChild(new Node()); + const c = frag.appendChild(new Node()); + expect(frag.childNodes).toEqual([ a, b, c ]); + + const parent = new Node(); + parent.appendChild(frag); + expect(parent.childNodes).toEqual([ a, b, c ]); + }); + + it('re-appending an existing child moves it to the end', () => { + const parent = new Node(); + const a = new Node(); + const b = new Node(); + parent.appendChild(a); + parent.appendChild(b); + expect(parent.childNodes).toEqual([ a, b ]); + + parent.appendChild(a); + expect(parent.childNodes).toEqual([ b, a ]); + }); + }); + + describe('insertBefore', () => { + it('inserts a node before a reference node', () => { + const parent = new Node(); + const a = new Node(); + const b = new Node(); + const c = new Node(); + parent.appendChild(a); + parent.appendChild(c); + + parent.insertBefore(b, c); + expect(b.parentNode).toBe(parent); + expect(parent.childNodes.filter(d => d === b).length).toBe(1); + }); + + it('inserts DocumentFragment children in order', () => { + const frag = new DocumentFragment(); + const a = frag.appendChild(new Node()); + const b = frag.appendChild(new Node()); + const c = frag.appendChild(new Node()); + expect(frag.childNodes).toEqual([ a, b, c ]); + + const parent = new Node(); + const z = new Node(); + parent.appendChild(z); // parent now only contains z + + parent.insertBefore(frag, z); + expect(parent.childNodes).toEqual([ a, b, c, z ]); + }); + + it('falls back to appendChild when referenceNode is null', () => { + const parent = new Node(); + const child = new Node(); + parent.insertBefore(child, null); + expect(parent.childNodes).toContain(child); + expect(child.parentNode).toBe(parent); + }); + + it('moves node from previous parent when inserting', () => { + const oldParent = new Node(); + const newParent = new Node(); + const ref = new Node(); + const child = new Node(); + + oldParent.appendChild(child); + newParent.appendChild(ref); + + newParent.insertBefore(child, ref); + expect(oldParent.childNodes).not.toContain(child); + expect(child.parentNode).toBe(newParent); + }); + }); + + describe('removeChild', () => { + it('removes a child and returns it', () => { + const parent = new Node(); + const child = new Node(); + parent.appendChild(child); + const removed = parent.removeChild(child); + expect(removed).toBe(child); + expect(parent.childNodes).not.toContain(child); + expect(child.parentNode).toBeNull(); + }); + + it('throws when no argument is provided', () => { + const parent = new Node(); + // @ts-expect-error - testing runtime error + expect(() => parent.removeChild()).toThrow('not of type'); + // @ts-expect-error - testing runtime error + expect(() => parent.removeChild(null)).toThrow('not of type'); + // @ts-expect-error - testing runtime error + expect(() => parent.removeChild(undefined)).toThrow('not of type'); + }); + + it('throws when the node is not a child', () => { + const parent = new Node(); + const orphan = new Node(); + expect(() => parent.removeChild(orphan)).toThrow( + 'The node to be removed is not a child of this node.' + ); + }); + + it('removes the correct child when there are multiple', () => { + const parent = new Node(); + const a = new Node(); + const b = new Node(); + const c = new Node(); + parent.appendChild(a); + parent.appendChild(b); + parent.appendChild(c); + + parent.removeChild(b); + expect(parent.childNodes).toEqual([ a, c ]); + expect(b.parentNode).toBeNull(); + }); + + it('allows removing and re-adding a child', () => { + const parent = new Node(); + const child = new Node(); + parent.appendChild(child); + parent.removeChild(child); + expect(parent.childNodes.length).toBe(0); + expect(child.parentNode).toBeNull(); + + parent.appendChild(child); + expect(parent.childNodes).toEqual([ child ]); + expect(child.parentNode).toBe(parent); + }); + }); + + describe('preserveSpace', () => { + it('returns false by default on a root node', () => { + const node = new Node(); + expect(node.preserveSpace).toBe(false); + }); + + it('inherits preserveSpace from parent', () => { + // Use an Element with xml:space="preserve" as parent + const parent = new Element('root', { 'xml:space': 'preserve' }); + const child = new Node(); + parent.appendChild(child); + expect(child.preserveSpace).toBe(true); + }); + + it('returns false when parent does not preserve space', () => { + const parent = new Node(); + const child = new Node(); + parent.appendChild(child); + expect(child.preserveSpace).toBe(false); + }); + + it('propagates through multiple levels of ancestry', () => { + const grandparent = new Element('root', { 'xml:space': 'preserve' }); + const parent = new Node(); + const child = new Node(); + grandparent.appendChild(parent); + parent.appendChild(child); + expect(child.preserveSpace).toBe(true); + }); + }); + + describe('textContent', () => { + it('returns empty string for a node with no children', () => { + const node = new Node(); + expect(node.textContent).toBe(''); + }); + + it('returns text from TextNode children', () => { + const parent = new Node(); + const text = new TextNode('hello world'); + parent.appendChild(text); + expect(parent.textContent).toBe('hello world'); + }); + + it('concatenates text from multiple TextNode children', () => { + // A whitespace-only TextNode returns '' when preserveSpace is false, + // so 'foo' + '' + 'bar' = 'foobar' + const parent = new Node(); + parent.appendChild(new TextNode('foo')); + parent.appendChild(new TextNode(' ')); + parent.appendChild(new TextNode('bar')); + expect(parent.textContent).toBe('foobar'); + }); + + it('concatenates text including spaces when preserveSpace is true', () => { + const parent = new Element('div', { 'xml:space': 'preserve' }); + parent.appendChild(new TextNode('foo')); + parent.appendChild(new TextNode(' ')); + parent.appendChild(new TextNode('bar')); + expect(parent.textContent).toBe('foo bar'); + }); + + it('collects text content from nested children recursively', () => { + const root = new Element('root'); + const child = new Element('child'); + const text = new TextNode('deep'); + root.appendChild(child); + child.appendChild(text); + expect(root.textContent).toBe('deep'); + }); + + it('includes CDataNode values', () => { + const parent = new Node(); + parent.appendChild(new TextNode('before')); + parent.appendChild(new CDataNode(' cdata ')); + parent.appendChild(new TextNode('after')); + expect(parent.textContent).toBe('before cdata after'); + }); + + it('returns empty string for whitespace-only TextNode children (no preserveSpace)', () => { + const parent = new Element('div'); + parent.appendChild(new TextNode(' ')); + // TextNode with only whitespace returns '' when preserveSpace is false + expect(parent.textContent).toBe(''); + }); + + it('preserves whitespace-only TextNode when preserveSpace is true', () => { + const parent = new Element('div', { 'xml:space': 'preserve' }); + parent.appendChild(new TextNode(' ')); + expect(parent.textContent).toBe(' '); + }); + }); + + describe('toString', () => { + it('returns a string', () => { + const node = new Node(); + expect(typeof node.toString()).toBe('string'); + }); + + it('renders child elements to XML', () => { + const root = new Element('root'); + const child = new Element('child'); + root.appendChild(child); + const str = root.toString(); + expect(str).toContain(''); + expect(str).toContain(''); + }); + }); +}); diff --git a/lib/Node.ts b/lib/Node.ts index 4d6438b..6062825 100644 --- a/lib/Node.ts +++ b/lib/Node.ts @@ -1,4 +1,5 @@ import { appendChild } from './appendChild.js'; +import { DocumentFragment } from './DocumentFragment.ts'; import { prettyPrint } from './prettyPrint.js'; /** @@ -14,23 +15,83 @@ export class Node { /** The node's parent node. */ parentNode: Node | null = null; + /** + * Returns the node's first child in the tree, or null if the node has no children. + */ + get firstChild (): Node | null { + return this.childNodes.at(0) ?? null; + } + + /** + * Returns the node's last child in the tree, or null if the node has no children. + */ + get lastChild (): Node | null { + return this.childNodes.at(-1) ?? null; + } + /** * Appends a child node into the current one. * * @param node The new child node * @returns The same node that was passed in. */ - appendChild (node: Node): Node { + appendChild (node: T): T { if (!node) { throw new Error('1 argument required, but 0 present.'); } - if (!(node instanceof Node)) { + if (!(node instanceof Node) && !(node instanceof DocumentFragment)) { throw new Error('Cannot appendChild: Child is not a node'); } appendChild(this, node); return node; } + /** + * Inserts a node before a _reference node_ as a child of a specified _parent node_. + * + * @param newNode The node to be inserted. + * @param referenceNode The node before which newNode is inserted. If this is null, then newNode is inserted at the end of node's child nodes. + * @returns The added child (unless newNode is a DocumentFragment, in which case the empty DocumentFragment is returned). + */ + insertBefore (newNode: T, referenceNode: Node | null): T { + if (referenceNode) { + const index = this.childNodes.indexOf(referenceNode); + if (index > -1) { + const insertNodes = newNode instanceof Node ? [ newNode ] : newNode.childNodes; + // update parentage for all the new nodes + for (const node of insertNodes) { + node.parentNode?.removeChild(node); + node.parentNode = this; + } + // insert the new nodes + this.childNodes.splice(index, 0, ...insertNodes); + } + return newNode; + } + return this.appendChild(newNode); + } + + /** + * Removes a child node from the DOM and returns the removed node. + * @param node The child node to be removed. + * @returns The removed child node. + */ + removeChild (child: Node) { + if (!child) { + throw new TypeError('parameter 1 is not of type \'Node\''); + } + const index = this.childNodes.indexOf(child); + if (index === -1) { + // DOMException + throw new Error('The node to be removed is not a child of this node.'); + } + const node = this.childNodes.splice(index, 1).at(0); + if (node) { + node.parentNode = null; + } + return node; + } + /** * True if xml:space has been set to true for this node or any of its ancestors. */ diff --git a/lib/TextNode.ts b/lib/TextNode.ts index 5cd7848..cc86cae 100644 --- a/lib/TextNode.ts +++ b/lib/TextNode.ts @@ -12,13 +12,14 @@ export class TextNode extends Node { /** * Constructs a new TextNode instance. - * @param {string} [value] The data for the node + * + * @param [value] The data for the node. */ - constructor (value: string) { + constructor (value: any) { super(); this.nodeName = '#text'; this.nodeType = TEXT_NODE; - this.value = value || ''; + this.value = String(value); } // overwrites super diff --git a/lib/XMLAttr.ts b/lib/XMLAttr.ts new file mode 100644 index 0000000..f69dbdf --- /dev/null +++ b/lib/XMLAttr.ts @@ -0,0 +1 @@ +export type XMLAttr = Record; diff --git a/lib/appendChild.ts b/lib/appendChild.ts index 78e8bb3..83f3c2a 100644 --- a/lib/appendChild.ts +++ b/lib/appendChild.ts @@ -1,11 +1,25 @@ -import { Node } from './Node.js'; +import { DocumentFragment } from './DocumentFragment.ts'; +import type { Node } from './Node.js'; -export function appendChild (parent: Node, child: Node) { - // if node is attached to a parent, first detach it - if (child.parentNode) { - const p = child.parentNode; - p.childNodes = p.childNodes.filter(d => d !== child); - } - child.parentNode = parent; - parent.childNodes.push(child); +export function appendChild (parent: Node | DocumentFragment, child: Node | DocumentFragment) { + if (child instanceof DocumentFragment) { + // perform an append operation for every child in the fragment + for (const d of child.childNodes) { + appendChild(parent, d); + } + } + else if (parent === child) { + // XXX: there should really be a more elaborate tests here to determine that child does not contain parent + throw new Error('The new child element contains the parent.'); + } + else if (parent instanceof DocumentFragment) { + // appending to a fragment does not mess with the node's parentage + parent.childNodes.push(child); + } + else { + // if node is attached to a parent, first detach it + child.parentNode?.removeChild(child); + child.parentNode = parent; + parent.childNodes.push(child); + } } diff --git a/lib/constants.ts b/lib/constants.ts index b90e410..2ba3228 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -33,3 +33,5 @@ export const DOCUMENT_FRAGMENT_NODE: number = 11; /** A documentation node identifier */ export const NOTATION_NODE: number = 12; + +export const XML_DECLARATION = ''; diff --git a/lib/escape.ts b/lib/escape.ts index cd6d597..5ea8b7b 100644 --- a/lib/escape.ts +++ b/lib/escape.ts @@ -9,9 +9,10 @@ const entities: Record = { }; /** - * @ignore + * Escape XML entities in a string. + * * @param {string} s Unescaped string - * @returns {string} XML escaped string + * @returns {string} Escaped string */ export function escape (s: string): string { // eslint-disable-next-line no-control-regex diff --git a/lib/index.ts b/lib/index.ts index 390e19e..f6280d4 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -4,6 +4,13 @@ export { Element } from './Element.js'; export { Document } from './Document.js'; export { TextNode } from './TextNode.js'; export { CDataNode } from './CDataNode.js'; +export { DocumentFragment } from './DocumentFragment.ts'; +export { escape as escapeXML } from './escape.ts'; +export { isElement } from './isElement.ts'; +export { prettyPrint } from './prettyPrint.ts'; +export { simplePrint } from './simplePrint.ts'; +export type { CreateChildArgument } from './CreateChildArgument.js'; +export type { XMLAttr } from './XMLAttr.js'; export type { JsonMLElement, JsonMLAttr } from './JsonML.js'; export { ELEMENT_NODE, diff --git a/lib/parser.ts b/lib/parser.ts index d21144f..575d0b1 100644 --- a/lib/parser.ts +++ b/lib/parser.ts @@ -238,6 +238,7 @@ function posToLine (pos: number, src: string): number { * @param [options={}] Parsing options. * @param [options.emptyDoc=false] Permit "rootless" documents. * @param [options.laxAttr=false] Permit unquoted attributes (``). + * @param [options.ns=false] Validate xmlns and element namespaces as they are parsed. * @returns A DOM representing the XML node tree. */ export function parseXML ( @@ -245,11 +246,13 @@ export function parseXML ( options: { emptyDoc?: boolean; laxAttr?: boolean; + ns?: boolean; } = DEFAULTOPTIONS ): Document { // 2.11: before parsing, translate both the two-character sequence // \r\n and any \r that is not followed by \n to a single \n const xml = removeCR(source); + const doc = new Document(); let pos = 0; let root = NON_ELEMENT; @@ -290,6 +293,20 @@ export function parseXML ( while (m); } + function checkNS (elm: Element) { + for (const key in elm.attr) { + if (key === 'xmlns') { + doc.attachNS(elm.attr[key], ''); + } + if (key.startsWith('xmlns:')) { + doc.attachNS(elm.attr[key], key.slice(6)); + } + } + if (elm.ns && !doc.namespaces.getByPrefix(elm.ns)) { + throw new Error('Unknown namespace prefix ' + elm.ns); + } + } + // BOM if (xml.charCodeAt(pos) === 65279) { pos++; @@ -316,6 +333,7 @@ export function parseXML ( // root tag maybeMatchFn(fnTag, (_, t, a, c) => { root = new Element(t, parseAttr(a, options.laxAttr), !!c); + if (options.ns) checkNS(root); return true; }); @@ -351,7 +369,9 @@ export function parseXML ( }) || maybeMatchFn(fnTag, (_, t, a, c) => { - const elm = new Element(t, parseAttr(a, options.laxAttr), !!c); + const attr = parseAttr(a, options.laxAttr); + const elm = new Element(t, attr, !!c); + if (options.ns) checkNS(elm); current?.appendChild(elm); if (!elm.closed) { current = elm; @@ -384,7 +404,6 @@ export function parseXML ( throw new Error(`Expected got EOF`); } - const doc = new Document(); if (root !== NON_ELEMENT) { doc.appendChild(root); } diff --git a/lib/prettyPrint.ts b/lib/prettyPrint.ts index 2f8e036..7634473 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'; @@ -25,16 +25,19 @@ function printCData (node: CDataNode) { return `/g, ']]>')}]]>`; } -function printDocument (node: Node): string { +function printDocument (node: Node | DocumentFragment): string { return node.childNodes .map(n => prettyPrint(n)) .join('\n'); } -export function prettyPrint (node: Node, indent: string = ''): string { +export function prettyPrint (node: Node | DocumentFragment, indent: string = ''): string { + if (node instanceof DocumentFragment) { + return printDocument(node); + } const { preserveSpace } = node; if (node.nodeType === DOCUMENT_NODE) { - return printDocument(node as Document); + return printDocument(node); } else if (node.nodeType === CDATA_SECTION_NODE) { return printCData(node as CDataNode); @@ -43,7 +46,7 @@ export function prettyPrint (node: Node, indent: string = ''): string { return printTextNode(node as TextNode); } else if (isElement(node)) { - const tagName = node.tagName; + const tagName = node.fullName; const { childNodes } = node; let children = ''; diff --git a/lib/simplePrint.ts b/lib/simplePrint.ts new file mode 100644 index 0000000..9229214 --- /dev/null +++ b/lib/simplePrint.ts @@ -0,0 +1,44 @@ +import type { CDataNode } from './CDataNode.ts'; +import type { Document } from './Document.ts'; +import { DocumentFragment } from './DocumentFragment.ts'; +import type { Node } from './Node.js'; +import type { TextNode } from './TextNode.ts'; +import { CDATA_SECTION_NODE, DOCUMENT_NODE, TEXT_NODE } from './constants.js'; +import { escape } from './escape.js'; +import { isElement } from './isElement.ts'; + +export function simplePrint (node: Node | DocumentFragment): string { + if (node instanceof DocumentFragment) { + return node.childNodes.map(n => simplePrint(n)).join(''); + } + if (node.nodeType === DOCUMENT_NODE) { + const root = (node as Document).root; + if (!root) throw new Error('root element is missing'); + return simplePrint(root); + } + else if (node.nodeType === CDATA_SECTION_NODE) { + return `/g, ']]>')}]]>`; + } + else if (node.nodeType === TEXT_NODE) { + return escape((node as TextNode).value); + } + else if (isElement(node)) { + const tagName = node.fullName; + const { childNodes } = node; + let children = ''; + for (const n of childNodes) { + children += simplePrint(n); + } + let attrList = ''; + if (isElement(node)) { + const attr = node.attr; + for (const [ key, val ] of Object.entries(attr)) { + attrList += ` ${escape(key)}="${escape(val)}"`; + } + } + return children + ? `<${tagName}${attrList}>${children}` + : `<${tagName}${attrList} />`; + } + return ''; +} diff --git a/package-lock.json b/package-lock.json index 0b201a3..a108611 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.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@borgar/simple-xml", - "version": "2.2.2", + "version": "3.0.0-rc.0", "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", @@ -1383,6 +1384,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", @@ -7069,6 +7080,13 @@ "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", "dev": true }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, "node_modules/unified": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz", diff --git a/package.json b/package.json index ff68e93..63688f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@borgar/simple-xml", - "version": "2.2.2", + "version": "3.0.0-rc.0", "description": "A reasonably fast, simple and pure-JS XML parser with no dependencies", "type": "module", "source": "lib/index.ts", @@ -55,6 +55,7 @@ "devDependencies": { "@borgar/eslint-config": "~4.0.1", "@eslint/js": "~9.38.0", + "@types/node": "~25.5.0", "concat-md": "~0.5.1", "eslint": "~9.38.0", "jsdoc": "~4.0.5", diff --git a/test/domQuery-dedup.spec.ts b/test/domQuery-dedup.spec.ts index ff5caec..d2b1503 100644 --- a/test/domQuery-dedup.spec.ts +++ b/test/domQuery-dedup.spec.ts @@ -5,7 +5,7 @@ describe('domQuery dedup fix', () => { it('preserves document order for single-group selectors', () => { const doc = parseXML(''); const result = doc.root!.querySelectorAll('*'); - expect(result.map(e => e.tagName)).toEqual(['x', 'y', 'z']); + expect(result.map(e => e.tagName)).toEqual([ 'x', 'y', 'z' ]); }); it('preserves document order for multi-group (comma) selectors', () => { @@ -13,7 +13,7 @@ describe('domQuery dedup fix', () => { // Even if groups are listed out of document order, results should // be in document order (via the getElementsByTagName tree walk). const result = doc.root!.querySelectorAll('c, a'); - expect(result.map(e => e.tagName)).toEqual(['a', 'c']); + expect(result.map(e => e.tagName)).toEqual([ 'a', 'c' ]); }); it('deduplicates descendant combinator results', () => { @@ -21,7 +21,7 @@ describe('domQuery dedup fix', () => { const doc = parseXML(''); // "a c" matches through both 's; should find exactly one const result = doc.root!.querySelectorAll('a c'); - expect(result.map(e => e.tagName)).toEqual(['c']); + expect(result.map(e => e.tagName)).toEqual([ 'c' ]); }); it('deduplicates multi-group (comma) selector results', () => { diff --git a/test/namespaces.spec.ts b/test/namespaces.spec.ts new file mode 100644 index 0000000..edf0370 --- /dev/null +++ b/test/namespaces.spec.ts @@ -0,0 +1,44 @@ +import { describe, it, expect } from 'vitest'; +import { parseXML } from '../lib/index.ts'; + +const ooxml = ` + + + + + + + + + + + + + + + + +`; + +describe('namespace validation', () => { + it('simple tag', () => { + const doc = parseXML(ooxml, { ns: true }); + expect(doc.namespaces.list()).toStrictEqual([ + [ 'http://schemas.openxmlformats.org/spreadsheetml/2006/main', '' ], + [ 'http://schemas.openxmlformats.org/markup-compatibility/2006', 'mc' ], + [ 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac', 'x14ac' ], + [ 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/main', 'x14' ], + [ 'http://schemas.microsoft.com/office/spreadsheetml/2010/11/main', 'x15' ] + ]); + }); +}); + +// test: +// - missing decl +// - decl collisions (reused prefix) +// - decl collisions (reused uri) diff --git a/test/parse-data.spec.ts b/test/parse-data.spec.ts index d21256f..fa7505c 100644 --- a/test/parse-data.spec.ts +++ b/test/parse-data.spec.ts @@ -1,4 +1,5 @@ -/* eslint-disable quotes, quote-props, indent */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { describe, it, expect } from 'vitest'; import fs from 'fs'; import { parseXML } from '../lib/index.js'; @@ -23,20 +24,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 +89,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 +128,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 +242,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 +355,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 +468,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 + } }