Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Bi-directional support

(Experimental for now)

Squashed commit of the following:

commit 486a7aa
Author: Jack Senechal <jack@jacksenechal.com>
Date:   Thu Oct 31 15:36:12 2013 -0700

    revert mods to test/fixtures/sample.xml

commit 3819482
Author: Jack Senechal <jack@jacksenechal.com>
Date:   Thu Oct 31 15:31:57 2013 -0700

    adding diff to devDependencies

commit 969a354
Author: Jack Senechal <jack@jacksenechal.com>
Date:   Thu Oct 31 15:25:47 2013 -0700

    Updated README.md

commit 3f9f626
Author: Jack Senechal <jack@jacksenechal.com>
Date:   Thu Oct 31 15:16:34 2013 -0700

    exposing renderOpts and fixing a few tests

commit f61b652
Author: Jack Senechal <jack@jacksenechal.com>
Date:   Thu Oct 31 03:09:53 2013 -0700

    lots of unit tests for the builder

commit c499afd
Merge: 26626d0 a2a4938
Author: Jack Senechal <jack@jacksenechal.com>
Date:   Mon Oct 28 17:34:12 2013 -0700

    Merge remote-tracking branch 'upstream/master'
    'npm test' passing

    Conflicts:
    	README.md
    	lib/xml2js.js
    	package.json
    	src/xml2js.coffee

commit 26626d0
Author: Jack Senechal <jack@jacksenechal.com>
Date:   Fri Oct 25 04:10:08 2013 -0700

    Cleaned up and fixed a few bugs

commit 690b1c8
Author: setumiami <guido@bitstorm.it>
Date:   Thu Jan 10 01:39:55 2013 +0100

    README.md modified.

commit 9bc9e9f
Author: setumiami <guido@bitstorm.it>
Date:   Thu Jan 10 01:37:10 2013 +0100

    Added bi-directional conversion (xml2js and viceversa)
  • Loading branch information...
