├── .gitignore
├── .travis.yml
├── .npmignore
├── src
├── bom.coffee
├── processors.coffee
└── xml2js.coffee
├── test
├── bom.test.coffee
├── processors.test.coffee
├── fixtures
│ ├── build_sample.xml
│ └── sample.xml
├── builder.test.coffee
└── parser.test.coffee
├── lib
├── bom.js
├── processors.js
└── xml2js.js
├── Cakefile
├── CONTRIBUTING.md
├── LICENSE
├── package.json
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | node_modules
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - "0.10"
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | .idea
3 | node_modules
4 | src
5 | test
6 | Cakefile
--------------------------------------------------------------------------------
/src/bom.coffee:
--------------------------------------------------------------------------------
1 | xml2js = require '../lib/xml2js'
2 |
3 | exports.stripBOM = (str) ->
4 | if str[0] == '\uFEFF'
5 | str.substring(1)
6 | else
7 | str
8 |
9 |
--------------------------------------------------------------------------------
/test/bom.test.coffee:
--------------------------------------------------------------------------------
1 | xml2js = require '../lib/xml2js'
2 | assert = require 'assert'
3 | equ = assert.equal
4 |
5 | module.exports =
6 | 'test decoded BOM': (test) ->
7 | demo = '\uFEFFbar'
8 | xml2js.parseString demo, (err, res) ->
9 | equ err, undefined
10 | equ res.xml.foo[0], 'bar'
11 | test.done()
12 |
--------------------------------------------------------------------------------
/lib/bom.js:
--------------------------------------------------------------------------------
1 | // Generated by CoffeeScript 1.7.1
2 | (function() {
3 | var xml2js;
4 |
5 | xml2js = require('../lib/xml2js');
6 |
7 | exports.stripBOM = function(str) {
8 | if (str[0] === '\uFEFF') {
9 | return str.substring(1);
10 | } else {
11 | return str;
12 | }
13 | };
14 |
15 | }).call(this);
16 |
17 | //# sourceMappingURL=bom.map
18 |
--------------------------------------------------------------------------------
/src/processors.coffee:
--------------------------------------------------------------------------------
1 | # matches all xml prefixes, except for `xmlns:`
2 | prefixMatch = new RegExp /(?!xmlns)^.*:/
3 |
4 | exports.normalize = (str) ->
5 | return str.toLowerCase()
6 |
7 | exports.firstCharLowerCase = (str) ->
8 | return str.charAt(0).toLowerCase() + str.slice(1)
9 |
10 | exports.stripPrefix = (str) ->
11 | return str.replace(prefixMatch, '')
12 |
13 |
--------------------------------------------------------------------------------
/Cakefile:
--------------------------------------------------------------------------------
1 | {spawn, exec} = require 'child_process'
2 |
3 | task 'build', 'continually build the JavaScript code', ->
4 | coffee = spawn 'coffee', ['-cw', '-o', 'lib', 'src']
5 | coffee.stdout.on 'data', (data) -> console.log data.toString().trim()
6 |
7 | task 'doc', 'rebuild the Docco documentation', ->
8 | exec([
9 | 'docco src/xml2js.coffee'
10 | ].join(' && '), (err) ->
11 | throw err if err
12 | )
13 |
--------------------------------------------------------------------------------
/lib/processors.js:
--------------------------------------------------------------------------------
1 | // Generated by CoffeeScript 1.7.1
2 | (function() {
3 | var prefixMatch;
4 |
5 | prefixMatch = new RegExp(/(?!xmlns)^.*:/);
6 |
7 | exports.normalize = function(str) {
8 | return str.toLowerCase();
9 | };
10 |
11 | exports.firstCharLowerCase = function(str) {
12 | return str.charAt(0).toLowerCase() + str.slice(1);
13 | };
14 |
15 | exports.stripPrefix = function(str) {
16 | return str.replace(prefixMatch, '');
17 | };
18 |
19 | }).call(this);
20 |
21 | //# sourceMappingURL=processors.map
22 |
--------------------------------------------------------------------------------
/test/processors.test.coffee:
--------------------------------------------------------------------------------
1 | processors = require '../lib/processors'
2 | assert = require 'assert'
3 | equ = assert.equal
4 |
5 | module.exports =
6 | 'test normalize': (test) ->
7 | demo = 'This shOUld BE loWErcase'
8 | result = processors.normalize demo
9 | equ result, 'this should be lowercase'
10 | test.done()
11 |
12 | 'test firstCharLowerCase': (test) ->
13 | demo = 'ThiS SHould OnlY LOwercase the fIRST cHar'
14 | result = processors.firstCharLowerCase demo
15 | equ result, 'thiS SHould OnlY LOwercase the fIRST cHar'
16 | test.done()
17 |
18 | 'test stripPrefix': (test) ->
19 | demo = 'stripMe:DoNotTouch'
20 | result = processors.stripPrefix demo
21 | equ result, 'DoNotTouch'
22 | test.done()
23 |
24 | 'test stripPrefix, ignore xmlns': (test) ->
25 | demo = 'xmlns:shouldHavePrefix'
26 | result = processors.stripPrefix demo
27 | equ result, 'xmlns:shouldHavePrefix'
28 | test.done()
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to contribute
2 |
3 | We're always happy about useful new pull requests. Keep in mind that the better
4 | your pull request is, the easier it can be added to `xml2js`. As such please
5 | make sure your patch is ok:
6 |
7 | * `xml2js` is written in CoffeeScript. Please don't send patches to
8 | the JavaScript source, as it get's overwritten by the CoffeeScript
9 | compiler. The reason we have the JS code in the repository is for easier
10 | use with eg. `git submodule`
11 | * Make sure that the unit tests still all pass. Failing unit tests mean that
12 | someone *will* run into a bug, if we accept your pull request.
13 | * Please, add a unit test with your pull request, to show what was broken and
14 | is now fixed or what was impossible and now works due to your new code.
15 | * If you add a new feature, please add some documentation that it exists.
16 |
17 | If you like, you can add yourself in the `package.json` as contributor if you
18 | deem your contribution significant enough. Otherwise, we will decide and maybe
19 | add you.
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2010, 2011, 2012, 2013. All rights reserved.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to
5 | deal in the Software without restriction, including without limitation the
6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7 | sell copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
19 | IN THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/test/fixtures/build_sample.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Character data here!
4 |
5 |
6 |
7 | -
8 | This is
9 | character
10 | data!
11 | Foo(1)
12 | Foo(2)
13 | Foo(3)
14 | Foo(4)
15 |
16 | - Qux.
17 | - Quux.
18 |
19 |
20 | -
21 | Baz.
22 |
23 | -
24 | Foo.
25 | Bar.
26 |
27 |
28 |
29 |
30 |
31 |
32 | something
33 | something else
34 | something third
35 |
36 |
37 | 1
38 | 4
39 | 2
40 | 5
41 | 3
42 | 6
43 |
44 |
45 |
46 |
47 |
48 |
49 | - Bar.
50 |
51 | 42
52 | 43
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/test/fixtures/sample.xml:
--------------------------------------------------------------------------------
1 |
2 | Character data here!
3 |
4 |
5 |
6 |
7 |
8 | Line One
9 | Line Two
10 |
11 |
12 | -
13 | This Foo(1) is
14 | Foo(2)
15 | character
16 | Foo(3)
17 | data!
18 | Foo(4)
19 |
20 | - Qux.
21 | - Quux.
22 | Single
23 |
24 |
25 | - Baz.
26 | - Foo.Bar.
27 |
28 |
29 |
30 | something
31 | something else
32 | something third
33 |
34 |
35 | 1
36 | 2
37 | 3
38 | 4
39 | 5
40 | 6
41 |
42 |
43 |
44 |
45 | - Bar.
46 |
47 | 42
48 | 43
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name" : "xml2js",
3 | "description" : "Simple XML to JavaScript object converter.",
4 | "keywords" : ["xml", "json"],
5 | "homepage" : "https://github.com/Leonidas-from-XIV/node-xml2js",
6 | "version" : "0.4.4",
7 | "author" : "Marek Kubica (http://xivilization.net)",
8 | "contributors" : [
9 | "maqr (https://github.com/maqr)",
10 | "Ben Weaver (http://benweaver.com/)",
11 | "Jae Kwon (https://github.com/jaekwon)",
12 | "Jim Robert",
13 | "Ștefan Rusu (http://www.saltwaterc.eu/)",
14 | "Carter Cole (http://cartercole.com/)",
15 | "Kurt Raschke (http://www.kurtraschke.com/)",
16 | "Contra (https://github.com/Contra)",
17 | "Marcelo Diniz (https://github.com/mdiniz)",
18 | "Michael Hart (https://github.com/mhart)",
19 | "Zachary Scott (http://zacharyscott.net/)",
20 | "Raoul Millais (https://github.com/raoulmillais)",
21 | "Salsita Software (http://www.salsitasoft.com/)",
22 | "Mike Schilling (http://www.emotive.com/)",
23 | "Jackson Tian (http://weibo.com/shyvo)",
24 | "Mikhail Zyatin (https://github.com/Sitin)",
25 | "Chris Tavares (https://github.com/christav)",
26 | "Frank Xu (http://f2e.us/)",
27 | "Guido D'Albore (http://www.bitstorm.it/)",
28 | "Jack Senechal (http://jacksenechal.com/)",
29 | "Matthias Hölzl (https://github.com/hoelzl)",
30 | "Camille Reynders (http://www.creynders.be/)",
31 | "Taylor Gautier (https://github.com/tsgautier)",
32 | "Todd Bryan (https://github.com/toddrbryan)",
33 | "Leore Avidar (http://leoreavidar.com/)",
34 | "Dave Aitken (http://www.actionshrimp.com/)",
35 | "Shaney Orrowe "
36 | ],
37 | "main" : "./lib/xml2js",
38 | "directories" : {
39 | "lib": "./lib"
40 | },
41 | "scripts" : {
42 | "test": "zap"
43 | },
44 | "repository" : {
45 | "type" : "git",
46 | "url" : "https://github.com/Leonidas-from-XIV/node-xml2js.git"
47 | },
48 | "dependencies" : {
49 | "sax" : "0.6.x",
50 | "xmlbuilder" : ">=1.0.0"
51 | },
52 | "devDependencies" : {
53 | "coffee-script" : ">=1.7.1",
54 | "zap" : ">=0.2.6",
55 | "docco" : ">=0.6.2",
56 | "diff" : ">=1.0.8"
57 | },
58 | "licenses": [
59 | {
60 | "type": "MIT",
61 | "url": "https://raw.github.com/Leonidas-from-XIV/node-xml2js/master/LICENSE"
62 | }
63 | ]
64 | }
65 |
--------------------------------------------------------------------------------
/test/builder.test.coffee:
--------------------------------------------------------------------------------
1 | # use zap to run tests, it also detects CoffeeScript files
2 | xml2js = require '../lib/xml2js'
3 | assert = require 'assert'
4 | fs = require 'fs'
5 | path = require 'path'
6 | diff = require 'diff'
7 |
8 | # fileName = path.join __dirname, '/fixtures/sample.xml'
9 |
10 | # shortcut, because it is quite verbose
11 | equ = assert.equal
12 |
13 | # equality test with diff output
14 | diffeq = (expected, actual) ->
15 | diffless = "Index: test\n===================================================================\n--- test\texpected\n+++ test\tactual\n"
16 | patch = diff.createPatch('test', expected.trim(), actual.trim(), 'expected', 'actual')
17 | throw patch unless patch is diffless
18 |
19 | module.exports =
20 | 'test building basic XML structure': (test) ->
21 | expected = '5850440872586764820'
22 | obj = {"xml":{"Label":[""],"MsgId":["5850440872586764820"]}}
23 | builder = new xml2js.Builder renderOpts: pretty: false
24 | actual = builder.buildObject obj
25 | diffeq expected, actual
26 | test.finish()
27 |
28 | 'test setting XML declaration': (test) ->
29 | expected = ''
30 | opts =
31 | renderOpts: pretty: false
32 | xmldec: 'version': '1.2', 'encoding': 'WTF-8', 'standalone': false
33 | builder = new xml2js.Builder opts
34 | actual = builder.buildObject {}
35 | diffeq expected, actual
36 | test.finish()
37 |
38 | 'test pretty by default': (test) ->
39 | expected = """
40 |
41 |
42 | 5850440872586764820
43 |
44 |
45 | """
46 | builder = new xml2js.Builder()
47 | obj = {"xml":{"MsgId":["5850440872586764820"]}}
48 | actual = builder.buildObject obj
49 | diffeq expected, actual
50 | test.finish()
51 |
52 | 'test setting indentation': (test) ->
53 | expected = """
54 |
55 |
56 | 5850440872586764820
57 |
58 |
59 | """
60 | opts = renderOpts: pretty: true, indent: ' '
61 | builder = new xml2js.Builder opts
62 | obj = {"xml":{"MsgId":["5850440872586764820"]}}
63 | actual = builder.buildObject obj
64 | diffeq expected, actual
65 | test.finish()
66 |
67 | 'test headless option': (test) ->
68 | expected = """
69 |
70 | 5850440872586764820
71 |
72 |
73 | """
74 | opts =
75 | renderOpts: pretty: true, indent: ' '
76 | headless: true
77 | builder = new xml2js.Builder opts
78 | obj = {"xml":{"MsgId":["5850440872586764820"]}}
79 | actual = builder.buildObject obj
80 | diffeq expected, actual
81 | test.finish()
82 |
83 | 'test explicit rootName is always used: 1. when there is only one element': (test) ->
84 | expected = '5850440872586764820'
85 | opts = renderOpts: {pretty: false}, rootName: 'FOO'
86 | builder = new xml2js.Builder opts
87 | obj = {"MsgId":["5850440872586764820"]}
88 | actual = builder.buildObject obj
89 | diffeq expected, actual
90 | test.finish()
91 |
92 | 'test explicit rootName is always used: 2. when there are multiple elements': (test) ->
93 | expected = '5850440872586764820'
94 | opts = renderOpts: {pretty: false}, rootName: 'FOO'
95 | builder = new xml2js.Builder opts
96 | obj = {"MsgId":["5850440872586764820"]}
97 | actual = builder.buildObject obj
98 | diffeq expected, actual
99 | test.finish()
100 |
101 | 'test default rootName is used when there is more than one element in the hash': (test) ->
102 | expected = '5850440872586764820bar'
103 | opts = renderOpts: pretty: false
104 | builder = new xml2js.Builder opts
105 | obj = {"MsgId":["5850440872586764820"],"foo":"bar"}
106 | actual = builder.buildObject obj
107 | diffeq expected, actual
108 | test.finish()
109 |
110 | 'test when there is only one first-level element in the hash, that is used as root': (test) ->
111 | expected = '5850440872586764820bar'
112 | opts = renderOpts: pretty: false
113 | builder = new xml2js.Builder opts
114 | obj = {"first":{"MsgId":["5850440872586764820"],"foo":"bar"}}
115 | actual = builder.buildObject obj
116 | diffeq expected, actual
117 | test.finish()
118 |
119 | 'test parser -> builder roundtrip': (test) ->
120 | fileName = path.join __dirname, '/fixtures/build_sample.xml'
121 | fs.readFile fileName, (err, xmlData) ->
122 | xmlExpected = xmlData.toString()
123 | xml2js.parseString xmlData, {'trim': true}, (err, obj) ->
124 | equ err, null
125 | builder = new xml2js.Builder({})
126 | xmlActual = builder.buildObject obj
127 | diffeq xmlExpected, xmlActual
128 | test.finish()
129 |
--------------------------------------------------------------------------------
/src/xml2js.coffee:
--------------------------------------------------------------------------------
1 | sax = require 'sax'
2 | events = require 'events'
3 | builder = require 'xmlbuilder'
4 | bom = require './bom'
5 | processors = require './processors'
6 |
7 | # Underscore has a nice function for this, but we try to go without dependencies
8 | isEmpty = (thing) ->
9 | return typeof thing is "object" && thing? && Object.keys(thing).length is 0
10 |
11 | processName = (processors, processedName) ->
12 | processedName = process(processedName) for process in processors
13 | return processedName
14 |
15 | exports.processors = processors
16 |
17 | exports.defaults =
18 | "0.1":
19 | explicitCharkey: false
20 | trim: true
21 | # normalize implicates trimming, just so you know
22 | normalize: true
23 | # normalize tag names to lower case
24 | normalizeTags: false
25 | # set default attribute object key
26 | attrkey: "@"
27 | # set default char object key
28 | charkey: "#"
29 | # always put child nodes in an array
30 | explicitArray: false
31 | # ignore all attributes regardless
32 | ignoreAttrs: false
33 | # merge attributes and child elements onto parent object. this may
34 | # cause collisions.
35 | mergeAttrs: false
36 | explicitRoot: false
37 | validator: null
38 | xmlns : false
39 | # fold children elements into dedicated property (works only in 0.2)
40 | explicitChildren: false
41 | childkey: '@@'
42 | charsAsChildren: false
43 | # callbacks are async? not in 0.1 mode
44 | async: false
45 | strict: true
46 | attrNameProcessors: null
47 | tagNameProcessors: null
48 |
49 | "0.2":
50 | explicitCharkey: false
51 | trim: false
52 | normalize: false
53 | normalizeTags: false
54 | attrkey: "$"
55 | charkey: "_"
56 | explicitArray: true
57 | ignoreAttrs: false
58 | mergeAttrs: false
59 | explicitRoot: true
60 | validator: null
61 | xmlns : false
62 | explicitChildren: false
63 | childkey: '$$'
64 | charsAsChildren: false
65 | # not async in 0.2 mode either
66 | async: false
67 | strict: true
68 | attrNameProcessors: null
69 | tagNameProcessors: null
70 | # xml building options
71 | rootName: 'root'
72 | xmldec: {'version': '1.0', 'encoding': 'UTF-8', 'standalone': true}
73 | doctype: null
74 | renderOpts: { 'pretty': true, 'indent': ' ', 'newline': '\n' }
75 | headless: false
76 |
77 | class exports.ValidationError extends Error
78 | constructor: (message) ->
79 | @message = message
80 |
81 | class exports.Builder
82 | constructor: (opts) ->
83 | # copy this versions default options
84 | @options = {}
85 | @options[key] = value for own key, value of exports.defaults["0.2"]
86 | # overwrite them with the specified options, if any
87 | @options[key] = value for own key, value of opts
88 |
89 | buildObject: (rootObj) ->
90 | attrkey = @options.attrkey
91 | charkey = @options.charkey
92 |
93 | # If there is a sane-looking first element to use as the root,
94 | # and the user hasn't specified a non-default rootName,
95 | if ( Object.keys(rootObj).length is 1 ) and ( @options.rootName == exports.defaults['0.2'].rootName )
96 | # we'll take the first element as the root element
97 | rootName = Object.keys(rootObj)[0]
98 | rootObj = rootObj[rootName]
99 | else
100 | # otherwise we'll use whatever they've set, or the default
101 | rootName = @options.rootName
102 |
103 | render = (element, obj) ->
104 | if typeof obj isnt 'object'
105 | # single element, just append it as text
106 | element.txt obj
107 | else
108 | for own key, child of obj
109 | # Case #1 Attribute
110 | if key is attrkey
111 | if typeof child is "object"
112 | # Inserts tag attributes
113 | for attr, value of child
114 | element = element.att(attr, value)
115 |
116 | # Case #2 Char data (CDATA, etc.)
117 | else if key is charkey
118 | element = element.txt(child)
119 |
120 | # Case #3 Array data
121 | else if typeof child is 'object' and child instanceof Array
122 | for own index, entry of child
123 | if typeof entry is 'string'
124 | element = element.ele(key, entry).up()
125 | else
126 | element = arguments.callee(element.ele(key), entry).up()
127 |
128 | # Case #4 Objects
129 | else if typeof child is "object"
130 | element = arguments.callee(element.ele(key), child).up()
131 |
132 | # Case #5 String and remaining types
133 | else
134 | element = element.ele(key, child.toString()).up()
135 |
136 | element
137 |
138 | rootElement = builder.create(rootName, @options.xmldec, @options.doctype,
139 | headless: @options.headless)
140 |
141 | render(rootElement, rootObj).end(@options.renderOpts)
142 |
143 | class exports.Parser extends events.EventEmitter
144 | constructor: (opts) ->
145 | # if this was called without 'new', create an instance with new and return
146 | return new exports.Parser opts unless @ instanceof exports.Parser
147 | # copy this versions default options
148 | @options = {}
149 | @options[key] = value for own key, value of exports.defaults["0.2"]
150 | # overwrite them with the specified options, if any
151 | @options[key] = value for own key, value of opts
152 | # define the key used for namespaces
153 | if @options.xmlns
154 | @options.xmlnskey = @options.attrkey + "ns"
155 | if @options.normalizeTags
156 | if ! @options.tagNameProcessors
157 | @options.tagNameProcessors = []
158 | @options.tagNameProcessors.unshift processors.normalize
159 |
160 | @reset()
161 |
162 | assignOrPush: (obj, key, newValue) =>
163 | if key not of obj
164 | if not @options.explicitArray
165 | obj[key] = newValue
166 | else
167 | obj[key] = [newValue]
168 | else
169 | obj[key] = [obj[key]] if not (obj[key] instanceof Array)
170 | obj[key].push newValue
171 |
172 | reset: =>
173 | # remove all previous listeners for events, to prevent event listener
174 | # accumulation
175 | @removeAllListeners()
176 | # make the SAX parser. tried trim and normalize, but they are not
177 | # very helpful
178 | @saxParser = sax.parser @options.strict, {
179 | trim: false,
180 | normalize: false,
181 | xmlns: @options.xmlns
182 | }
183 |
184 | # emit one error event if the sax parser fails. this is mostly a hack, but
185 | # the sax parser isn't state of the art either.
186 | @saxParser.errThrown = false
187 | @saxParser.onerror = (error) =>
188 | @saxParser.resume()
189 | if ! @saxParser.errThrown
190 | @saxParser.errThrown = true
191 | @emit "error", error
192 |
193 | # another hack to avoid throwing exceptions when the parsing has ended
194 | # but the user-supplied callback throws an error
195 | @saxParser.ended = false
196 |
197 | # always use the '#' key, even if there are no subkeys
198 | # setting this property by and is deprecated, yet still supported.
199 | # better pass it as explicitCharkey option to the constructor
200 | @EXPLICIT_CHARKEY = @options.explicitCharkey
201 | @resultObject = null
202 | stack = []
203 | # aliases, so we don't have to type so much
204 | attrkey = @options.attrkey
205 | charkey = @options.charkey
206 |
207 | @saxParser.onopentag = (node) =>
208 | obj = {}
209 | obj[charkey] = ""
210 | unless @options.ignoreAttrs
211 | for own key of node.attributes
212 | if attrkey not of obj and not @options.mergeAttrs
213 | obj[attrkey] = {}
214 | newValue = node.attributes[key]
215 | processedKey = if @options.attrNameProcessors then processName(@options.attrNameProcessors, key) else key
216 | if @options.mergeAttrs
217 | @assignOrPush obj, processedKey, newValue
218 | else
219 | obj[attrkey][processedKey] = newValue
220 |
221 | # need a place to store the node name
222 | obj["#name"] = if @options.tagNameProcessors then processName(@options.tagNameProcessors, node.name) else node.name
223 | if (@options.xmlns)
224 | obj[@options.xmlnskey] = {uri: node.uri, local: node.local}
225 | stack.push obj
226 |
227 | @saxParser.onclosetag = =>
228 | obj = stack.pop()
229 | nodeName = obj["#name"]
230 | delete obj["#name"]
231 |
232 | cdata = obj.cdata
233 | delete obj.cdata
234 |
235 | s = stack[stack.length - 1]
236 | # remove the '#' key altogether if it's blank
237 | if obj[charkey].match(/^\s*$/) and not cdata
238 | emptyStr = obj[charkey]
239 | delete obj[charkey]
240 | else
241 | obj[charkey] = obj[charkey].trim() if @options.trim
242 | obj[charkey] = obj[charkey].replace(/\s{2,}/g, " ").trim() if @options.normalize
243 | # also do away with '#' key altogether, if there's no subkeys
244 | # unless EXPLICIT_CHARKEY is set
245 | if Object.keys(obj).length == 1 and charkey of obj and not @EXPLICIT_CHARKEY
246 | obj = obj[charkey]
247 |
248 | if (isEmpty obj)
249 | obj = if @options.emptyTag != undefined
250 | @options.emptyTag
251 | else
252 | emptyStr
253 |
254 | if @options.validator?
255 | xpath = "/" + (node["#name"] for node in stack).concat(nodeName).join("/")
256 | try
257 | obj = @options.validator(xpath, s and s[nodeName], obj)
258 | catch err
259 | @emit "error", err
260 |
261 | # put children into property and unfold chars if necessary
262 | if @options.explicitChildren and not @options.mergeAttrs and typeof obj is 'object'
263 | node = {}
264 | # separate attributes
265 | if @options.attrkey of obj
266 | node[@options.attrkey] = obj[@options.attrkey]
267 | delete obj[@options.attrkey]
268 | # separate char data
269 | if not @options.charsAsChildren and @options.charkey of obj
270 | node[@options.charkey] = obj[@options.charkey]
271 | delete obj[@options.charkey]
272 |
273 | if Object.getOwnPropertyNames(obj).length > 0
274 | node[@options.childkey] = obj
275 |
276 | obj = node
277 |
278 | # check whether we closed all the open tags
279 | if stack.length > 0
280 | @assignOrPush s, nodeName, obj
281 | else
282 | # if explicitRoot was specified, wrap stuff in the root tag name
283 | if @options.explicitRoot
284 | # avoid circular references
285 | old = obj
286 | obj = {}
287 | obj[nodeName] = old
288 |
289 | @resultObject = obj
290 | # parsing has ended, mark that so we won't throw exceptions from
291 | # here anymore
292 | @saxParser.ended = true
293 | @emit "end", @resultObject
294 |
295 | ontext = (text) =>
296 | s = stack[stack.length - 1]
297 | if s
298 | s[charkey] += text
299 | s
300 |
301 | @saxParser.ontext = ontext
302 | @saxParser.oncdata = (text) =>
303 | s = ontext text
304 | if s
305 | s.cdata = true
306 |
307 | parseString: (str, cb) =>
308 | if cb? and typeof cb is "function"
309 | @on "end", (result) ->
310 | @reset()
311 | if @options.async
312 | process.nextTick ->
313 | cb null, result
314 | else
315 | cb null, result
316 | @on "error", (err) ->
317 | @reset()
318 | if @options.async
319 | process.nextTick ->
320 | cb err
321 | else
322 | cb err
323 |
324 | if str.toString().trim() is ''
325 | @emit "end", null
326 | return true
327 |
328 | try
329 | @saxParser.write(bom.stripBOM str.toString()).close()
330 | catch err
331 | unless @saxParser.errThrown or @saxParser.ended
332 | @emit 'error', err
333 | @saxParser.errThrown = true
334 |
335 | exports.parseString = (str, a, b) ->
336 | # let's determine what we got as arguments
337 | if b?
338 | if typeof b == 'function'
339 | cb = b
340 | if typeof a == 'object'
341 | options = a
342 | else
343 | # well, b is not set, so a has to be a callback
344 | if typeof a == 'function'
345 | cb = a
346 | # and options should be empty - default
347 | options = {}
348 |
349 | # the rest is super-easy
350 | parser = new exports.Parser options
351 | parser.parseString str, cb
352 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | node-xml2js
2 | ===========
3 |
4 | Ever had the urge to parse XML? And wanted to access the data in some sane,
5 | easy way? Don't want to compile a C parser, for whatever reason? Then xml2js is
6 | what you're looking for!
7 |
8 | Description
9 | ===========
10 |
11 | Simple XML to JavaScript object converter. It supports bi-directional conversion.
12 | Uses [sax-js](https://github.com/isaacs/sax-js/) and
13 | [xmlbuilder-js](https://github.com/oozcitak/xmlbuilder-js/).
14 |
15 | Note: If you're looking for a full DOM parser, you probably want
16 | [JSDom](https://github.com/tmpvar/jsdom).
17 |
18 | Installation
19 | ============
20 |
21 | Simplest way to install `xml2js` is to use [npm](http://npmjs.org), just `npm
22 | install xml2js` which will download xml2js and all dependencies.
23 |
24 | Usage
25 | =====
26 |
27 | No extensive tutorials required because you are a smart developer! The task of
28 | parsing XML should be an easy one, so let's make it so! Here's some examples.
29 |
30 | Shoot-and-forget usage
31 | ----------------------
32 |
33 | You want to parse XML as simple and easy as possible? It's dangerous to go
34 | alone, take this:
35 |
36 | ```javascript
37 | var parseString = require('xml2js').parseString;
38 | var xml = "Hello xml2js!"
39 | parseString(xml, function (err, result) {
40 | console.dir(result);
41 | });
42 | ```
43 |
44 | Can't get easier than this, right? This works starting with `xml2js` 0.2.3.
45 | With CoffeeScript it looks like this:
46 |
47 | ```coffeescript
48 | {parseString} = require 'xml2js'
49 | xml = "Hello xml2js!"
50 | parseString xml, (err, result) ->
51 | console.dir result
52 | ```
53 |
54 | If you need some special options, fear not, `xml2js` supports a number of
55 | options (see below), you can specify these as second argument:
56 |
57 | ```javascript
58 | parseString(xml, {trim: true}, function (err, result) {
59 | });
60 | ```
61 |
62 | Simple as pie usage
63 | -------------------
64 |
65 | That's right, if you have been using xml-simple or a home-grown
66 | wrapper, this was added in 0.1.11 just for you:
67 |
68 | ```javascript
69 | var fs = require('fs'),
70 | xml2js = require('xml2js');
71 |
72 | var parser = new xml2js.Parser();
73 | fs.readFile(__dirname + '/foo.xml', function(err, data) {
74 | parser.parseString(data, function (err, result) {
75 | console.dir(result);
76 | console.log('Done');
77 | });
78 | });
79 | ```
80 |
81 | Look ma, no event listeners!
82 |
83 | You can also use `xml2js` from
84 | [CoffeeScript](http://jashkenas.github.com/coffee-script/), further reducing
85 | the clutter:
86 |
87 | ```coffeescript
88 | fs = require 'fs',
89 | xml2js = require 'xml2js'
90 |
91 | parser = new xml2js.Parser()
92 | fs.readFile __dirname + '/foo.xml', (err, data) ->
93 | parser.parseString data, (err, result) ->
94 | console.dir result
95 | console.log 'Done.'
96 | ```
97 |
98 | But what happens if you forget the `new` keyword to create a new `Parser`? In
99 | the middle of a nightly coding session, it might get lost, after all. Worry
100 | not, we got you covered! Starting with 0.2.8 you can also leave it out, in
101 | which case `xml2js` will helpfully add it for you, no bad surprises and
102 | inexplicable bugs!
103 |
104 | "Traditional" usage
105 | -------------------
106 |
107 | Alternatively you can still use the traditional `addListener` variant that was
108 | supported since forever:
109 |
110 | ```javascript
111 | var fs = require('fs'),
112 | xml2js = require('xml2js');
113 |
114 | var parser = new xml2js.Parser();
115 | parser.addListener('end', function(result) {
116 | console.dir(result);
117 | console.log('Done.');
118 | });
119 | fs.readFile(__dirname + '/foo.xml', function(err, data) {
120 | parser.parseString(data);
121 | });
122 | ```
123 |
124 | If you want to parse multiple files, you have multiple possibilites:
125 |
126 | * You can create one `xml2js.Parser` per file. That's the recommended one
127 | and is promised to always *just work*.
128 | * You can call `reset()` on your parser object.
129 | * You can hope everything goes well anyway. This behaviour is not
130 | guaranteed work always, if ever. Use option #1 if possible. Thanks!
131 |
132 | So you wanna some JSON?
133 | -----------------------
134 |
135 | Just wrap the `result` object in a call to `JSON.stringify` like this
136 | `JSON.stringify(result)`. You get a string containing the JSON representation
137 | of the parsed object that you can feed to JSON-hungry consumers.
138 |
139 | Displaying results
140 | ------------------
141 |
142 | You might wonder why, using `console.dir` or `console.log` the output at some
143 | level is only `[Object]`. Don't worry, this is not because xml2js got lazy.
144 | That's because Node uses `util.inspect` to convert the object into strings and
145 | that function stops after `depth=2` which is a bit low for most XML.
146 |
147 | To display the whole deal, you can use `console.log(util.inspect(result, false,
148 | null))`, which displays the whole result.
149 |
150 | So much for that, but what if you use
151 | [eyes](https://github.com/cloudhead/eyes.js) for nice colored output and it
152 | truncates the output with `…`? Don't fear, there's also a solution for that,
153 | you just need to increase the `maxLength` limit by creating a custom inspector
154 | `var inspect = require('eyes').inspector({maxLength: false})` and then you can
155 | easily `inspect(result)`.
156 |
157 | XML builder usage
158 | -----------------
159 |
160 | Since 0.4.0, objects can be also be used to build XML:
161 |
162 | ```javascript
163 | var fs = require('fs'),
164 | xml2js = require('xml2js');
165 |
166 | var obj = {name: "Super", Surname: "Man", age: 23};
167 |
168 | var builder = new xml2js.Builder();
169 | var xml = builder.buildObject(obj);
170 | ```
171 |
172 | At the moment, a one to one bi-directional conversion is guaranteed only for
173 | default configuration, except for `attrkey`, `charkey` and `explicitArray` options
174 | you can redefine to your taste. Writing CDATA is not currently supported.
175 |
176 | Processing attribute and tag names
177 | ----------------------------------
178 |
179 | Since 0.4.1 you can optionally provide the parser with attribute and tag name processors:
180 |
181 | ```javascript
182 |
183 | function nameToUpperCase(name){
184 | return name.toUpperCase();
185 | }
186 |
187 | //transform all attribute and tag names to uppercase
188 | parseString(xml, {tagNameProcessors: [nameToUpperCase], attrNameProcessors: [nameToUpperCase]}, function (err, result) {
189 | });
190 | ```
191 |
192 | The `tagNameProcessors` and `attrNameProcessors` options both accept an `Array` of functions with the following signature:
193 | ```javascript
194 | function (name){
195 | //do something with `name`
196 | return name
197 | }
198 | ```
199 |
200 | Some processors are provided out-of-the-box and can be found in `lib/processors.js`:
201 |
202 | - `normalize`: transforms the name to lowercase.
203 | (Automatically used when `options.normalize` is set to `true`)
204 |
205 | - `firstCharLowerCase`: transforms the first character to lower case.
206 | E.g. 'MyTagName' becomes 'myTagName'
207 |
208 | - `stripPrefix`: strips the xml namespace prefix. E.g `` will become 'Bar'.
209 | (N.B.: the `xmlns` prefix is NOT stripped.)
210 |
211 | Options
212 | =======
213 |
214 | Apart from the default settings, there are a number of options that can be
215 | specified for the parser. Options are specified by ``new Parser({optionName:
216 | value})``. Possible options are:
217 |
218 | * `attrkey` (default: `$`): Prefix that is used to access the attributes.
219 | Version 0.1 default was `@`.
220 | * `charkey` (default: `_`): Prefix that is used to access the character
221 | content. Version 0.1 default was `#`.
222 | * `explicitCharkey` (default: `false`)
223 | * `trim` (default: `false`): Trim the whitespace at the beginning and end of
224 | text nodes.
225 | * `normalizeTags` (default: `false`): Normalize all tag names to lowercase.
226 | * `normalize` (default: `false`): Trim whitespaces inside text nodes.
227 | * `explicitRoot` (default: `true`): Set this if you want to get the root
228 | node in the resulting object.
229 | * `emptyTag` (default: `undefined`): what will the value of empty nodes be.
230 | Default is `{}`.
231 | * `explicitArray` (default: `true`): Always put child nodes in an array if
232 | true; otherwise an array is created only if there is more than one.
233 | * `ignoreAttrs` (default: `false`): Ignore all XML attributes and only create
234 | text nodes.
235 | * `mergeAttrs` (default: `false`): Merge attributes and child elements as
236 | properties of the parent, instead of keying attributes off a child
237 | attribute object. This option is ignored if `ignoreAttrs` is `false`.
238 | * `validator` (default `null`): You can specify a callable that validates
239 | the resulting structure somehow, however you want. See unit tests
240 | for an example.
241 | * `xmlns` (default `false`): Give each element a field usually called '$ns'
242 | (the first character is the same as attrkey) that contains its local name
243 | and namespace URI.
244 | * `explicitChildren` (default `false`): Put child elements to separate
245 | property. Doesn't work with `mergeAttrs = true`. If element has no children
246 | then "children" won't be created. Added in 0.2.5.
247 | * `childkey` (default `$$`): Prefix that is used to access child elements if
248 | `explicitChildren` is set to `true`. Added in 0.2.5.
249 | * `charsAsChildren` (default `false`): Determines whether chars should be
250 | considered children if `explicitChildren` is on. Added in 0.2.5.
251 | * `async` (default `false`): Should the callbacks be async? This *might* be
252 | an incompatible change if your code depends on sync execution of callbacks.
253 | xml2js 0.3 might change this default, so the recommendation is to not
254 | depend on sync execution anyway. Added in 0.2.6.
255 | * `strict` (default `true`): Set sax-js to strict or non-strict parsing mode.
256 | Defaults to `true` which is *highly* recommended, since parsing HTML which
257 | is not well-formed XML might yield just about anything. Added in 0.2.7.
258 | * `attrNameProcessors` (default: `null`): Allows the addition of attribute name processing functions.
259 | Accepts an `Array` of functions with following signature:
260 | ```javascript
261 | function (name){
262 | //do something with `name`
263 | return name
264 | }
265 | ```
266 | Added in 0.4.1
267 | * `tagNameProcessors` (default: `null`):Allows the addition of tag name processing functions.
268 | Accepts an `Array` of functions with following signature:
269 | ```javascript
270 | function (name){
271 | //do something with `name`
272 | return name
273 | }
274 | ```
275 | Added in 0.4.1
276 |
277 | Options for the `Builder` class
278 | -------------------------------
279 |
280 | * `rootName` (default `root`): root element name to be used in case
281 | `explicitRoot` is `false` or to override the root element name.
282 | * `renderOpts` (default `{ 'pretty': true, 'indent': ' ', 'newline': '\n' }`):
283 | Rendering options for xmlbuilder-js.
284 | * pretty: prettify generated XML
285 | * indent: whitespace for indentation (only when pretty)
286 | * newline: newline char (only when pretty)
287 | * `xmldec` (default `{ 'version': '1.0', 'encoding': 'UTF-8', 'standalone': true }`:
288 | XML declaration attributes.
289 | * `xmldec.version` A version number string, e.g. 1.0
290 | * `xmldec.encoding` Encoding declaration, e.g. UTF-8
291 | * `xmldec.standalone` standalone document declaration: true or false
292 | * `doctype` (default `null`): optional DTD. Eg. `{'ext': 'hello.dtd'}`
293 | * `headless` (default: `false`): omit the XML header. Added in 0.4.3.
294 |
295 | `renderOpts`, `xmldec`,`doctype` and `headless` pass through to
296 | [xmlbuilder-js](https://github.com/oozcitak/xmlbuilder-js).
297 |
298 | Updating to new version
299 | =======================
300 |
301 | Version 0.2 changed the default parsing settings, but version 0.1.14 introduced
302 | the default settings for version 0.2, so these settings can be tried before the
303 | migration.
304 |
305 | ```javascript
306 | var xml2js = require('xml2js');
307 | var parser = new xml2js.Parser(xml2js.defaults["0.2"]);
308 | ```
309 |
310 | To get the 0.1 defaults in version 0.2 you can just use
311 | `xml2js.defaults["0.1"]` in the same place. This provides you with enough time
312 | to migrate to the saner way of parsing in xml2js 0.2. We try to make the
313 | migration as simple and gentle as possible, but some breakage cannot be
314 | avoided.
315 |
316 | So, what exactly did change and why? In 0.2 we changed some defaults to parse
317 | the XML in a more universal and sane way. So we disabled `normalize` and `trim`
318 | so xml2js does not cut out any text content. You can reenable this at will of
319 | course. A more important change is that we return the root tag in the resulting
320 | JavaScript structure via the `explicitRoot` setting, so you need to access the
321 | first element. This is useful for anybody who wants to know what the root node
322 | is and preserves more information. The last major change was to enable
323 | `explicitArray`, so everytime it is possible that one might embed more than one
324 | sub-tag into a tag, xml2js >= 0.2 returns an array even if the array just
325 | includes one element. This is useful when dealing with APIs that return
326 | variable amounts of subtags.
327 |
328 | Running tests, development
329 | ==========================
330 |
331 | [](https://travis-ci.org/Leonidas-from-XIV/node-xml2js)
332 | [](https://david-dm.org/Leonidas-from-XIV/node-xml2js)
333 |
334 | The development requirements are handled by npm, you just need to install them.
335 | We also have a number of unit tests, they can be run using `npm test` directly
336 | from the project root. This runs zap to discover all the tests and execute
337 | them.
338 |
339 | If you like to contribute, keep in mind that xml2js is written in CoffeeScript,
340 | so don't develop on the JavaScript files that are checked into the repository
341 | for convenience reasons. Also, please write some unit test to check your
342 | behaviour and if it is some user-facing thing, add some documentation to this
343 | README, so people will know it exists. Thanks in advance!
344 |
--------------------------------------------------------------------------------
/lib/xml2js.js:
--------------------------------------------------------------------------------
1 | // Generated by CoffeeScript 1.7.1
2 | (function() {
3 | var bom, builder, events, isEmpty, processName, processors, sax,
4 | __hasProp = {}.hasOwnProperty,
5 | __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; },
6 | __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
7 |
8 | sax = require('sax');
9 |
10 | events = require('events');
11 |
12 | builder = require('xmlbuilder');
13 |
14 | bom = require('./bom');
15 |
16 | processors = require('./processors');
17 |
18 | isEmpty = function(thing) {
19 | return typeof thing === "object" && (thing != null) && Object.keys(thing).length === 0;
20 | };
21 |
22 | processName = function(processors, processedName) {
23 | var process, _i, _len;
24 | for (_i = 0, _len = processors.length; _i < _len; _i++) {
25 | process = processors[_i];
26 | processedName = process(processedName);
27 | }
28 | return processedName;
29 | };
30 |
31 | exports.processors = processors;
32 |
33 | exports.defaults = {
34 | "0.1": {
35 | explicitCharkey: false,
36 | trim: true,
37 | normalize: true,
38 | normalizeTags: false,
39 | attrkey: "@",
40 | charkey: "#",
41 | explicitArray: false,
42 | ignoreAttrs: false,
43 | mergeAttrs: false,
44 | explicitRoot: false,
45 | validator: null,
46 | xmlns: false,
47 | explicitChildren: false,
48 | childkey: '@@',
49 | charsAsChildren: false,
50 | async: false,
51 | strict: true,
52 | attrNameProcessors: null,
53 | tagNameProcessors: null
54 | },
55 | "0.2": {
56 | explicitCharkey: false,
57 | trim: false,
58 | normalize: false,
59 | normalizeTags: false,
60 | attrkey: "&",
61 | charkey: "_",
62 | explicitArray: true,
63 | ignoreAttrs: false,
64 | mergeAttrs: false,
65 | explicitRoot: true,
66 | validator: null,
67 | xmlns: false,
68 | explicitChildren: false,
69 | childkey: '&&',
70 | charsAsChildren: false,
71 | async: false,
72 | strict: true,
73 | attrNameProcessors: null,
74 | tagNameProcessors: null,
75 | rootName: 'root',
76 | xmldec: {
77 | 'version': '1.0',
78 | 'encoding': 'UTF-8',
79 | 'standalone': true
80 | },
81 | doctype: null,
82 | renderOpts: {
83 | 'pretty': true,
84 | 'indent': ' ',
85 | 'newline': '\n'
86 | },
87 | headless: false
88 | }
89 | };
90 |
91 | exports.ValidationError = (function(_super) {
92 | __extends(ValidationError, _super);
93 |
94 | function ValidationError(message) {
95 | this.message = message;
96 | }
97 |
98 | return ValidationError;
99 |
100 | })(Error);
101 |
102 | exports.Builder = (function() {
103 | function Builder(opts) {
104 | var key, value, _ref;
105 | this.options = {};
106 | _ref = exports.defaults["0.2"];
107 | for (key in _ref) {
108 | if (!__hasProp.call(_ref, key)) continue;
109 | value = _ref[key];
110 | this.options[key] = value;
111 | }
112 | for (key in opts) {
113 | if (!__hasProp.call(opts, key)) continue;
114 | value = opts[key];
115 | this.options[key] = value;
116 | }
117 | }
118 |
119 | Builder.prototype.buildObject = function(rootObj) {
120 | var attrkey, charkey, render, rootElement, rootName;
121 | attrkey = this.options.attrkey;
122 | charkey = this.options.charkey;
123 | if ((Object.keys(rootObj).length === 1) && (this.options.rootName === exports.defaults['0.2'].rootName)) {
124 | rootName = Object.keys(rootObj)[0];
125 | rootObj = rootObj[rootName];
126 | } else {
127 | rootName = this.options.rootName;
128 | }
129 | render = function(element, obj) {
130 | var attr, child, entry, index, key, value;
131 | if (typeof obj !== 'object') {
132 | element.txt(obj);
133 | } else {
134 | for (key in obj) {
135 | if (!__hasProp.call(obj, key)) continue;
136 | child = obj[key];
137 | if (key === attrkey) {
138 | if (typeof child === "object") {
139 | for (attr in child) {
140 | value = child[attr];
141 | element = element.att(attr, value);
142 | }
143 | }
144 | } else if (key === charkey) {
145 | element = element.txt(child);
146 | } else if (typeof child === 'object' && child instanceof Array) {
147 | for (index in child) {
148 | if (!__hasProp.call(child, index)) continue;
149 | entry = child[index];
150 | if (typeof entry === 'string') {
151 | element = element.ele(key, entry).up();
152 | } else {
153 | element = arguments.callee(element.ele(key), entry).up();
154 | }
155 | }
156 | } else if (typeof child === "object") {
157 | element = arguments.callee(element.ele(key), child).up();
158 | } else {
159 | element = element.ele(key, child.toString()).up();
160 | }
161 | }
162 | }
163 | return element;
164 | };
165 | rootElement = builder.create(rootName, this.options.xmldec, this.options.doctype, {
166 | headless: this.options.headless
167 | });
168 | return render(rootElement, rootObj).end(this.options.renderOpts);
169 | };
170 |
171 | return Builder;
172 |
173 | })();
174 |
175 | exports.Parser = (function(_super) {
176 | __extends(Parser, _super);
177 |
178 | function Parser(opts) {
179 | this.parseString = __bind(this.parseString, this);
180 | this.reset = __bind(this.reset, this);
181 | this.assignOrPush = __bind(this.assignOrPush, this);
182 | var key, value, _ref;
183 | if (!(this instanceof exports.Parser)) {
184 | return new exports.Parser(opts);
185 | }
186 | this.options = {};
187 | _ref = exports.defaults["0.2"];
188 | for (key in _ref) {
189 | if (!__hasProp.call(_ref, key)) continue;
190 | value = _ref[key];
191 | this.options[key] = value;
192 | }
193 | for (key in opts) {
194 | if (!__hasProp.call(opts, key)) continue;
195 | value = opts[key];
196 | this.options[key] = value;
197 | }
198 | if (this.options.xmlns) {
199 | this.options.xmlnskey = this.options.attrkey + "ns";
200 | }
201 | if (this.options.normalizeTags) {
202 | if (!this.options.tagNameProcessors) {
203 | this.options.tagNameProcessors = [];
204 | }
205 | this.options.tagNameProcessors.unshift(processors.normalize);
206 | }
207 | this.reset();
208 | }
209 |
210 | Parser.prototype.assignOrPush = function(obj, key, newValue) {
211 | if (!(key in obj)) {
212 | if (!this.options.explicitArray) {
213 | return obj[key] = newValue;
214 | } else {
215 | return obj[key] = [newValue];
216 | }
217 | } else {
218 | if (!(obj[key] instanceof Array)) {
219 | obj[key] = [obj[key]];
220 | }
221 | return obj[key].push(newValue);
222 | }
223 | };
224 |
225 | Parser.prototype.reset = function() {
226 | var attrkey, charkey, ontext, stack;
227 | this.removeAllListeners();
228 | this.saxParser = sax.parser(this.options.strict, {
229 | trim: false,
230 | normalize: false,
231 | xmlns: this.options.xmlns
232 | });
233 | this.saxParser.errThrown = false;
234 | this.saxParser.onerror = (function(_this) {
235 | return function(error) {
236 | _this.saxParser.resume();
237 | if (!_this.saxParser.errThrown) {
238 | _this.saxParser.errThrown = true;
239 | return _this.emit("error", error);
240 | }
241 | };
242 | })(this);
243 | this.saxParser.ended = false;
244 | this.EXPLICIT_CHARKEY = this.options.explicitCharkey;
245 | this.resultObject = null;
246 | stack = [];
247 | attrkey = this.options.attrkey;
248 | charkey = this.options.charkey;
249 | this.saxParser.onopentag = (function(_this) {
250 | return function(node) {
251 | var key, newValue, obj, processedKey, _ref;
252 | obj = {};
253 | obj[charkey] = "";
254 | if (!_this.options.ignoreAttrs) {
255 | _ref = node.attributes;
256 | for (key in _ref) {
257 | if (!__hasProp.call(_ref, key)) continue;
258 | if (!(attrkey in obj) && !_this.options.mergeAttrs) {
259 | obj[attrkey] = {};
260 | }
261 | newValue = node.attributes[key];
262 | processedKey = _this.options.attrNameProcessors ? processName(_this.options.attrNameProcessors, key) : key;
263 | if (_this.options.mergeAttrs) {
264 | _this.assignOrPush(obj, processedKey, newValue);
265 | } else {
266 | obj[attrkey][processedKey] = newValue;
267 | }
268 | }
269 | }
270 | obj["#name"] = _this.options.tagNameProcessors ? processName(_this.options.tagNameProcessors, node.name) : node.name;
271 | if (_this.options.xmlns) {
272 | obj[_this.options.xmlnskey] = {
273 | uri: node.uri,
274 | local: node.local
275 | };
276 | }
277 | return stack.push(obj);
278 | };
279 | })(this);
280 | this.saxParser.onclosetag = (function(_this) {
281 | return function() {
282 | var cdata, emptyStr, err, node, nodeName, obj, old, s, xpath;
283 | obj = stack.pop();
284 | nodeName = obj["#name"];
285 | delete obj["#name"];
286 | cdata = obj.cdata;
287 | delete obj.cdata;
288 | s = stack[stack.length - 1];
289 | if (obj[charkey].match(/^\s*$/) && !cdata) {
290 | emptyStr = obj[charkey];
291 | delete obj[charkey];
292 | } else {
293 | if (_this.options.trim) {
294 | obj[charkey] = obj[charkey].trim();
295 | }
296 | if (_this.options.normalize) {
297 | obj[charkey] = obj[charkey].replace(/\s{2,}/g, " ").trim();
298 | }
299 | if (Object.keys(obj).length === 1 && charkey in obj && !_this.EXPLICIT_CHARKEY) {
300 | obj = obj[charkey];
301 | }
302 | }
303 | if (isEmpty(obj)) {
304 | obj = _this.options.emptyTag !== void 0 ? _this.options.emptyTag : emptyStr;
305 | }
306 | if (_this.options.validator != null) {
307 | xpath = "/" + ((function() {
308 | var _i, _len, _results;
309 | _results = [];
310 | for (_i = 0, _len = stack.length; _i < _len; _i++) {
311 | node = stack[_i];
312 | _results.push(node["#name"]);
313 | }
314 | return _results;
315 | })()).concat(nodeName).join("/");
316 | try {
317 | obj = _this.options.validator(xpath, s && s[nodeName], obj);
318 | } catch (_error) {
319 | err = _error;
320 | _this.emit("error", err);
321 | }
322 | }
323 | if (_this.options.explicitChildren && !_this.options.mergeAttrs && typeof obj === 'object') {
324 | node = {};
325 | if (_this.options.attrkey in obj) {
326 | node[_this.options.attrkey] = obj[_this.options.attrkey];
327 | delete obj[_this.options.attrkey];
328 | }
329 | if (!_this.options.charsAsChildren && _this.options.charkey in obj) {
330 | node[_this.options.charkey] = obj[_this.options.charkey];
331 | delete obj[_this.options.charkey];
332 | }
333 | if (Object.getOwnPropertyNames(obj).length > 0) {
334 | node[_this.options.childkey] = obj;
335 | }
336 | obj = node;
337 | }
338 | if (stack.length > 0) {
339 | return _this.assignOrPush(s, nodeName, obj);
340 | } else {
341 | if (_this.options.explicitRoot) {
342 | old = obj;
343 | obj = {};
344 | obj[nodeName] = old;
345 | }
346 | _this.resultObject = obj;
347 | _this.saxParser.ended = true;
348 | return _this.emit("end", _this.resultObject);
349 | }
350 | };
351 | })(this);
352 | ontext = (function(_this) {
353 | return function(text) {
354 | var s;
355 | s = stack[stack.length - 1];
356 | if (s) {
357 | s[charkey] += text;
358 | return s;
359 | }
360 | };
361 | })(this);
362 | this.saxParser.ontext = ontext;
363 | return this.saxParser.oncdata = (function(_this) {
364 | return function(text) {
365 | var s;
366 | s = ontext(text);
367 | if (s) {
368 | return s.cdata = true;
369 | }
370 | };
371 | })(this);
372 | };
373 |
374 | Parser.prototype.parseString = function(str, cb) {
375 | var err;
376 | if ((cb != null) && typeof cb === "function") {
377 | this.on("end", function(result) {
378 | this.reset();
379 | if (this.options.async) {
380 | return process.nextTick(function() {
381 | return cb(null, result);
382 | });
383 | } else {
384 | return cb(null, result);
385 | }
386 | });
387 | this.on("error", function(err) {
388 | this.reset();
389 | if (this.options.async) {
390 | return process.nextTick(function() {
391 | return cb(err);
392 | });
393 | } else {
394 | return cb(err);
395 | }
396 | });
397 | }
398 | if (str.toString().trim() === '') {
399 | this.emit("end", null);
400 | return true;
401 | }
402 | try {
403 | return this.saxParser.write(bom.stripBOM(str.toString())).close();
404 | } catch (_error) {
405 | err = _error;
406 | if (!(this.saxParser.errThrown || this.saxParser.ended)) {
407 | this.emit('error', err);
408 | return this.saxParser.errThrown = true;
409 | }
410 | }
411 | };
412 |
413 | return Parser;
414 |
415 | })(events.EventEmitter);
416 |
417 | exports.parseString = function(str, a, b) {
418 | var cb, options, parser;
419 | if (b != null) {
420 | if (typeof b === 'function') {
421 | cb = b;
422 | }
423 | if (typeof a === 'object') {
424 | options = a;
425 | }
426 | } else {
427 | if (typeof a === 'function') {
428 | cb = a;
429 | }
430 | options = {};
431 | }
432 | parser = new exports.Parser(options);
433 | return parser.parseString(str, cb);
434 | };
435 |
436 | }).call(this);
437 |
438 | //# sourceMappingURL=xml2js.map
439 |
--------------------------------------------------------------------------------
/test/parser.test.coffee:
--------------------------------------------------------------------------------
1 | # use zap to run tests, it also detects CoffeeScript files
2 | xml2js = require '../lib/xml2js'
3 | fs = require 'fs'
4 | util = require 'util'
5 | assert = require 'assert'
6 | path = require 'path'
7 | os = require 'os'
8 |
9 | fileName = path.join __dirname, '/fixtures/sample.xml'
10 |
11 | skeleton = (options, checks) ->
12 | (test) ->
13 | xmlString = options?.__xmlString
14 | delete options?.__xmlString
15 | x2js = new xml2js.Parser options
16 | x2js.addListener 'end', (r) ->
17 | checks r
18 | test.finish()
19 | if not xmlString
20 | fs.readFile fileName, 'utf8', (err, data) ->
21 | data = data.split(os.EOL).join('\n')
22 | x2js.parseString data
23 | else
24 | x2js.parseString xmlString
25 |
26 | nameToUpperCase = (name) ->
27 | return name.toUpperCase()
28 |
29 | nameCutoff = (name) ->
30 | return name.substr(0, 4)
31 |
32 | ###
33 | The `validator` function validates the value at the XPath. It also transforms the value
34 | if necessary to conform to the schema or other validation information being used. If there
35 | is an existing value at this path it is supplied in `currentValue` (e.g. this is the second or
36 | later item in an array).
37 | If the validation fails it should throw a `ValidationError`.
38 | ###
39 | validator = (xpath, currentValue, newValue) ->
40 | if xpath == '/sample/validatortest/numbertest'
41 | return Number(newValue)
42 | else if xpath in ['/sample/arraytest', '/sample/validatortest/emptyarray', '/sample/validatortest/oneitemarray']
43 | if not newValue or not ('item' of newValue)
44 | return {'item': []}
45 | else if xpath in ['/sample/arraytest/item', '/sample/validatortest/emptyarray/item', '/sample/validatortest/oneitemarray/item']
46 | if not currentValue
47 | return newValue
48 | else if xpath == '/validationerror'
49 | throw new xml2js.ValidationError("Validation error!")
50 | return newValue
51 |
52 | # shortcut, because it is quite verbose
53 | equ = assert.strictEqual
54 |
55 | module.exports =
56 | 'test parse with defaults': skeleton(undefined, (r) ->
57 | console.log 'Result object: ' + util.inspect r, false, 10
58 | equ r.sample.chartest[0].$.desc, 'Test for CHARs'
59 | equ r.sample.chartest[0]._, 'Character data here!'
60 | equ r.sample.cdatatest[0].$.desc, 'Test for CDATA'
61 | equ r.sample.cdatatest[0].$.misc, 'true'
62 | equ r.sample.cdatatest[0]._, 'CDATA here!'
63 | equ r.sample.nochartest[0].$.desc, 'No data'
64 | equ r.sample.nochartest[0].$.misc, 'false'
65 | equ r.sample.listtest[0].item[0]._, '\n This is\n \n character\n \n data!\n \n '
66 | equ r.sample.listtest[0].item[0].subitem[0], 'Foo(1)'
67 | equ r.sample.listtest[0].item[0].subitem[1], 'Foo(2)'
68 | equ r.sample.listtest[0].item[0].subitem[2], 'Foo(3)'
69 | equ r.sample.listtest[0].item[0].subitem[3], 'Foo(4)'
70 | equ r.sample.listtest[0].item[1], 'Qux.'
71 | equ r.sample.listtest[0].item[2], 'Quux.'
72 | # determine number of items in object
73 | equ Object.keys(r.sample.tagcasetest[0]).length, 3)
74 |
75 | 'test parse with explicitCharkey': skeleton(explicitCharkey: true, (r) ->
76 | console.log 'Result object: ' + util.inspect r, false, 10
77 | equ r.sample.chartest[0].$.desc, 'Test for CHARs'
78 | equ r.sample.chartest[0]._, 'Character data here!'
79 | equ r.sample.cdatatest[0].$.desc, 'Test for CDATA'
80 | equ r.sample.cdatatest[0].$.misc, 'true'
81 | equ r.sample.cdatatest[0]._, 'CDATA here!'
82 | equ r.sample.nochartest[0].$.desc, 'No data'
83 | equ r.sample.nochartest[0].$.misc, 'false'
84 | equ r.sample.listtest[0].item[0]._, '\n This is\n \n character\n \n data!\n \n '
85 | equ r.sample.listtest[0].item[0].subitem[0]._, 'Foo(1)'
86 | equ r.sample.listtest[0].item[0].subitem[1]._, 'Foo(2)'
87 | equ r.sample.listtest[0].item[0].subitem[2]._, 'Foo(3)'
88 | equ r.sample.listtest[0].item[0].subitem[3]._, 'Foo(4)'
89 | equ r.sample.listtest[0].item[1]._, 'Qux.'
90 | equ r.sample.listtest[0].item[2]._, 'Quux.')
91 |
92 | 'test parse with mergeAttrs': skeleton(mergeAttrs: true, (r) ->
93 | console.log 'Result object: ' + util.inspect r, false, 10
94 | equ r.sample.chartest[0].desc[0], 'Test for CHARs'
95 | equ r.sample.chartest[0]._, 'Character data here!'
96 | equ r.sample.cdatatest[0].desc[0], 'Test for CDATA'
97 | equ r.sample.cdatatest[0].misc[0], 'true'
98 | equ r.sample.cdatatest[0]._, 'CDATA here!'
99 | equ r.sample.nochartest[0].desc[0], 'No data'
100 | equ r.sample.nochartest[0].misc[0], 'false'
101 | equ r.sample.listtest[0].item[0].subitem[0], 'Foo(1)'
102 | equ r.sample.listtest[0].item[0].subitem[1], 'Foo(2)'
103 | equ r.sample.listtest[0].item[0].subitem[2], 'Foo(3)'
104 | equ r.sample.listtest[0].item[0].subitem[3], 'Foo(4)'
105 | equ r.sample.listtest[0].item[1], 'Qux.'
106 | equ r.sample.listtest[0].item[2], 'Quux.'
107 | equ r.sample.listtest[0].single[0], 'Single'
108 | equ r.sample.listtest[0].attr[0], 'Attribute')
109 |
110 | 'test parse with mergeAttrs and not explicitArray': skeleton(mergeAttrs: true, explicitArray: false, (r) ->
111 | console.log 'Result object: ' + util.inspect r, false, 10
112 | equ r.sample.chartest.desc, 'Test for CHARs'
113 | equ r.sample.chartest._, 'Character data here!'
114 | equ r.sample.cdatatest.desc, 'Test for CDATA'
115 | equ r.sample.cdatatest.misc, 'true'
116 | equ r.sample.cdatatest._, 'CDATA here!'
117 | equ r.sample.nochartest.desc, 'No data'
118 | equ r.sample.nochartest.misc, 'false'
119 | equ r.sample.listtest.item[0].subitem[0], 'Foo(1)'
120 | equ r.sample.listtest.item[0].subitem[1], 'Foo(2)'
121 | equ r.sample.listtest.item[0].subitem[2], 'Foo(3)'
122 | equ r.sample.listtest.item[0].subitem[3], 'Foo(4)'
123 | equ r.sample.listtest.item[1], 'Qux.'
124 | equ r.sample.listtest.item[2], 'Quux.'
125 | equ r.sample.listtest.single, 'Single'
126 | equ r.sample.listtest.attr, 'Attribute')
127 |
128 | 'test parse with explicitChildren': skeleton(explicitChildren: true, (r) ->
129 | console.log 'Result object: ' + util.inspect r, false, 10
130 | equ r.sample.$$.chartest[0].$.desc, 'Test for CHARs'
131 | equ r.sample.$$.chartest[0]._, 'Character data here!'
132 | equ r.sample.$$.cdatatest[0].$.desc, 'Test for CDATA'
133 | equ r.sample.$$.cdatatest[0].$.misc, 'true'
134 | equ r.sample.$$.cdatatest[0]._, 'CDATA here!'
135 | equ r.sample.$$.nochartest[0].$.desc, 'No data'
136 | equ r.sample.$$.nochartest[0].$.misc, 'false'
137 | equ r.sample.$$.listtest[0].$$.item[0]._, '\n This is\n \n character\n \n data!\n \n '
138 | equ r.sample.$$.listtest[0].$$.item[0].$$.subitem[0], 'Foo(1)'
139 | equ r.sample.$$.listtest[0].$$.item[0].$$.subitem[1], 'Foo(2)'
140 | equ r.sample.$$.listtest[0].$$.item[0].$$.subitem[2], 'Foo(3)'
141 | equ r.sample.$$.listtest[0].$$.item[0].$$.subitem[3], 'Foo(4)'
142 | equ r.sample.$$.listtest[0].$$.item[1], 'Qux.'
143 | equ r.sample.$$.listtest[0].$$.item[2], 'Quux.'
144 | equ r.sample.$$.nochildrentest[0].$$, undefined
145 | # determine number of items in object
146 | equ Object.keys(r.sample.$$.tagcasetest[0].$$).length, 3)
147 |
148 | 'test element without children': skeleton(explicitChildren: true, (r) ->
149 | console.log 'Result object: ' + util.inspect r, false, 10
150 | equ r.sample.$$.nochildrentest[0].$$, undefined)
151 |
152 | 'test parse with explicitChildren and charsAsChildren': skeleton(explicitChildren: true, charsAsChildren: true, (r) ->
153 | console.log 'Result object: ' + util.inspect r, false, 10
154 | equ r.sample.$$.chartest[0].$$._, 'Character data here!'
155 | equ r.sample.$$.cdatatest[0].$$._, 'CDATA here!'
156 | equ r.sample.$$.listtest[0].$$.item[0].$$._, '\n This is\n \n character\n \n data!\n \n '
157 | # determine number of items in object
158 | equ Object.keys(r.sample.$$.tagcasetest[0].$$).length, 3)
159 |
160 | 'test text trimming, normalize': skeleton(trim: true, normalize: true, (r) ->
161 | equ r.sample.whitespacetest[0]._, 'Line One Line Two')
162 |
163 | 'test text trimming, no normalizing': skeleton(trim: true, normalize: false, (r) ->
164 | equ r.sample.whitespacetest[0]._, 'Line One\n Line Two')
165 |
166 | 'test text no trimming, normalize': skeleton(trim: false, normalize: true, (r) ->
167 | equ r.sample.whitespacetest[0]._, 'Line One Line Two')
168 |
169 | 'test text no trimming, no normalize': skeleton(trim: false, normalize: false, (r) ->
170 | equ r.sample.whitespacetest[0]._, '\n Line One\n Line Two\n ')
171 |
172 | 'test enabled root node elimination': skeleton(__xmlString: '', explicitRoot: false, (r) ->
173 | console.log 'Result object: ' + util.inspect r, false, 10
174 | assert.deepEqual r, '')
175 |
176 | 'test disabled root node elimination': skeleton(__xmlString: '', explicitRoot: true, (r) ->
177 | assert.deepEqual r, {root: ''})
178 |
179 | 'test default empty tag result': skeleton(undefined, (r) ->
180 | assert.deepEqual r.sample.emptytest, [''])
181 |
182 | 'test empty tag result specified null': skeleton(emptyTag: null, (r) ->
183 | equ r.sample.emptytest[0], null)
184 |
185 | 'test invalid empty XML file': skeleton(__xmlString: ' ', (r) ->
186 | equ r, null)
187 |
188 | 'test enabled normalizeTags': skeleton(normalizeTags: true, (r) ->
189 | console.log 'Result object: ' + util.inspect r, false, 10
190 | equ Object.keys(r.sample.tagcasetest).length, 1)
191 |
192 | 'test parse with custom char and attribute object keys': skeleton(attrkey: 'attrobj', charkey: 'charobj', (r) ->
193 | console.log 'Result object: ' + util.inspect r, false, 10
194 | equ r.sample.chartest[0].attrobj.desc, 'Test for CHARs'
195 | equ r.sample.chartest[0].charobj, 'Character data here!'
196 | equ r.sample.cdatatest[0].attrobj.desc, 'Test for CDATA'
197 | equ r.sample.cdatatest[0].attrobj.misc, 'true'
198 | equ r.sample.cdatatest[0].charobj, 'CDATA here!'
199 | equ r.sample.cdatawhitespacetest[0].charobj, ' '
200 | equ r.sample.nochartest[0].attrobj.desc, 'No data'
201 | equ r.sample.nochartest[0].attrobj.misc, 'false')
202 |
203 | 'test child node without explicitArray': skeleton(explicitArray: false, (r) ->
204 | console.log 'Result object: ' + util.inspect r, false, 10
205 | equ r.sample.arraytest.item[0].subitem, 'Baz.'
206 | equ r.sample.arraytest.item[1].subitem[0], 'Foo.'
207 | equ r.sample.arraytest.item[1].subitem[1], 'Bar.')
208 |
209 | 'test child node with explicitArray': skeleton(explicitArray: true, (r) ->
210 | console.log 'Result object: ' + util.inspect r, false, 10
211 | equ r.sample.arraytest[0].item[0].subitem[0], 'Baz.'
212 | equ r.sample.arraytest[0].item[1].subitem[0], 'Foo.'
213 | equ r.sample.arraytest[0].item[1].subitem[1], 'Bar.')
214 |
215 | 'test ignore attributes': skeleton(ignoreAttrs: true, (r) ->
216 | console.log 'Result object: ' + util.inspect r, false, 10
217 | equ r.sample.chartest[0], 'Character data here!'
218 | equ r.sample.cdatatest[0], 'CDATA here!'
219 | equ r.sample.nochartest[0], ''
220 | equ r.sample.listtest[0].item[0]._, '\n This is\n \n character\n \n data!\n \n '
221 | equ r.sample.listtest[0].item[0].subitem[0], 'Foo(1)'
222 | equ r.sample.listtest[0].item[0].subitem[1], 'Foo(2)'
223 | equ r.sample.listtest[0].item[0].subitem[2], 'Foo(3)'
224 | equ r.sample.listtest[0].item[0].subitem[3], 'Foo(4)'
225 | equ r.sample.listtest[0].item[1], 'Qux.'
226 | equ r.sample.listtest[0].item[2], 'Quux.')
227 |
228 | 'test simple callback mode': (test) ->
229 | x2js = new xml2js.Parser()
230 | fs.readFile fileName, (err, data) ->
231 | equ err, null
232 | x2js.parseString data, (err, r) ->
233 | equ err, null
234 | # just a single test to check whether we parsed anything
235 | equ r.sample.chartest[0]._, 'Character data here!'
236 | test.finish()
237 |
238 | 'test double parse': (test) ->
239 | x2js = new xml2js.Parser()
240 | fs.readFile fileName, (err, data) ->
241 | equ err, null
242 | x2js.parseString data, (err, r) ->
243 | equ err, null
244 | # make sure we parsed anything
245 | equ r.sample.chartest[0]._, 'Character data here!'
246 | x2js.parseString data, (err, r) ->
247 | equ err, null
248 | equ r.sample.chartest[0]._, 'Character data here!'
249 | test.finish()
250 |
251 | 'test element with garbage XML': (test) ->
252 | x2js = new xml2js.Parser()
253 | xmlString = "<<>fdfsdfsdf<><<>?><<><>!<>!!."
254 | x2js.parseString xmlString, (err, result) ->
255 | assert.notEqual err, null
256 | test.finish()
257 |
258 | 'test simple function without options': (test) ->
259 | fs.readFile fileName, (err, data) ->
260 | xml2js.parseString data, (err, r) ->
261 | equ err, null
262 | equ r.sample.chartest[0]._, 'Character data here!'
263 | test.finish()
264 |
265 | 'test simple function with options': (test) ->
266 | fs.readFile fileName, (err, data) ->
267 | # well, {} still counts as option, right?
268 | xml2js.parseString data, {}, (err, r) ->
269 | equ err, null
270 | equ r.sample.chartest[0]._, 'Character data here!'
271 | test.finish()
272 |
273 | 'test validator': skeleton(validator: validator, (r) ->
274 | console.log 'Result object: ' + util.inspect r, false, 10
275 | equ typeof r.sample.validatortest[0].stringtest[0], 'string'
276 | equ typeof r.sample.validatortest[0].numbertest[0], 'number'
277 | assert.ok r.sample.validatortest[0].emptyarray[0].item instanceof Array
278 | equ r.sample.validatortest[0].emptyarray[0].item.length, 0
279 | assert.ok r.sample.validatortest[0].oneitemarray[0].item instanceof Array
280 | equ r.sample.validatortest[0].oneitemarray[0].item.length, 1
281 | equ r.sample.validatortest[0].oneitemarray[0].item[0], 'Bar.'
282 | assert.ok r.sample.arraytest[0].item instanceof Array
283 | equ r.sample.arraytest[0].item.length, 2
284 | equ r.sample.arraytest[0].item[0].subitem[0], 'Baz.'
285 | equ r.sample.arraytest[0].item[1].subitem[0], 'Foo.'
286 | equ r.sample.arraytest[0].item[1].subitem[1], 'Bar.')
287 |
288 | 'test validation error': (test) ->
289 | x2js = new xml2js.Parser({validator: validator})
290 | x2js.parseString '', (err, r) ->
291 | equ err.message, 'Validation error!'
292 | test.finish()
293 |
294 | 'test error throwing': (test) ->
295 | xml = 'content is ok'
296 | try
297 | xml2js.parseString xml, (err, parsed) ->
298 | throw new Error 'error throwing in callback'
299 | throw new Error 'error throwing outside'
300 | catch e
301 | # the stream is finished by the time the parseString method is called
302 | # so the callback, which is synchronous, will bubble the inner error
303 | # out to here, make sure that happens
304 | equ e.message, 'error throwing in callback'
305 | test.finish()
306 |
307 | 'test xmlns': skeleton(xmlns: true, (r) ->
308 | console.log 'Result object: ' + util.inspect r, false, 10
309 | equ r.sample["pfx:top"][0].$ns.local, 'top'
310 | equ r.sample["pfx:top"][0].$ns.uri, 'http://foo.com'
311 | equ r.sample["pfx:top"][0].$["pfx:attr"].value, 'baz'
312 | equ r.sample["pfx:top"][0].$["pfx:attr"].local, 'attr'
313 | equ r.sample["pfx:top"][0].$["pfx:attr"].uri, 'http://foo.com'
314 | equ r.sample["pfx:top"][0].middle[0].$ns.local, 'middle'
315 | equ r.sample["pfx:top"][0].middle[0].$ns.uri, 'http://bar.com')
316 |
317 | 'test callback should be called once': (test) ->
318 | xml = 'test'
319 | i = 0
320 | try
321 | xml2js.parseString xml, (err, parsed) ->
322 | i = i + 1
323 | # throw something custom
324 | throw new Error 'Custom error message'
325 | catch e
326 | equ i, 1
327 | equ e.message, 'Custom error message'
328 | test.finish()
329 |
330 | 'test no error event after end': (test) ->
331 | xml = 'test'
332 | i = 0
333 | x2js = new xml2js.Parser()
334 | x2js.on 'error', ->
335 | i = i + 1
336 |
337 | x2js.on 'end', ->
338 | #This is a userland callback doing something with the result xml.
339 | #Errors in here should not be passed to the parser's 'error' callbacks
340 | throw new Error('some error in happy path')
341 |
342 | x2js.parseString(xml)
343 |
344 | equ i, 0
345 | test.finish()
346 |
347 | 'test empty CDATA': (test) ->
348 | xml = '5850440872586764820'
349 | xml2js.parseString xml, (err, parsed) ->
350 | equ parsed.xml.Label[0], ''
351 | test.finish()
352 |
353 | 'test CDATA whitespaces result': (test) ->
354 | xml = ''
355 | xml2js.parseString xml, (err, parsed) ->
356 | equ parsed.spacecdatatest, ' '
357 | test.finish()
358 |
359 | 'test non-strict parsing': (test) ->
360 | html = '
'
361 | xml2js.parseString html, strict: false, (err, parsed) ->
362 | equ err, null
363 | test.finish()
364 |
365 | 'test construction with new and without': (test) ->
366 | demo = 'Bar'
367 | withNew = new xml2js.Parser
368 | withNew.parseString demo, (err, resWithNew) ->
369 | equ err, null
370 | withoutNew = xml2js.Parser()
371 | withoutNew.parseString demo, (err, resWithoutNew) ->
372 | equ err, null
373 | assert.deepEqual resWithNew, resWithoutNew
374 | test.finish()
375 |
376 | 'test not closed but well formed xml': (test) ->
377 | xml = ""
378 | xml2js.parseString xml, (err, parsed) ->
379 | assert.equal err.message, 'Unclosed root tag\nLine: 0\nColumn: 6\nChar: '
380 | test.finish()
381 |
382 | 'test single attrNameProcessors': skeleton(attrNameProcessors: [nameToUpperCase], (r)->
383 | console.log 'Result object: ' + util.inspect r, false, 10
384 | equ r.sample.attrNameProcessTest[0].$.hasOwnProperty('CAMELCASEATTR'), true
385 | equ r.sample.attrNameProcessTest[0].$.hasOwnProperty('LOWERCASEATTR'), true)
386 |
387 | 'test multiple attrNameProcessors': skeleton(attrNameProcessors: [nameToUpperCase, nameCutoff], (r)->
388 | console.log 'Result object: ' + util.inspect r, false, 10
389 | equ r.sample.attrNameProcessTest[0].$.hasOwnProperty('CAME'), true
390 | equ r.sample.attrNameProcessTest[0].$.hasOwnProperty('LOWE'), true)
391 |
392 | 'test single tagNameProcessors': skeleton(tagNameProcessors: [nameToUpperCase], (r)->
393 | console.log 'Result object: ' + util.inspect r, false, 10
394 | equ r.hasOwnProperty('SAMPLE'), true
395 | equ r.SAMPLE.hasOwnProperty('TAGNAMEPROCESSTEST'), true)
396 |
397 | 'test multiple tagNameProcessors': skeleton(tagNameProcessors: [nameToUpperCase, nameCutoff], (r)->
398 | console.log 'Result object: ' + util.inspect r, false, 10
399 | equ r.hasOwnProperty('SAMP'), true
400 | equ r.SAMP.hasOwnProperty('TAGN'), true)
401 |
--------------------------------------------------------------------------------