diff --git a/spec/multipleRoots_spec.js b/spec/multipleRoots_spec.js new file mode 100644 index 00000000..30b068c9 --- /dev/null +++ b/spec/multipleRoots_spec.js @@ -0,0 +1,601 @@ +"use strict"; + +import { XMLParser } from "../src/fxp.js"; + +describe("XMLParser - Multiple Roots Handling", function() { + + describe("Positive Scenarios - multipleRoots: true", function() { + + it("should parse XML with two root elements when multipleRoots is true", function() { + const xmlData = `value1value2`; + const expected = { + "root1": "value1", + "root2": "value2" + }; + + const options = { + multipleRoots: true + }; + const parser = new XMLParser(options); + let result = parser.parse(xmlData); + expect(result).toEqual(expected); + }); + + it("should parse XML with three root elements when multipleRoots is true", function() { + const xmlData = `data1data2data3`; + const expected = { + "first": "data1", + "second": "data2", + "third": "data3" + }; + + const options = { + multipleRoots: true + }; + const parser = new XMLParser(options); + let result = parser.parse(xmlData); + expect(result).toEqual(expected); + }); + + it("should parse complex XML with multiple root elements containing nested tags", function() { + const xmlData = ` + + 123 + John Doe + + + 456 + Jane Smith + + `; + const expected = { + "order": [ + { + "id": 123, + "customer": "John Doe" + }, + { + "id": 456, + "customer": "Jane Smith" + } + ] + }; + + const options = { + multipleRoots: true + }; + const parser = new XMLParser(options); + let result = parser.parse(xmlData); + expect(result).toEqual(expected); + }); + + it("should parse different tag names as multiple roots", function() { + const xmlData = `WidgetSupport`; + const expected = { + "product": "Widget", + "service": "Support" + }; + + const options = { + multipleRoots: true + }; + const parser = new XMLParser(options); + let result = parser.parse(xmlData); + expect(result).toEqual(expected); + }); + + it("should parse single root element when multipleRoots is true", function() { + const xmlData = `only one root`; + const expected = { + "single": "only one root" + }; + + const options = { + multipleRoots: true + }; + const parser = new XMLParser(options); + let result = parser.parse(xmlData); + expect(result).toEqual(expected); + }); + + it("should ignore comments and PIs when checking for multiple roots", function() { + const xmlData = `value1value2`; + const expected = { + "?xml": "", + "root1": "value1", + "root2": "value2" + }; + + const options = { + multipleRoots: true, + }; + const parser = new XMLParser(options); + let result = parser.parse(xmlData); + expect(result).toEqual(expected); + }); + + it("should parse multiple roots with mixed content", function() { + const xmlData = `text1text2deep`; + const expected = { + "elem1": "text1", + "elem2": "text2", + "elem3": { + "nested": "deep" + } + }; + + const options = { + multipleRoots: true + }; + const parser = new XMLParser(options); + let result = parser.parse(xmlData); + expect(result).toEqual(expected); + }); + + it("should parse multiple roots with whitespace between them", function() { + const xmlData = ` + + value1 + + + + value2 + + `; + const expected = { + "root1": { + "data": "value1" + }, + "root2": { + "data": "value2" + } + }; + + const options = { + multipleRoots: true + }; + const parser = new XMLParser(options); + let result = parser.parse(xmlData); + expect(result).toEqual(expected); + }); + }); + + describe("Negative Scenarios - multipleRoots: false (default)", function() { + + it("should throw error when multiple root elements found and multipleRoots is false", function() { + const xmlData = `value1value2`; + + const options = { + multipleRoots: false + }; + const parser = new XMLParser(options); + + expect(() => { + parser.parse(xmlData); + }).toThrow(new Error("Multiple root tags found. Root tags: root1, root2")); + }); + + it("should throw error with correct root tag names in error message", function() { + const xmlData = `data1data2data3`; + + const options = { + multipleRoots: false + }; + const parser = new XMLParser(options); + + expect(() => { + parser.parse(xmlData); + }).toThrow(new Error("Multiple root tags found. Root tags: firstTag, secondTag, thirdTag")); + }); + + it("should throw error when duplicate root elements found", function() { + const xmlData = `123456`; + + const options = { + multipleRoots: false + }; + const parser = new XMLParser(options); + + expect(() => { + parser.parse(xmlData); + }).toThrow(); + }); + + it("should not throw error for single root element when multipleRoots is false", function() { + const xmlData = `value`; + const expected = { + "root": { + "child": "value" + } + }; + + const options = { + multipleRoots: false + }; + const parser = new XMLParser(options); + let result = parser.parse(xmlData); + expect(result).toEqual(expected); + }); + + it("should ignore comments when checking for multiple roots (should not treat comments as roots)", function() { + const xmlData = `value`; + const expected = { + "root": "value" + }; + + const options = { + multipleRoots: false + }; + const parser = new XMLParser(options); + let result = parser.parse(xmlData); + expect(result).toEqual(expected); + }); + + it("should ignore processing instructions when checking for multiple roots", function() { + const xmlData = `value`; + const expected = { + "root": "value" + }; + + const options = { + multipleRoots: false, + ignoreDeclaration: true + }; + const parser = new XMLParser(options); + let result = parser.parse(xmlData); + expect(result).toEqual(expected); + }); + + it("should ignore CDATA sections when checking for multiple roots", function() { + const xmlData = ``; + const expected = { + "root": "some data" + }; + + const options = { + multipleRoots: false + }; + const parser = new XMLParser(options); + let result = parser.parse(xmlData); + expect(result).toEqual(expected); + }); + + it("should throw error when root1 and root2 exist even with whitespace between them", function() { + const xmlData = ` + + value1 + + + + value2 + + `; + + const options = { + multipleRoots: false + }; + const parser = new XMLParser(options); + + expect(() => { + parser.parse(xmlData); + }).toThrow(); + }); + + it("should throw error when multiple roots with attributes exist", function() { + const xmlData = `FirstSecond`; + + const options = { + multipleRoots: false + }; + const parser = new XMLParser(options); + + expect(() => { + parser.parse(xmlData); + }).toThrow(); + }); + + it("should correctly identify and report root tags even with complex nesting", function() { + const xmlData = `datadata`; + + const options = { + multipleRoots: false + }; + const parser = new XMLParser(options); + + expect(() => { + parser.parse(xmlData); + }).toThrow(new Error("Multiple root tags found. Root tags: parent1, parent2")); + }); + }); + + describe("Edge Cases - Comment and Non-Element Mixed", function() { + + it("should handle multiple roots with comments interspersed", function() { + const xmlData = ` + + value1 + + value2 + + `; + + const options = { + multipleRoots: true + }; + const parser = new XMLParser(options); + let result = parser.parse(xmlData); + + expect(result).toEqual({ + "root1": "value1", + "root2": "value2" + }); + }); + + it("should throw error for multiple roots even with comments interspersed when multipleRoots is false", function() { + const xmlData = ` + + value1 + + value2 + + `; + + const options = { + multipleRoots: false + }; + const parser = new XMLParser(options); + + expect(() => { + parser.parse(xmlData); + }).toThrow(); + }); + + it("should ignore text nodes at root level when counting roots", function() { + const xmlData = ` + Some text + value + More text + `; + const expected = { + "root": "value" + }; + + const options = { + multipleRoots: false + }; + const parser = new XMLParser(options); + let result = parser.parse(xmlData); + expect(result).toEqual(expected); + }); + }); + + describe("Default Behavior", function() { + + it("should use multipleRoots: false as default and throw error for multiple roots", function() { + const xmlData = `value1value2`; + + const parser = new XMLParser(); // No options provided + expect(() => { + parser.parse(xmlData); + }).toThrow(); + }); + + it("should parse single root successfully with default options", function() { + const xmlData = `value`; + const expected = { + "root": "value" + }; + + const parser = new XMLParser(); // No options provided + let result = parser.parse(xmlData); + expect(result).toEqual(expected); + }); + }); + + describe("Custom commentPropName and cdataPropName Options", function() { + + it("should capture comments as properties when commentPropName is set to a string", function() { + const xmlData = `value`; + const expected = { + "#comment": [ + " Comment 1 ", + " Comment 2 " + ], + "root": "value" + }; + + const options = { + multipleRoots: false, + commentPropName: "#comment" + }; + const parser = new XMLParser(options); + let result = parser.parse(xmlData); + + expect(result).toEqual(expected); + }); + + it("should capture both comments and CDATA when both options are set to string values", function() { + const xmlData = ``; + const expected = { + "#comment": " Comment ", + "root": { + "#cdata": "data" + } + }; + + const options = { + multipleRoots: false, + commentPropName: "#comment", + cdataPropName: "#cdata" + }; + const parser = new XMLParser(options); + let result = parser.parse(xmlData); + + expect(result).toEqual(expected); + }); + + it("should throw error for multiple roots even with commentPropName and cdataPropName set when multipleRoots is false", function() { + const xmlData = `data1data2`; + + const options = { + multipleRoots: false, + commentPropName: "#comment", + cdataPropName: "#cdata" + }; + const parser = new XMLParser(options); + + expect(() => { + parser.parse(xmlData); + }).toThrow(); + }); + + it("should parse CDATA sections when cdataPropName is false (default)", function() { + const xmlData = ``; + + const options = { + multipleRoots: true, + }; + const parser = new XMLParser(options); + let result = parser.parse(xmlData); + + expect(result).toEqual({ + "root1": "some data", + "root2": "more data" + }); + }); + + it("should handle multiple roots with mixed comments and CDATA when both options are enabled", function() { + const xmlData = ` + + + text1 + + + + + text2 + + + + `; + const expected = { + "#comment": [ + " Start ", + " Middle ", + " End " + ], + "root1": { + "#text": "text1", + "#cdata": "cdata1" + }, + "root2": { + "#text": "text2", + "#cdata": "cdata2" + } + }; + + const options = { + multipleRoots: true, + commentPropName: "#comment", + cdataPropName: "#cdata" + }; + const parser = new XMLParser(options); + let result = parser.parse(xmlData); + + expect(result).toEqual(expected); + }); + + it("should correctly count root elements when custom commentPropName is not a standard comment marker", function() { + const xmlData = `data1data2`; + + const options = { + multipleRoots: false, + commentPropName: "customComment" + }; + const parser = new XMLParser(options); + + expect(() => { + parser.parse(xmlData); + }).toThrow(new Error("Multiple root tags found. Root tags: elem1, elem2")); + }); + + it("should parse multiple roots when commentPropName is explicitly set to false (disabled)", function() { + const xmlData = `value1value2`; + + const options = { + multipleRoots: true, + }; + const parser = new XMLParser(options); + let result = parser.parse(xmlData); + + // When commentPropName is false, comments are not captured as properties + expect(result.root1).toBe("value1"); + expect(result.root2).toBe("value2"); + }); + + it("should parse multiple roots when cdataPropName is explicitly set to false (disabled)", function() { + const xmlData = ``; + + const options = { + multipleRoots: true, + }; + const parser = new XMLParser(options); + let result = parser.parse(xmlData); + + expect(result.root1).toBe("data1"); + expect(result.root2).toBe("data2"); + }); + + it("should throw error for multiple roots with custom property names when multipleRoots is false", function() { + const xmlData = ``; + + const options = { + multipleRoots: false, + cdataPropName: "__cdata" + }; + const parser = new XMLParser(options); + + expect(() => { + parser.parse(xmlData); + }).toThrow(); + }); + + it("should correctly identify multiple roots even with empty comments when commentPropName is enabled", function() { + const xmlData = `firstsecond`; + + const options = { + multipleRoots: false, + commentPropName: "#comment" + }; + const parser = new XMLParser(options); + + expect(() => { + parser.parse(xmlData); + }).toThrow(new Error("Multiple root tags found. Root tags: primary, secondary")); + }); + + it("should handle multiple roots with complex CDATA content and custom property names", function() { + const xmlData = ` + content with < and > and &]]> + + `; + const expected = { + "message1": { + "#cdata": "content with < and > and &" + }, + "message2": { + "#cdata": "Another message with special chars" + } + }; + + const options = { + multipleRoots: true, + cdataPropName: "#cdata" + }; + const parser = new XMLParser(options); + let result = parser.parse(xmlData); + + expect(result).toEqual(expected); + }); + }); +}); diff --git a/spec/xmlParser_spec.js b/spec/xmlParser_spec.js index ca48506f..dc4f903c 100644 --- a/spec/xmlParser_spec.js +++ b/spec/xmlParser_spec.js @@ -451,7 +451,7 @@ describe("XMLParser", function() { }; const options = { - + multipleRoots: true }; const parser = new XMLParser(options); let result = parser.parse(xmlData); diff --git a/src/xmlparser/OptionsBuilder.js b/src/xmlparser/OptionsBuilder.js index 861cb7e5..863f0217 100644 --- a/src/xmlparser/OptionsBuilder.js +++ b/src/xmlparser/OptionsBuilder.js @@ -40,6 +40,7 @@ export const defaultOptions = { captureMetaData: false, maxNestedTags: 100, strictReservedNames: true, + multipleRoots: false, //if true, it will parse multiple root level tags and return an array of objects. If false, it will throw error if there are multiple root level tags }; /** diff --git a/src/xmlparser/OrderedObjParser.js b/src/xmlparser/OrderedObjParser.js index 187b5dee..a392069d 100644 --- a/src/xmlparser/OrderedObjParser.js +++ b/src/xmlparser/OrderedObjParser.js @@ -447,6 +447,25 @@ const parseXml = function (xmlData) { textData += xmlData[i]; } } + if (xmlObj.child) { + // count only actual element roots, ignoring processing instructions, comments, doctype, CDATA, etc. + const rootItems = xmlObj.child.filter(item => { + const tag = Object.keys(item)[0]; + // ignore declarations and PIs that start with '?', comments, doctype and cdata + if (tag.startsWith('?') || tag.startsWith('!--') || tag.startsWith('!D') || tag.startsWith('![') || tag.startsWith(this.options.commentPropName) || tag.startsWith(this.options.cdataPropName)) { + return false; + } + + return true; + }); + + if (rootItems.length > 1 && !this.options.multipleRoots) { + throw new Error( + "Multiple root tags found. Root tags: " + + rootItems.map(t => Object.keys(t)[0]).join(", ") + ); + } + } return xmlObj.child; }