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 = `- First
- Second
`;
+
+ 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;
}