├── .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 | [![Build Status](https://secure.travis-ci.org/Leonidas-from-XIV/node-xml2js.png?branch=master)](https://travis-ci.org/Leonidas-from-XIV/node-xml2js) 332 | [![Dependency Status](https://david-dm.org/Leonidas-from-XIV/node-xml2js.png)](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 | --------------------------------------------------------------------------------