'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 + '' + textValue + '${item}`; } 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}`; } } } } 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}`; } } else { // Primitive value content += `<${key}>${value}`; } } 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 = '' + val + tagEndExp); } else if (this.options.commentPropName !== false && key === this.options.commentPropName && piClosingChar.length === 0) { return this.indentate(level) + `` + 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 = `>/g, ']]]]>'); return this.indentate(level) + `` + this.newLine; } else if (this.options.commentPropName !== false && key === this.options.commentPropName) { const safeVal = String(val) .replace(/--/g, '- -') // -- is illegal anywhere in comment content .replace(/-$/, '- '); // trailing - would form -- with the closing --> return this.indentate(level) + `` + 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 + ' 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; } }