Last active
April 23, 2020 16:52
-
-
Save uhop/7acab057b9d71bcab1e066194d5e9c4f to your computer and use it in GitHub Desktop.
Simple generic XML builder and utilities.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
// Loosely based on JSONx: https://tools.ietf.org/html/draft-rsalz-jsonx-00 | |
const escapeValueDict = {'&': '&', '<': '<'}; | |
const escapeValue = s => ('' + s).replace(/[&<]/g, m => escapeValueDict[m]); | |
const escapeAttrDict = {'&': '&', '<': '<', '"': '"'}; | |
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