commit 07e71f0555a0daee1671a1ec9aef24128799da9e 1 parent a2a4938
@Leonidas-from-XIV authored
View
43 README.md
@@ -8,8 +8,9 @@ what you're looking for!
Description
===========
-Simple XML to JavaScript object converter. Uses
-[sax-js](https://github.com/isaacs/sax-js/).
+Simple XML to JavaScript object converter. It supports bi-directional conversion.
+Uses [sax-js](https://github.com/isaacs/sax-js/) and
+[xmlbuilder-js](https://github.com/oozcitak/xmlbuilder-js/).
Note: If you're looking for a full DOM parser, you probably want
[JSDom](https://github.com/tmpvar/jsdom).
@@ -153,6 +154,25 @@ you just need to increase the `maxLength` limit by creating a custom inspector
`var inspect = require('eyes').inspector({maxLength: false})` and then you can
easily `inspect(result)`.
+XML builder usage
+-----------------
+
+Objects can be also be used to build XML:
+
+```javascript
+var fs = require('fs'),
+ xml2js = require('xml2js');
+
+var obj = { name: "Super", Surname: "Man", age: 23};
+
+var builder = new xml2js.Builder();
+var xml = builder.buildObject(obj);
+```
+
+At the moment, a one to one bi-directional conversion is guaranteed only for
+default configuration, except for `attrkey`, `charkey` and `explicitArray` options
+you can redefine to your taste. Writing CDATA is not currently supported.
+
Options
=======
@@ -201,6 +221,25 @@ value})``. Possible options are:
Defaults to `true` which is *highly* recommended, since parsing HTML which
is not well-formed XML might yield just about anything. Added in 0.2.7.
+Options for the `Builder` class
+-------------------------------
+
+ * `rootName` (default `root`): root element name to be used in case
+ `explicitiRoot` is `false` or to override the root element name.

Typo, explicitiRoot should be explicitRoot

Thanks, I fixed that just now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ * `renderOpts` (default `{ 'pretty': true, 'indent': ' ', 'newline': '\n' }`):
+ Rendering options for xmlbuilder-js.
+ * pretty: prettify generated XML
+ * indent: whitespace for indentation (only when pretty)
+ * newline: newline char (only when pretty)
+ * `xmldec` (default `{ 'version': '1.0', 'encoding': 'UTF-8', 'standalone': true }`:
+ XML declaration attributes.
+ * `xmldec.version` A version number string, e.g. 1.0
+ * `xmldec.encoding` Encoding declaration, e.g. UTF-8
+ * `xmldec.standalone` standalone document declaration: true or false
+ * `doctype` (default `null`): optional DTD. Eg. `{'ext': 'hello.dtd'}`
+
+renderOpts, xmldec and doctype pass through to [xmlbuilder-js](https://github.com/oozcitak/xmlbuilder-js)
+
Updating to new version
=======================
View
110 lib/xml2js.js
@@ -1,14 +1,16 @@
-// Generated by CoffeeScript 1.6.3
+// Generated by CoffeeScript 1.6.1
(function() {
- var bom, events, isEmpty, sax,
+ var bom, builder, events, isEmpty, sax,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
- __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ _this = this;
sax = require('sax');
events = require('events');
+ builder = require('xmlbuilder');
+
bom = require('./bom');
isEmpty = function(thing) {
@@ -52,11 +54,24 @@
childkey: '$$',
charsAsChildren: false,
async: false,
- strict: true
+ strict: true,
+ rootName: 'root',
+ xmldec: {
+ 'version': '1.0',
+ 'encoding': 'UTF-8',
+ 'standalone': true
+ },
+ doctype: null,
+ renderOpts: {
+ 'pretty': true,
+ 'indent': ' ',
+ 'newline': '\n'
+ }
}
};
exports.ValidationError = (function(_super) {
+
__extends(ValidationError, _super);
function ValidationError(message) {
@@ -67,13 +82,91 @@
})(Error);
+ exports.Builder = (function() {
+
+ function Builder(opts) {
+ var key, value, _ref;
+ this.options = {};
+ _ref = exports.defaults["0.2"];
+ for (key in _ref) {
+ if (!__hasProp.call(_ref, key)) continue;
+ value = _ref[key];
+ this.options[key] = value;
+ }
+ for (key in opts) {
+ if (!__hasProp.call(opts, key)) continue;
+ value = opts[key];
+ this.options[key] = value;
+ }
+ }
+
+ Builder.prototype.buildObject = function(rootObj) {
+ var attrkey, charkey, render, rootElement, rootName;
+ attrkey = this.options.attrkey;
+ charkey = this.options.charkey;
+ if ((Object.keys(rootObj).length === 1) && (this.options.rootName === exports.defaults['0.2'].rootName)) {
+ rootName = Object.keys(rootObj)[0];
+ rootObj = rootObj[rootName];
+ } else {
+ rootName = this.options.rootName;
+ }
+ render = function(element, obj) {
+ var attr, child, entry, index, key, value, _ref, _ref1;
+ if (typeof obj !== 'object') {
+ element.txt(obj);
+ } 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) {
+ element = element.txt(child);
+ } else if (typeof child === 'object' && ((child != null ? child.constructor : void 0) != null) && ((child != null ? (_ref = child.constructor) != null ? _ref.name : void 0 : void 0) != null) && (child != null ? (_ref1 = child.constructor) != null ? _ref1.name : void 0 : void 0) === 'Array') {
+ for (index in child) {
+ if (!__hasProp.call(child, index)) continue;
+ entry = child[index];
+ if (typeof entry === 'string') {
+ element = element.ele(key, entry).up();
+ } else {
+ element = arguments.callee(element.ele(key), entry).up();
+ }
+ }
+ } else if (typeof child === "object") {
+ element = arguments.callee(element.ele(key), child).up();
+ } else {
+ element = element.ele(key, child.toString()).up();
+ }
+ }
+ }
+ return element;
+ };
+ rootElement = builder.create(rootName, this.options.xmldec, this.options.doctype);
+ return render(rootElement, rootObj).end(this.options.renderOpts);
+ };
+
+ return Builder;
+
+ })();
+
exports.Parser = (function(_super) {
+
__extends(Parser, _super);
function Parser(opts) {
- this.parseString = __bind(this.parseString, this);
- this.reset = __bind(this.reset, this);
- var key, value, _ref;
+ var key, value, _ref,
+ _this = this;
+ this.parseString = function(str, cb) {
+ return Parser.prototype.parseString.apply(_this, arguments);
+ };
+ this.reset = function() {
+ return Parser.prototype.reset.apply(_this, arguments);
+ };
if (!(this instanceof exports.Parser)) {
return new exports.Parser(opts);
}
@@ -180,8 +273,7 @@
})()).concat(nodeName).join("/");
try {
obj = _this.options.validator(xpath, s && s[nodeName], obj);
- } catch (_error) {
- err = _error;
+ } catch (err) {
_this.emit("error", err);
}
}
View
6 package.json
@@ -37,12 +37,14 @@
"url" : "https://github.com/Leonidas-from-XIV/node-xml2js.git"
},
"dependencies" : {
- "sax" : "0.5.x"
+ "sax" : "0.5.x",
+ "xmlbuilder" : ">=0.4.2"
},
"devDependencies" : {
"coffee-script" : ">=1.6.3",
"zap" : ">=0.2.5",
- "docco" : ">=0.6.2"
+ "docco" : ">=0.6.2",
+ "diff" : ">=1.0.7"
},
"licenses": [
{
View
67 src/xml2js.coffee
@@ -1,5 +1,6 @@
sax = require 'sax'
events = require 'events'
+builder = require 'xmlbuilder'
bom = require './bom'
# Underscore has a nice function for this, but we try to go without dependencies
@@ -55,11 +56,77 @@ exports.defaults =
# not async in 0.2 mode either
async: false
strict: true
+ # xml building options
+ rootName: 'root'
+ xmldec: {'version': '1.0', 'encoding': 'UTF-8', 'standalone': true}
+ doctype: null
+ renderOpts: { 'pretty': true, 'indent': ' ', 'newline': '\n' }
class exports.ValidationError extends Error
constructor: (message) ->
@message = message
+class exports.Builder
+ constructor: (opts) ->
+ # copy this versions default options
+ @options = {}
+ @options[key] = value for own key, value of exports.defaults["0.2"]
+ # overwrite them with the specified options, if any
+ @options[key] = value for own key, value of opts
+
+ buildObject: (rootObj) ->
+ attrkey = @options.attrkey
+ charkey = @options.charkey
+
+ # If there is a sane-looking first element to use as the root,
+ # and the user hasn't specified a non-default rootName,
+ if ( Object.keys(rootObj).length is 1 ) and ( @options.rootName == exports.defaults['0.2'].rootName )
+ # we'll take the first element as the root element
+ rootName = Object.keys(rootObj)[0]
+ rootObj = rootObj[rootName]
+ else
+ # otherwise we'll use whatever they've set, or the default
+ rootName = @options.rootName
+
+ render = (element, obj) ->
+ if typeof obj isnt 'object'
+ # single element, just append it as text
+ element.txt obj
+ 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
+ element = element.txt(child)
+
+ # Case #3 Array data
+ else if typeof child is 'object' and child?.constructor? and child?.constructor?.name? and child?.constructor?.name is 'Array'
+ for own index, entry of child
+ if typeof entry is 'string'
+ element = element.ele(key, entry).up()
+ else
+ element = arguments.callee(element.ele(key), entry).up()
+
+ # Case #4 Objects
+ else if typeof child is "object"
+ element = arguments.callee(element.ele(key), child).up()
+
+ # Case #5 String and remaining types
+ else
+ element = element.ele(key, child.toString()).up()
+
+ element
+
+ rootElement = builder.create(rootName, @options.xmldec, @options.doctype)
+
+ render(rootElement, rootObj).end(@options.renderOpts)
+
class exports.Parser extends events.EventEmitter
constructor: (opts) ->
# if this was called without 'new', create an instance with new and return
View
113 test/builder.test.coffee
@@ -0,0 +1,113 @@
+# use zap to run tests, it also detects CoffeeScript files
+xml2js = require '../lib/xml2js'
+assert = require 'assert'
+fs = require 'fs'
+path = require 'path'
+diff = require 'diff'
+
+# fileName = path.join __dirname, '/fixtures/sample.xml'
+
+# shortcut, because it is quite verbose
+equ = assert.equal
+
+# equality test with diff output
+diffeq = (expected, actual) ->
+ diffless = "Index: test\n===================================================================\n--- test\texpected\n+++ test\tactual\n"
+ patch = diff.createPatch('test', expected, actual, 'expected', 'actual')
+ throw patch unless patch is diffless
+
+module.exports =
+ 'test building basic XML structure': (test) ->
+ expected = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><xml><Label></Label><MsgId>5850440872586764820</MsgId></xml>'
+ obj = {"xml":{"Label":[""],"MsgId":["5850440872586764820"]}}
+ builder = new xml2js.Builder(renderOpts: { pretty: false })
+ actual = builder.buildObject obj
+ diffeq expected, actual
+ test.finish()
+
+ 'test setting XML declaration': (test) ->
+ expected = '<?xml version="1.2" encoding="WTF-8" standalone="no"?><root/>'
+ opts = {
+ renderOpts: { pretty: false }
+ xmldec: {'version': '1.2', 'encoding': 'WTF-8', 'standalone': false}
+ }
+ builder = new xml2js.Builder(opts)
+ actual = builder.buildObject {}
+ diffeq expected, actual
+ test.finish()
+
+ 'test pretty by default': (test) ->
+ expected = """
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+ <xml>
+ <MsgId>5850440872586764820</MsgId>
+ </xml>
+
+ """
+ builder = new xml2js.Builder()
+ obj = {"xml":{"MsgId":["5850440872586764820"]}}
+ actual = builder.buildObject obj
+ diffeq expected, actual
+ test.finish()
+
+ 'test setting indentation': (test) ->
+ expected = """
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+ <xml>
+ <MsgId>5850440872586764820</MsgId>
+ </xml>
+
+ """
+ opts = { renderOpts: { pretty: true, indent: ' ' } }
+ builder = new xml2js.Builder(opts)
+ obj = {"xml":{"MsgId":["5850440872586764820"]}}
+ actual = builder.buildObject obj
+ diffeq expected, actual
+ test.finish()
+
+ 'test explicit rootName is always used: 1. when there is only one element': (test) ->
+ expected = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><FOO><MsgId>5850440872586764820</MsgId></FOO>'
+ opts = { renderOpts: { pretty: false }, rootName: 'FOO' }
+ builder = new xml2js.Builder(opts)
+ obj = {"MsgId":["5850440872586764820"]}
+ actual = builder.buildObject obj
+ diffeq expected, actual
+ test.finish()
+
+ 'test explicit rootName is always used: 2. when there are multiple elements': (test) ->
+ expected = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><FOO><MsgId>5850440872586764820</MsgId></FOO>'
+ opts = { renderOpts: { pretty: false }, rootName: 'FOO' }
+ builder = new xml2js.Builder(opts)
+ obj = {"MsgId":["5850440872586764820"]}
+ actual = builder.buildObject obj
+ diffeq expected, actual
+ test.finish()
+
+ 'test default rootName is used when there is more than one element in the hash': (test) ->
+ expected = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><root><MsgId>5850440872586764820</MsgId><foo>bar</foo></root>'
+ opts = { renderOpts: { pretty: false } }
+ builder = new xml2js.Builder(opts)
+ obj = {"MsgId":["5850440872586764820"],"foo":"bar"}
+ actual = builder.buildObject obj
+ diffeq expected, actual
+ test.finish()
+
+ 'test when there is only one first-level element in the hash, that is used as root': (test) ->
+ expected = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><first><MsgId>5850440872586764820</MsgId><foo>bar</foo></first>'
+ opts = { renderOpts: { pretty: false } }
+ builder = new xml2js.Builder(opts)
+ obj = {"first":{"MsgId":["5850440872586764820"],"foo":"bar"}}
+ actual = builder.buildObject obj
+ diffeq expected, actual
+ test.finish()
+
+ 'test parser -> builder roundtrip': (test) ->
+ fileName = path.join __dirname, '/fixtures/build_sample.xml'
+ fs.readFile fileName, (err, xmlData) ->
+ xmlExpected = xmlData.toString()
+ xml2js.parseString xmlData, {'trim': true}, (err, obj) ->
+ equ err, null
+ builder = new xml2js.Builder({})
+ xmlActual = builder.buildObject obj
+ diffeq xmlExpected, xmlActual
+ test.finish()
View
57 test/fixtures/build_sample.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<sample>
+ <chartest desc="Test for CHARs">Character data here!</chartest>
+ <nochartest desc="No data" misc="false"/>
+ <nochildrentest desc="No data" misc="false"/>
+ <listtest>
+ <item>
+ This is
+ character
+ data!
+ <subitem>Foo(1)</subitem>
+ <subitem>Foo(2)</subitem>
+ <subitem>Foo(3)</subitem>
+ <subitem>Foo(4)</subitem>
+ </item>
+ <item>Qux.</item>
+ <item>Quux.</item>
+ </listtest>
+ <arraytest>
+ <item>
+ <subitem>Baz.</subitem>
+ </item>
+ <item>
+ <subitem>Foo.</subitem>
+ <subitem>Bar.</subitem>
+ </item>
+ </arraytest>
+ <emptytest>
+
+ </emptytest>
+ <tagcasetest>
+ <tAg>something</tAg>
+ <TAG>something else</TAG>
+ <tag>something third</tag>
+ </tagcasetest>
+ <ordertest>
+ <one>1</one>
+ <one>4</one>
+ <two>2</two>
+ <two>5</two>
+ <three>3</three>
+ <three>6</three>
+ </ordertest>
+ <validatortest>
+ <emptyarray>
+
+ </emptyarray>
+ <oneitemarray>
+ <item>Bar.</item>
+ </oneitemarray>
+ <numbertest>42</numbertest>
+ <stringtest>43</stringtest>
+ </validatortest>
+ <pfx:top xmlns:pfx="http://foo.com" pfx:attr="baz">
+ <middle xmlns="http://bar.com"/>
+ </pfx:top>
+</sample>
View
3  test/xml2js.test.coffee → test/parser.test.coffee
@@ -22,6 +22,7 @@ skeleton = (options, checks) ->
x2js.parseString data
else
x2js.parseString xmlString
+
###
The `validator` function validates the value at the XPath. It also transforms the value
if necessary to conform to the schema or other validation information being used. If there
@@ -280,7 +281,7 @@ module.exports =
'test callback should be called once': (test) ->
xml = '<?xml version="1.0" encoding="utf-8"?><test>test</test>'
- i = 0;
+ i = 0
try
xml2js.parseString xml, (err, parsed) ->
i = i + 1
Please sign in to comment.
Something went wrong with that request. Please try again.