lambda-live-debugger
Version:
Debug Lambda functions locally like it is running in the cloud
529 lines (478 loc) • 18.8 kB
JavaScript
'use strict';
//parse Empty Node as self closing node
import buildFromOrderedJs from './orderedJs2Xml.js';
import getIgnoreAttributesFn from "./ignoreAttributes.js";
import { Expression, Matcher } from 'path-expression-matcher';
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,
maxNestedTags: 100,
jPath: true // When true, callbacks receive string jPath; when false, receive Matcher instance
};
export default function Builder(options) {
this.options = Object.assign({}, defaultOptions, options);
// Convert old-style stopNodes for backward compatibility
// Old syntax: "*.tag" meant "tag anywhere in tree"
// New syntax: "..tag" means "tag anywhere in tree"
if (this.options.stopNodes && Array.isArray(this.options.stopNodes)) {
this.options.stopNodes = this.options.stopNodes.map(node => {
if (typeof node === 'string' && node.startsWith('*.')) {
// Convert old wildcard syntax to deep wildcard
return '..' + node.substring(2);
}
return node;
});
}
// Pre-compile stopNode expressions for pattern matching
this.stopNodeExpressions = [];
if (this.options.stopNodes && Array.isArray(this.options.stopNodes)) {
for (let i = 0; i < this.options.stopNodes.length; i++) {
const node = this.options.stopNodes[i];
if (typeof node === 'string') {
this.stopNodeExpressions.push(new Expression(node));
} else if (node instanceof Expression) {
this.stopNodeExpressions.push(node);
}
}
}
if (this.options.ignoreAttributes === true || this.options.attributesGroupName) {
this.isAttribute = function (/*a*/) {
return false;
};
} else {
this.ignoreAttributesFn = getIgnoreAttributesFn(this.options.ignoreAttributes)
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
}
}
// Initialize matcher for path tracking
const matcher = new Matcher();
return this.j2x(jObj, 0, matcher).val;
}
};
Builder.prototype.j2x = function (jObj, level, matcher) {
let attrStr = '';
let val = '';
if (this.options.maxNestedTags && matcher.getDepth() >= this.options.maxNestedTags) {
throw new Error("Maximum nested tags exceeded");
}
// Get jPath based on option: string for backward compatibility, or Matcher for new features
const jPath = this.options.jPath ? matcher.toString() : matcher;
// Check if current node is a stopNode (will be used for attribute encoding)
const isCurrentStopNode = this.checkStopNode(matcher);
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 === this.options.cdataPropName) {
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, matcher);
} else if (typeof jObj[key] !== 'object') {
//premitive type
const attr = this.isAttribute(key);
if (attr && !this.ignoreAttributesFn(attr, jPath)) {
attrStr += this.buildAttrPairStr(attr, '' + jObj[key], isCurrentStopNode);
} else if (!attr) {
//tag value
if (key === this.options.textNodeName) {
let newval = this.options.tagValueProcessor(key, '' + jObj[key]);
val += this.replaceEntitiesValue(newval);
} else {
// Check if this is a stopNode before building
matcher.push(key);
const isStopNode = this.checkStopNode(matcher);
matcher.pop();
if (isStopNode) {
// Build as raw content without encoding
const textValue = '' + jObj[key];
if (textValue === '') {
val += this.indentate(level) + '<' + key + this.closeTag(key) + this.tagEndChar;
} else {
val += this.indentate(level) + '<' + key + '>' + textValue + '</' + key + this.tagEndChar;
}
} else {
val += this.buildTextValNode(jObj[key], key, '', level, matcher);
}
}
}
} else if (Array.isArray(jObj[key])) {
//repeated nodes
const arrLen = jObj[key].length;
let listTagVal = "";
let listTagAttr = "";
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) {
// Push tag to matcher before recursive call
matcher.push(key);
const result = this.j2x(item, level + 1, matcher);
// Pop tag from matcher after recursive call
matcher.pop();
listTagVal += result.val;
if (this.options.attributesGroupName && item.hasOwnProperty(this.options.attributesGroupName)) {
listTagAttr += result.attrStr
}
} else {
listTagVal += this.processTextOrObjNode(item, key, level, matcher)
}
} else {
if (this.options.oneListGroup) {
let textValue = this.options.tagValueProcessor(key, item);
textValue = this.replaceEntitiesValue(textValue);
listTagVal += textValue;
} else {
// Check if this is a stopNode before building
matcher.push(key);
const isStopNode = this.checkStopNode(matcher);
matcher.pop();
if (isStopNode) {
// Build as raw content without encoding
const textValue = '' + item;
if (textValue === '') {
listTagVal += this.indentate(level) + '<' + key + this.closeTag(key) + this.tagEndChar;
} else {
listTagVal += this.indentate(level) + '<' + key + '>' + textValue + '</' + key + this.tagEndChar;
}
} else {
listTagVal += this.buildTextValNode(item, key, '', level, matcher);
}
}
}
}
if (this.options.oneListGroup) {
listTagVal = this.buildObjectNode(listTagVal, key, listTagAttr, 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]], isCurrentStopNode);
}
} else {
val += this.processTextOrObjNode(jObj[key], key, level, matcher)
}
}
}
return { attrStr: attrStr, val: val };
};
Builder.prototype.buildAttrPairStr = function (attrName, val, isStopNode) {
if (!isStopNode) {
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, matcher) {
// Extract attributes to pass to matcher
const attrValues = this.extractAttributes(object);
// Push tag to matcher before recursion WITH attributes
matcher.push(key, attrValues);
// Check if this entire node is a stopNode
const isStopNode = this.checkStopNode(matcher);
if (isStopNode) {
// For stopNodes, build raw content without entity encoding
const rawContent = this.buildRawContent(object);
const attrStr = this.buildAttributesForStopNode(object);
matcher.pop();
return this.buildObjectNode(rawContent, key, attrStr, level);
}
const result = this.j2x(object, level + 1, matcher);
// Pop tag from matcher after recursion
matcher.pop();
if (object[this.options.textNodeName] !== undefined && Object.keys(object).length === 1) {
return this.buildTextValNode(object[this.options.textNodeName], key, result.attrStr, level, matcher);
} else {
return this.buildObjectNode(result.val, key, result.attrStr, level);
}
}
// Helper method to extract attributes from an object
Builder.prototype.extractAttributes = function (obj) {
if (!obj || typeof obj !== 'object') return null;
const attrValues = {};
let hasAttrs = false;
// Check for attributesGroupName (when attributes are grouped)
if (this.options.attributesGroupName && obj[this.options.attributesGroupName]) {
const attrGroup = obj[this.options.attributesGroupName];
for (let attrKey in attrGroup) {
if (!Object.prototype.hasOwnProperty.call(attrGroup, attrKey)) continue;
// Remove attribute prefix if present
const cleanKey = attrKey.startsWith(this.options.attributeNamePrefix)
? attrKey.substring(this.options.attributeNamePrefix.length)
: attrKey;
attrValues[cleanKey] = attrGroup[attrKey];
hasAttrs = true;
}
} else {
// Look for individual attributes (prefixed with attributeNamePrefix)
for (let key in obj) {
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
const attr = this.isAttribute(key);
if (attr) {
attrValues[attr] = obj[key];
hasAttrs = true;
}
}
}
return hasAttrs ? attrValues : null;
};
// Build raw content for stopNode without entity encoding
Builder.prototype.buildRawContent = function (obj) {
if (typeof obj === 'string') {
return obj; // Already a string, return as-is
}
if (typeof obj !== 'object' || obj === null) {
return String(obj);
}
// Check if this is a stopNode data from parser: { "#text": "raw xml", "@_attr": "val" }
if (obj[this.options.textNodeName] !== undefined) {
return obj[this.options.textNodeName]; // Return raw text without encoding
}
// Build raw XML from nested structure
let content = '';
for (let key in obj) {
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
// Skip attributes
if (this.isAttribute(key)) continue;
if (this.options.attributesGroupName && key === this.options.attributesGroupName) continue;
const value = obj[key];
if (key === this.options.textNodeName) {
content += value; // Raw text
} else if (Array.isArray(value)) {
// Array of same tag
for (let item of value) {
if (typeof item === 'string' || typeof item === 'number') {
content += `<${key}>${item}</${key}>`;
} else if (typeof item === 'object' && item !== null) {
const nestedContent = this.buildRawContent(item);
const nestedAttrs = this.buildAttributesForStopNode(item);
if (nestedContent === '') {
content += `<${key}${nestedAttrs}/>`;
} else {
content += `<${key}${nestedAttrs}>${nestedContent}</${key}>`;
}
}
}
} else if (typeof value === 'object' && value !== null) {
// Nested object
const nestedContent = this.buildRawContent(value);
const nestedAttrs = this.buildAttributesForStopNode(value);
if (nestedContent === '') {
content += `<${key}${nestedAttrs}/>`;
} else {
content += `<${key}${nestedAttrs}>${nestedContent}</${key}>`;
}
} else {
// Primitive value
content += `<${key}>${value}</${key}>`;
}
}
return content;
};
// Build attribute string for stopNode (no entity encoding)
Builder.prototype.buildAttributesForStopNode = function (obj) {
if (!obj || typeof obj !== 'object') return '';
let attrStr = '';
// Check for attributesGroupName (when attributes are grouped)
if (this.options.attributesGroupName && obj[this.options.attributesGroupName]) {
const attrGroup = obj[this.options.attributesGroupName];
for (let attrKey in attrGroup) {
if (!Object.prototype.hasOwnProperty.call(attrGroup, attrKey)) continue;
const cleanKey = attrKey.startsWith(this.options.attributeNamePrefix)
? attrKey.substring(this.options.attributeNamePrefix.length)
: attrKey;
const val = attrGroup[attrKey];
if (val === true && this.options.suppressBooleanAttributes) {
attrStr += ' ' + cleanKey;
} else {
attrStr += ' ' + cleanKey + '="' + val + '"'; // No encoding for stopNode
}
}
} else {
// Look for individual attributes
for (let key in obj) {
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
const attr = this.isAttribute(key);
if (attr) {
const val = obj[key];
if (val === true && this.options.suppressBooleanAttributes) {
attrStr += ' ' + attr;
} else {
attrStr += ' ' + attr + '="' + val + '"'; // No encoding for stopNode
}
}
}
}
return attrStr;
};
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;
}
Builder.prototype.checkStopNode = function (matcher) {
if (!this.stopNodeExpressions || this.stopNodeExpressions.length === 0) return false;
for (let i = 0; i < this.stopNodeExpressions.length; i++) {
if (matcher.matches(this.stopNodeExpressions[i])) {
return true;
}
}
return false;
}
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, matcher) {
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 {
// Normal processing: apply tagValueProcessor and entity replacement
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;
}
}