271 lines
8.9 KiB
Plaintext
271 lines
8.9 KiB
Plaintext
|
'use strict';
|
||
|
//parse Empty Node as self closing node
|
||
|
const buildFromOrderedJs = require('./orderedJs2Xml');
|
||
|
|
||
|
const defaultOptions = {
|
||
|
attributeNamePrefix: '@_',
|
||
|
attributesGroupName: false,
|
||
|
textNodeName: '#text',
|
||
|
ignoreAttributes: true,
|
||
|
cdataPropName: false,
|
||
|
format: false,
|
||
|
indentBy: ' ',
|
||
|
suppressEmptyNode: false,
|
||
|
suppressUnpairedNode: true,
|
||
|
suppressBooleanAttributes: true,
|
||
|
tagValueProcessor: function(key, a) {
|
||
|
return a;
|
||
|
},
|
||
|
attributeValueProcessor: function(attrName, a) {
|
||
|
return a;
|
||
|
},
|
||
|
preserveOrder: false,
|
||
|
commentPropName: false,
|
||
|
unpairedTags: [],
|
||
|
entities: [
|
||
|
{ regex: new RegExp("&", "g"), val: "&" },//it must be on top
|
||
|
{ regex: new RegExp(">", "g"), val: ">" },
|
||
|
{ regex: new RegExp("<", "g"), val: "<" },
|
||
|
{ regex: new RegExp("\'", "g"), val: "'" },
|
||
|
{ regex: new RegExp("\"", "g"), val: """ }
|
||
|
],
|
||
|
processEntities: true,
|
||
|
stopNodes: [],
|
||
|
// transformTagName: false,
|
||
|
// transformAttributeName: false,
|
||
|
oneListGroup: false
|
||
|
};
|
||
|
|
||
|
function Builder(options) {
|
||
|
this.options = Object.assign({}, defaultOptions, options);
|
||
|
if (this.options.ignoreAttributes || this.options.attributesGroupName) {
|
||
|
this.isAttribute = function(/*a*/) {
|
||
|
return false;
|
||
|
};
|
||
|
} else {
|
||
|
this.attrPrefixLen = this.options.attributeNamePrefix.length;
|
||
|
this.isAttribute = isAttribute;
|
||
|
}
|
||
|
|
||
|
this.processTextOrObjNode = processTextOrObjNode
|
||
|
|
||
|
if (this.options.format) {
|
||
|
this.indentate = indentate;
|
||
|
this.tagEndChar = '>\n';
|
||
|
this.newLine = '\n';
|
||
|
} else {
|
||
|
this.indentate = function() {
|
||
|
return '';
|
||
|
};
|
||
|
this.tagEndChar = '>';
|
||
|
this.newLine = '';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Builder.prototype.build = function(jObj) {
|
||
|
if(this.options.preserveOrder){
|
||
|
return buildFromOrderedJs(jObj, this.options);
|
||
|
}else {
|
||
|
if(Array.isArray(jObj) && this.options.arrayNodeName && this.options.arrayNodeName.length > 1){
|
||
|
jObj = {
|
||
|
[this.options.arrayNodeName] : jObj
|
||
|
}
|
||
|
}
|
||
|
return this.j2x(jObj, 0).val;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
Builder.prototype.j2x = function(jObj, level) {
|
||
|
let attrStr = '';
|
||
|
let val = '';
|
||
|
for (let key in jObj) {
|
||
|
if(!Object.prototype.hasOwnProperty.call(jObj, key)) continue;
|
||
|
if (typeof jObj[key] === 'undefined') {
|
||
|
// supress undefined node only if it is not an attribute
|
||
|
if (this.isAttribute(key)) {
|
||
|
val += '';
|
||
|
}
|
||
|
} else if (jObj[key] === null) {
|
||
|
// null attribute should be ignored by the attribute list, but should not cause the tag closing
|
||
|
if (this.isAttribute(key)) {
|
||
|
val += '';
|
||
|
} else if (key[0] === '?') {
|
||
|
val += this.indentate(level) + '<' + key + '?' + this.tagEndChar;
|
||
|
} else {
|
||
|
val += this.indentate(level) + '<' + key + '/' + this.tagEndChar;
|
||
|
}
|
||
|
// val += this.indentate(level) + '<' + key + '/' + this.tagEndChar;
|
||
|
} else if (jObj[key] instanceof Date) {
|
||
|
val += this.buildTextValNode(jObj[key], key, '', level);
|
||
|
} else if (typeof jObj[key] !== 'object') {
|
||
|
//premitive type
|
||
|
const attr = this.isAttribute(key);
|
||
|
if (attr) {
|
||
|
attrStr += this.buildAttrPairStr(attr, '' + jObj[key]);
|
||
|
}else {
|
||
|
//tag value
|
||
|
if (key === this.options.textNodeName) {
|
||
|
let newval = this.options.tagValueProcessor(key, '' + jObj[key]);
|
||
|
val += this.replaceEntitiesValue(newval);
|
||
|
} else {
|
||
|
val += this.buildTextValNode(jObj[key], key, '', level);
|
||
|
}
|
||
|
}
|
||
|
} else if (Array.isArray(jObj[key])) {
|
||
|
//repeated nodes
|
||
|
const arrLen = jObj[key].length;
|
||
|
let listTagVal = "";
|
||
|
for (let j = 0; j < arrLen; j++) {
|
||
|
const item = jObj[key][j];
|
||
|
if (typeof item === 'undefined') {
|
||
|
// supress undefined node
|
||
|
} else if (item === null) {
|
||
|
if(key[0] === "?") val += this.indentate(level) + '<' + key + '?' + this.tagEndChar;
|
||
|
else val += this.indentate(level) + '<' + key + '/' + this.tagEndChar;
|
||
|
// val += this.indentate(level) + '<' + key + '/' + this.tagEndChar;
|
||
|
} else if (typeof item === 'object') {
|
||
|
if(this.options.oneListGroup ){
|
||
|
listTagVal += this.j2x(item, level + 1).val;
|
||
|
}else{
|
||
|
listTagVal += this.processTextOrObjNode(item, key, level)
|
||
|
}
|
||
|
} else {
|
||
|
listTagVal += this.buildTextValNode(item, key, '', level);
|
||
|
}
|
||
|
}
|
||
|
if(this.options.oneListGroup){
|
||
|
listTagVal = this.buildObjectNode(listTagVal, key, '', level);
|
||
|
}
|
||
|
val += listTagVal;
|
||
|
} else {
|
||
|
//nested node
|
||
|
if (this.options.attributesGroupName && key === this.options.attributesGroupName) {
|
||
|
const Ks = Object.keys(jObj[key]);
|
||
|
const L = Ks.length;
|
||
|
for (let j = 0; j < L; j++) {
|
||
|
attrStr += this.buildAttrPairStr(Ks[j], '' + jObj[key][Ks[j]]);
|
||
|
}
|
||
|
} else {
|
||
|
val += this.processTextOrObjNode(jObj[key], key, level)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return {attrStr: attrStr, val: val};
|
||
|
};
|
||
|
|
||
|
Builder.prototype.buildAttrPairStr = function(attrName, val){
|
||
|
val = this.options.attributeValueProcessor(attrName, '' + val);
|
||
|
val = this.replaceEntitiesValue(val);
|
||
|
if (this.options.suppressBooleanAttributes && val === "true") {
|
||
|
return ' ' + attrName;
|
||
|
} else return ' ' + attrName + '="' + val + '"';
|
||
|
}
|
||
|
|
||
|
function processTextOrObjNode (object, key, level) {
|
||
|
const result = this.j2x(object, level + 1);
|
||
|
if (object[this.options.textNodeName] !== undefined && Object.keys(object).length === 1) {
|
||
|
return this.buildTextValNode(object[this.options.textNodeName], key, result.attrStr, level);
|
||
|
} else {
|
||
|
return this.buildObjectNode(result.val, key, result.attrStr, level);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Builder.prototype.buildObjectNode = function(val, key, attrStr, level) {
|
||
|
if(val === ""){
|
||
|
if(key[0] === "?") return this.indentate(level) + '<' + key + attrStr+ '?' + this.tagEndChar;
|
||
|
else {
|
||
|
return this.indentate(level) + '<' + key + attrStr + this.closeTag(key) + this.tagEndChar;
|
||
|
}
|
||
|
}else{
|
||
|
|
||
|
let tagEndExp = '</' + key + this.tagEndChar;
|
||
|
let piClosingChar = "";
|
||
|
|
||
|
if(key[0] === "?") {
|
||
|
piClosingChar = "?";
|
||
|
tagEndExp = "";
|
||
|
}
|
||
|
|
||
|
// attrStr is an empty string in case the attribute came as undefined or null
|
||
|
if ((attrStr || attrStr === '') && val.indexOf('<') === -1) {
|
||
|
return ( this.indentate(level) + '<' + key + attrStr + piClosingChar + '>' + val + tagEndExp );
|
||
|
} else if (this.options.commentPropName !== false && key === this.options.commentPropName && piClosingChar.length === 0) {
|
||
|
return this.indentate(level) + `<!--${val}-->` + this.newLine;
|
||
|
}else {
|
||
|
return (
|
||
|
this.indentate(level) + '<' + key + attrStr + piClosingChar + this.tagEndChar +
|
||
|
val +
|
||
|
this.indentate(level) + tagEndExp );
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Builder.prototype.closeTag = function(key){
|
||
|
let closeTag = "";
|
||
|
if(this.options.unpairedTags.indexOf(key) !== -1){ //unpaired
|
||
|
if(!this.options.suppressUnpairedNode) closeTag = "/"
|
||
|
}else if(this.options.suppressEmptyNode){ //empty
|
||
|
closeTag = "/";
|
||
|
}else{
|
||
|
closeTag = `></${key}`
|
||
|
}
|
||
|
return closeTag;
|
||
|
}
|
||
|
|
||
|
function buildEmptyObjNode(val, key, attrStr, level) {
|
||
|
if (val !== '') {
|
||
|
return this.buildObjectNode(val, key, attrStr, level);
|
||
|
} else {
|
||
|
if(key[0] === "?") return this.indentate(level) + '<' + key + attrStr+ '?' + this.tagEndChar;
|
||
|
else {
|
||
|
return this.indentate(level) + '<' + key + attrStr + '/' + this.tagEndChar;
|
||
|
// return this.buildTagStr(level,key, attrStr);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Builder.prototype.buildTextValNode = function(val, key, attrStr, level) {
|
||
|
if (this.options.cdataPropName !== false && key === this.options.cdataPropName) {
|
||
|
return this.indentate(level) + `<![CDATA[${val}]]>` + this.newLine;
|
||
|
}else if (this.options.commentPropName !== false && key === this.options.commentPropName) {
|
||
|
return this.indentate(level) + `<!--${val}-->` + this.newLine;
|
||
|
}else if(key[0] === "?") {//PI tag
|
||
|
return this.indentate(level) + '<' + key + attrStr+ '?' + this.tagEndChar;
|
||
|
}else{
|
||
|
let textValue = this.options.tagValueProcessor(key, val);
|
||
|
textValue = this.replaceEntitiesValue(textValue);
|
||
|
|
||
|
if( textValue === ''){
|
||
|
return this.indentate(level) + '<' + key + attrStr + this.closeTag(key) + this.tagEndChar;
|
||
|
}else{
|
||
|
return this.indentate(level) + '<' + key + attrStr + '>' +
|
||
|
textValue +
|
||
|
'</' + key + this.tagEndChar;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Builder.prototype.replaceEntitiesValue = function(textValue){
|
||
|
if(textValue && textValue.length > 0 && this.options.processEntities){
|
||
|
for (let i=0; i<this.options.entities.length; i++) {
|
||
|
const entity = this.options.entities[i];
|
||
|
textValue = textValue.replace(entity.regex, entity.val);
|
||
|
}
|
||
|
}
|
||
|
return textValue;
|
||
|
}
|
||
|
|
||
|
function indentate(level) {
|
||
|
return this.options.indentBy.repeat(level);
|
||
|
}
|
||
|
|
||
|
function isAttribute(name /*, options*/) {
|
||
|
if (name.startsWith(this.options.attributeNamePrefix) && name !== this.options.textNodeName) {
|
||
|
return name.substr(this.attrPrefixLen);
|
||
|
} else {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = Builder;
|