webgme-engine
Version:
WebGME server and Client API without a GUI
275 lines (258 loc) • 10.9 kB
JavaScript
/*globals define*/
/*eslint-env node, browser*/
/**
* Converter from XML to Json using sax parser. See the doc of constructor for info on how to use.
* @author pmeijer / https://github.com/pmeijer
*/
define(['common/util/sax'], function (sax) {
'use strict';
/**
* XML2JSON converter, when instantiated invoke convert(xmlString) to get the corresponding JavaScript object.
* @param {object} options - optional options.
* @param {object} options.arrayElements - Dictionary where keys evaluated to true are treated as arrays in the
* generated javascript object. If not provided these will be inferred by number of occurrences of the elements.
* @param {string} options.attrTag - will be prepended to attributes keys, default "@".
* @param {string} options.textTag - the key values for text items, default "#text".
* @param {boolean} options.skipWSText - if true then text made up of only white-space (including \n, \r, etc.)
* will not be generated as text-items in the javascript object, default false.
* @constructor
*/
var XML2JSON = function (options) {
var self = this,
opts = options || {},
attrTag = opts.attrTag,
textTag = opts.textTag || '#text',
skipWS = opts.skipWSText;
if (attrTag === undefined) {
attrTag = '@';
}
self.rootNode = {};
self.stack = [];
self.nsStack = [];
self.parser = sax.parser(true);
// TODO make this configurable
self.nsMap = {
'http://www.w3.org/2001/XMLSchema-instance': 'xsi',
'http://www.w3.org/2001/XMLSchema': 'xsd'
};
self.parser.ontext = function (text) {
if (self.stack.length > 0) {
if (skipWS) {
if (text.replace(/\s+?/g, '')) {
self.stack[self.stack.length - 1][textTag] = text;
}
} else {
self.stack[self.stack.length - 1][textTag] = text;
}
}
};
function mapNamespace(ns, value) {
var colon = value.indexOf(':');
if (colon === -1) {
return value;
}
var namespace = value.substr(0, colon);
if (namespace in ns) {
return (self.nsMap[ns[namespace]] || ns[namespace]) + ':' + value.substr(colon + 1);
}
return value;
}
self.parser.onopentag = function (node) {
var key,
i,
parentNode,
nodeName,
jsonNode = {};
var ns = {};
for (key in node.attributes) {
if (Object.hasOwn(node.attributes, key)) {
if (key.substr(0, 6) === 'xmlns:') {
ns[key.substr('xmlns:'.length)] = node.attributes[key];
}
if (key === 'xmlns') {
ns[''] = node.attributes.xmlns;
}
}
}
if (Object.getOwnPropertyNames(ns).length === 0) {
if (self.nsStack.length > 0) {
ns = self.nsStack[self.nsStack.length - 1];
}
self.nsStack.push(ns);
} else {
for (i = self.nsStack.length - 1; i >= 0; i--) {
for (key in self.nsStack[i]) {
if (!Object.hasOwn(ns, key) && Object.hasOwn(self.nsStack[i], key)) {
ns[key] = self.nsStack[i][key];
}
}
}
self.nsStack.push(ns);
}
nodeName = mapNamespace(ns, node.name);
if (self.stack.length === 0) {
self.rootNode[nodeName] = jsonNode;
} else {
parentNode = self.stack[self.stack.length - 1];
if (opts.arrayElements) {
self.arrayElements = opts.arrayElements;
if (self.arrayElements[nodeName]) {
if (Object.hasOwn(parentNode, nodeName)) {
parentNode[nodeName].push(jsonNode);
} else {
parentNode[nodeName] = [jsonNode];
}
} else {
parentNode[nodeName] = jsonNode;
}
} else {
if (Object.hasOwn(parentNode, nodeName)) {
if (parentNode[nodeName] instanceof Array) {
parentNode[nodeName].push(jsonNode);
} else {
parentNode[nodeName] = [parentNode[nodeName], jsonNode];
}
} else {
parentNode[nodeName] = jsonNode;
}
}
}
self.stack.push(jsonNode);
for (key in node.attributes) {
if (Object.hasOwn(node.attributes, key)) {
var namespaceKey = mapNamespace(ns, key);
if (namespaceKey === 'xsi:type') {
// the attribute value should be mapped too
jsonNode[attrTag + namespaceKey] = mapNamespace(ns, node.attributes[key]);
} else {
jsonNode[attrTag + namespaceKey] = node.attributes[key];
}
}
}
};
self.parser.onclosetag = function (/*node*/) {
self.stack.pop();
self.nsStack.pop();
};
self.parser.onerror = function (error) {
self.rootNode = error;
self.parser.error = null;
};
};
/**
* Converts the xml in the given string to a javascript object. For bigger xmls use convertFromBuffer instead.
* @param {string} xmlString - xml string representation to convert.
* @returns {object|Error} - Javascript object inferred from the xml, Error object if failed.
*/
XML2JSON.prototype.convertFromString = function (xmlString) {
this.rootNode = {};
this.stack = [];
this.parser.write(xmlString).close();
return this.rootNode;
};
/**
* Converts the xml to a javascript object (JSON).
* @param xmlBuffer {ArrayBuffer} - xml to convert.
* @param options {object} - optional options.
* @param options.segmentSize {int} - length of string segments, default 10000.
* @param options.encoding {function(new:TypedArray)} - encoding of the ArrayBuffer, default Uint8Array.
* @returns {object|Error} - Javascript object inferred from the xml, Error object if failed.
*/
XML2JSON.prototype.convertFromBuffer = function (xmlBuffer, options) {
var opts = options || {},
segmentSize = opts.segmentSize || 10000,
Encode = opts.encoding || Uint8Array,
data = new Encode(xmlBuffer),
dataSegment,
nbrOfIterations = Math.ceil(data.length / segmentSize),
startIndex = 0,
i;
this.rootNode = {};
this.stack = [];
for (i = 0; i < nbrOfIterations; i += 1) {
dataSegment = data.subarray(startIndex, startIndex + segmentSize);
startIndex += segmentSize;
if (i < nbrOfIterations - 1) {
this.parser.write(String.fromCharCode.apply(null, dataSegment));
} else {
this.parser.write(String.fromCharCode.apply(null, dataSegment)).close();
}
}
return this.rootNode;
};
/**
* XML2JSON converter, when instantiated invoke convert(xmlString) to get the corresponding JavaScript object.
* @param {object} options - optional options.
* @param {string} options.attrTag - keys with this will be treated as attributes, default "@".
* @param {string} options.textTag - the key values for text items, default "#text".
* @param {string} options.xmlDeclaration - the xmlDeclaration, default "<?xml version="1.0"?>".
* @constructor
*/
var JSON2XML = function (options) {
var opts = options || {},
attrTag = opts.attrTag,
textTag = opts.textTag || '#text',
xmlDeclaration = opts.xmlDeclaration || '<?xml version="1.0"?>';
if (attrTag === undefined) {
attrTag = '@';
}
this.attrTag = attrTag;
this.attrTagIndex = this.attrTag.length;
this.textTag = textTag;
this.xmlDeclaration = xmlDeclaration;
};
JSON2XML.prototype._convertToStringRec = function (key, value) {
var subKeys,
elemTag = '',
i,
content = '';
if (value instanceof Array) {
for (i = 0; i < value.length; i += 1) {
content += this._convertToStringRec(key, value[i]);
}
return content;
}
if (value instanceof Object) {
subKeys = Object.keys(value);
for (i = 0; i < subKeys.length; i += 1) {
if (value[subKeys[i]] instanceof Object) {
content += this._convertToStringRec(subKeys[i], value[subKeys[i]]);
} else {
if (subKeys[i].slice(0, this.attrTag.length) === this.attrTag) {
if (value[subKeys[i]] === null) {
elemTag += ' ' + subKeys[i].substr(this.attrTagIndex) + '=""';
} else {
elemTag += ' ' + subKeys[i].substr(this.attrTagIndex) + '="' +
value[subKeys[i]].toString() + '"';
}
} else if (subKeys[i] === this.textTag) {
content += value[subKeys[i]].toString();
} else {
content += this._convertToStringRec(subKeys[i], value[subKeys[i]]);
}
}
}
} else if (value) {
content += '<' + value.toString() + '></' + value.toString() + '>';
}
if (content) {
return '<' + key + elemTag + '>' + content + '</' + key + '>';
}
return '<' + key + elemTag + '/>';
};
JSON2XML.prototype.convertToString = function (jsonObj) {
var keys = Object.keys(jsonObj),
i;
this.xml = this.xmlDeclaration;
for (i = 0; i < keys.length; i += 1) {
this.xml += this._convertToStringRec(keys[i], jsonObj[keys[i]]);
}
return this.xml;
};
return {
Xml2json: XML2JSON,
Json2xml: JSON2XML,
JsonToXml: JSON2XML,
XmlToJson: XML2JSON
};
});