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
JavaScript
"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="<value>" />');
(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><Hello& World></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.