UNPKG

partial-xml-stream-parser

Version:

A lenient XML stream parser for Node.js and browsers that can handle incomplete or malformed XML data, with depth control, CDATA support for XML serialization and round-trip parsing, wildcard pattern support for stopNodes, and CDATA handling within stopNo

1,082 lines 128 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const vitest_1 = require("vitest"); const index_1 = require("../index"); (0, vitest_1.describe)("PartialXMLStreamParser", () => { let parser; (0, vitest_1.beforeEach)(() => { // Default parser for most tests, now implies alwaysCreateTextNode: true parser = new index_1.PartialXMLStreamParser({ textNodeName: "#text" }); }); (0, vitest_1.it)("should parse a stream chunk by chunk correctly", () => { let streamResult; streamResult = parser.parseStream("<read>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: true }, xml: [{ read: {} }], }); streamResult = parser.parseStream("<args>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: true }, xml: [{ read: { args: {} } }], }); streamResult = parser.parseStream("<file><name>as"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: true }, xml: [{ read: { args: { file: { name: { "#text": "as" } } } } }], }); streamResult = parser.parseStream("d</name>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: true }, xml: [{ read: { args: { file: { name: { "#text": "asd" } } } } }], }); streamResult = parser.parseStream("</file></args>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: true }, xml: [{ read: { args: { file: { name: { "#text": "asd" } } } } }], }); streamResult = parser.parseStream("</read>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ read: { args: { file: { name: { "#text": "asd" } } } } }], }); streamResult = parser.parseStream(null); // Signal end of stream (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ read: { args: { file: { name: { "#text": "asd" } } } } }], }); }); (0, vitest_1.it)("should handle a single incomplete chunk, then completion", () => { let streamResult; const singleChunk = "<request><id>123</id><data>value<da"; streamResult = parser.parseStream(singleChunk); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: true }, xml: [{ request: { id: { "#text": "123" }, data: { "#text": "value<da" } } }], }); streamResult = parser.parseStream("ta></request>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [ { request: { id: { "#text": "123" }, data: { "#text": "value<data>" }, }, }, ], }); streamResult = parser.parseStream(null); // Signal end (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [ { request: { id: { "#text": "123" }, data: { "#text": "value<data>" }, }, }, ], }); }); (0, vitest_1.it)("should handle a text-only stream", () => { let streamResult; streamResult = parser.parseStream("Just some text"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: ["Just some text"], }); streamResult = parser.parseStream(null); // End stream (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: ["Just some text"], }); }); (0, vitest_1.it)("should handle self-closing tags and mixed content", () => { let streamResult; streamResult = parser.parseStream("<root><item/>Text after item<another/></root>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ root: { item: {}, "#text": "Text after item", another: {} } }], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ root: { item: {}, "#text": "Text after item", another: {} } }], }); }); (0, vitest_1.it)("should handle XML entities in text nodes", () => { parser = new index_1.PartialXMLStreamParser({ textNodeName: "#text" }); // Re-init to be sure about options let streamResult = parser.parseStream("<doc>Hello & \"World\" 'Test'</doc>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ doc: { "#text": "Hello & \"World\" 'Test'" } }], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ doc: { "#text": "Hello & \"World\" 'Test'" } }], }); }); (0, vitest_1.it)("should handle XML entities in attribute values", () => { parser = new index_1.PartialXMLStreamParser({ textNodeName: "#text", attributeNamePrefix: "@", }); let streamResult = parser.parseStream('<doc val="&lt;value&gt;" />'); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ doc: { "@val": "<value>" } }], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ doc: { "@val": "<value>" } }], }); }); (0, vitest_1.it)("should handle numeric XML entities (decimal and hex)", () => { parser = new index_1.PartialXMLStreamParser({ textNodeName: "#text" }); let streamResult = parser.parseStream("<doc>&#60;Hello&#x26;&#32;World&#x3E;</doc>"); // <Hello& World> (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ doc: { "#text": "<Hello& World>" } }], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ doc: { "#text": "<Hello& World>" } }], }); }); (0, vitest_1.it)("should correctly parse multiple chunks that form a complete XML", () => { parser = new index_1.PartialXMLStreamParser({ textNodeName: "#text", attributeNamePrefix: "@", }); parser.parseStream("<data><item"); parser.parseStream(' key="value">Te'); parser.parseStream("st</item><item2"); let streamResult = parser.parseStream("/></data>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ data: { item: { "@key": "value", "#text": "Test" }, item2: {} } }], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ data: { item: { "@key": "value", "#text": "Test" }, item2: {} } }], }); }); (0, vitest_1.it)("should return empty array xml for empty stream", () => { parser = new index_1.PartialXMLStreamParser(); let streamResult = parser.parseStream(""); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: true }, xml: [] }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [] }); }); (0, vitest_1.it)("should handle stream with only XML declaration and comments", () => { parser = new index_1.PartialXMLStreamParser(); let streamResult = parser.parseStream('<?xml version="1.0"?><!-- comment -->'); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [] }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [] }); }); (0, vitest_1.it)("should handle custom attributeNamePrefix", () => { parser = new index_1.PartialXMLStreamParser({ attributeNamePrefix: "_" }); let streamResult = parser.parseStream('<doc attr="val" />'); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ doc: { _attr: "val" } }], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ doc: { _attr: "val" } }], }); parser = new index_1.PartialXMLStreamParser({ attributeNamePrefix: "" }); streamResult = parser.parseStream('<doc attr="val" />'); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ doc: { attr: "val" } }], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ doc: { attr: "val" } }], }); }); (0, vitest_1.it)("should parse CDATA sections correctly", () => { parser = new index_1.PartialXMLStreamParser({ textNodeName: "#text" }); let streamResult = parser.parseStream("<root><![CDATA[This is <CDATA> text with & special chars]]></root>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ root: { "#text": "This is <CDATA> text with & special chars" } }], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ root: { "#text": "This is <CDATA> text with & special chars" } }], }); }); (0, vitest_1.it)("should handle unterminated CDATA section", () => { parser = new index_1.PartialXMLStreamParser({ textNodeName: "#text" }); let streamResult = parser.parseStream("<root><![CDATA[Unterminated cdata"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: true }, xml: [{ root: { "#text": "Unterminated cdata" } }], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: true }, xml: [{ root: { "#text": "Unterminated cdata" } }], }); }); (0, vitest_1.it)("should handle CDATA at root level if it is the only content", () => { parser = new index_1.PartialXMLStreamParser(); let streamResult = parser.parseStream("<![CDATA[Root CDATA]]>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: ["Root CDATA"], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: ["Root CDATA"], }); }); (0, vitest_1.it)("should handle unterminated comments", () => { parser = new index_1.PartialXMLStreamParser(); let streamResult = parser.parseStream("<root><!-- This is an unterminated comment"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: true }, xml: [{ root: {} }], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: true }, xml: [{ root: {} }], }); }); (0, vitest_1.it)("should handle unterminated DOCTYPE", () => { parser = new index_1.PartialXMLStreamParser(); let streamResult = parser.parseStream('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"'); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: true }, xml: [] }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [] }); }); (0, vitest_1.it)("should handle unterminated XML declaration", () => { parser = new index_1.PartialXMLStreamParser(); let streamResult = parser.parseStream('<?xml version="1.0" encoding="UTF-8"'); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: true }, xml: [] }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [] }); }); (0, vitest_1.it)("should leniently handle mismatched closing tags", () => { parser = new index_1.PartialXMLStreamParser({ textNodeName: "#text" }); let streamResult = parser.parseStream("<root><item>text</mismatched></item></root>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ root: { item: { "#text": "text</mismatched>" } } }], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ root: { item: { "#text": "text</mismatched>" } } }], }); }); (0, vitest_1.it)("should handle attributes without explicit values (boolean attributes) as true", () => { parser = new index_1.PartialXMLStreamParser({ attributeNamePrefix: "@" }); let streamResult = parser.parseStream('<input disabled checked="checked" required />'); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [ { input: { "@disabled": true, "@checked": "checked", "@required": true, }, }, ], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [ { input: { "@disabled": true, "@checked": "checked", "@required": true, }, }, ], }); }); (0, vitest_1.it)("should correctly simplify text-only elements", () => { // This test now reflects alwaysCreateTextNode: true behavior from beforeEach let streamResult = parser.parseStream("<parent><child>simple text</child></parent>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ parent: { child: { "#text": "simple text" } } }], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ parent: { child: { "#text": "simple text" } } }], }); }); (0, vitest_1.it)("should not simplify elements with attributes even if they also have text", () => { // This test already aligns with alwaysCreateTextNode: true behavior parser = new index_1.PartialXMLStreamParser({ textNodeName: "#text", attributeNamePrefix: "@", }); let streamResult = parser.parseStream('<parent><child attr="val">text content</child></parent>'); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ parent: { child: { "@attr": "val", "#text": "text content" } } }], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ parent: { child: { "@attr": "val", "#text": "text content" } } }], }); }); (0, vitest_1.it)("should not simplify elements with child elements", () => { // This test's expectation doesn't change with alwaysCreateTextNode parser = new index_1.PartialXMLStreamParser(); // Uses new default alwaysCreateTextNode: true let streamResult = parser.parseStream("<parent><child><grandchild/></child></parent>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ parent: { child: { grandchild: {} } } }], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ parent: { child: { grandchild: {} } } }], }); }); (0, vitest_1.it)("should ignore text nodes containing only whitespace by default", () => { // Expectation changes due to alwaysCreateTextNode: true from beforeEach let streamResult = parser.parseStream("<root> <item>text</item> </root>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ root: { item: { "#text": "text" } } }], // Whitespace around item is trimmed, text inside item gets #text }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ root: { item: { "#text": "text" } } }], }); }); (0, vitest_1.it)("should omit whitespace text nodes even if alwaysCreateTextNode is true", () => { parser = new index_1.PartialXMLStreamParser({ textNodeName: "#text", alwaysCreateTextNode: true, }); let streamResult = parser.parseStream("<root> <item>text</item> </root>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ root: { item: { "#text": "text" } } }], // Whitespace-only nodes between tags are omitted }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ root: { item: { "#text": "text" } } }], }); }); (0, vitest_1.it)("should handle text at root level before any tags", () => { parser = new index_1.PartialXMLStreamParser(); // Uses new default let streamResult = parser.parseStream("Leading text<root/>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: ["Leading text", { root: {} }], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: ["Leading text", { root: {} }], }); }); (0, vitest_1.it)("should handle text at root level after all tags are closed", () => { parser = new index_1.PartialXMLStreamParser(); // Uses new default let streamResult = parser.parseStream("<root/>Trailing text"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ root: {} }, "Trailing text"], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ root: {} }, "Trailing text"], }); }); (0, vitest_1.it)("should handle multiple root elements", () => { // Expectation changes due to alwaysCreateTextNode: true from beforeEach let streamResult = parser.parseStream("<rootA/><rootB>text</rootB>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ rootA: {} }, { rootB: { "#text": "text" } }], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ rootA: {} }, { rootB: { "#text": "text" } }], }); }); (0, vitest_1.it)("should handle multiple root elements in specific order", () => { // Expectation changes due to alwaysCreateTextNode: true from beforeEach const xml = "<thinking>a</thinking><some-tool></some-tool>"; let streamResult = parser.parseStream(xml); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ thinking: { "#text": "a" } }, { "some-tool": {} }], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ thinking: { "#text": "a" } }, { "some-tool": {} }], }); }); (0, vitest_1.it)("should handle Buffer input", () => { // Expectation changes due to alwaysCreateTextNode: true from beforeEach let streamResult = parser.parseStream(Buffer.from("<data>value</data>")); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ data: { "#text": "value" } }], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ data: { "#text": "value" } }], }); }); (0, vitest_1.it)("should handle multiple attributes correctly", () => { parser = new index_1.PartialXMLStreamParser({ attributeNamePrefix: "@" }); let streamResult = parser.parseStream("<tag attr1=\"val1\" attr2='val2' attr3=val3 />"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ tag: { "@attr1": "val1", "@attr2": "val2", "@attr3": "val3" } }], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ tag: { "@attr1": "val1", "@attr2": "val2", "@attr3": "val3" } }], }); }); (0, vitest_1.it)("should handle incomplete tags at end of chunk and then completed", () => { parser = new index_1.PartialXMLStreamParser({ // Uses new default alwaysCreateTextNode: true textNodeName: "#text", attributeNamePrefix: "@", }); parser.parseStream("<root><item"); let streamResult = parser.parseStream(" attr='1'>Text</item></r"); (0, vitest_1.expect)(streamResult.xml[0].root.item).toEqual({ "@attr": "1", "#text": "Text", }); (0, vitest_1.expect)(streamResult.xml[0].root["#text"]).toBe("</r"); // This part becomes text (0, vitest_1.expect)(streamResult.metadata.partial).toBe(true); parser = new index_1.PartialXMLStreamParser({ textNodeName: "#text", attributeNamePrefix: "@", }); parser.parseStream("<root><item"); streamResult = parser.parseStream(" attr='1'>Text</item></root>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ root: { item: { "@attr": "1", "#text": "Text" } } }], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ root: { item: { "@attr": "1", "#text": "Text" } } }], }); }); (0, vitest_1.it)("should handle empty string chunks in midst of stream", () => { // Expectation changes due to alwaysCreateTextNode: true from beforeEach parser.parseStream("<doc>"); parser.parseStream(""); let streamResult = parser.parseStream("<content>Hello</content>"); (0, vitest_1.expect)(streamResult.xml[0].doc.content).toEqual({ "#text": "Hello" }); (0, vitest_1.expect)(streamResult.metadata.partial).toBe(true); let finalDocStreamResult = parser.parseStream("</doc>"); (0, vitest_1.expect)(finalDocStreamResult.xml[0].doc.content).toEqual({ "#text": "Hello", }); (0, vitest_1.expect)(finalDocStreamResult.metadata.partial).toBe(false); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ doc: { content: { "#text": "Hello" } } }], }); }); (0, vitest_1.it)("should set partial:true when stream ends with an incomplete tag", () => { parser = new index_1.PartialXMLStreamParser({ textNodeName: "#text" }); let streamResult = parser.parseStream("<root><incompleteTag"); streamResult = parser.parseStream(null); // End stream (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: true }, xml: [{ root: { "#text": "<incompleteTag" } }], // The fragment is treated as text of parent }); parser.reset(); parser = new index_1.PartialXMLStreamParser({ textNodeName: "#text" }); streamResult = parser.parseStream("<root><item>Text</item></incompleteCl"); streamResult = parser.parseStream(null); // End stream (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: true }, xml: [{ root: { item: { "#text": "Text" }, "#text": "</incompleteCl" } }], // Fragment as text }); parser.reset(); parser = new index_1.PartialXMLStreamParser({ textNodeName: "#text" }); streamResult = parser.parseStream("<root><item>Text</item><"); // Just '<' streamResult = parser.parseStream(null); // End stream (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: true }, xml: [{ root: { item: { "#text": "Text" }, "#text": "<" } }], // Fragment as text }); parser.reset(); parser = new index_1.PartialXMLStreamParser({ textNodeName: "#text" }); streamResult = parser.parseStream("<root attr='val"); streamResult = parser.parseStream(null); // End stream (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: true }, // Depending on how strictly attributes are parsed before '>', // this might be an empty root or root with partial text. // Current behavior treats "<root attr='val" as text if not closed by ">" xml: [{ "#text": "<root attr='val" }], }); }); (0, vitest_1.describe)("maxDepth feature", () => { (0, vitest_1.it)("should treat tags beyond maxDepth as stopNodes", () => { parser = new index_1.PartialXMLStreamParser({ maxDepth: 2, textNodeName: "#text", }); let streamResult = parser.parseStream("<root><level1><level2><level3>content</level3></level2></level1></root>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [ { root: { level1: { level2: { "#text": "<level3>content</level3>" }, }, }, }, ], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [ { root: { level1: { level2: { "#text": "<level3>content</level3>" }, }, }, }, ], }); }); (0, vitest_1.it)("should handle maxDepth with nested tags and attributes", () => { parser = new index_1.PartialXMLStreamParser({ maxDepth: 3, textNodeName: "#text", attributeNamePrefix: "@", }); let streamResult = parser.parseStream('<root><a><b><c id="test"><d>deep content</d></c></b></a></root>'); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [ { root: { a: { b: { c: { "#text": "<d>deep content</d>", "@id": "test", }, }, }, }, }, ], }); }); (0, vitest_1.it)("should work with maxDepth 1 (only root level allowed)", () => { parser = new index_1.PartialXMLStreamParser({ maxDepth: 1, textNodeName: "#text", }); let streamResult = parser.parseStream("<root><child>content</child></root>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [ { root: { child: { "#text": "content" }, }, }, ], }); }); (0, vitest_1.it)("should work with maxDepth 0 (treat everything as text)", () => { parser = new index_1.PartialXMLStreamParser({ maxDepth: 0, textNodeName: "#text", }); let streamResult = parser.parseStream("<root><child>content</child></root>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: ["<root><child>content</child></root>"], }); }); (0, vitest_1.it)("should combine maxDepth with existing stopNodes", () => { parser = new index_1.PartialXMLStreamParser({ maxDepth: 3, stopNodes: ["root.level1.script"], textNodeName: "#text", }); let streamResult = parser.parseStream("<root><level1><script>code</script><level2><level3>content</level3></level2></level1></root>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [ { root: { level1: { script: { "#text": "code" }, level2: { level3: { "#text": "content" }, }, }, }, }, ], }); }); (0, vitest_1.it)("should handle self-closing tags at max depth", () => { parser = new index_1.PartialXMLStreamParser({ maxDepth: 2, textNodeName: "#text", }); let streamResult = parser.parseStream("<root><level1><selfclosing/><level2>content</level2></level1></root>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [ { root: { level1: { selfclosing: {}, level2: { "#text": "content" }, }, }, }, ], }); }); (0, vitest_1.it)("should handle null maxDepth (no depth limit)", () => { parser = new index_1.PartialXMLStreamParser({ maxDepth: null, textNodeName: "#text", }); let streamResult = parser.parseStream("<root><a><b><c><d><e>deep content</e></d></c></b></a></root>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [ { root: { a: { b: { c: { d: { e: { "#text": "deep content" }, }, }, }, }, }, }, ], }); }); }); (0, vitest_1.describe)("stopNodes feature", () => { (0, vitest_1.it)("should treat content of a stopNode as text", () => { parser = new index_1.PartialXMLStreamParser({ stopNodes: ["script"], textNodeName: "#text", // alwaysCreateTextNode is true by default }); let streamResult = parser.parseStream("<root><script>let a = 1; console.log(a);</script></root>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ root: { script: { "#text": "let a = 1; console.log(a);" } } }], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ root: { script: { "#text": "let a = 1; console.log(a);" } } }], }); }); (0, vitest_1.it)("should parse attributes of a stopNode", () => { parser = new index_1.PartialXMLStreamParser({ stopNodes: ["script"], attributeNamePrefix: "@", textNodeName: "#text", // alwaysCreateTextNode is true by default }); let streamResult = parser.parseStream('<root><script type="text/javascript" src="app.js">let b = 2;</script></root>'); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [ { root: { script: { "@type": "text/javascript", "@src": "app.js", "#text": "let b = 2;", }, }, }, ], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [ { root: { script: { "@type": "text/javascript", "@src": "app.js", "#text": "let b = 2;", }, }, }, ], }); }); (0, vitest_1.it)("should not parse XML tags inside a stopNode", () => { parser = new index_1.PartialXMLStreamParser({ stopNodes: ["data"], textNodeName: "#text", // alwaysCreateTextNode is true by default }); let streamResult = parser.parseStream("<root><data><item>one</item><value>100</value></data></root>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [ { root: { data: { "#text": "<item>one</item><value>100</value>" } }, }, ], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [ { root: { data: { "#text": "<item>one</item><value>100</value>" } }, }, ], }); }); (0, vitest_1.it)("should handle multiple stopNode types", () => { parser = new index_1.PartialXMLStreamParser({ stopNodes: ["script", "style"], textNodeName: "#text", // alwaysCreateTextNode is true by default }); let streamResult = parser.parseStream("<root><script>var c=3;</script><style>.cls{color:red}</style></root>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [ { root: { script: { "#text": "var c=3;" }, style: { "#text": ".cls{color:red}" }, }, }, ], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [ { root: { script: { "#text": "var c=3;" }, style: { "#text": ".cls{color:red}" }, }, }, ], }); }); (0, vitest_1.it)("should handle self-closing tags within stopNode content", () => { parser = new index_1.PartialXMLStreamParser({ stopNodes: ["htmlData"], textNodeName: "#text", // alwaysCreateTextNode is true by default }); let streamResult = parser.parseStream('<doc><htmlData>Some text <br/> and more <img src="test.png"/></htmlData></doc>'); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [ { doc: { htmlData: { "#text": 'Some text <br/> and more <img src="test.png"/>', }, }, }, ], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [ { doc: { htmlData: { "#text": 'Some text <br/> and more <img src="test.png"/>', }, }, }, ], }); }); (0, vitest_1.it)("should handle unterminated stopNode at end of stream", () => { parser = new index_1.PartialXMLStreamParser({ stopNodes: ["raw"], textNodeName: "#text", // alwaysCreateTextNode is true by default }); let streamResult = parser.parseStream("<root><raw>This content is not closed"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: true }, xml: [{ root: { raw: { "#text": "This content is not closed" } } }], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: true }, xml: [{ root: { raw: { "#text": "This content is not closed" } } }], }); }); (0, vitest_1.it)("should correctly handle nested stopNodes of the same name", () => { parser = new index_1.PartialXMLStreamParser({ stopNodes: ["codeblock"], textNodeName: "#text", // alwaysCreateTextNode is true by default }); const xml = "<doc><codeblock>Outer <codeblock>Inner</codeblock> Content</codeblock></doc>"; let streamResult = parser.parseStream(xml); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [ { doc: { codeblock: { "#text": "Outer <codeblock>Inner</codeblock> Content", }, }, }, ], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [ { doc: { codeblock: { "#text": "Outer <codeblock>Inner</codeblock> Content", }, }, }, ], }); }); (0, vitest_1.it)("should handle stopNode as the root element", () => { parser = new index_1.PartialXMLStreamParser({ stopNodes: ["rawhtml"], textNodeName: "#text", // alwaysCreateTextNode is true by default }); let streamResult = parser.parseStream("<rawhtml><head></head><body><p>Hello</p></body></rawhtml>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ rawhtml: { "#text": "<head></head><body><p>Hello</p></body>" } }], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ rawhtml: { "#text": "<head></head><body><p>Hello</p></body>" } }], }); }); (0, vitest_1.it)("should handle empty stopNode", () => { parser = new index_1.PartialXMLStreamParser({ stopNodes: ["emptyContent"], textNodeName: "#text", // alwaysCreateTextNode is true by default }); let streamResult = parser.parseStream("<data><emptyContent></emptyContent></data>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ data: { emptyContent: { "#text": "" } } }], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ data: { emptyContent: { "#text": "" } } }], }); }); (0, vitest_1.it)("should handle stopNode with only whitespace content", () => { parser = new index_1.PartialXMLStreamParser({ stopNodes: ["whitespaceNode"], textNodeName: "#text", // alwaysCreateTextNode is true by default }); let streamResult = parser.parseStream("<data><whitespaceNode> \n\t </whitespaceNode></data>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ data: { whitespaceNode: { "#text": " \n\t " } } }], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ data: { whitespaceNode: { "#text": " \n\t " } } }], }); }); (0, vitest_1.it)("should handle stopNode content split across multiple chunks", () => { parser = new index_1.PartialXMLStreamParser({ stopNodes: ["log"], textNodeName: "#text", // alwaysCreateTextNode is true by default }); parser.parseStream("<system><log>Part 1 data "); let streamResult = parser.parseStream("Part 2 data <inner>tag</inner> and more"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: true }, xml: [ { system: { log: { "#text": "Part 1 data Part 2 data <inner>tag</inner> and more", }, }, }, ], }); streamResult = parser.parseStream(" final part.</log></system>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [ { system: { log: { "#text": "Part 1 data Part 2 data <inner>tag</inner> and more final part.", }, }, }, ], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [ { system: { log: { "#text": "Part 1 data Part 2 data <inner>tag</inner> and more final part.", }, }, }, ], }); }); (0, vitest_1.it)("should handle stopNode with attributes and content split across chunks", () => { parser = new index_1.PartialXMLStreamParser({ stopNodes: ["customTag"], attributeNamePrefix: "@", textNodeName: "#text", // alwaysCreateTextNode is true by default }); parser.parseStream('<root><customTag id="123" '); parser.parseStream('name="test">This is the '); let streamResult = parser.parseStream("content with wewnętrzny tag <tag/>.</customTag></root>"); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [ { root: { customTag: { "@id": "123", "@name": "test", "#text": "This is the content with wewnętrzny tag <tag/>.", }, }, }, ], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [ { root: { customTag: { "@id": "123", "@name": "test", "#text": "This is the content with wewnętrzny tag <tag/>.", }, }, }, ], }); }); (0, vitest_1.it)("should handle stop node when stopNodes option is a string", () => { parser = new index_1.PartialXMLStreamParser({ stopNodes: "script", textNodeName: "#text", // alwaysCreateTextNode is true by default }); let streamResult = parser.parseStream('<root><script>alert("hello");</script></root>'); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [{ root: { script: { "#text": 'alert("hello");' } } }], }); }); (0, vitest_1.it)("should handle path-based stopNode correctly", () => { parser = new index_1.PartialXMLStreamParser({ stopNodes: ["read.file.metadata"], textNodeName: "#text", // alwaysCreateTextNode is true by default }); const xml = "<read><metadata><item>one</item></metadata><file><metadata><item>two</item><subitem>three</subitem></metadata><other>data</other></file></read>"; let streamResult = parser.parseStream(xml); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [ { read: { metadata: { item: { "#text": "one" } }, // Not a stopNode file: { metadata: { "#text": "<item>two</item><subitem>three</subitem>", }, // Is a stopNode other: { "#text": "data" }, // Not a stopNode }, }, }, ], }); streamResult = parser.parseStream(null); (0, vitest_1.expect)(streamResult).toEqual({ metadata: { partial: false }, xml: [ { read: { metadata: { item: { "#text": "one" } }, file: { metadata: { "#text": "<item>two</item><subitem>three</subitem>", }, other: { "#text": "data" }, }, }, }, ], }); }); (0, vitest_1.it)("should prioritize path-based stopNode over simple name if both could match", () => { parser = new index_1.PartialXMLStreamParser({ stopNodes: ["read.file.