UNPKG

xml2js-parser

Version:

Simple XML to JavaScript object converter.

314 lines (283 loc) 9.25 kB
'use strict'; const sax = require('sax'); const events = require('events'); const DEFAULTS = { explicitCharkey: false, trim: false, normalize: false, normalizeTags: false, attrkey: '$', charkey: '_', explicitArray: true, ignoreAttrs: false, mergeAttrs: false, explicitRoot: true, validator: null, xmlns: false, explicitChildren: false, preserveChildrenOrder: false, childkey: '$$', charsAsChildren: false, includeWhiteChars: false, strict: true, attrNameProcessors: null, attrValueProcessors: null, tagNameProcessors: null, valueProcessors: null, chunkSize: 10000, emptyTag: '', }; const PRE_MATCH = /(?!xmlns)^.*:/; const BOOL_MATCH = /^(?:true|false)$/i; const PROCESSORS = { normalize(str) { return str.toLowerCase(); }, firstCharLowerCase(str) { return str.charAt(0).toLowerCase() + str.slice(1); }, stripPrefix(str) { return str.replace(PRE_MATCH, ''); }, parseNumbers(str) { return isNaN(str) ? str : (str % 1 === 0 ? parseInt(str, 10) : parseFloat(str)); }, parseBooleans(str) { return BOOL_MATCH.test(str) ? str.toLowerCase() === 'true' : str; } }; class ValidationError extends Error {} module.exports = class Parser extends events.EventEmitter { static get defaults() { return DEFAULTS; } static get processors() { return PROCESSORS; } static get ValidationError() { return ValidationError; } static parseStringSync(str, options = {}) { const parser = new Parser(options); return parser.parseStringSync(str); } static parseString(str, a, b) { let cb, options = {}; if (b != null) { if (typeof b === 'function') cb = b; if (typeof a === 'object') options = a; } else { if (typeof a === 'function') cb = a; } const parser = new Parser(options); parser.parseString(str, cb); } constructor(options) { super(); Object.assign(this, DEFAULTS, options); if (this.xmlns) this.xmlnskey = this.attrkey + 'ns'; if (this.normalizeTags) { this.tagNameProcessors = this.tagNameProcessors || []; this.tagNameProcessors.unshift(PROCESSORS.normalize); } this.reset(); } processAsync(remaining) { try { if (remaining.length <= this.chunkSize) { this.saxParser.write(remaining).close(); } else { const chunk = remaining.substr(0, this.chunkSize); remaining = remaining.substr(this.chunkSize, remaining.length); this.saxParser.write(chunk); setImmediate(() => this.processAsync(remaining)); } } catch (err) { if (!this.saxParser.errThrown) { this.saxParser.errThrown = true; this.emit(err); } } } reset() { this.removeAllListeners(); this.stack = []; this.result = null; this.saxParser = sax.parser(this.strict, { trim: false, normalize: false, xmlns: this.xmlns }); this.saxParser.errThrown = false; this.saxParser.ended = false; this.saxParser.onopentag = (node) => this._openTag(node); this.saxParser.onclosetag = () => this._closeTag(); this.saxParser.ontext = (text) => this._onText(text); this.saxParser.oncdata = (text) => this._onCData(text); this.saxParser.onend = () => this._onEnd(); this.saxParser.onerror = (err) => this._onError(err); } parseStringSync(str) { let result, error; this.on('end', (res) => result = res); this.on('error', (err) => error = err); try { str = stripBOM(str.toString()).trim(); if (!str) return null; this.saxParser.write(str).close(); } finally { this.reset(); } if (error) throw error; return result; } parseString(str, cb) { const promise = new Promise((reolve, reject) => { this.on('end', (result) => { this.reset(); reolve(result); }); this.on('error', (err) => { this.reset(); reject(err); }); try { str = stripBOM(str.toString()).trim(); if (!str) return this.emit('end', null); setImmediate(() => this.processAsync(str)); } catch (err) { this.emit('error', err); } }); if (typeof cb != 'function') return promise; promise.then((res) => cb(null, res), (err) => cb(err)); } _openTag(node) { const obj = {[this.charkey]: ''}; if (!this.ignoreAttrs) { for (let key of Object.keys(node.attributes)) { if (!(this.attrkey in obj) && !this.mergeAttrs) { obj[this.attrkey] = {}; } const processedValue = preprocess(this.attrValueProcessors, node.attributes[key]); const processedKey = preprocess(this.attrNameProcessors, key); if (this.mergeAttrs) { assignOrPush(obj, processedKey, processedValue, this.explicitArray); } else { obj[this.attrkey][processedKey] = processedValue; } } } obj['#name'] = preprocess(this.tagNameProcessors, node.name); if (this.xmlns) { obj[this.xmlnskey] = { uri: node.uri, local: node.local }; } this.stack.push(obj); } _closeTag() { let obj = this.stack.pop(); const nodeName = obj['#name']; if (!this.explicitChildren || !this.preserveChildrenOrder) { delete obj['#name']; } const cdata = obj.cdata === true; if (cdata) { delete obj.cdata; } let emptyStr = ''; const s = this.stack[this.stack.length - 1]; if (obj[this.charkey].match(/^\s*$/) && !cdata) { emptyStr = obj[this.charkey]; delete obj[this.charkey]; } else { if (this.trim) { obj[this.charkey] = obj[this.charkey].trim(); } if (this.normalize) { obj[this.charkey] = obj[this.charkey].replace(/\s{2,}/g, ' ').trim(); } obj[this.charkey] = preprocess(this.valueProcessors, obj[this.charkey]); if (Object.keys(obj).length === 1 && this.charkey in obj && !this.explicitCharkey) { obj = obj[this.charkey]; } } if (!Object.keys(obj).length) { obj = this.emptyTag !== '' ? this.emptyTag : emptyStr; } if (this.validator != null) { const xpath = '/' + this.stack.map(n => n['#name']).concat(nodeName).join('/'); try { obj = this.validator(xpath, s && s[nodeName], obj); } catch (err) { this.emit('error', err); } } if (this.explicitChildren && !this.mergeAttrs && typeof obj === 'object') { if (!this.preserveChildrenOrder) { const node = {}; if (this.attrkey in obj) { node[this.attrkey] = obj[this.attrkey]; delete obj[this.attrkey]; } if (!this.charsAsChildren && this.charkey in obj) { node[this.charkey] = obj[this.charkey]; delete obj[this.charkey]; } if (Object.keys(obj).length) { node[this.childkey] = obj; } obj = node; } else if (s) { s[this.childkey] = s[this.childkey] || []; s[this.childkey].push(Object.assign({}, obj)); delete obj['#name']; if (Object.keys(obj).length === 1 && this.charkey in obj && !this.explicitCharkey) { obj = obj[this.charkey]; } } } if (this.stack.length > 0) { assignOrPush(s, nodeName, obj, this.explicitArray); } else { if (this.explicitRoot) { obj = {[nodeName]: obj}; } this.result = obj; } } _onText(text) { const s = this.stack[this.stack.length - 1]; if (s) { s[this.charkey] += text; if (this.explicitChildren && this.preserveChildrenOrder && this.charsAsChildren && (this.includeWhiteChars || text.replace(/\\n/g, '').trim() !== '')) { s[this.childkey] = s[this.childkey] || []; const charChild = {'#name': '__text__'}; charChild[this.charkey] = text; if (this.normalize) { charChild[this.charkey] = charChild[this.charkey].replace(/\s{2,}/g, ' ').trim(); } s[this.childkey].push(charChild); } } return s; } _onCData(text) { const s = this._onText(text); if (s) s.cdata = true; } _onEnd() { if (this.saxParser.ended) return; this.saxParser.ended = true; this.emit('end', this.result); } _onError(err) { this.saxParser.resume(); if (this.saxParser.errThrown) return; this.saxParser.errThrown = true; this.emit('error', err); } }; function assignOrPush(obj, key, value, explicit) { if (!(key in obj)) { obj[key] = explicit ? [value] : value; } else { if (!(obj[key] instanceof Array)) obj[key] = [obj[key]]; obj[key].push(value); } } function preprocess(processors, value) { if (!processors) return value; return processors.reduce((v, func) => func(v), value); } function stripBOM(str) { return str[0] === '\uFEFF' ? str.substring(1) : str; } module.exports.Parser = function(opts) { return new module.exports(opts); };