From 33b2961a4bd51efa9a039bafad8a181c2699d011 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 17:17:03 +0000 Subject: [PATCH 1/2] Bump fast-xml-builder from 1.1.5 to 1.2.0 Bumps [fast-xml-builder](https://github.com/NaturalIntelligence/fast-xml-builder) from 1.1.5 to 1.2.0. - [Changelog](https://github.com/NaturalIntelligence/fast-xml-builder/blob/main/CHANGELOG.md) - [Commits](https://github.com/NaturalIntelligence/fast-xml-builder/compare/v1.1.5...v1.2.0) --- updated-dependencies: - dependency-name: fast-xml-builder dependency-version: 1.2.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 87 +++++++++++------------------------------------ 1 file changed, 20 insertions(+), 67 deletions(-) diff --git a/package-lock.json b/package-lock.json index b1c6525..0145efc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1672,9 +1672,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1689,9 +1686,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1706,9 +1700,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1723,9 +1714,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1740,9 +1728,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1757,9 +1742,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1774,9 +1756,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1791,9 +1770,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1808,9 +1784,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1825,9 +1798,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1842,9 +1812,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1859,9 +1826,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1876,9 +1840,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2468,9 +2429,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2485,9 +2443,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2502,9 +2457,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2519,9 +2471,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2536,9 +2485,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2553,9 +2499,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2570,9 +2513,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2587,9 +2527,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4235,9 +4172,9 @@ "license": "MIT" }, "node_modules/fast-xml-builder": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz", - "integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", "funding": [ { "type": "github", @@ -4246,7 +4183,8 @@ ], "license": "MIT", "dependencies": { - "path-expression-matcher": "^1.1.3" + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" } }, "node_modules/fast-xml-parser": { @@ -7246,6 +7184,21 @@ "node": ">=0.10.0" } }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", From 91aa6dc36f114942d56105a57c9d927d11a5bbaa Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Fri, 8 May 2026 15:08:09 -0500 Subject: [PATCH 2/2] Regenerate dist --- dist/index.js | 577 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 492 insertions(+), 85 deletions(-) diff --git a/dist/index.js b/dist/index.js index 5e0f718..57dc07f 100644 --- a/dist/index.js +++ b/dist/index.js @@ -70338,20 +70338,349 @@ class Matcher { return this._view; } } +;// CONCATENATED MODULE: ./node_modules/fast-xml-builder/src/util.js + + +function safeComment(val) { + return String(val) + .replace(/--/g, '- -') // -- is illegal anywhere in comment content + .replace(/--/g, '- -') // handle the scenario when 2 consiucative dashes appears + .replace(/-$/, '- '); // trailing - would form -- with the closing --> +} + +function safeCdata(val) { + return String(val).replace(/\]\]>/g, ']]]]>') +} + +function escapeAttribute(val) { + return String(val).replace(/"/g, '"').replace(/'/g, ''') +} +;// CONCATENATED MODULE: ./node_modules/xml-naming/src/index.js +/** + * xml-naming + * Validates XML Name productions as defined in the XML 1.0 and 1.1 specifications. + * Covers: Name, NCName, QName, NMToken, NMTokens + * + * XML 1.0 spec: https://www.w3.org/TR/xml/#NT-Name + * XML 1.1 spec: https://www.w3.org/TR/xml11/#NT-NameStartChar + * XML NS spec: https://www.w3.org/TR/xml-names/#NT-NCName + */ + +// --------------------------------------------------------------------------- +// Character class strings — XML 1.0 +// +// NameStartChar ::= ":" | [A-Z] | "_" | [a-z] +// | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] +// | [#x370-#x37D] | [#x37F-#x1FFF] <- split to exclude #x0487 +// | [#x200C-#x200D] +// | [#x2070-#x218F] | [#x2C00-#x2FEF] +// | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] +// +// NameChar ::= NameStartChar | "-" | "." | [0-9] +// | #xB7 | [#x0300-#x036F] | [#x203F-#x2040] +// +// Note: \u0487 (Combining Cyrillic Millions Sign) was added in Unicode 4.0, +// after XML 1.0 was defined against Unicode 2.0. It falls inside the range +// \u037F-\u1FFF but must be excluded. We split that range into +// \u037F-\u0486 and \u0488-\u1FFF to exclude it explicitly. +// --------------------------------------------------------------------------- + +const nameStartChar10 = + ':A-Za-z_' + + '\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF' + + '\u0370-\u037D' + + '\u037F-\u0486\u0488-\u1FFF' + // split to exclude \u0487 + '\u200C-\u200D' + + '\u2070-\u218F' + + '\u2C00-\u2FEF' + + '\u3001-\uD7FF' + + '\uF900-\uFDCF' + + '\uFDF0-\uFFFD'; + +const nameChar10 = + nameStartChar10 + + '\\-\\.\\d' + + '\u00B7' + + '\u0300-\u036F' + + '\u203F-\u2040'; + +// --------------------------------------------------------------------------- +// Character class strings — XML 1.1 +// +// Differences from XML 1.0: +// +// NameStartChar: +// 1.0 has split ranges: \u00C0-\u00D6, \u00D8-\u00F6, \u00F8-\u02FF +// 1.1 merges them into: \u00C0-\u02FF +// (\u00D7 x and \u00F7 / are division symbols, excluded in both versions) +// +// 1.0 tops out at \uFFFD (BMP only) +// 1.1 adds \u{10000}-\u{EFFFF} (supplementary planes) +// These require the /u flag on the RegExp — see buildRegexes below. +// +// NameChar: +// 1.1 adds \u0487 (Combining Cyrillic Millions Sign, added in Unicode 4.0) +// --------------------------------------------------------------------------- + +const nameStartChar11 = + ':A-Za-z_' + + '\u00C0-\u02FF' + // merged — 1.0 had three split ranges here + '\u0370-\u037D' + + '\u037F-\u0486\u0488-\u1FFF' + // split to exclude \u0487 (combining mark, never a NameStartChar) + '\u200C-\u200D' + + '\u2070-\u218F' + + '\u2C00-\u2FEF' + + '\u3001-\uD7FF' + + '\uF900-\uFDCF' + + '\uFDF0-\uFFFD' + + '\u{10000}-\u{EFFFF}'; // supplementary planes — REQUIRES /u flag on RegExp + +const nameChar11 = + nameStartChar11 + + '\\-\\.\\d' + + '\u00B7' + + '\u0300-\u036F' + + '\u0487' + // Combining Cyrillic Millions Sign — valid in 1.1, not 1.0 + '\u203F-\u2040'; + +// --------------------------------------------------------------------------- +// Regex builders +// +// XML 1.0 regexes: no flags — BMP only, standard JS regex behaviour. +// XML 1.1 regexes: /u flag — required for \u{10000}-\u{EFFFF} to match actual +// supplementary code points rather than lone surrogates (which are illegal XML). +// --------------------------------------------------------------------------- + +const buildRegexes = (startChar, char, flags = '') => { + const ncStart = startChar.replace(':', ''); + const ncChar = char.replace(':', ''); + const ncNamePat = `[${ncStart}][${ncChar}]*`; + + return { + name: new RegExp(`^[${startChar}][${char}]*$`, flags), + ncName: new RegExp(`^${ncNamePat}$`, flags), + qName: new RegExp(`^${ncNamePat}(?::${ncNamePat})?$`, flags), + nmToken: new RegExp(`^[${char}]+$`, flags), + nmTokens: new RegExp(`^[${char}]+(?:\\s+[${char}]+)*$`, flags), + }; +}; + +const regexes10 = buildRegexes(nameStartChar10, nameChar10); // no /u — BMP only +const regexes11 = buildRegexes(nameStartChar11, nameChar11, 'u'); // /u — enables \u{10000}-\u{EFFFF} + +const getRegexes = (xmlVersion = '1.0') => + xmlVersion === '1.1' ? regexes11 : regexes10; + +// --------------------------------------------------------------------------- +// Boolean validators +// --------------------------------------------------------------------------- + +/** + * Returns true if the string is a valid XML Name. + * Colons are allowed anywhere (Name production). + * Used for: DOCTYPE entity names, notation names, DTD element declarations. + */ +const src_name = (str, { xmlVersion = '1.0' } = {}) => + getRegexes(xmlVersion).name.test(str); + +/** + * Returns true if the string is a valid NCName (Non-Colonized Name). + * Colons are not permitted. + * Used for: namespace prefixes, local names, SVG id attributes. + */ +const ncName = (str, { xmlVersion = '1.0' } = {}) => + getRegexes(xmlVersion).ncName.test(str); + +/** + * Returns true if the string is a valid QName (Qualified Name). + * Allows exactly one colon as a prefix separator: prefix:localName. + * Used for: element and attribute names in namespace-aware XML/SVG. + */ +const qName = (str, { xmlVersion = '1.0' } = {}) => + getRegexes(xmlVersion).qName.test(str); + +/** + * Returns true if the string is a valid NMToken. + * Like Name but no restriction on the first character. + * Used for: DTD NMTOKEN attribute values. + */ +const nmToken = (str, { xmlVersion = '1.0' } = {}) => + getRegexes(xmlVersion).nmToken.test(str); + +/** + * Returns true if the string is a valid NMTokens value. + * A whitespace-separated list of NMToken values. + * Used for: DTD NMTOKENS attribute values. + */ +const nmTokens = (str, { xmlVersion = '1.0' } = {}) => + getRegexes(xmlVersion).nmTokens.test(str); + +// --------------------------------------------------------------------------- +// Diagnostic validator +// --------------------------------------------------------------------------- + +const PRODUCTIONS = (/* unused pure expression or super */ null && (['name', 'ncName', 'qName', 'nmToken', 'nmTokens'])); + +/** + * Validates a string against a named production and returns a detailed result. + * + * @param {string} str + * @param {'name'|'ncName'|'qName'|'nmToken'|'nmTokens'} production + * @param {{ xmlVersion?: '1.0'|'1.1' }} [opts] + * @returns {{ valid: boolean, production: string, input: string, reason?: string, position?: number }} + */ +const validate = (str, production, { xmlVersion = '1.0' } = {}) => { + if (!PRODUCTIONS.includes(production)) { + throw new TypeError( + `Unknown production "${production}". Must be one of: ${PRODUCTIONS.join(', ')}` + ); + } + + const validators = { name: src_name, ncName, qName, nmToken, nmTokens }; + const isValid = validators[production](str, { xmlVersion }); + + if (isValid) return { valid: true, production, input: str }; + + let reason = 'Does not match the production rules'; + let position; + + if (str.length === 0) { + reason = 'Input is empty'; + } else if (production === 'ncName' && str.includes(':')) { + position = str.indexOf(':'); + reason = 'Colon is not allowed in NCName'; + } else if (production === 'qName' && str.startsWith(':')) { + reason = 'QName cannot start with a colon'; + position = 0; + } else if (production === 'qName' && str.endsWith(':')) { + reason = 'QName cannot end with a colon'; + position = str.length - 1; + } else if (production === 'qName' && (str.match(/:/g) || []).length > 1) { + reason = 'QName can have at most one colon'; + position = str.lastIndexOf(':'); + } else if ( + ['name', 'ncName', 'qName'].includes(production) && + !/^[:A-Za-z_\u00C0-\uFFFD]/.test(str[0]) + ) { + reason = `First character "${str[0]}" is not a valid NameStartChar`; + position = 0; + } else { + for (let i = 0; i < str.length; i++) { + if (!/[\w\-\\.:\u00B7\u00C0-\uFFFD]/.test(str[i])) { + reason = `Character "${str[i]}" at position ${i} is not a valid NameChar`; + position = i; + break; + } + } + } + + return { valid: false, production, input: str, reason, position }; +}; + +// --------------------------------------------------------------------------- +// Batch validator +// --------------------------------------------------------------------------- + +/** + * Validates an array of strings against a named production. + * + * @param {string[]} strings + * @param {'name'|'ncName'|'qName'|'nmToken'|'nmTokens'} production + * @param {{ xmlVersion?: '1.0'|'1.1' }} [opts] + * @returns {Array<{ valid: boolean, production: string, input: string, reason?: string, position?: number }>} + */ +const validateAll = (strings, production, opts = {}) => + strings.map(str => validate(str, production, opts)); + +// --------------------------------------------------------------------------- +// Sanitizer +// --------------------------------------------------------------------------- + +/** + * Transforms an invalid string into the nearest valid XML name for the given production. + * + * @param {string} str + * @param {'name'|'ncName'|'qName'|'nmToken'|'nmTokens'} production + * @param {{ replacement?: string }} [opts] + * @returns {string} + */ +const sanitize = (str, production = 'name', { replacement = '_' } = {}) => { + if (!str) return replacement; + + let result = str; + + // Strip colons for NCName + if (production === 'ncName') { + result = result.replace(/:/g, ''); + } + + // Replace illegal characters + result = result.replace(/[^\w\-\.:\u00B7\u00C0-\uFFFD]/g, replacement); + + // Fix invalid start character for Name / NCName / QName + if (production !== 'nmToken' && production !== 'nmTokens') { + if (/^[\-\.\d]/.test(result)) { + result = replacement + result; + } + } + + return result || replacement; +}; ;// CONCATENATED MODULE: ./node_modules/fast-xml-builder/src/orderedJs2Xml.js + + const EOL = "\n"; /** - * - * @param {array} jArray - * @param {any} options - * @returns + * Detect XML version from the first element of the ordered array input. + * The first element must be a ?xml processing instruction with a version attribute. + * Returns '1.0' if not found. + * + * @param {array} jArray + * @param {object} options + */ +function detectXmlVersionFromArray(jArray, options) { + if (!Array.isArray(jArray) || jArray.length === 0) return '1.0'; + const first = jArray[0]; + const firstKey = propName(first); + if (firstKey === '?xml') { + const attrs = first[':@']; + if (attrs) { + const versionKey = options.attributeNamePrefix + 'version'; + if (attrs[versionKey]) return attrs[versionKey]; + } + } + return '1.0'; +} + +/** + * Resolve a tag or attribute name through sanitizeName if configured. + * Validation via xml-naming's qName is performed first; the sanitizeName + * callback is invoked only when the name is invalid. If sanitizeName is + * false (default), no validation occurs and the name is used as-is. + * + * @param {string} name - raw name from the JS object + * @param {boolean} isAttribute - true when resolving an attribute name + * @param {object} options + * @param {Matcher} matcher - current matcher state (readonly from callback perspective) + * @param {string} xmlVersion - '1.0' or '1.1', forwarded to xml-naming + */ +function resolveTagName(name, isAttribute, options, matcher, xmlVersion) { + if (!options.sanitizeName) return name; + if (qName(name, { xmlVersion })) return name; + return options.sanitizeName(name, { isAttribute, matcher: matcher.readOnly() }); +} + +/** + * @param {array} jArray + * @param {any} options + * @returns */ function toXml(jArray, options) { let indentation = ""; - if (options.format && options.indentBy.length > 0) { + if (options.format) { indentation = EOL; } @@ -70368,13 +70697,16 @@ function toXml(jArray, options) { } } + // Detect XML version for use in name validation + const xmlVersion = detectXmlVersionFromArray(jArray, options); + // Initialize matcher for path tracking const matcher = new Matcher(); - return arrToStr(jArray, options, indentation, matcher, stopNodeExpressions); + return arrToStr(jArray, options, indentation, matcher, stopNodeExpressions, xmlVersion); } -function arrToStr(arr, options, indentation, matcher, stopNodeExpressions) { +function arrToStr(arr, options, indentation, matcher, stopNodeExpressions, xmlVersion) { let xmlStr = ""; let isPreviousElementTag = false; @@ -70394,20 +70726,32 @@ function arrToStr(arr, options, indentation, matcher, stopNodeExpressions) { for (let i = 0; i < arr.length; i++) { const tagObj = arr[i]; - const tagName = propName(tagObj); - if (tagName === undefined) continue; + const rawTagName = propName(tagObj); + if (rawTagName === undefined) continue; + + // Special names are exempt from sanitizeName: internal conventions and PI tags + // are not user-supplied XML element names. + const isSpecialName = rawTagName === options.textNodeName + || rawTagName === options.cdataPropName + || rawTagName === options.commentPropName + || rawTagName[0] === '?'; + + // Resolve tag name (may transform it; may throw for invalid names) + const tagName = isSpecialName + ? rawTagName + : resolveTagName(rawTagName, false, options, matcher, xmlVersion); // Extract attributes from ":@" property const attrValues = extractAttributeValues(tagObj[":@"], options); - // Push tag to matcher WITH attributes + // Push resolved tag to matcher WITH attributes matcher.push(tagName, attrValues); // Check if this is a stop node using Expression matching const isStopNode = checkStopNode(matcher, stopNodeExpressions); if (tagName === options.textNodeName) { - let tagText = tagObj[tagName]; + let tagText = tagObj[rawTagName]; if (!isStopNode) { tagText = options.tagValueProcessor(tagName, tagText); tagText = replaceEntitiesValue(tagText, options); @@ -70423,27 +70767,25 @@ function arrToStr(arr, options, indentation, matcher, stopNodeExpressions) { if (isPreviousElementTag) { xmlStr += indentation; } - const val = tagObj[tagName][0][options.textNodeName]; - const safeVal = String(val).replace(/\]\]>/g, ']]]]>'); + const val = tagObj[rawTagName][0][options.textNodeName]; + const safeVal = safeCdata(val); xmlStr += ``; isPreviousElementTag = false; matcher.pop(); continue; } else if (tagName === options.commentPropName) { - const val = tagObj[tagName][0][options.textNodeName] - const safeVal = String(val) - .replace(/--/g, '- -') // -- is illegal anywhere in comment content - .replace(/-$/, '- '); // trailing - would form -- with the closing --> + const val = tagObj[rawTagName][0][options.textNodeName]; + const safeVal = safeComment(val); xmlStr += indentation + ``; isPreviousElementTag = true; matcher.pop(); continue; } else if (tagName[0] === "?") { - const attStr = attr_to_str(tagObj[":@"], options, isStopNode); + const attStr = attr_to_str(tagObj[":@"], options, isStopNode, matcher, xmlVersion); const tempInd = tagName === "?xml" ? "" : indentation; - let piTextNodeName = tagObj[tagName][0][options.textNodeName]; - piTextNodeName = piTextNodeName.length !== 0 ? " " + piTextNodeName : ""; //remove extra spacing - xmlStr += tempInd + `<${tagName}${piTextNodeName}${attStr}?>`; + // Text node content on PI/XML declaration tags is intentionally ignored. + // Only attributes are valid on these tags per the XML spec. + xmlStr += tempInd + `<${tagName}${attStr}?>`; isPreviousElementTag = true; matcher.pop(); continue; @@ -70455,16 +70797,15 @@ function arrToStr(arr, options, indentation, matcher, stopNodeExpressions) { } // Pass isStopNode to attr_to_str so attributes are also not processed for stopNodes - const attStr = attr_to_str(tagObj[":@"], options, isStopNode); + const attStr = attr_to_str(tagObj[":@"], options, isStopNode, matcher, xmlVersion); const tagStart = indentation + `<${tagName}${attStr}`; // If this is a stopNode, get raw content without processing let tagValue; if (isStopNode) { - tagValue = orderedJs2Xml_getRawContent(tagObj[tagName], options); + tagValue = orderedJs2Xml_getRawContent(tagObj[rawTagName], options); } else { - - tagValue = arrToStr(tagObj[tagName], options, newIdentation, matcher, stopNodeExpressions); + tagValue = arrToStr(tagObj[rawTagName], options, newIdentation, matcher, stopNodeExpressions, xmlVersion); } if (options.unpairedTags.indexOf(tagName) !== -1) { @@ -70508,7 +70849,7 @@ function extractAttributeValues(attrMap, options) { const cleanAttrName = attr.startsWith(options.attributeNamePrefix) ? attr.substr(options.attributeNamePrefix.length) : attr; - attrValues[cleanAttrName] = attrMap[attr]; + attrValues[cleanAttrName] = escapeAttribute(attrMap[attr]); hasAttrs = true; } @@ -70546,9 +70887,7 @@ function orderedJs2Xml_getRawContent(arr, options) { // Processing instruction - skip for stopNodes continue; } else if (tagName) { - // Nested tags within stopNode - // Recursively get raw content and reconstruct the tag - // For stopNodes, we don't process attributes either + // Nested tags within stopNode — no sanitizeName, content is raw const attStr = attr_to_str_raw(item[":@"], options); const nestedContent = orderedJs2Xml_getRawContent(item[tagName], options); @@ -70575,7 +70914,7 @@ function attr_to_str_raw(attrMap, options) { if (attrVal === true && options.suppressBooleanAttributes) { attrStr += ` ${attr.substr(options.attributeNamePrefix.length)}`; } else { - attrStr += ` ${attr.substr(options.attributeNamePrefix.length)}="${attrVal}"`; + attrStr += ` ${attr.substr(options.attributeNamePrefix.length)}="${escapeAttribute(attrVal)}"`; } } } @@ -70591,13 +70930,23 @@ function propName(obj) { } } -function attr_to_str(attrMap, options, isStopNode) { +/** + * Build attribute string, resolving attribute names through sanitizeName when configured. + * Accepts matcher so the callback has path context. + */ +function attr_to_str(attrMap, options, isStopNode, matcher, xmlVersion) { let attrStr = ""; if (attrMap && !options.ignoreAttributes) { for (let attr in attrMap) { if (!Object.prototype.hasOwnProperty.call(attrMap, attr)) continue; - let attrVal; + // Strip prefix to get the clean XML attribute name, then optionally sanitize it + const cleanAttrName = attr.substr(options.attributeNamePrefix.length); + const resolvedAttrName = isStopNode + ? cleanAttrName // stopNodes are raw — skip sanitizeName for attr names too + : resolveTagName(cleanAttrName, true, options, matcher, xmlVersion); + + let attrVal; if (isStopNode) { // For stopNodes, use raw value without any processing attrVal = attrMap[attr]; @@ -70608,9 +70957,9 @@ function attr_to_str(attrMap, options, isStopNode) { } if (attrVal === true && options.suppressBooleanAttributes) { - attrStr += ` ${attr.substr(options.attributeNamePrefix.length)}`; + attrStr += ` ${resolvedAttrName}`; } else { - attrStr += ` ${attr.substr(options.attributeNamePrefix.length)}="${attrVal}"`; + attrStr += ` ${resolvedAttrName}="${escapeAttribute(attrVal)}"`; } } } @@ -70637,14 +70986,6 @@ function replaceEntitiesValue(textValue, options) { } return textValue; } - -function cdataVal(val) { - -} - -function commentVal(val) { - -} ;// CONCATENATED MODULE: ./node_modules/fast-xml-builder/src/ignoreAttributes.js function getIgnoreAttributesFn(ignoreAttributes) { if (typeof ignoreAttributes === 'function') { @@ -70671,6 +71012,8 @@ function getIgnoreAttributesFn(ignoreAttributes) { + + const fxb_defaultOptions = { attributeNamePrefix: '@_', attributesGroupName: false, @@ -70704,7 +71047,11 @@ const fxb_defaultOptions = { // transformAttributeName: false, oneListGroup: false, maxNestedTags: 100, - jPath: true // When true, callbacks receive string jPath; when false, receive Matcher instance + jPath: true, // When true, callbacks receive string jPath; when false, receive Matcher instance + sanitizeName: false // false = allow all names as-is (default, backward-compatible). + // Set to a function (name, { isAttribute, matcher }) => string to + // validate/sanitize tag and attribute names. Throw inside the function + // to reject an invalid name. }; function Builder(options) { @@ -70761,6 +71108,44 @@ function Builder(options) { } } +/** + * Detect XML version from the ?xml declaration at the root of a plain-object input. + * Checks both attributesGroupName and flat attribute forms. + * Returns '1.0' if no declaration is found. + */ +function detectXmlVersionFromObj(jObj, options) { + const decl = jObj['?xml']; + if (decl && typeof decl === 'object') { + // attributesGroupName path e.g. { '$$': { '@_version': '1.1' } } + if (options.attributesGroupName && decl[options.attributesGroupName]) { + const v = decl[options.attributesGroupName][options.attributeNamePrefix + 'version']; + if (v) return v; + } + // flat attribute path e.g. { '@_version': '1.1' } + const v = decl[options.attributeNamePrefix + 'version']; + if (v) return v; + } + return '1.0'; +} + +/** + * Resolve a tag or attribute name through sanitizeName if configured. + * Validation via xml-naming's qName is performed first; the sanitizeName + * callback is invoked only when the name is invalid. If sanitizeName is + * false (default), no validation occurs and the name is used as-is. + * + * @param {string} name - raw name from the JS object + * @param {boolean} isAttribute - true when resolving an attribute name + * @param {object} options + * @param {Matcher} matcher - current matcher state (readonly from callback perspective) + * @param {string} xmlVersion - '1.0' or '1.1', forwarded to xml-naming + */ +function fxb_resolveTagName(name, isAttribute, options, matcher, xmlVersion) { + if (!options.sanitizeName) return name; + if (qName(name, { xmlVersion })) return name; + return options.sanitizeName(name, { isAttribute, matcher: matcher.readOnly() }); +} + Builder.prototype.build = function (jObj) { if (this.options.preserveOrder) { return toXml(jObj, this.options); @@ -70772,11 +71157,12 @@ Builder.prototype.build = function (jObj) { } // Initialize matcher for path tracking const matcher = new Matcher(); - return this.j2x(jObj, 0, matcher).val; + const xmlVersion = detectXmlVersionFromObj(jObj, this.options); + return this.j2x(jObj, 0, matcher, xmlVersion).val; } }; -Builder.prototype.j2x = function (jObj, level, matcher) { +Builder.prototype.j2x = function (jObj, level, matcher, xmlVersion) { let attrStr = ''; let val = ''; if (this.options.maxNestedTags && matcher.getDepth() >= this.options.maxNestedTags) { @@ -70790,6 +71176,22 @@ Builder.prototype.j2x = function (jObj, level, matcher) { for (let key in jObj) { if (!Object.prototype.hasOwnProperty.call(jObj, key)) continue; + + // Resolve the key through sanitizeName before any use. + // Special keys (textNodeName, cdataPropName, commentPropName, attributeNamePrefix, + // attributesGroupName, "?" PI tags) are exempt — they are builder-internal conventions, + // not user-supplied XML names. + const isSpecialKey = key === this.options.textNodeName + || key === this.options.cdataPropName + || key === this.options.commentPropName + || (this.options.attributesGroupName && key === this.options.attributesGroupName) + || this.isAttribute(key) + || key[0] === '?'; + + const resolvedKey = isSpecialKey + ? key + : fxb_resolveTagName(key, false, this.options, matcher, xmlVersion); + if (typeof jObj[key] === 'undefined') { // supress undefined node only if it is not an attribute if (this.isAttribute(key)) { @@ -70799,21 +71201,22 @@ Builder.prototype.j2x = function (jObj, level, matcher) { // 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) { + } else if (resolvedKey === this.options.cdataPropName || resolvedKey === this.options.commentPropName) { val += ''; - } else if (key[0] === '?') { - val += this.indentate(level) + '<' + key + '?' + this.tagEndChar; + } else if (resolvedKey[0] === '?') { + val += this.indentate(level) + '<' + resolvedKey + '?' + this.tagEndChar; } else { - val += this.indentate(level) + '<' + key + '/' + this.tagEndChar; + val += this.indentate(level) + '<' + resolvedKey + '/' + this.tagEndChar; } - // val += this.indentate(level) + '<' + key + '/' + this.tagEndChar; } else if (jObj[key] instanceof Date) { - val += this.buildTextValNode(jObj[key], key, '', level, matcher); + val += this.buildTextValNode(jObj[key], resolvedKey, '', 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); + // Resolve the attribute name through sanitizeName + const resolvedAttr = fxb_resolveTagName(attr, true, this.options, matcher, xmlVersion); + attrStr += this.buildAttrPairStr(resolvedAttr, '' + jObj[key], isCurrentStopNode); } else if (!attr) { //tag value if (key === this.options.textNodeName) { @@ -70821,7 +71224,7 @@ Builder.prototype.j2x = function (jObj, level, matcher) { val += this.replaceEntitiesValue(newval); } else { // Check if this is a stopNode before building - matcher.push(key); + matcher.push(resolvedKey); const isStopNode = this.checkStopNode(matcher); matcher.pop(); @@ -70829,12 +71232,12 @@ Builder.prototype.j2x = function (jObj, level, matcher) { // Build as raw content without encoding const textValue = '' + jObj[key]; if (textValue === '') { - val += this.indentate(level) + '<' + key + this.closeTag(key) + this.tagEndChar; + val += this.indentate(level) + '<' + resolvedKey + this.closeTag(resolvedKey) + this.tagEndChar; } else { - val += this.indentate(level) + '<' + key + '>' + textValue + '' + textValue + '' + textValue + '' + textValue + '/g, ']]]]>'); + const safeVal = safeCdata(val); 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 --> + const safeVal = safeComment(val); return this.indentate(level) + `` + this.newLine; } else if (key[0] === "?") {//PI tag return this.indentate(level) + '<' + key + attrStr + '?' + this.tagEndChar; @@ -71278,7 +71685,7 @@ const validator_defaultOptions = { }; //const tagsPattern = new RegExp("<\\/?([\\w:\\-_\.]+)\\s*\/?>","g"); -function validate(xmlData, options) { +function validator_validate(xmlData, options) { options = Object.assign({}, validator_defaultOptions, options); //xmlData = xmlData.replace(/(\r\n|\n|\r)/gm,"");//make it single line @@ -71702,7 +72109,7 @@ function getPositionFromMatch(match) { const XMLValidator = { - validate: validate + validate: validator_validate } ;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/xmlparser/OptionsBuilder.js @@ -75477,7 +75884,7 @@ class XMLParser { if (validationOption) { if (validationOption === true) validationOption = {}; //validate with default options - const result = validate(xmlData, validationOption); + const result = validator_validate(xmlData, validationOption); if (result !== true) { throw Error(`${result.err.msg}:${result.err.line}:${result.err.col}`) }