diff --git a/lib/builder.js b/lib/builder.js
index 58f36384..1a1b948d 100644
--- a/lib/builder.js
+++ b/lib/builder.js
@@ -38,9 +38,10 @@
}
Builder.prototype.buildObject = function(rootObj) {
- var attrkey, charkey, render, rootElement, rootName;
+ var attrkey, charkey, childkey, render, rootElement, rootName;
attrkey = this.options.attrkey;
charkey = this.options.charkey;
+ childkey = this.options.childkey;
if ((Object.keys(rootObj).length === 1) && (this.options.rootName === defaults['0.2'].rootName)) {
rootName = Object.keys(rootObj)[0];
rootObj = rootObj[rootName];
@@ -49,7 +50,7 @@
}
render = (function(_this) {
return function(element, obj) {
- var attr, child, entry, index, key, value;
+ var attr, child, children_as_array, entry, index, key, name, value;
if (typeof obj !== 'object') {
if (_this.options.cdata && requiresCDATA(obj)) {
element.raw(wrapCDATA(obj));
@@ -66,46 +67,75 @@
}
}
} else {
- for (key in obj) {
- if (!hasProp.call(obj, key)) continue;
- child = obj[key];
- if (key === attrkey) {
- if (typeof child === "object") {
- for (attr in child) {
- value = child[attr];
- element = element.att(attr, value);
- }
- }
- } else if (key === charkey) {
- if (_this.options.cdata && requiresCDATA(child)) {
- element = element.raw(wrapCDATA(child));
- } else {
- element = element.txt(child);
- }
- } else if (Array.isArray(child)) {
+ children_as_array = false;
+ if ((obj != null) && attrkey in obj && typeof obj[attrkey] === "object") {
+ child = obj[attrkey];
+ for (attr in child) {
+ value = child[attr];
+ element = element.att(attr, value);
+ }
+ }
+ if ((obj != null) && charkey in obj) {
+ child = obj[charkey];
+ if (_this.options.cdata && requiresCDATA(child)) {
+ element = element.raw(wrapCDATA(child));
+ } else {
+ element = element.txt(child);
+ }
+ }
+ if ((obj != null) && childkey in obj) {
+ child = obj[childkey];
+ if (Array.isArray(child)) {
+ children_as_array = true;
for (index in child) {
if (!hasProp.call(child, index)) continue;
entry = child[index];
- if (typeof entry === 'string') {
- if (_this.options.cdata && requiresCDATA(entry)) {
- element = element.ele(key).raw(wrapCDATA(entry)).up();
+ if (typeof entry === "object") {
+ if (Object.keys(entry).length === 1) {
+ name = Object.keys(entry)[0];
+ element = render(element.ele(name), entry[name]).up();
+ } else if ('#name' in entry) {
+ element = render(element.ele(entry['#name']), entry).up();
} else {
- element = element.ele(key, entry).up();
+ throw new Error('Missing #name attribute when children');
}
- } else {
- element = render(element.ele(key), entry).up();
}
}
} else if (typeof child === "object") {
- element = render(element.ele(key), child).up();
- } else {
- if (typeof child === 'string' && _this.options.cdata && requiresCDATA(child)) {
- element = element.ele(key).raw(wrapCDATA(child)).up();
+ element = render(element, child);
+ }
+ }
+ if (!children_as_array) {
+ for (key in obj) {
+ if (!hasProp.call(obj, key)) continue;
+ child = obj[key];
+ if (key === '#name' || key === attrkey || key === charkey || key === childkey) {
+ continue;
+ } else if (Array.isArray(child)) {
+ for (index in child) {
+ if (!hasProp.call(child, index)) continue;
+ entry = child[index];
+ if (typeof entry === 'string') {
+ if (_this.options.cdata && requiresCDATA(entry)) {
+ element = element.ele(key).raw(wrapCDATA(entry)).up();
+ } else {
+ element = element.ele(key, entry).up();
+ }
+ } else {
+ element = render(element.ele(key), entry).up();
+ }
+ }
+ } else if (typeof child === "object") {
+ element = render(element.ele(key), child).up();
} else {
- if (child == null) {
- child = '';
+ if (typeof child === 'string' && _this.options.cdata && requiresCDATA(child)) {
+ element = element.ele(key).raw(wrapCDATA(child)).up();
+ } else {
+ if (child == null) {
+ child = '';
+ }
+ element = element.ele(key, child.toString()).up();
}
- element = element.ele(key, child.toString()).up();
}
}
}
diff --git a/src/builder.coffee b/src/builder.coffee
index 5653fde0..a5aab00a 100644
--- a/src/builder.coffee
+++ b/src/builder.coffee
@@ -30,6 +30,7 @@ class exports.Builder
buildObject: (rootObj) ->
attrkey = @options.attrkey
charkey = @options.charkey
+ childkey = @options.childkey
# If there is a sane-looking first element to use as the root,
# and the user hasn't specified a non-default rootName,
@@ -54,44 +55,73 @@ class exports.Builder
for key, entry of child
element = render(element.ele(key), entry).up()
else
- for own key, child of obj
- # Case #1 Attribute
- if key is attrkey
- if typeof child is "object"
- # Inserts tag attributes
- for attr, value of child
- element = element.att(attr, value)
-
- # Case #2 Char data (CDATA, etc.)
- else if key is charkey
- if @options.cdata && requiresCDATA child
- element = element.raw wrapCDATA child
- else
- element = element.txt child
+ children_as_array = false
+ # First analyze some metadata keys
+
+ # Attributes
+ if obj? and attrkey of obj and typeof obj[attrkey] is "object"
+ child = obj[attrkey]
+ # Inserts tag attributes
+ for attr, value of child
+ element = element.att(attr, value)
+
+ # Char data (CDATA, etc.)
+ if obj? and charkey of obj
+ child = obj[charkey]
+ if @options.cdata && requiresCDATA child
+ element = element.raw wrapCDATA child
+ else
+ element = element.txt child
- # Case #3 Array data
- else if Array.isArray child
+ # Objects with explicitChildren
+ if obj? and childkey of obj
+ child = obj[childkey]
+ if Array.isArray child
+ children_as_array = true
for own index, entry of child
- if typeof entry is 'string'
- if @options.cdata && requiresCDATA entry
- element = element.ele(key).raw(wrapCDATA entry).up()
+ if typeof entry is "object"
+ if ( Object.keys(entry).length is 1 )
+ name = Object.keys(entry)[0]
+ element = render(element.ele(name),entry[name]).up()
+ else if '#name' of entry
+ element = render(element.ele(entry['#name']), entry).up()
else
- element = element.ele(key, entry).up()
- else
- element = render(element.ele(key), entry).up()
-
- # Case #4 Objects
+ throw new Error('Missing #name attribute when children')
else if typeof child is "object"
- element = render(element.ele(key), child).up()
+ element = render(element, child)
- # Case #5 String and remaining types
- else
- if typeof child is 'string' && @options.cdata && requiresCDATA child
- element = element.ele(key).raw(wrapCDATA child).up()
+ if not children_as_array
+ # With the preserverChildrenOrder option, the parser will include
+ # the children element both as an array under 'childkey'
+ # and as individual keys.
+ for own key, child of obj
+ # Skip metadata keys that we have already covered
+ if key is '#name' or key is attrkey or key is charkey or key is childkey
+ continue
+
+ # Case #3 Array data
+ else if Array.isArray child
+ for own index, entry of child
+ if typeof entry is 'string'
+ if @options.cdata && requiresCDATA entry
+ element = element.ele(key).raw(wrapCDATA entry).up()
+ else
+ element = element.ele(key, entry).up()
+ else
+ element = render(element.ele(key), entry).up()
+
+ # Case #4 Objects
+ else if typeof child is "object"
+ element = render(element.ele(key), child).up()
+
+ # Case #5 String and remaining types
else
- if not child?
- child = ''
- element = element.ele(key, child.toString()).up()
+ if typeof child is 'string' && @options.cdata && requiresCDATA child
+ element = element.ele(key).raw(wrapCDATA child).up()
+ else
+ if not child?
+ child = ''
+ element = element.ele(key, child.toString()).up()
element
diff --git a/test/builder.test.coffee b/test/builder.test.coffee
index e9c12be3..dfeadd3b 100644
--- a/test/builder.test.coffee
+++ b/test/builder.test.coffee
@@ -266,6 +266,30 @@ module.exports =
diffeq expected, actual
test.finish()
+ 'test obj is a string with cdata': (test) ->
+ expected = """
+
+
+ """
+ opts = cdata: true
+ builder = new xml2js.Builder opts
+ obj = "& <<"
+ actual = builder.buildObject obj
+ diffeq expected, actual
+ test.finish()
+
+ 'test obj content with cdata': (test) ->
+ expected = """
+
+
+ """
+ opts = cdata: true
+ builder = new xml2js.Builder opts
+ obj = { root: { '_': "& <<" } }
+ actual = builder.buildObject obj
+ diffeq expected, actual
+ test.finish()
+
'test building obj with array': (test) ->
expected = """
@@ -281,3 +305,121 @@ module.exports =
actual = builder.buildObject obj
diffeq expected, actual
test.finish()
+
+ 'test round-trip explicitChildren': (test) ->
+ xml = 'Text B1Text C2Text B3'
+ expected = """
+
+
+ Text B1
+ Text B3
+ Text C2
+
+
+ """
+ opts = cdata: true, explicitChildren: true
+ parser_opts = explicitChildren: true
+ parser = new xml2js.Parser parser_opts
+ builder = new xml2js.Builder opts
+ parser.parseString xml, (err, data) ->
+ equ err, null
+ actual = builder.buildObject data
+ diffeq expected, actual
+ test.finish()
+
+ 'test round-trip explicitChildren & preserveChildrenOrder': (test) ->
+ xml = 'Text B1Text C2Text B3'
+ expected = """
+
+
+ Text B1
+ Text C2
+ Text B3
+
+
+ """
+ opts = cdata: true, explicitChildren: true, preserveChildrenOrder: true
+ parser_opts = explicitChildren: true, preserveChildrenOrder: true
+ parser = new xml2js.Parser parser_opts
+ builder = new xml2js.Builder opts
+ parser.parseString xml, (err, data) ->
+ equ err, null
+ actual = builder.buildObject data
+ diffeq expected, actual
+ test.finish()
+ 'test children as single key objects': (test) ->
+ obj = {
+ a: {
+ '$': { id: 0 },
+ '$$': [
+ {
+ b: {
+ '$': { id: 1},
+ '_': 'Text B1'
+ }
+ },
+ {
+ c: {
+ '$': { id: 2},
+ '_': 'Text C2'
+ }
+ },
+ {
+ b: {
+ '$': { id: 3},
+ '_': 'Text B3'
+ }
+ }
+ ]
+ }
+ }
+ expected = """
+
+
+ Text B1
+ Text C2
+ Text B3
+
+
+ """
+ opts = {}
+ builder = new xml2js.Builder opts
+ actual = builder.buildObject obj
+ diffeq expected, actual
+ test.finish()
+ 'test children without any name': (test) ->
+ obj = {
+ a: {
+ '$': { id: 0 },
+ '$$': [
+ {
+ '$': { id: 1},
+ '_': 'Text B1'
+ },
+ {
+ '$': { id: 2},
+ '_': 'Text C2'
+ },
+ {
+ '$': { id: 3},
+ '_': 'Text B3'
+ }
+ ]
+ }
+ }
+ expected = """
+
+
+ Text B1
+ Text C2
+ Text B3
+
+
+ """
+ opts = {}
+ builder = new xml2js.Builder opts
+ assert.throws(
+ () =>
+ actual = builder.buildObject obj
+ /Missing #name attribute when children/)
+ test.finish()
\ No newline at end of file