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