Skip to content

Instantly share code, notes, and snippets.

@uhop
Last active April 23, 2020 16:52
Show Gist options
  • Save uhop/7acab057b9d71bcab1e066194d5e9c4f to your computer and use it in GitHub Desktop.
Save uhop/7acab057b9d71bcab1e066194d5e9c4f to your computer and use it in GitHub Desktop.
Simple generic XML builder and utilities.
'use strict';
// Loosely based on JSONx: https://tools.ietf.org/html/draft-rsalz-jsonx-00
// Uses the lxt format for XML nodes
const xml2json = require('./xml2json');
const memoizedApplyXml = (object, node, path, cache) => {
if (node.attrs.a === 'remove') {
return; // undefined => remove property
}
if (node.name !== 'o' && node.name !== 'a') {
return xml2json(node);
}
if (!memoizedHasPatch(node, path, cache)) {
return xml2json(node);
}
if (node.name === 'a') {
if (object instanceof Array) {
for (let i = 0; i < node.children.length; ++i) {
const child = node.children[i];
if (!child || typeof child != 'object') continue;
const newPath = path + '.' + i,
value = memoizedApplyXml(object[i], child, newPath, cache);
if (typeof value === 'undefined') {
delete object[i];
} else {
object[i] = value;
}
}
}
return object;
}
if (node.name === 'o') {
if (object && typeof object == 'object') {
for (let i = 0; i < node.children.length; ++i) {
const child = node.children[i];
if (!child || typeof child != 'object') continue;
const name = child.attrs.n,
newPath = path + '.' + name,
value = memoizedApplyXml(object[name], child, newPath, cache);
if (typeof value === 'undefined') {
delete object[name];
} else {
object[name] = value;
}
}
}
return object;
}
return object;
};
const memoizedHasPatch = (node, path, cache) => {
if (cache[path] === 1) return true;
if (cache[path] === 0) return false;
if (node.attrs.a) {
cache[path] = 1;
return true;
}
if (node.name === 'o' || node.name === 'a') {
const result = node.children
.filter(child => child && typeof child == 'object')
.some((child, index) => {
const newPath = path + '.' + (child.attrs.hasOwnProperty('n') ? child.attrs.n : index);
return memoizedHasPatch(child, newPath, cache);
});
cache[path] = result ? 1 : 0;
return result;
}
cache[path] = 0;
return false;
};
const hasPatch = (node, cache = {}) => memoizedHasPatch(node, '', cache);
const applyXml = (object, node, cache = {}) => memoizedApplyXml(object, node, '', cache);
applyXml.hasPatch = hasPatch;
module.exports = applyXml;
'use strict';
// Loosely based on JSONx: https://tools.ietf.org/html/draft-rsalz-jsonx-00
// Uses the lxt format for XML nodes
const xml2json = node => {
switch (node.name) {
case 's':
case 'n':
case 'b':
const buffer = node.children.reduce((acc, child) => {
if (child && typeof child == 'string') {
acc += child;
}
return acc;
}, '');
switch (node.name) {
case 'n':
return +buffer;
case 'b':
return buffer.trim() === 'true';
}
return buffer; // s
case 'u':
return null;
case 'a':
const array = [];
node.children.forEach(child => {
if (child && typeof child == 'object') {
array.push(xml2json(child));
}
});
return array;
case 'o':
const object = {};
node.children.forEach(child => {
if (child && typeof child == 'object') {
object[child.attrs.n] = xml2json(child);
}
});
return object;
}
throw new Error('Wrong XML => JSON tag: ' + node.name);
};
module.exports = xml2json;
'use strict';
// Loosely based on JSONx: https://tools.ietf.org/html/draft-rsalz-jsonx-00
const escapeValueDict = {'&': '&amp;', '<': '&lt;'};
const escapeValue = s => ('' + s).replace(/[&<]/g, m => escapeValueDict[m]);
const escapeAttrDict = {'&': '&amp;', '<': '&lt;', '"': '&quot;'};
const escapeAttr = s => ('' + s).replace(/[&<"]/g, m => escapeAttrDict[m]);
const buildValue = (builder, value, attrs) => {
if (typeof value == 'string') {
return builder.element('string', attrs, value);
}
if (typeof value == 'number') {
return builder.element('number', attrs, value);
}
if (typeof value == 'boolean') {
return builder.element('boolean', attrs, value);
}
if (value instanceof Array) {
builder.open('array', attrs);
value.forEach(val => buildValue(builder, val));
return builder.close();
}
if (typeof value == 'object') {
if (value) {
builder.open('object', attrs);
Object.keys(value).forEach(key => {
buildValue(builder, value[key], {name: key});
});
return builder.close();
}
return builder.element('null', attrs);
}
return builder.element('string', attrs, '' + value);
};
class XmlBuilder {
constructor(options) {
options = options || {};
this.tab = options.tab;
this.buffer = typeof options.preamble == 'string' ? options.preamble : '<?xml version="1.0" encoding="utf-8"?>';
this.stack = [];
this.indent = '';
this.openRaw = this.open;
this.closeRaw = this.close;
this.elementRaw = this.element;
this.commentRaw = this.comment;
this.cdataRaw = this.cdata;
this.processRaw = this.process;
if (typeof this.tab == 'string' && this.tab) {
if (this.buffer.length && this.buffer.charAt(this.buffer.length - 1) !== '\n') {
this.buffer += '\n';
}
this._open = this._withFormat('openRaw');
this._close = this._withFormat('closeRaw');
this.element = this._withFormat('elementRaw');
this.comment = this._withFormat('commentRaw');
this.cdata = this._withFormat('cdataRaw');
this.process = this._withFormat('processRaw');
this.open = this._openWithFormat;
this.close = this._closeWithFormat;
}
}
_withFormat(name) {
return (...args) => {
this.buffer += this.indent;
this[name](...args);
this.buffer += '\n';
return this;
};
}
_openWithFormat(tag, attrs) {
this._open(tag, attrs);
this.indent += this.tab;
return this;
}
_closeWithFormat() {
this.indent = this.indent.slice(0, -this.tab.length);
return this._close();
}
writeRaw(value) {
this.buffer += value;
return this;
}
writeValue(value) {
this.buffer += escapeValue(value);
return this;
}
closeAll() {
while (this.stack.length) {
this.close();
}
return this;
}
flush() {
const buf = this.buffer;
this.buffer = '';
return buf;
}
ensureDone() {
if (this.stack.length) throw new Error('XmlBuilder: stack is not empty');
return this;
}
// primary methods
open(tag, attrs) {
this.stack.push(tag);
this.buffer += '<' + tag;
if (attrs) {
this.buffer +=
' ' +
Object.keys(attrs)
.map(key => key + '="' + escapeAttr(attrs[key]) + '"')
.join(' ');
}
this.buffer += '>';
return this;
}
close() {
if (!this.stack.length) throw new Error('XmlBuilder: stack is empty');
this.buffer += '</' + this.stack.pop() + '>';
return this;
}
element(tag, attrs, value) {
this.buffer += '<' + tag;
if (attrs) {
this.buffer +=
' ' +
Object.keys(attrs)
.map(key => key + '="' + escapeAttr(attrs[key]) + '"')
.join(' ');
}
if (value !== undefined) {
value = escapeValue(value);
}
this.buffer += value ? '>' + value + '</' + tag + '>' : '/>';
return this;
}
comment(value) {
this.buffer += '<!-- ' + ('' + value) + ' -->';
return this;
}
cdata(value) {
this.buffer += '<![CDATA[' + ('' + value) + ']]>';
return this;
}
process(value) {
this.buffer += '<?process ' + ('' + value) + '?>';
return this;
}
// high-level operations
dump(value) {
buildValue(this, value);
return this;
}
}
module.exports = XmlBuilder;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment