diff --git a/dist/restore-only/index.js b/dist/restore-only/index.js index 3b7ef12..c352eff 100644 --- a/dist/restore-only/index.js +++ b/dist/restore-only/index.js @@ -38848,7 +38848,7 @@ module.exports = { version: packageJson.version } /***/ 4012: /***/ ((module) => { -module.exports = /*#__PURE__*/JSON.parse('{"name":"@actions/cache","version":"6.0.0","description":"Actions cache lib","keywords":["github","actions","cache"],"homepage":"https://github.com/actions/toolkit/tree/main/packages/cache","license":"MIT","type":"module","main":"lib/cache.js","types":"lib/cache.d.ts","exports":{".":{"types":"./lib/cache.d.ts","import":"./lib/cache.js"}},"directories":{"lib":"lib","test":"__tests__"},"files":["lib","!.DS_Store"],"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/actions/toolkit.git","directory":"packages/cache"},"scripts":{"audit-moderate":"npm install && npm audit --json --audit-level=moderate > audit.json","test":"echo \\"Error: run tests from root\\" && exit 1","tsc":"tsc && cp src/internal/shared/package-version.cjs lib/internal/shared/"},"bugs":{"url":"https://github.com/actions/toolkit/issues"},"dependencies":{"@actions/core":"^3.0.0","@actions/exec":"^3.0.0","@actions/glob":"^0.6.1","@actions/http-client":"^4.0.0","@actions/io":"^3.0.0","@azure/core-rest-pipeline":"^1.22.0","@azure/storage-blob":"^12.30.0","@protobuf-ts/runtime-rpc":"^2.11.1","semver":"^7.7.3"},"devDependencies":{"@protobuf-ts/plugin":"^2.9.4","@types/node":"^25.1.0","@types/semver":"^7.7.1","typescript":"^5.2.2"},"overrides":{"uri-js":"npm:uri-js-replace@^1.0.1","node-fetch":"^3.3.2"}}'); +module.exports = /*#__PURE__*/JSON.parse('{"name":"@actions/cache","version":"6.0.1","description":"Actions cache lib","keywords":["github","actions","cache"],"homepage":"https://github.com/actions/toolkit/tree/main/packages/cache","license":"MIT","type":"module","main":"lib/cache.js","types":"lib/cache.d.ts","exports":{".":{"types":"./lib/cache.d.ts","import":"./lib/cache.js"}},"directories":{"lib":"lib","test":"__tests__"},"files":["lib","!.DS_Store"],"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/actions/toolkit.git","directory":"packages/cache"},"scripts":{"audit-moderate":"npm install && npm audit --json --audit-level=moderate > audit.json","test":"echo \\"Error: run tests from root\\" && exit 1","tsc":"tsc && cp src/internal/shared/package-version.cjs lib/internal/shared/"},"bugs":{"url":"https://github.com/actions/toolkit/issues"},"dependencies":{"@actions/core":"^3.0.1","@actions/exec":"^3.0.0","@actions/glob":"^0.6.1","@actions/http-client":"^4.0.1","@actions/io":"^3.0.2","@azure/core-rest-pipeline":"^1.23.0","@azure/storage-blob":"^12.31.0","@protobuf-ts/runtime-rpc":"^2.11.1","semver":"^7.7.4"},"devDependencies":{"@protobuf-ts/plugin":"^2.11.1","@types/node":"^25.6.0","@types/semver":"^7.7.1","typescript":"^5.9.3"},"overrides":{"uri-js":"npm:uri-js-replace@^1.0.1","node-fetch":"^3.3.2"}}'); /***/ }) @@ -46846,7 +46846,7 @@ async function util_userAgentPlatform_setPlatformSpecificData(map) { ;// CONCATENATED MODULE: ./node_modules/@azure/core-rest-pipeline/dist/esm/constants.js // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -const esm_constants_SDK_VERSION = "1.22.2"; +const esm_constants_SDK_VERSION = "1.22.3"; const esm_constants_DEFAULT_RETRY_POLICY_COUNT = 3; //# sourceMappingURL=constants.js.map ;// CONCATENATED MODULE: ./node_modules/@azure/core-rest-pipeline/dist/esm/util/userAgent.js @@ -46945,7 +46945,7 @@ async function computeSha256Hash(content, encoding) { //# sourceMappingURL=internal.js.map -;// CONCATENATED MODULE: ./node_modules/@azure/core-util/node_modules/@azure/abort-controller/dist/esm/AbortError.js +;// CONCATENATED MODULE: ./node_modules/@azure/abort-controller/dist/esm/AbortError.js // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. /** @@ -46973,7 +46973,7 @@ class AbortError_AbortError extends Error { } } //# sourceMappingURL=AbortError.js.map -;// CONCATENATED MODULE: ./node_modules/@azure/core-util/node_modules/@azure/abort-controller/dist/esm/index.js +;// CONCATENATED MODULE: ./node_modules/@azure/abort-controller/dist/esm/index.js // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. @@ -51564,6 +51564,7 @@ function convertHttpClient(requestPolicyClient) { + //# sourceMappingURL=index.js.map ;// CONCATENATED MODULE: ./node_modules/path-expression-matcher/src/Expression.js /** @@ -51583,11 +51584,11 @@ class Expression { * @param {Object} options - Configuration options * @param {string} options.separator - Path separator (default: '.') */ - constructor(pattern, options = {}) { + constructor(pattern, options = {}, data) { this.pattern = pattern; this.separator = options.separator || '.'; this.segments = this._parse(pattern); - + this.data = data; // Cache expensive checks for performance (O(1) instead of O(n)) this._hasDeepWildcard = this.segments.some(seg => seg.type === 'deep-wildcard'); this._hasAttributeCondition = this.segments.some(seg => seg.attrName !== undefined); @@ -51799,48 +51800,206 @@ class Expression { } } ;// CONCATENATED MODULE: ./node_modules/path-expression-matcher/src/Matcher.js + + /** - * Matcher - Tracks current path in XML/JSON tree and matches against Expressions - * + * MatcherView - A lightweight read-only view over a Matcher's internal state. + * + * Created once by Matcher and reused across all callbacks. Holds a direct + * reference to the parent Matcher so it always reflects current parser state + * with zero copying or freezing overhead. + * + * Users receive this via {@link Matcher#readOnly} or directly from parser + * callbacks. It exposes all query and matching methods but has no mutation + * methods — misuse is caught at the TypeScript level rather than at runtime. + * + * @example + * const matcher = new Matcher(); + * const view = matcher.readOnly(); + * + * matcher.push("root", {}); + * view.getCurrentTag(); // "root" + * view.getDepth(); // 1 + */ +class MatcherView { + /** + * @param {Matcher} matcher - The parent Matcher instance to read from. + */ + constructor(matcher) { + this._matcher = matcher; + } + + /** + * Get the path separator used by the parent matcher. + * @returns {string} + */ + get separator() { + return this._matcher.separator; + } + + /** + * Get current tag name. + * @returns {string|undefined} + */ + getCurrentTag() { + const path = this._matcher.path; + return path.length > 0 ? path[path.length - 1].tag : undefined; + } + + /** + * Get current namespace. + * @returns {string|undefined} + */ + getCurrentNamespace() { + const path = this._matcher.path; + return path.length > 0 ? path[path.length - 1].namespace : undefined; + } + + /** + * Get current node's attribute value. + * @param {string} attrName + * @returns {*} + */ + getAttrValue(attrName) { + const path = this._matcher.path; + if (path.length === 0) return undefined; + return path[path.length - 1].values?.[attrName]; + } + + /** + * Check if current node has an attribute. + * @param {string} attrName + * @returns {boolean} + */ + hasAttr(attrName) { + const path = this._matcher.path; + if (path.length === 0) return false; + const current = path[path.length - 1]; + return current.values !== undefined && attrName in current.values; + } + + /** + * Get current node's sibling position (child index in parent). + * @returns {number} + */ + getPosition() { + const path = this._matcher.path; + if (path.length === 0) return -1; + return path[path.length - 1].position ?? 0; + } + + /** + * Get current node's repeat counter (occurrence count of this tag name). + * @returns {number} + */ + getCounter() { + const path = this._matcher.path; + if (path.length === 0) return -1; + return path[path.length - 1].counter ?? 0; + } + + /** + * Get current node's sibling index (alias for getPosition). + * @returns {number} + * @deprecated Use getPosition() or getCounter() instead + */ + getIndex() { + return this.getPosition(); + } + + /** + * Get current path depth. + * @returns {number} + */ + getDepth() { + return this._matcher.path.length; + } + + /** + * Get path as string. + * @param {string} [separator] - Optional separator (uses default if not provided) + * @param {boolean} [includeNamespace=true] + * @returns {string} + */ + toString(separator, includeNamespace = true) { + return this._matcher.toString(separator, includeNamespace); + } + + /** + * Get path as array of tag names. + * @returns {string[]} + */ + toArray() { + return this._matcher.path.map(n => n.tag); + } + + /** + * Match current path against an Expression. + * @param {Expression} expression + * @returns {boolean} + */ + matches(expression) { + return this._matcher.matches(expression); + } + + /** + * Match any expression in the given set against the current path. + * @param {ExpressionSet} exprSet + * @returns {boolean} + */ + matchesAny(exprSet) { + return exprSet.matchesAny(this._matcher); + } +} + +/** + * Matcher - Tracks current path in XML/JSON tree and matches against Expressions. + * * The matcher maintains a stack of nodes representing the current path from root to * current tag. It only stores attribute values for the current (top) node to minimize * memory usage. Sibling tracking is used to auto-calculate position and counter. - * + * + * Use {@link Matcher#readOnly} to obtain a {@link MatcherView} safe to pass to + * user callbacks — it always reflects current state with no Proxy overhead. + * * @example * const matcher = new Matcher(); * matcher.push("root", {}); * matcher.push("users", {}); * matcher.push("user", { id: "123", type: "admin" }); - * + * * const expr = new Expression("root.users.user"); * matcher.matches(expr); // true */ class Matcher { /** - * Create a new Matcher - * @param {Object} options - Configuration options - * @param {string} options.separator - Default path separator (default: '.') + * Create a new Matcher. + * @param {Object} [options={}] + * @param {string} [options.separator='.'] - Default path separator */ constructor(options = {}) { this.separator = options.separator || '.'; this.path = []; this.siblingStacks = []; - // Each path node: { tag: string, values: object, position: number, counter: number } + // Each path node: { tag, values, position, counter, namespace? } // values only present for current (last) node // Each siblingStacks entry: Map tracking occurrences at each level + this._pathStringCache = null; + this._view = new MatcherView(this); } /** - * Push a new tag onto the path - * @param {string} tagName - Name of the tag - * @param {Object} attrValues - Attribute key-value pairs for current node (optional) - * @param {string} namespace - Namespace for the tag (optional) + * Push a new tag onto the path. + * @param {string} tagName + * @param {Object|null} [attrValues=null] + * @param {string|null} [namespace=null] */ push(tagName, attrValues = null, namespace = null) { + this._pathStringCache = null; + // Remove values from previous current node (now becoming ancestor) if (this.path.length > 0) { - const prev = this.path[this.path.length - 1]; - prev.values = undefined; + this.path[this.path.length - 1].values = undefined; } // Get or create sibling tracking for current level @@ -51873,12 +52032,10 @@ class Matcher { counter: counter }; - // Store namespace if provided if (namespace !== null && namespace !== undefined) { node.namespace = namespace; } - // Store values only for current node if (attrValues !== null && attrValues !== undefined) { node.values = attrValues; } @@ -51887,19 +52044,15 @@ class Matcher { } /** - * Pop the last tag from the path + * Pop the last tag from the path. * @returns {Object|undefined} The popped node */ pop() { - if (this.path.length === 0) { - return undefined; - } + if (this.path.length === 0) return undefined; + this._pathStringCache = null; const node = this.path.pop(); - // Clean up sibling tracking for levels deeper than current - // After pop, path.length is the new depth - // We need to clean up siblingStacks[path.length + 1] and beyond if (this.siblingStacks.length > this.path.length + 1) { this.siblingStacks.length = this.path.length + 1; } @@ -51908,9 +52061,9 @@ class Matcher { } /** - * Update current node's attribute values - * Useful when attributes are parsed after push - * @param {Object} attrValues - Attribute values + * Update current node's attribute values. + * Useful when attributes are parsed after push. + * @param {Object} attrValues */ updateCurrent(attrValues) { if (this.path.length > 0) { @@ -51922,7 +52075,7 @@ class Matcher { } /** - * Get current tag name + * Get current tag name. * @returns {string|undefined} */ getCurrentTag() { @@ -51930,7 +52083,7 @@ class Matcher { } /** - * Get current namespace + * Get current namespace. * @returns {string|undefined} */ getCurrentNamespace() { @@ -51938,19 +52091,18 @@ class Matcher { } /** - * Get current node's attribute value - * @param {string} attrName - Attribute name - * @returns {*} Attribute value or undefined + * Get current node's attribute value. + * @param {string} attrName + * @returns {*} */ getAttrValue(attrName) { if (this.path.length === 0) return undefined; - const current = this.path[this.path.length - 1]; - return current.values?.[attrName]; + return this.path[this.path.length - 1].values?.[attrName]; } /** - * Check if current node has an attribute - * @param {string} attrName - Attribute name + * Check if current node has an attribute. + * @param {string} attrName * @returns {boolean} */ hasAttr(attrName) { @@ -51960,7 +52112,7 @@ class Matcher { } /** - * Get current node's sibling position (child index in parent) + * Get current node's sibling position (child index in parent). * @returns {number} */ getPosition() { @@ -51969,7 +52121,7 @@ class Matcher { } /** - * Get current node's repeat counter (occurrence count of this tag name) + * Get current node's repeat counter (occurrence count of this tag name). * @returns {number} */ getCounter() { @@ -51978,7 +52130,7 @@ class Matcher { } /** - * Get current node's sibling index (alias for getPosition for backward compatibility) + * Get current node's sibling index (alias for getPosition). * @returns {number} * @deprecated Use getPosition() or getCounter() instead */ @@ -51987,7 +52139,7 @@ class Matcher { } /** - * Get current path depth + * Get current path depth. * @returns {number} */ getDepth() { @@ -51995,23 +52147,33 @@ class Matcher { } /** - * Get path as string - * @param {string} separator - Optional separator (uses default if not provided) - * @param {boolean} includeNamespace - Whether to include namespace in output (default: true) + * Get path as string. + * @param {string} [separator] - Optional separator (uses default if not provided) + * @param {boolean} [includeNamespace=true] * @returns {string} */ toString(separator, includeNamespace = true) { const sep = separator || this.separator; - return this.path.map(n => { - if (includeNamespace && n.namespace) { - return `${n.namespace}:${n.tag}`; + const isDefault = (sep === this.separator && includeNamespace === true); + + if (isDefault) { + if (this._pathStringCache !== null) { + return this._pathStringCache; } - return n.tag; - }).join(sep); + const result = this.path.map(n => + (n.namespace) ? `${n.namespace}:${n.tag}` : n.tag + ).join(sep); + this._pathStringCache = result; + return result; + } + + return this.path.map(n => + (includeNamespace && n.namespace) ? `${n.namespace}:${n.tag}` : n.tag + ).join(sep); } /** - * Get path as array of tag names + * Get path as array of tag names. * @returns {string[]} */ toArray() { @@ -52019,17 +52181,18 @@ class Matcher { } /** - * Reset the path to empty + * Reset the path to empty. */ reset() { + this._pathStringCache = null; this.path = []; this.siblingStacks = []; } /** - * Match current path against an Expression - * @param {Expression} expression - The expression to match against - * @returns {boolean} True if current path matches the expression + * Match current path against an Expression. + * @param {Expression} expression + * @returns {boolean} */ matches(expression) { const segments = expression.segments; @@ -52038,32 +52201,23 @@ class Matcher { return false; } - // Handle deep wildcard patterns if (expression.hasDeepWildcard()) { return this._matchWithDeepWildcard(segments); } - // Simple path matching (no deep wildcards) return this._matchSimple(segments); } /** - * Match simple path (no deep wildcards) * @private */ _matchSimple(segments) { - // Path must be same length as segments if (this.path.length !== segments.length) { return false; } - // Match each segment bottom-to-top for (let i = 0; i < segments.length; i++) { - const segment = segments[i]; - const node = this.path[i]; - const isCurrentNode = (i === this.path.length - 1); - - if (!this._matchSegment(segment, node, isCurrentNode)) { + if (!this._matchSegment(segments[i], this.path[i], i === this.path.length - 1)) { return false; } } @@ -52072,32 +52226,27 @@ class Matcher { } /** - * Match path with deep wildcards * @private */ _matchWithDeepWildcard(segments) { - let pathIdx = this.path.length - 1; // Start from current node (bottom) - let segIdx = segments.length - 1; // Start from last segment + let pathIdx = this.path.length - 1; + let segIdx = segments.length - 1; while (segIdx >= 0 && pathIdx >= 0) { const segment = segments[segIdx]; if (segment.type === 'deep-wildcard') { - // ".." matches zero or more levels segIdx--; if (segIdx < 0) { - // Pattern ends with "..", always matches return true; } - // Find where next segment matches in the path const nextSeg = segments[segIdx]; let found = false; for (let i = pathIdx; i >= 0; i--) { - const isCurrentNode = (i === this.path.length - 1); - if (this._matchSegment(nextSeg, this.path[i], isCurrentNode)) { + if (this._matchSegment(nextSeg, this.path[i], i === this.path.length - 1)) { pathIdx = i - 1; segIdx--; found = true; @@ -52109,9 +52258,7 @@ class Matcher { return false; } } else { - // Regular segment - const isCurrentNode = (pathIdx === this.path.length - 1); - if (!this._matchSegment(segment, this.path[pathIdx], isCurrentNode)) { + if (!this._matchSegment(segment, this.path[pathIdx], pathIdx === this.path.length - 1)) { return false; } pathIdx--; @@ -52119,38 +52266,25 @@ class Matcher { } } - // All segments must be consumed return segIdx < 0; } /** - * Match a single segment against a node * @private - * @param {Object} segment - Segment from Expression - * @param {Object} node - Node from path - * @param {boolean} isCurrentNode - Whether this is the current (last) node - * @returns {boolean} */ _matchSegment(segment, node, isCurrentNode) { - // Match tag name (* is wildcard) if (segment.tag !== '*' && segment.tag !== node.tag) { return false; } - // Match namespace if specified in segment if (segment.namespace !== undefined) { - // Segment has namespace - node must match it if (segment.namespace !== '*' && segment.namespace !== node.namespace) { return false; } } - // If segment has no namespace, it matches nodes with or without namespace - // Match attribute name (check if node has this attribute) - // Can only check for current node since ancestors don't have values if (segment.attrName !== undefined) { if (!isCurrentNode) { - // Can't check attributes for ancestor nodes (values not stored) return false; } @@ -52158,20 +52292,15 @@ class Matcher { return false; } - // Match attribute value (only possible for current node) if (segment.attrValue !== undefined) { - const actualValue = node.values[segment.attrName]; - // Both should be strings - if (String(actualValue) !== String(segment.attrValue)) { + if (String(node.values[segment.attrName]) !== String(segment.attrValue)) { return false; } } } - // Match position (only for current node) if (segment.position !== undefined) { if (!isCurrentNode) { - // Can't check position for ancestor nodes return false; } @@ -52183,10 +52312,8 @@ class Matcher { return false; } else if (segment.position === 'even' && counter % 2 !== 0) { return false; - } else if (segment.position === 'nth') { - if (counter !== segment.positionValue) { - return false; - } + } else if (segment.position === 'nth' && counter !== segment.positionValue) { + return false; } } @@ -52194,8 +52321,17 @@ class Matcher { } /** - * Create a snapshot of current state - * @returns {Object} State snapshot + * Match any expression in the given set against the current path. + * @param {ExpressionSet} exprSet + * @returns {boolean} + */ + matchesAny(exprSet) { + return exprSet.matchesAny(this); + } + + /** + * Create a snapshot of current state. + * @returns {Object} */ snapshot() { return { @@ -52205,28 +52341,378 @@ class Matcher { } /** - * Restore state from snapshot - * @param {Object} snapshot - State snapshot + * Restore state from snapshot. + * @param {Object} snapshot */ restore(snapshot) { + this._pathStringCache = null; this.path = snapshot.path.map(node => ({ ...node })); this.siblingStacks = snapshot.siblingStacks.map(map => new Map(map)); } + + /** + * Return the read-only {@link MatcherView} for this matcher. + * + * The same instance is returned on every call — no allocation occurs. + * It always reflects the current parser state and is safe to pass to + * user callbacks without risk of accidental mutation. + * + * @returns {MatcherView} + * + * @example + * const view = matcher.readOnly(); + * // pass view to callbacks — it stays in sync automatically + * view.matches(expr); // ✓ + * view.getCurrentTag(); // ✓ + * // view.push(...) // ✗ method does not exist — caught by TypeScript + */ + readOnly() { + 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; } @@ -52243,13 +52729,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; @@ -52269,20 +52758,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); @@ -52298,21 +52799,25 @@ function arrToStr(arr, options, indentation, matcher, stopNodeExpressions) { if (isPreviousElementTag) { xmlStr += indentation; } - xmlStr += ``; + const val = tagObj[rawTagName][0][options.textNodeName]; + const safeVal = safeCdata(val); + xmlStr += ``; isPreviousElementTag = false; matcher.pop(); continue; } else if (tagName === options.commentPropName) { - xmlStr += indentation + ``; + 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; @@ -52324,16 +52829,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) { @@ -52377,7 +52881,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; } @@ -52415,9 +52919,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); @@ -52444,7 +52946,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)}"`; } } } @@ -52460,13 +52962,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]; @@ -52477,9 +52989,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)}"`; } } } @@ -52532,6 +53044,8 @@ function getIgnoreAttributesFn(ignoreAttributes) { + + const defaultOptions = { attributeNamePrefix: '@_', attributesGroupName: false, @@ -52565,7 +53079,11 @@ const 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) { @@ -52622,6 +53140,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); @@ -52633,11 +53189,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) { @@ -52651,6 +53208,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)) { @@ -52660,21 +53233,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) { @@ -52682,7 +53256,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(); @@ -52690,12 +53264,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 + '` + this.newLine; + const safeVal = safeCdata(val); + return this.indentate(level) + `` + this.newLine; } else if (this.options.commentPropName !== false && key === this.options.commentPropName) { - return this.indentate(level) + `` + this.newLine; + const safeVal = safeComment(val); + return this.indentate(level) + `` + this.newLine; } else if (key[0] === "?") {//PI tag return this.indentate(level) + '<' + key + attrStr + '?' + this.tagEndChar; } else { @@ -53135,7 +53717,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 @@ -53559,12 +54141,13 @@ function getPositionFromMatch(match) { const XMLValidator = { - validate: validate + validate: validator_validate } ;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/xmlparser/OptionsBuilder.js + const defaultOnDangerousProperty = (name) => { if (DANGEROUS_PROPERTY_NAMES.includes(name)) { return "__" + name; @@ -53604,6 +54187,7 @@ const OptionsBuilder_defaultOptions = { unpairedTags: [], processEntities: true, htmlEntities: false, + entityDecoder: null, ignoreDeclaration: false, ignorePiTags: false, transformTagName: false, @@ -53650,32 +54234,34 @@ function validatePropertyName(propertyName, optionName) { * @param {boolean|object} value * @returns {object} Always returns normalized object */ -function normalizeProcessEntities(value) { +function normalizeProcessEntities(value, htmlEntities) { // Boolean backward compatibility if (typeof value === 'boolean') { return { enabled: value, // true or false maxEntitySize: 10000, - maxExpansionDepth: 10, - maxTotalExpansions: 1000, + maxExpansionDepth: 10000, + maxTotalExpansions: Infinity, maxExpandedLength: 100000, - maxEntityCount: 100, + maxEntityCount: 1000, allowedTags: null, - tagFilter: null + tagFilter: null, + appliesTo: "all", }; } // Object config - merge with defaults if (typeof value === 'object' && value !== null) { return { - enabled: value.enabled !== false, // default true if not specified - maxEntitySize: value.maxEntitySize ?? 10000, - maxExpansionDepth: value.maxExpansionDepth ?? 10, - maxTotalExpansions: value.maxTotalExpansions ?? 1000, - maxExpandedLength: value.maxExpandedLength ?? 100000, - maxEntityCount: value.maxEntityCount ?? 100, + enabled: value.enabled !== false, + maxEntitySize: Math.max(1, value.maxEntitySize ?? 10000), + maxExpansionDepth: Math.max(1, value.maxExpansionDepth ?? 10000), + maxTotalExpansions: Math.max(1, value.maxTotalExpansions ?? Infinity), + maxExpandedLength: Math.max(1, value.maxExpandedLength ?? 100000), + maxEntityCount: Math.max(1, value.maxEntityCount ?? 1000), allowedTags: value.allowedTags ?? null, - tagFilter: value.tagFilter ?? null + tagFilter: value.tagFilter ?? null, + appliesTo: value.appliesTo ?? "all", }; } @@ -53706,8 +54292,8 @@ const buildOptions = function (options) { } // Always normalize processEntities for backward compatibility and validation - built.processEntities = normalizeProcessEntities(built.processEntities); - + built.processEntities = normalizeProcessEntities(built.processEntities, built.htmlEntities); + built.unpairedTagsSet = new Set(built.unpairedTags); // Convert old-style stopNodes for backward compatibility if (built.stopNodes && Array.isArray(built.stopNodes)) { built.stopNodes = built.stopNodes.map(node => { @@ -53768,11 +54354,15 @@ class XmlNode { class DocTypeReader { - constructor(options) { + constructor(options, xmlVersion) { this.suppressValidationErr = !options; this.options = options; + this.xmlVersion = xmlVersion || 1.0; } + setXmlVersion(xmlVersion = 1.0) { + this.xmlVersion = xmlVersion; + } readDocType(xmlData, i) { const entities = Object.create(null); let entityCount = 0; @@ -53795,18 +54385,15 @@ class DocTypeReader { [entityName, val, i] = this.readEntityExp(xmlData, i + 1, this.suppressValidationErr); if (val.indexOf("&") === -1) { //Parameter entities are not supported if (this.options.enabled !== false && - this.options.maxEntityCount && + this.options.maxEntityCount != null && entityCount >= this.options.maxEntityCount) { throw new Error( `Entity count (${entityCount + 1}) exceeds maximum allowed (${this.options.maxEntityCount})` ); } //const escaped = entityName.replace(/[.\-+*:]/g, '\\.'); - const escaped = entityName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - entities[entityName] = { - regx: RegExp(`&${escaped};`, "g"), - val: val - }; + //const escaped = entityName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + entities[entityName] = val; entityCount++; } } @@ -53867,12 +54454,13 @@ class DocTypeReader { i = skipWhitespace(xmlData, i); // Read entity name - let entityName = ""; + const startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i]) && xmlData[i] !== '"' && xmlData[i] !== "'") { - entityName += xmlData[i]; i++; } - validateEntityName(entityName); + let entityName = xmlData.substring(startIndex, i); + + validateEntityName(entityName, { xmlVersion: this.xmlVersion }); // Skip whitespace after entity name i = skipWhitespace(xmlData, i); @@ -53892,7 +54480,7 @@ class DocTypeReader { // Validate entity size if (this.options.enabled !== false && - this.options.maxEntitySize && + this.options.maxEntitySize != null && entityValue.length > this.options.maxEntitySize) { throw new Error( `Entity "${entityName}" size (${entityValue.length}) exceeds maximum allowed size (${this.options.maxEntitySize})` @@ -53908,12 +54496,14 @@ class DocTypeReader { i = skipWhitespace(xmlData, i); // Read notation name - let notationName = ""; + + const startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - notationName += xmlData[i]; i++; } - !this.suppressValidationErr && validateEntityName(notationName); + let notationName = xmlData.substring(startIndex, i); + + !this.suppressValidationErr && validateEntityName(notationName, { xmlVersion: this.xmlVersion }); // Skip whitespace after notation name i = skipWhitespace(xmlData, i); @@ -53962,10 +54552,11 @@ class DocTypeReader { } i++; + const startIndex = i; while (i < xmlData.length && xmlData[i] !== startChar) { - identifierVal += xmlData[i]; i++; } + identifierVal = xmlData.substring(startIndex, i); if (xmlData[i] !== startChar) { throw new Error(`Unterminated ${type} value`); @@ -53985,14 +54576,14 @@ class DocTypeReader { i = skipWhitespace(xmlData, i); // Read element name - let elementName = ""; + const startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - elementName += xmlData[i]; i++; } + let elementName = xmlData.substring(startIndex, i); // Validate element name - if (!this.suppressValidationErr && !isName(elementName)) { + if (!this.suppressValidationErr && !qName(elementName, { xmlVersion: this.xmlVersion })) { throw new Error(`Invalid element name: "${elementName}"`); } @@ -54006,10 +54597,12 @@ class DocTypeReader { i++; // Move past '(' // Read content model + const startIndex = i; while (i < xmlData.length && xmlData[i] !== ")") { - contentModel += xmlData[i]; i++; } + contentModel = xmlData.substring(startIndex, i); + if (xmlData[i] !== ")") { throw new Error("Unterminated content model"); } @@ -54030,27 +54623,27 @@ class DocTypeReader { i = skipWhitespace(xmlData, i); // Read element name - let elementName = ""; + let startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - elementName += xmlData[i]; i++; } + let elementName = xmlData.substring(startIndex, i); // Validate element name - validateEntityName(elementName) + validateEntityName(elementName, { xmlVersion: this.xmlVersion }) // Skip whitespace after element name i = skipWhitespace(xmlData, i); // Read attribute name - let attributeName = ""; + startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - attributeName += xmlData[i]; i++; } + let attributeName = xmlData.substring(startIndex, i); // Validate attribute name - if (!validateEntityName(attributeName)) { + if (!validateEntityName(attributeName, { xmlVersion: this.xmlVersion })) { throw new Error(`Invalid attribute name: "${attributeName}"`); } @@ -54075,15 +54668,17 @@ class DocTypeReader { // Read the list of allowed notations let allowedNotations = []; while (i < xmlData.length && xmlData[i] !== ")") { - let notation = ""; + + + const startIndex = i; while (i < xmlData.length && xmlData[i] !== "|" && xmlData[i] !== ")") { - notation += xmlData[i]; i++; } + let notation = xmlData.substring(startIndex, i); // Validate notation name notation = notation.trim(); - if (!validateEntityName(notation)) { + if (!validateEntityName(notation, { xmlVersion: this.xmlVersion })) { throw new Error(`Invalid notation name: "${notation}"`); } @@ -54105,10 +54700,11 @@ class DocTypeReader { attributeType += " (" + allowedNotations.join("|") + ")"; } else { // Handle simple types (e.g., CDATA, ID, IDREF, etc.) + const startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - attributeType += xmlData[i]; i++; } + attributeType += xmlData.substring(startIndex, i); // Validate simple attribute type const validTypes = ["CDATA", "ID", "IDREF", "IDREFS", "ENTITY", "ENTITIES", "NMTOKEN", "NMTOKENS"]; @@ -54160,115 +54756,124 @@ function hasSeq(data, seq, i) { return true; } -function validateEntityName(name) { - if (isName(name)) +function validateEntityName(name, xmlVersion) { + if (qName(name, { xmlVersion: xmlVersion })) return name; else throw new Error(`Invalid entity name ${name}`); } ;// CONCATENATED MODULE: ./node_modules/strnum/strnum.js const hexRegex = /^[-+]?0x[a-fA-F0-9]+$/; +const binRegex = /^0b[01]+$/; +const octRegex = /^0o[0-7]+$/; const numRegex = /^([\-\+])?(0*)([0-9]*(\.[0-9]*)?)$/; -// const octRegex = /^0x[a-z0-9]+/; -// const binRegex = /0x[a-z0-9]+/; - const consider = { - hex : true, - // oct: false, + hex: true, + binary: false, + octal: false, leadingZeros: true, decimalPoint: "\.", eNotation: true, - //skipLike: /regex/ + //skipLike: /regex/, + infinity: "original", // "null", "infinity" (Infinity type), "string" ("Infinity" (the string literal)) }; -function toNumber(str, options = {}){ - options = Object.assign({}, consider, options ); - if(!str || typeof str !== "string" ) return str; - - let trimmedStr = str.trim(); - - if(options.skipLike !== undefined && options.skipLike.test(trimmedStr)) return str; - else if(str==="0") return 0; +function toNumber(str, options = {}) { + options = Object.assign({}, consider, options); + if (!str || typeof str !== "string") return str; + + let trimmedStr = str.trim(); + + if (trimmedStr.length === 0) return str; + else if (options.skipLike !== undefined && options.skipLike.test(trimmedStr)) return str; + else if (trimmedStr === "0") return 0; else if (options.hex && hexRegex.test(trimmedStr)) { return parse_int(trimmedStr, 16); - // }else if (options.oct && octRegex.test(str)) { - // return Number.parseInt(val, 8); - }else if (trimmedStr.includes('e') || trimmedStr.includes('E')) { //eNotation - return resolveEnotation(str,trimmedStr,options); - // }else if (options.parseBin && binRegex.test(str)) { - // return Number.parseInt(val, 2); - }else{ + } else if (options.binary && binRegex.test(trimmedStr)) { + return parse_int(trimmedStr, 2); + } else if (options.octal && octRegex.test(trimmedStr)) { + return parse_int(trimmedStr, 8); + } else if (!isFinite(trimmedStr)) { //Infinity + return handleInfinity(str, Number(trimmedStr), options); + } else if (trimmedStr.includes('e') || trimmedStr.includes('E')) { //eNotation + return resolveEnotation(str, trimmedStr, options); + } else { //separate negative sign, leading zeros, and rest number const match = numRegex.exec(trimmedStr); // +00.123 => [ , '+', '00', '.123', .. - if(match){ + if (match) { const sign = match[1] || ""; const leadingZeros = match[2]; let numTrimmedByZeros = trimZeros(match[3]); //complete num without leading zeros const decimalAdjacentToLeadingZeros = sign ? // 0., -00., 000. - str[leadingZeros.length+1] === "." + str[leadingZeros.length + 1] === "." : str[leadingZeros.length] === "."; //trim ending zeros for floating number - if(!options.leadingZeros //leading zeros are not allowed - && (leadingZeros.length > 1 - || (leadingZeros.length === 1 && !decimalAdjacentToLeadingZeros))){ + if (!options.leadingZeros //leading zeros are not allowed + && (leadingZeros.length > 1 + || (leadingZeros.length === 1 && !decimalAdjacentToLeadingZeros))) { // 00, 00.3, +03.24, 03, 03.24 return str; } - else{//no leading zeros or leading zeros are allowed + else {//no leading zeros or leading zeros are allowed const num = Number(trimmedStr); const parsedStr = String(num); - if( num === 0) return num; - if(parsedStr.search(/[eE]/) !== -1){ //given number is long and parsed to eNotation - if(options.eNotation) return num; + if (num === 0) return num; + if (parsedStr.search(/[eE]/) !== -1) { //given number is long and parsed to eNotation + if (options.eNotation) return num; else return str; - }else if(trimmedStr.indexOf(".") !== -1){ //floating number - if(parsedStr === "0") return num; //0.0 - else if(parsedStr === numTrimmedByZeros) return num; //0.456. 0.79000 - else if( parsedStr === `${sign}${numTrimmedByZeros}`) return num; + } else if (trimmedStr.indexOf(".") !== -1) { //floating number + if (parsedStr === "0") return num; //0.0 + else if (parsedStr === numTrimmedByZeros) return num; //0.456. 0.79000 + else if (parsedStr === `${sign}${numTrimmedByZeros}`) return num; else return str; } - - let n = leadingZeros? numTrimmedByZeros : trimmedStr; - if(leadingZeros){ + + let n = leadingZeros ? numTrimmedByZeros : trimmedStr; + if (leadingZeros) { // -009 => -9 - return (n === parsedStr) || (sign+n === parsedStr) ? num : str - }else { + return (n === parsedStr) || (sign + n === parsedStr) ? num : str + } else { // +9 - return (n === parsedStr) || (n === sign+parsedStr) ? num : str + return (n === parsedStr) || (n === sign + parsedStr) ? num : str } } - }else{ //non-numeric string + } else { //non-numeric string return str; } } } const eNotationRegx = /^([-+])?(0*)(\d*(\.\d*)?[eE][-\+]?\d+)$/; -function resolveEnotation(str,trimmedStr,options){ - if(!options.eNotation) return str; - const notation = trimmedStr.match(eNotationRegx); - if(notation){ +function resolveEnotation(str, trimmedStr, options) { + if (!options.eNotation) return str; + const notation = trimmedStr.match(eNotationRegx); + if (notation) { let sign = notation[1] || ""; const eChar = notation[3].indexOf("e") === -1 ? "E" : "e"; const leadingZeros = notation[2]; const eAdjacentToLeadingZeros = sign ? // 0E. - str[leadingZeros.length+1] === eChar + str[leadingZeros.length + 1] === eChar : str[leadingZeros.length] === eChar; - if(leadingZeros.length > 1 && eAdjacentToLeadingZeros) return str; - else if(leadingZeros.length === 1 - && (notation[3].startsWith(`.${eChar}`) || notation[3][0] === eChar)){ - return Number(trimmedStr); - }else if(options.leadingZeros && !eAdjacentToLeadingZeros){ //accept with leading zeros - //remove leading 0s - trimmedStr = (notation[1] || "") + notation[3]; + if (leadingZeros.length > 1 && eAdjacentToLeadingZeros) return str; + else if (leadingZeros.length === 1 + && (notation[3].startsWith(`.${eChar}`) || notation[3][0] === eChar)) { return Number(trimmedStr); - }else return str; - }else{ + } else if (leadingZeros.length > 0) { + // Has leading zeros — only accept if leadingZeros option allows it + if (options.leadingZeros && !eAdjacentToLeadingZeros) { + trimmedStr = (notation[1] || "") + notation[3]; + return Number(trimmedStr); + } else return str; + } else { + // No leading zeros — always valid e-notation, parse it + return Number(trimmedStr); + } + } else { return str; } } @@ -54278,23 +54883,48 @@ function resolveEnotation(str,trimmedStr,options){ * @param {string} numStr without leading zeros * @returns */ -function trimZeros(numStr){ - if(numStr && numStr.indexOf(".") !== -1){//float +function trimZeros(numStr) { + if (numStr && numStr.indexOf(".") !== -1) {//float numStr = numStr.replace(/0+$/, ""); //remove ending zeros - if(numStr === ".") numStr = "0"; - else if(numStr[0] === ".") numStr = "0"+numStr; - else if(numStr[numStr.length-1] === ".") numStr = numStr.substring(0,numStr.length-1); + if (numStr === ".") numStr = "0"; + else if (numStr[0] === ".") numStr = "0" + numStr; + else if (numStr[numStr.length - 1] === ".") numStr = numStr.substring(0, numStr.length - 1); return numStr; } return numStr; } -function parse_int(numStr, base){ - //polyfill - if(parseInt) return parseInt(numStr, base); - else if(Number.parseInt) return Number.parseInt(numStr, base); - else if(window && window.parseInt) return window.parseInt(numStr, base); - else throw new Error("parseInt, Number.parseInt, window.parseInt are not supported") +function parse_int(numStr, base) { + const str = numStr.trim(); + if (base === 2 || base === 8) numStr = str.substring(2); + + if (parseInt) return parseInt(numStr, base); + else if (Number.parseInt) return Number.parseInt(numStr, base); + else if (window && window.parseInt) return window.parseInt(numStr, base); + else throw new Error("parseInt, Number.parseInt, window.parseInt are not supported"); +} + +/** + * Handle infinite values based on user option + * @param {string} str - original input string + * @param {number} num - parsed number (Infinity or -Infinity) + * @param {object} options - user options + * @returns {string|number|null} based on infinity option + */ +function handleInfinity(str, num, options) { + const isPositive = num === Infinity; + + switch (options.infinity.toLowerCase()) { + case "null": + return null; + case "infinity": + return num; // Return Infinity or -Infinity + case "string": + return isPositive ? "Infinity" : "-Infinity"; + case "original": + default: + return str; // Return original string like "1e1000" + } } ;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/ignoreAttributes.js function ignoreAttributes_getIgnoreAttributesFn(ignoreAttributes) { @@ -54315,6 +54945,1939 @@ function ignoreAttributes_getIgnoreAttributesFn(ignoreAttributes) { } return () => false } +;// CONCATENATED MODULE: ./node_modules/path-expression-matcher/src/ExpressionSet.js +/** + * ExpressionSet - An indexed collection of Expressions for efficient bulk matching + * + * Instead of iterating all expressions on every tag, ExpressionSet pre-indexes + * them at insertion time by depth and terminal tag name. At match time, only + * the relevant bucket is evaluated — typically reducing checks from O(E) to O(1) + * lookup plus O(small bucket) matches. + * + * Three buckets are maintained: + * - `_byDepthAndTag` — exact depth + exact tag name (tightest, used first) + * - `_wildcardByDepth` — exact depth + wildcard tag `*` (depth-matched only) + * - `_deepWildcards` — expressions containing `..` (cannot be depth-indexed) + * + * @example + * import { Expression, ExpressionSet } from 'fast-xml-tagger'; + * + * // Build once at config time + * const stopNodes = new ExpressionSet(); + * stopNodes.add(new Expression('root.users.user')); + * stopNodes.add(new Expression('root.config.setting')); + * stopNodes.add(new Expression('..script')); + * + * // Query on every tag — hot path + * if (stopNodes.matchesAny(matcher)) { ... } + */ +class ExpressionSet { + constructor() { + /** @type {Map} depth:tag → expressions */ + this._byDepthAndTag = new Map(); + + /** @type {Map} depth → wildcard-tag expressions */ + this._wildcardByDepth = new Map(); + + /** @type {import('./Expression.js').default[]} expressions containing deep wildcard (..) */ + this._deepWildcards = []; + + /** @type {Set} pattern strings already added — used for deduplication */ + this._patterns = new Set(); + + /** @type {boolean} whether the set is sealed against further additions */ + this._sealed = false; + } + + /** + * Add an Expression to the set. + * Duplicate patterns (same pattern string) are silently ignored. + * + * @param {import('./Expression.js').default} expression - A pre-constructed Expression instance + * @returns {this} for chaining + * @throws {TypeError} if called after seal() + * + * @example + * set.add(new Expression('root.users.user')); + * set.add(new Expression('..script')); + */ + add(expression) { + if (this._sealed) { + throw new TypeError( + 'ExpressionSet is sealed. Create a new ExpressionSet to add more expressions.' + ); + } + + // Deduplicate by pattern string + if (this._patterns.has(expression.pattern)) return this; + this._patterns.add(expression.pattern); + + if (expression.hasDeepWildcard()) { + this._deepWildcards.push(expression); + return this; + } + + const depth = expression.length; + const lastSeg = expression.segments[expression.segments.length - 1]; + const tag = lastSeg?.tag; + + if (!tag || tag === '*') { + // Can index by depth but not by tag + if (!this._wildcardByDepth.has(depth)) this._wildcardByDepth.set(depth, []); + this._wildcardByDepth.get(depth).push(expression); + } else { + // Tightest bucket: depth + tag + const key = `${depth}:${tag}`; + if (!this._byDepthAndTag.has(key)) this._byDepthAndTag.set(key, []); + this._byDepthAndTag.get(key).push(expression); + } + + return this; + } + + /** + * Add multiple expressions at once. + * + * @param {import('./Expression.js').default[]} expressions - Array of Expression instances + * @returns {this} for chaining + * + * @example + * set.addAll([ + * new Expression('root.users.user'), + * new Expression('root.config.setting'), + * ]); + */ + addAll(expressions) { + for (const expr of expressions) this.add(expr); + return this; + } + + /** + * Check whether a pattern string is already present in the set. + * + * @param {import('./Expression.js').default} expression + * @returns {boolean} + */ + has(expression) { + return this._patterns.has(expression.pattern); + } + + /** + * Number of expressions in the set. + * @type {number} + */ + get size() { + return this._patterns.size; + } + + /** + * Seal the set against further modifications. + * Useful to prevent accidental mutations after config is built. + * Calling add() or addAll() on a sealed set throws a TypeError. + * + * @returns {this} + */ + seal() { + this._sealed = true; + return this; + } + + /** + * Whether the set has been sealed. + * @type {boolean} + */ + get isSealed() { + return this._sealed; + } + + /** + * Test whether the matcher's current path matches any expression in the set. + * + * Evaluation order (cheapest → most expensive): + * 1. Exact depth + tag bucket — O(1) lookup, typically 0–2 expressions + * 2. Depth-only wildcard bucket — O(1) lookup, rare + * 3. Deep-wildcard list — always checked, but usually small + * + * @param {import('./Matcher.js').default} matcher - Matcher instance (or readOnly view) + * @returns {boolean} true if any expression matches the current path + * + * @example + * if (stopNodes.matchesAny(matcher)) { + * // handle stop node + * } + */ + matchesAny(matcher) { + return this.findMatch(matcher) !== null; + } + /** + * Find and return the first Expression that matches the matcher's current path. + * + * Uses the same evaluation order as matchesAny (cheapest → most expensive): + * 1. Exact depth + tag bucket + * 2. Depth-only wildcard bucket + * 3. Deep-wildcard list + * + * @param {import('./Matcher.js').default} matcher - Matcher instance (or readOnly view) + * @returns {import('./Expression.js').default | null} the first matching Expression, or null + * + * @example + * const expr = stopNodes.findMatch(matcher); + * if (expr) { + * // access expr.config, expr.pattern, etc. + * } + */ + findMatch(matcher) { + const depth = matcher.getDepth(); + const tag = matcher.getCurrentTag(); + + // 1. Tightest bucket — most expressions live here + const exactKey = `${depth}:${tag}`; + const exactBucket = this._byDepthAndTag.get(exactKey); + if (exactBucket) { + for (let i = 0; i < exactBucket.length; i++) { + if (matcher.matches(exactBucket[i])) return exactBucket[i]; + } + } + + // 2. Depth-matched wildcard-tag expressions + const wildcardBucket = this._wildcardByDepth.get(depth); + if (wildcardBucket) { + for (let i = 0; i < wildcardBucket.length; i++) { + if (matcher.matches(wildcardBucket[i])) return wildcardBucket[i]; + } + } + + // 3. Deep wildcards — cannot be pre-filtered by depth or tag + for (let i = 0; i < this._deepWildcards.length; i++) { + if (matcher.matches(this._deepWildcards[i])) return this._deepWildcards[i]; + } + + return null; + } +} + +;// CONCATENATED MODULE: ./node_modules/@nodable/entities/src/entities.js +// --------------------------------------------------------------------------- +// Complete HTML5 named entity reference +// Organized by logical categories for easy maintenance and selective importing +// --------------------------------------------------------------------------- + +/** + * Basic Latin & Special Characters + * @type {Record} + */ +const BASIC_LATIN = { + amp: '&', + AMP: '&', + lt: '<', + LT: '<', + gt: '>', + GT: '>', + quot: '"', + QUOT: '"', + apos: "'", + lsquo: '‘', + rsquo: '’', + ldquo: '“', + rdquo: '”', + lsquor: '‚', + rsquor: '’', + ldquor: '„', + bdquo: '„', + comma: ',', + period: '.', + colon: ':', + semi: ';', + excl: '!', + quest: '?', + num: '#', + dollar: '$', + percent: '%', + amp: '&', + ast: '*', + commat: '@', + lowbar: '_', + verbar: '|', + vert: '|', + sol: '/', + bsol: '\\', + lbrace: '{', + rbrace: '}', + lbrack: '[', + rbrack: ']', + lpar: '(', + rpar: ')', + nbsp: '\u00a0', + iexcl: '¡', + cent: '¢', + pound: '£', + curren: '¤', + yen: '¥', + brvbar: '¦', + sect: '§', + uml: '¨', + copy: '©', + COPY: '©', + ordf: 'ª', + laquo: '«', + not: '¬', + shy: '\u00ad', + reg: '®', + REG: '®', + macr: '¯', + deg: '°', + plusmn: '±', + sup2: '²', + sup3: '³', + acute: '´', + micro: 'µ', + para: '¶', + middot: '·', + cedil: '¸', + sup1: '¹', + ordm: 'º', + raquo: '»', + frac14: '¼', + frac12: '½', + half: '½', + frac34: '¾', + iquest: '¿', + times: '×', + div: '÷', + divide: '÷', +}; + +/** + * Latin Extended & Accented Letters (A-Z) + * @type {Record} + */ +const LATIN_ACCENTS = { + Agrave: 'À', + agrave: 'à', + Aacute: 'Á', + aacute: 'á', + Acirc: 'Â', + acirc: 'â', + Atilde: 'Ã', + atilde: 'ã', + Auml: 'Ä', + auml: 'ä', + Aring: 'Å', + aring: 'å', + AElig: 'Æ', + aelig: 'æ', + Ccedil: 'Ç', + ccedil: 'ç', + Egrave: 'È', + egrave: 'è', + Eacute: 'É', + eacute: 'é', + Ecirc: 'Ê', + ecirc: 'ê', + Euml: 'Ë', + euml: 'ë', + Igrave: 'Ì', + igrave: 'ì', + Iacute: 'Í', + iacute: 'í', + Icirc: 'Î', + icirc: 'î', + Iuml: 'Ï', + iuml: 'ï', + ETH: 'Ð', + eth: 'ð', + Ntilde: 'Ñ', + ntilde: 'ñ', + Ograve: 'Ò', + ograve: 'ò', + Oacute: 'Ó', + oacute: 'ó', + Ocirc: 'Ô', + ocirc: 'ô', + Otilde: 'Õ', + otilde: 'õ', + Ouml: 'Ö', + ouml: 'ö', + Oslash: 'Ø', + oslash: 'ø', + Ugrave: 'Ù', + ugrave: 'ù', + Uacute: 'Ú', + uacute: 'ú', + Ucirc: 'Û', + ucirc: 'û', + Uuml: 'Ü', + uuml: 'ü', + Yacute: 'Ý', + yacute: 'ý', + THORN: 'Þ', + thorn: 'þ', + szlig: 'ß', + yuml: 'ÿ', + Yuml: 'Ÿ', +}; + +/** + * Latin Extended (Letters with diacritics) + * @type {Record} + */ +const LATIN_EXTENDED = { + Amacr: 'Ā', + amacr: 'ā', + Abreve: 'Ă', + abreve: 'ă', + Aogon: 'Ą', + aogon: 'ą', + Cacute: 'Ć', + cacute: 'ć', + Ccirc: 'Ĉ', + ccirc: 'ĉ', + Cdot: 'Ċ', + cdot: 'ċ', + Ccaron: 'Č', + ccaron: 'č', + Dcaron: 'Ď', + dcaron: 'ď', + Dstrok: 'Đ', + dstrok: 'đ', + Emacr: 'Ē', + emacr: 'ē', + Ecaron: 'Ě', + ecaron: 'ě', + Edot: 'Ė', + edot: 'ė', + Eogon: 'Ę', + eogon: 'ę', + Gcirc: 'Ĝ', + gcirc: 'ĝ', + Gbreve: 'Ğ', + gbreve: 'ğ', + Gdot: 'Ġ', + gdot: 'ġ', + Gcedil: 'Ģ', + Hcirc: 'Ĥ', + hcirc: 'ĥ', + Hstrok: 'Ħ', + hstrok: 'ħ', + Itilde: 'Ĩ', + itilde: 'ĩ', + Imacr: 'Ī', + imacr: 'ī', + Iogon: 'Į', + iogon: 'į', + Idot: 'İ', + IJlig: 'IJ', + ijlig: 'ij', + Jcirc: 'Ĵ', + jcirc: 'ĵ', + Kcedil: 'Ķ', + kcedil: 'ķ', + kgreen: 'ĸ', + Lacute: 'Ĺ', + lacute: 'ĺ', + Lcedil: 'Ļ', + lcedil: 'ļ', + Lcaron: 'Ľ', + lcaron: 'ľ', + Lmidot: 'Ŀ', + lmidot: 'ŀ', + Lstrok: 'Ł', + lstrok: 'ł', + Nacute: 'Ń', + nacute: 'ń', + Ncaron: 'Ň', + ncaron: 'ň', + Ncedil: 'Ņ', + ncedil: 'ņ', + ENG: 'Ŋ', + eng: 'ŋ', + Omacr: 'Ō', + omacr: 'ō', + Odblac: 'Ő', + odblac: 'ő', + OElig: 'Œ', + oelig: 'œ', + Racute: 'Ŕ', + racute: 'ŕ', + Rcaron: 'Ř', + rcaron: 'ř', + Rcedil: 'Ŗ', + rcedil: 'ŗ', + Sacute: 'Ś', + sacute: 'ś', + Scirc: 'Ŝ', + scirc: 'ŝ', + Scedil: 'Ş', + scedil: 'ş', + Scaron: 'Š', + scaron: 'š', + Tcedil: 'Ţ', + tcedil: 'ţ', + Tcaron: 'Ť', + tcaron: 'ť', + Tstrok: 'Ŧ', + tstrok: 'ŧ', + Utilde: 'Ũ', + utilde: 'ũ', + Umacr: 'Ū', + umacr: 'ū', + Ubreve: 'Ŭ', + ubreve: 'ŭ', + Uring: 'Ů', + uring: 'ů', + Udblac: 'Ű', + udblac: 'ű', + Uogon: 'Ų', + uogon: 'ų', + Wcirc: 'Ŵ', + wcirc: 'ŵ', + Ycirc: 'Ŷ', + ycirc: 'ŷ', + Zacute: 'Ź', + zacute: 'ź', + Zdot: 'Ż', + zdot: 'ż', + Zcaron: 'Ž', + zcaron: 'ž', +}; + +/** + * Greek Letters + * @type {Record} + */ +const GREEK = { + Alpha: 'Α', + alpha: 'α', + Beta: 'Β', + beta: 'β', + Gamma: 'Γ', + gamma: 'γ', + Delta: 'Δ', + delta: 'δ', + Epsilon: 'Ε', + epsilon: 'ε', + epsiv: 'ϵ', + varepsilon: 'ϵ', + Zeta: 'Ζ', + zeta: 'ζ', + Eta: 'Η', + eta: 'η', + Theta: 'Θ', + theta: 'θ', + thetasym: 'ϑ', + vartheta: 'ϑ', + Iota: 'Ι', + iota: 'ι', + Kappa: 'Κ', + kappa: 'κ', + kappav: 'ϰ', + varkappa: 'ϰ', + Lambda: 'Λ', + lambda: 'λ', + Mu: 'Μ', + mu: 'μ', + Nu: 'Ν', + nu: 'ν', + Xi: 'Ξ', + xi: 'ξ', + Omicron: 'Ο', + omicron: 'ο', + Pi: 'Π', + pi: 'π', + piv: 'ϖ', + varpi: 'ϖ', + Rho: 'Ρ', + rho: 'ρ', + rhov: 'ϱ', + varrho: 'ϱ', + Sigma: 'Σ', + sigma: 'σ', + sigmaf: 'ς', + sigmav: 'ς', + varsigma: 'ς', + Tau: 'Τ', + tau: 'τ', + Upsilon: 'Υ', + upsilon: 'υ', + upsi: 'υ', + Upsi: 'ϒ', + upsih: 'ϒ', + Phi: 'Φ', + phi: 'φ', + phiv: 'ϕ', + varphi: 'ϕ', + Chi: 'Χ', + chi: 'χ', + Psi: 'Ψ', + psi: 'ψ', + Omega: 'Ω', + omega: 'ω', + ohm: 'Ω', + Gammad: 'Ϝ', + gammad: 'ϝ', + digamma: 'ϝ', +}; + +/** + * Cyrillic Letters + * @type {Record} + */ +const CYRILLIC = { + Afr: '𝔄', + afr: '𝔞', + Acy: 'А', + acy: 'а', + Bcy: 'Б', + bcy: 'б', + Vcy: 'В', + vcy: 'в', + Gcy: 'Г', + gcy: 'г', + Dcy: 'Д', + dcy: 'д', + IEcy: 'Е', + iecy: 'е', + IOcy: 'Ё', + iocy: 'ё', + ZHcy: 'Ж', + zhcy: 'ж', + Zcy: 'З', + zcy: 'з', + Icy: 'И', + icy: 'и', + Jcy: 'Й', + jcy: 'й', + Kcy: 'К', + kcy: 'к', + Lcy: 'Л', + lcy: 'л', + Mcy: 'М', + mcy: 'м', + Ncy: 'Н', + ncy: 'н', + Ocy: 'О', + ocy: 'о', + Pcy: 'П', + pcy: 'п', + Rcy: 'Р', + rcy: 'р', + Scy: 'С', + scy: 'с', + Tcy: 'Т', + tcy: 'т', + Ucy: 'У', + ucy: 'у', + Fcy: 'Ф', + fcy: 'ф', + KHcy: 'Х', + khcy: 'х', + TScy: 'Ц', + tscy: 'ц', + CHcy: 'Ч', + chcy: 'ч', + SHcy: 'Ш', + shcy: 'ш', + SHCHcy: 'Щ', + shchcy: 'щ', + HARDcy: 'Ъ', + hardcy: 'ъ', + Ycy: 'Ы', + ycy: 'ы', + SOFTcy: 'Ь', + softcy: 'ь', + Ecy: 'Э', + ecy: 'э', + YUcy: 'Ю', + yucy: 'ю', + YAcy: 'Я', + yacy: 'я', + DJcy: 'Ђ', + djcy: 'ђ', + GJcy: 'Ѓ', + gjcy: 'ѓ', + Jukcy: 'Є', + jukcy: 'є', + DScy: 'Ѕ', + dscy: 'ѕ', + Iukcy: 'І', + iukcy: 'і', + YIcy: 'Ї', + yicy: 'ї', + Jsercy: 'Ј', + jsercy: 'ј', + LJcy: 'Љ', + ljcy: 'љ', + NJcy: 'Њ', + njcy: 'њ', + TSHcy: 'Ћ', + tshcy: 'ћ', + KJcy: 'Ќ', + kjcy: 'ќ', + Ubrcy: 'Ў', + ubrcy: 'ў', + DZcy: 'Џ', + dzcy: 'џ', +}; + +/** + * Mathematical Operators & Relations + * @type {Record} + */ +const MATH = { + plus: '+', + minus: '−', + mnplus: '∓', + mp: '∓', + pm: '±', + times: '×', + div: '÷', + divide: '÷', + sdot: '⋅', + star: '☆', + starf: '★', + bigstar: '★', + lowast: '∗', + ast: '*', + midast: '*', + compfn: '∘', + smallcircle: '∘', + bullet: '•', + bull: '•', + nbsp: '\u00a0', + hellip: '…', + mldr: '…', + prime: '′', + Prime: '″', + tprime: '‴', + bprime: '‵', + backprime: '‵', + minus: '−', + minusd: '∸', + dotminus: '∸', + plusdo: '∔', + dotplus: '∔', + plusmn: '±', + minusplus: '∓', + mnplus: '∓', + mp: '∓', + setminus: '∖', + smallsetminus: '∖', + Backslash: '∖', + setmn: '∖', + ssetmn: '∖', + lowbar: '_', + verbar: '|', + vert: '|', + VerticalLine: '|', + colon: ':', + Colon: '∷', + Proportion: '∷', + ratio: '∶', + equals: '=', + ne: '≠', + nequiv: '≢', + equiv: '≡', + Congruent: '≡', + sim: '∼', + thicksim: '∼', + thksim: '∼', + sime: '≃', + simeq: '≃', + TildeEqual: '≃', + asymp: '≈', + approx: '≈', + thickapprox: '≈', + thkap: '≈', + TildeTilde: '≈', + ncong: '≇', + cong: '≅', + TildeFullEqual: '≅', + asympeq: '≍', + CupCap: '≍', + bump: '≎', + Bumpeq: '≎', + HumpDownHump: '≎', + bumpe: '≏', + bumpeq: '≏', + HumpEqual: '≏', + dotminus: '∸', + minusd: '∸', + plusdo: '∔', + dotplus: '∔', + le: '≤', + LessEqual: '≤', + ge: '≥', + GreaterEqual: '≥', + lesseqgtr: '⋚', + lesseqqgtr: '⪋', + greater: '>', + less: '<', +}; + +/** + * Mathematical Operators (Advanced) + * @type {Record} + */ +const MATH_ADVANCED = { + alefsym: 'ℵ', + aleph: 'ℵ', + beth: 'ℶ', + gimel: 'ℷ', + daleth: 'ℸ', + forall: '∀', + ForAll: '∀', + part: '∂', + PartialD: '∂', + exist: '∃', + Exists: '∃', + nexist: '∄', + nexists: '∄', + empty: '∅', + emptyset: '∅', + emptyv: '∅', + varnothing: '∅', + nabla: '∇', + Del: '∇', + isin: '∈', + isinv: '∈', + in: '∈', + Element: '∈', + notin: '∉', + notinva: '∉', + ni: '∋', + niv: '∋', + SuchThat: '∋', + ReverseElement: '∋', + notni: '∌', + notniva: '∌', + prod: '∏', + Product: '∏', + coprod: '∐', + Coproduct: '∐', + sum: '∑', + Sum: '∑', + minus: '−', + mp: '∓', + plusdo: '∔', + dotplus: '∔', + setminus: '∖', + lowast: '∗', + radic: '√', + Sqrt: '√', + prop: '∝', + propto: '∝', + Proportional: '∝', + varpropto: '∝', + infin: '∞', + infintie: '⧝', + ang: '∠', + angle: '∠', + angmsd: '∡', + measuredangle: '∡', + angsph: '∢', + mid: '∣', + VerticalBar: '∣', + nmid: '∤', + nsmid: '∤', + npar: '∦', + parallel: '∥', + spar: '∥', + nparallel: '∦', + nspar: '∦', + and: '∧', + wedge: '∧', + or: '∨', + vee: '∨', + cap: '∩', + cup: '∪', + int: '∫', + Integral: '∫', + conint: '∮', + ContourIntegral: '∮', + Conint: '∯', + DoubleContourIntegral: '∯', + Cconint: '∰', + there4: '∴', + therefore: '∴', + Therefore: '∴', + becaus: '∵', + because: '∵', + Because: '∵', + ratio: '∶', + Proportion: '∷', + minusd: '∸', + dotminus: '∸', + mDDot: '∺', + homtht: '∻', + sim: '∼', + bsimg: '∽', + backsim: '∽', + ac: '∾', + mstpos: '∾', + acd: '∿', + VerticalTilde: '≀', + wr: '≀', + wreath: '≀', + nsime: '≄', + nsimeq: '≄', + nsimeq: '≄', + ncong: '≇', + simne: '≆', + ncongdot: '⩭̸', + ngsim: '≵', + nsim: '≁', + napprox: '≉', + nap: '≉', + ngeq: '≱', + nge: '≱', + nleq: '≰', + nle: '≰', + ngtr: '≯', + ngt: '≯', + nless: '≮', + nlt: '≮', + nprec: '⊀', + npr: '⊀', + nsucc: '⊁', + nsc: '⊁', +}; + +/** + * Arrows + * @type {Record} + */ +const ARROWS = { + larr: '←', + leftarrow: '←', + LeftArrow: '←', + uarr: '↑', + uparrow: '↑', + UpArrow: '↑', + rarr: '→', + rightarrow: '→', + RightArrow: '→', + darr: '↓', + downarrow: '↓', + DownArrow: '↓', + harr: '↔', + leftrightarrow: '↔', + LeftRightArrow: '↔', + varr: '↕', + updownarrow: '↕', + UpDownArrow: '↕', + nwarr: '↖', + nwarrow: '↖', + UpperLeftArrow: '↖', + nearr: '↗', + nearrow: '↗', + UpperRightArrow: '↗', + searr: '↘', + searrow: '↘', + LowerRightArrow: '↘', + swarr: '↙', + swarrow: '↙', + LowerLeftArrow: '↙', + lArr: '⇐', + Leftarrow: '⇐', + uArr: '⇑', + Uparrow: '⇑', + rArr: '⇒', + Rightarrow: '⇒', + dArr: '⇓', + Downarrow: '⇓', + hArr: '⇔', + Leftrightarrow: '⇔', + iff: '⇔', + vArr: '⇕', + Updownarrow: '⇕', + lAarr: '⇚', + Lleftarrow: '⇚', + rAarr: '⇛', + Rrightarrow: '⇛', + lrarr: '⇆', + leftrightarrows: '⇆', + rlarr: '⇄', + rightleftarrows: '⇄', + lrhar: '⇋', + leftrightharpoons: '⇋', + ReverseEquilibrium: '⇋', + rlhar: '⇌', + rightleftharpoons: '⇌', + Equilibrium: '⇌', + udarr: '⇅', + UpArrowDownArrow: '⇅', + duarr: '⇵', + DownArrowUpArrow: '⇵', + llarr: '⇇', + leftleftarrows: '⇇', + rrarr: '⇉', + rightrightarrows: '⇉', + ddarr: '⇊', + downdownarrows: '⇊', + har: '↽', + lhard: '↽', + leftharpoondown: '↽', + lharu: '↼', + leftharpoonup: '↼', + rhard: '⇁', + rightharpoondown: '⇁', + rharu: '⇀', + rightharpoonup: '⇀', + lsh: '↰', + Lsh: '↰', + rsh: '↱', + Rsh: '↱', + ldsh: '↲', + rdsh: '↳', + hookleftarrow: '↩', + hookrightarrow: '↪', + mapstoleft: '↤', + mapstoup: '↥', + map: '↦', + mapsto: '↦', + mapstodown: '↧', + crarr: '↵', + nwarrow: '↖', + nearrow: '↗', + searrow: '↘', + swarrow: '↙', + nleftarrow: '↚', + nleftrightarrow: '↮', + nrightarrow: '↛', + nrarr: '↛', + larrtl: '↢', + rarrtl: '↣', + leftarrowtail: '↢', + rightarrowtail: '↣', + twoheadleftarrow: '↞', + twoheadrightarrow: '↠', + Larr: '↞', + Rarr: '↠', + larrhk: '↩', + rarrhk: '↪', + larrlp: '↫', + looparrowleft: '↫', + rarrlp: '↬', + looparrowright: '↬', + harrw: '↭', + leftrightsquigarrow: '↭', + nrarrw: '↝̸', + rarrw: '↝', + rightsquigarrow: '↝', + larrbfs: '⤟', + rarrbfs: '⤠', + nvHarr: '⤄', + nvlArr: '⤂', + nvrArr: '⤃', + larrfs: '⤝', + rarrfs: '⤞', + Map: '⤅', + larrsim: '⥳', + rarrsim: '⥴', + harrcir: '⥈', + Uarrocir: '⥉', + lurdshar: '⥊', + ldrdhar: '⥧', + ldrushar: '⥋', + rdldhar: '⥩', + lrhard: '⥭', + rlhar: '⇌', + uharr: '↾', + uharl: '↿', + dharr: '⇂', + dharl: '⇃', + Uarr: '↟', + Darr: '↡', + zigrarr: '⇝', + nwArr: '⇖', + neArr: '⇗', + seArr: '⇘', + swArr: '⇙', + nharr: '↮', + nhArr: '⇎', + nlarr: '↚', + nlArr: '⇍', + nrarr: '↛', + nrArr: '⇏', + larrb: '⇤', + LeftArrowBar: '⇤', + rarrb: '⇥', + RightArrowBar: '⇥', +}; + +/** + * Geometric Shapes + * @type {Record} + */ +const SHAPES = { + square: '□', + Square: '□', + squ: '□', + squf: '▪', + squarf: '▪', + blacksquar: '▪', + blacksquare: '▪', + FilledVerySmallSquare: '▪', + blk34: '▓', + blk12: '▒', + blk14: '░', + block: '█', + srect: '▭', + rect: '▭', + sdot: '⋅', + sdotb: '⊡', + dotsquare: '⊡', + triangle: '▵', + tri: '▵', + trine: '▵', + utri: '▵', + triangledown: '▿', + dtri: '▿', + tridown: '▿', + triangleleft: '◃', + ltri: '◃', + triangleright: '▹', + rtri: '▹', + blacktriangle: '▴', + utrif: '▴', + blacktriangledown: '▾', + dtrif: '▾', + blacktriangleleft: '◂', + ltrif: '◂', + blacktriangleright: '▸', + rtrif: '▸', + loz: '◊', + lozenge: '◊', + blacklozenge: '⧫', + lozf: '⧫', + bigcirc: '◯', + xcirc: '◯', + circ: 'ˆ', + Circle: '○', + cir: '○', + o: '○', + bullet: '•', + bull: '•', + hellip: '…', + mldr: '…', + nldr: '‥', + boxh: '─', + HorizontalLine: '─', + boxv: '│', + boxdr: '┌', + boxdl: '┐', + boxur: '└', + boxul: '┘', + boxvr: '├', + boxvl: '┤', + boxhd: '┬', + boxhu: '┴', + boxvh: '┼', + boxH: '═', + boxV: '║', + boxdR: '╒', + boxDr: '╓', + boxDR: '╔', + boxDl: '╕', + boxdL: '╖', + boxDL: '╗', + boxuR: '╘', + boxUr: '╙', + boxUR: '╚', + boxUl: '╜', + boxuL: '╛', + boxUL: '╝', + boxvR: '╞', + boxVr: '╟', + boxVR: '╠', + boxVl: '╢', + boxvL: '╡', + boxVL: '╣', + boxHd: '╤', + boxhD: '╥', + boxHD: '╦', + boxHu: '╧', + boxhU: '╨', + boxHU: '╩', + boxvH: '╪', + boxVh: '╫', + boxVH: '╬', +}; + +/** + * Punctuation & Diacritics + * @type {Record} + */ +const PUNCTUATION = { + excl: '!', + iexcl: '¡', + brvbar: '¦', + sect: '§', + uml: '¨', + copy: '©', + ordf: 'ª', + laquo: '«', + not: '¬', + shy: '\u00ad', + reg: '®', + macr: '¯', + deg: '°', + plusmn: '±', + sup2: '²', + sup3: '³', + acute: '´', + micro: 'µ', + para: '¶', + middot: '·', + cedil: '¸', + sup1: '¹', + ordm: 'º', + raquo: '»', + frac14: '¼', + frac12: '½', + frac34: '¾', + iquest: '¿', + nbsp: '\u00a0', + comma: ',', + period: '.', + colon: ':', + semi: ';', + vert: '|', + Verbar: '‖', + verbar: '|', + dblac: '˝', + circ: 'ˆ', + caron: 'ˇ', + breve: '˘', + dot: '˙', + ring: '˚', + ogon: '˛', + tilde: '˜', + DiacriticalGrave: '`', + DiacriticalAcute: '´', + DiacriticalTilde: '˜', + DiacriticalDot: '˙', + DiacriticalDoubleAcute: '˝', + grave: '`', + acute: '´', +}; + +/** + * Currency Symbols + * @type {Record} + */ +const CURRENCY = { + cent: '¢', + pound: '£', + curren: '¤', + yen: '¥', + euro: '€', + dollar: '$', + euro: '€', + fnof: 'ƒ', + inr: '₹', + af: '؋', + birr: 'ብር', + peso: '₱', + rub: '₽', + won: '₩', + yuan: '¥', + cedil: '¸', +}; + +/** + * Fractions + * @type {Record} + */ +const FRACTIONS = { + frac12: '½', + half: '½', + frac13: '⅓', + frac14: '¼', + frac15: '⅕', + frac16: '⅙', + frac18: '⅛', + frac23: '⅔', + frac25: '⅖', + frac34: '¾', + frac35: '⅗', + frac38: '⅜', + frac45: '⅘', + frac56: '⅚', + frac58: '⅝', + frac78: '⅞', + frasl: '⁄', +}; + +/** + * Miscellaneous Symbols + * @type {Record} + */ +const MISC_SYMBOLS = { + trade: '™', + TRADE: '™', + telrec: '⌕', + target: '⌖', + ulcorn: '⌜', + ulcorner: '⌜', + urcorn: '⌝', + urcorner: '⌝', + dlcorn: '⌞', + llcorner: '⌞', + drcorn: '⌟', + lrcorner: '⌟', + intercal: '⊺', + intcal: '⊺', + oplus: '⊕', + CirclePlus: '⊕', + ominus: '⊖', + CircleMinus: '⊖', + otimes: '⊗', + CircleTimes: '⊗', + osol: '⊘', + odot: '⊙', + CircleDot: '⊙', + oast: '⊛', + circledast: '⊛', + odash: '⊝', + circleddash: '⊝', + ocirc: '⊚', + circledcirc: '⊚', + boxplus: '⊞', + plusb: '⊞', + boxminus: '⊟', + minusb: '⊟', + boxtimes: '⊠', + timesb: '⊠', + boxdot: '⊡', + sdotb: '⊡', + veebar: '⊻', + vee: '∨', + barvee: '⊽', + and: '∧', + wedge: '∧', + Cap: '⋒', + Cup: '⋓', + Fork: '⋔', + pitchfork: '⋔', + epar: '⋕', + ltlarr: '⥶', + nvap: '≍⃒', + nvsim: '∼⃒', + nvge: '≥⃒', + nvle: '≤⃒', + nvlt: '<⃒', + nvgt: '>⃒', + nvltrie: '⊴⃒', + nvrtrie: '⊵⃒', + Vdash: '⊩', + dashv: '⊣', + vDash: '⊨', + Vdash: '⊩', + Vvdash: '⊪', + nvdash: '⊬', + nvDash: '⊭', + nVdash: '⊮', + nVDash: '⊯', +}; + +/** + * All entities combined (if you need everything) + * @type {Record} + */ +const ALL_ENTITIES = { + ...BASIC_LATIN, + ...LATIN_ACCENTS, + ...LATIN_EXTENDED, + ...GREEK, + ...CYRILLIC, + ...MATH, + ...MATH_ADVANCED, + ...ARROWS, + ...SHAPES, + ...PUNCTUATION, + ...CURRENCY, + ...FRACTIONS, + ...MISC_SYMBOLS, +}; + +const XML = { + amp: "&", + apos: "'", + gt: ">", + lt: "<", + quot: "\"" +} +const COMMON_HTML = { + nbsp: '\u00a0', + copy: '\u00a9', + reg: '\u00ae', + trade: '\u2122', + mdash: '\u2014', + ndash: '\u2013', + hellip: '\u2026', + laquo: '\u00ab', + raquo: '\u00bb', + lsquo: '\u2018', + rsquo: '\u2019', + ldquo: '\u201c', + rdquo: '\u201d', + bull: '\u2022', + para: '\u00b6', + sect: '\u00a7', + deg: '\u00b0', + frac12: '\u00bd', + frac14: '\u00bc', + frac34: '\u00be', +} +// --------------------------------------------------------------------------- +// Note: NUMERIC_ENTITIES (&#NNN; / &#xHH;) are handled by the scanner directly +// via String.fromCodePoint() without any map lookup. +// --------------------------------------------------------------------------- +;// CONCATENATED MODULE: ./node_modules/@nodable/entities/src/EntityDecoder.js +// --------------------------------------------------------------------------- +// Built-in named entity map (name → replacement string) +// No regex, no {regex,val} objects — just flat key/value pairs. +// --------------------------------------------------------------------------- + + + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +const SPECIAL_CHARS = new Set('!?\\\\/[]$%{}^&*()<>|+'); + +/** + * Validate that an entity name contains no dangerous characters. + * @param {string} name + * @returns {string} the name, unchanged + * @throws {Error} on invalid characters + */ +function EntityDecoder_validateEntityName(name) { + if (name[0] === '#') { + throw new Error(`[EntityReplacer] Invalid character '#' in entity name: "${name}"`); + } + for (const ch of name) { + if (SPECIAL_CHARS.has(ch)) { + throw new Error(`[EntityReplacer] Invalid character '${ch}' in entity name: "${name}"`); + } + } + return name; +} + +/** + * Merge one or more entity maps into a flat name→string map. + * Accepts either: + * - plain string values: { amp: '&' } + * - legacy {regex,val} / {regx,val}: { lt: { regex: /.../, val: '<' } } + * + * Values containing '&' are skipped (recursive expansion risk). + * + * @param {...object} maps + * @returns {Record} + */ +function mergeEntityMaps(...maps) { + const out = Object.create(null); + for (const map of maps) { + if (!map) continue; + for (const key of Object.keys(map)) { + const raw = map[key]; + if (typeof raw === 'string') { + out[key] = raw; + } else if (raw && typeof raw === 'object' && raw.val !== undefined) { + // Legacy {regex,val} or {regx,val} — extract the string val only + const val = raw.val; + if (typeof val === 'string') { + out[key] = val; + } + // function vals are not supported in the scanner — skip + } + } + } + return out; +} + +// --------------------------------------------------------------------------- +// applyLimitsTo helpers +// --------------------------------------------------------------------------- + +const LIMIT_TIER_EXTERNAL = 'external'; // input/runtime + persistent external maps +const LIMIT_TIER_BASE = 'base'; // DEFAULT_XML_ENTITIES + namedEntities (system) maps +const LIMIT_TIER_ALL = 'all'; // every entity regardless of tier + +/** + * Resolve `applyLimitsTo` option into a normalised Set of tier strings. + * Accepted values: 'external' | 'base' | 'all' | string[] + * Default: 'external' (only untrusted injected entities are counted). + * @param {string|string[]|undefined} raw + * @returns {Set} + */ +function parseLimitTiers(raw) { + if (!raw || raw === LIMIT_TIER_EXTERNAL) return new Set([LIMIT_TIER_EXTERNAL]); + if (raw === LIMIT_TIER_ALL) return new Set([LIMIT_TIER_ALL]); + if (raw === LIMIT_TIER_BASE) return new Set([LIMIT_TIER_BASE]); + if (Array.isArray(raw)) return new Set(raw); + return new Set([LIMIT_TIER_EXTERNAL]); // safe default for unrecognised values +} + +// --------------------------------------------------------------------------- +// NCR (Numeric Character Reference) classification +// --------------------------------------------------------------------------- + +// Severity order — higher number = stricter action. +// Used to enforce minimum action levels for specific codepoint ranges. +const NCR_LEVEL = Object.freeze({ allow: 0, leave: 1, remove: 2, throw: 3 }); + +// XML 1.0 §2.2: allowed chars are #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] +// Restricted C0: U+0001–U+001F excluding U+0009, U+000A, U+000D +const XML10_ALLOWED_C0 = new Set([0x09, 0x0A, 0x0D]); + +/** + * Parse the `ncr` constructor option into flat, hot-path-friendly fields. + * @param {object|undefined} ncr + * @returns {{ xmlVersion: number, onLevel: number, nullLevel: number }} + */ +function parseNCRConfig(ncr) { + if (!ncr) { + return { xmlVersion: 1.0, onLevel: NCR_LEVEL.allow, nullLevel: NCR_LEVEL.remove }; + } + const xmlVersion = ncr.xmlVersion === 1.1 ? 1.1 : 1.0; + const onLevel = NCR_LEVEL[ncr.onNCR] ?? NCR_LEVEL.allow; + const nullLevel = NCR_LEVEL[ncr.nullNCR] ?? NCR_LEVEL.remove; + // 'allow' is not meaningful for null — clamp to at least 'remove' + const clampedNull = Math.max(nullLevel, NCR_LEVEL.remove); + return { xmlVersion, onLevel, nullLevel: clampedNull }; +} + +// --------------------------------------------------------------------------- +// EntityReplacer +// --------------------------------------------------------------------------- + +/** + * Single-pass, zero-regex entity replacer for XML/HTML content. + * + * Algorithm: scan the string once for '&', read to ';', resolve via map + * or direct codepoint conversion, build output chunks, join once at the end. + * + * Entity lookup priority (highest → lowest): + * 1. input / runtime (DOCTYPE entities for current document) + * 2. persistent external (survive across documents) + * 3. base named map (DEFAULT_XML_ENTITIES + user-supplied namedEntities) + * + * Both input and external resolve as the 'external' tier for limit purposes. + * Base map entities resolve as the 'base' tier. + * + * Numeric / hex references (&#NNN; / &#xHH;) are resolved directly via + * String.fromCodePoint() — no map needed. They count as 'base' tier. + * + * @example + * const replacer = new EntityReplacer({ namedEntities: COMMON_HTML }); + * replacer.setExternalEntities({ brand: 'Acme' }); + * + * const instance = replacer.reset(); + * instance.addInputEntities({ version: '1.0' }); + * instance.encode('&brand; v&version; <'); // 'Acme v1.0 <' + */ +class EntityDecoder { + /** + * @param {object} [options] + * @param {object|null} [options.namedEntities] — extra named entities merged into base map + * @param {object} [options.limit] — security limits + * @param {number} [options.limit.maxTotalExpansions=0] — 0 = unlimited + * @param {number} [options.limit.maxExpandedLength=0] — 0 = unlimited + * @param {'external'|'base'|'all'|string[]} [options.limit.applyLimitsTo='external'] + * Which entity tiers count against the security limits: + * - 'external' (default) — only input/runtime + persistent external entities + * - 'base' — only DEFAULT_XML_ENTITIES + namedEntities + * - 'all' — every entity regardless of tier + * - string[] — explicit combination, e.g. ['external', 'base'] + * @param {((resolved: string, original: string) => string)|null} [options.postCheck=null] + * @param {string[]} [options.remove=[]] — entity names (e.g. ['nbsp', '#13']) to delete (replace with empty string) + * @param {string[]} [options.leave=[]] — entity names to keep as literal (unchanged in output) + * @param {object} [options.ncr] — Numeric Character Reference controls + * @param {1.0|1.1} [options.ncr.xmlVersion=1.0] + * XML version governing which codepoint ranges are restricted: + * - 1.0 — C0 controls U+0001–U+001F (except U+0009/000A/000D) are prohibited + * - 1.1 — C0 controls are allowed when written as NCRs; C1 (U+007F–U+009F) decoded as-is + * @param {'allow'|'leave'|'remove'|'throw'} [options.ncr.onNCR='allow'] + * Base action for numeric references. Severity order: allow < leave < remove < throw. + * For codepoint ranges that carry a minimum level (surrogates → remove, XML 1.0 C0 → remove), + * the effective action is max(onNCR, rangeMinimum). + * @param {'remove'|'throw'} [options.ncr.nullNCR='remove'] + * Action for U+0000 (null). 'allow' and 'leave' are clamped to 'remove' since null is never safe. + */ + constructor(options = {}) { + this._limit = options.limit || {}; + this._maxTotalExpansions = this._limit.maxTotalExpansions || 0; + this._maxExpandedLength = this._limit.maxExpandedLength || 0; + this._postCheck = typeof options.postCheck === 'function' ? options.postCheck : r => r; + this._limitTiers = parseLimitTiers(this._limit.applyLimitsTo ?? LIMIT_TIER_EXTERNAL); + this._numericAllowed = options.numericAllowed ?? true; + // Base map: DEFAULT_XML_ENTITIES + user-supplied extras. Immutable after construction. + this._baseMap = mergeEntityMaps(XML, options.namedEntities || null); + + // Persistent external entities — survive across documents. + // Stored as a separate map so reset() never touches them. + /** @type {Record} */ + this._externalMap = Object.create(null); + + // Input / runtime entities — current document only, wiped on reset(). + /** @type {Record} */ + this._inputMap = Object.create(null); + + // Per-document counters + this._totalExpansions = 0; + this._expandedLength = 0; + + // --- New: remove / leave sets --- + /** @type {Set} */ + this._removeSet = new Set(options.remove && Array.isArray(options.remove) ? options.remove : []); + /** @type {Set} */ + this._leaveSet = new Set(options.leave && Array.isArray(options.leave) ? options.leave : []); + + // --- NCR config (parsed into flat fields for hot-path speed) --- + const ncrCfg = parseNCRConfig(options.ncr); + this._ncrXmlVersion = ncrCfg.xmlVersion; + this._ncrOnLevel = ncrCfg.onLevel; + this._ncrNullLevel = ncrCfg.nullLevel; + } + + // ------------------------------------------------------------------------- + // Persistent external entity registration + // ------------------------------------------------------------------------- + + /** + * Replace the full set of persistent external entities. + * All keys are validated — throws on invalid characters. + * @param {Record} map + */ + setExternalEntities(map) { + if (map) { + for (const key of Object.keys(map)) { + EntityDecoder_validateEntityName(key); + } + } + this._externalMap = mergeEntityMaps(map); + } + + /** + * Add a single persistent external entity. + * @param {string} key + * @param {string} value + */ + addExternalEntity(key, value) { + EntityDecoder_validateEntityName(key); + if (typeof value === 'string' && value.indexOf('&') === -1) { + this._externalMap[key] = value; + } + } + + // ------------------------------------------------------------------------- + // Input / runtime entity registration (per document) + // ------------------------------------------------------------------------- + + /** + * Inject DOCTYPE entities for the current document. + * Also resets per-document expansion counters. + * @param {Record} map + */ + addInputEntities(map) { + this._totalExpansions = 0; + this._expandedLength = 0; + this._inputMap = mergeEntityMaps(map); + } + + // ------------------------------------------------------------------------- + // Per-document reset + // ------------------------------------------------------------------------- + + /** + * Wipe input/runtime entities and reset counters. + * Call this before processing each new document. + * @returns {this} + */ + reset() { + this._inputMap = Object.create(null); + this._totalExpansions = 0; + this._expandedLength = 0; + return this; + } + + // ------------------------------------------------------------------------- + // XML version (can be set after construction, e.g. once parser reads ) + // ------------------------------------------------------------------------- + + /** + * Update the XML version used for NCR classification. + * Call this as soon as the document's `` declaration is parsed. + * @param {1.0|1.1|number} version + */ + setXmlVersion(version) { + this._ncrXmlVersion = version === 1.1 ? 1.1 : 1.0; + } + + // ------------------------------------------------------------------------- + // Primary API + // ------------------------------------------------------------------------- + + /** + * Replace all entity references in `str` in a single pass. + * + * @param {string} str + * @returns {string} + */ + decode(str) { + if (typeof str !== 'string' || str.length === 0) return str; + //TODO: check if needed + //if (str.indexOf('&') === -1) return str; // fast path — no entities at all + + const original = str; + const chunks = []; + const len = str.length; + let last = 0; // start of next unprocessed literal chunk + let i = 0; + + const limitExpansions = this._maxTotalExpansions > 0; + const limitLength = this._maxExpandedLength > 0; + const checkLimits = limitExpansions || limitLength; + + while (i < len) { + // Scan forward to next '&' + if (str.charCodeAt(i) !== 38 /* '&' */) { i++; continue; } + + // --- Found '&' at position i --- + + // Scan forward to ';' + let j = i + 1; + while (j < len && str.charCodeAt(j) !== 59 /* ';' */ && (j - i) <= 32) j++; + + if (j >= len || str.charCodeAt(j) !== 59) { + // No closing ';' within window — treat '&' as literal + i++; + continue; + } + + // Raw token between '&' and ';' (exclusive) + const token = str.slice(i + 1, j); + if (token.length === 0) { i++; continue; } + + let replacement; + let tier; // which limit tier this entity belongs to + + if (this._removeSet.has(token)) { + // Remove entity: replace with empty string + replacement = ''; + // If entity was unknown (replacement undefined), we still need a tier for limits. + // Treat as external tier because it's user-directed removal of an unknown reference. + if (tier === undefined) { + tier = LIMIT_TIER_EXTERNAL; + } + } else if (this._leaveSet.has(token)) { + // Do not replace — keep original &token; as literal + i++; + continue; + } else if (token.charCodeAt(0) === 35 /* '#' */) { + // ---- Numeric / NCR reference ---- + // NCR classification always runs first — prohibited codepoints must be + // caught regardless of numericAllowed. + const ncrResult = this._resolveNCR(token); + if (ncrResult === undefined) { + // 'leave' action — keep original &token; as-is + i++; + continue; + } + replacement = ncrResult; // '' for remove, char string for allow + tier = LIMIT_TIER_BASE; + } else { + // ---- Named reference ---- + const resolved = this._resolveName(token); + replacement = resolved?.value; + tier = resolved?.tier; + } + + if (replacement === undefined) { + // Unknown entity — leave as-is, advance past '&' only + i++; + continue; + } + + // Flush literal chunk before this entity + if (i > last) chunks.push(str.slice(last, i)); + chunks.push(replacement); + last = j + 1; // skip past ';' + i = last; + + // Apply expansion limits only if this tier is being tracked + if (checkLimits && this._tierCounts(tier)) { + if (limitExpansions) { + this._totalExpansions++; + if (this._totalExpansions > this._maxTotalExpansions) { + throw new Error( + `[EntityReplacer] Entity expansion count limit exceeded: ` + + `${this._totalExpansions} > ${this._maxTotalExpansions}` + ); + } + } + if (limitLength) { + // delta: replacement.length minus the raw &token; length (token.length + 2 for '&' and ';') + const delta = replacement.length - (token.length + 2); + if (delta > 0) { + this._expandedLength += delta; + if (this._expandedLength > this._maxExpandedLength) { + throw new Error( + `[EntityReplacer] Expanded content length limit exceeded: ` + + `${this._expandedLength} > ${this._maxExpandedLength}` + ); + } + } + } + } + } + + // Flush trailing literal + if (last < len) chunks.push(str.slice(last)); + + // If nothing was replaced, chunks is empty — return original + const result = chunks.length === 0 ? str : chunks.join(''); + + return this._postCheck(result, original); + } + + // ------------------------------------------------------------------------- + // Private: limit tier check + // ------------------------------------------------------------------------- + + /** + * Returns true if a resolved entity of the given tier should count + * against the expansion/length limits. + * @param {string} tier — LIMIT_TIER_EXTERNAL | LIMIT_TIER_BASE + * @returns {boolean} + */ + _tierCounts(tier) { + if (this._limitTiers.has(LIMIT_TIER_ALL)) return true; + return this._limitTiers.has(tier); + } + + // ------------------------------------------------------------------------- + // Private: entity resolution + // ------------------------------------------------------------------------- + + /** + * Resolve a named entity token (without & and ;). + * Priority: inputMap > externalMap > baseMap + * Returns the resolved value tagged with its limit tier. + * + * @param {string} name + * @returns {{ value: string, tier: string }|undefined} + */ + _resolveName(name) { + // input and external both count as 'external' tier for limit purposes — + // they are injected at runtime and are the untrusted surface. + if (name in this._inputMap) return { value: this._inputMap[name], tier: LIMIT_TIER_EXTERNAL }; + if (name in this._externalMap) return { value: this._externalMap[name], tier: LIMIT_TIER_EXTERNAL }; + if (name in this._baseMap) return { value: this._baseMap[name], tier: LIMIT_TIER_BASE }; + return undefined; + } + + /** + * Classify a codepoint and return the minimum action level that must be applied. + * Returns -1 when no minimum is imposed (normal allow path). + * + * Ranges checked (in priority order): + * 1. U+0000 — null, governed by nullNCR (always ≥ remove) + * 2. U+D800–U+DFFF — surrogates, always prohibited (min: remove) + * 3. U+0001–U+001F \ {0x09,0x0A,0x0D} — XML 1.0 restricted C0 (min: remove) + * (skipped in XML 1.1 — C0 controls are allowed when written as NCRs) + * + * @param {number} cp — codepoint + * @returns {number} — minimum NCR_LEVEL value, or -1 for no restriction + */ + _classifyNCR(cp) { + // 1. Null + if (cp === 0) return this._ncrNullLevel; + + // 2. Surrogates — always prohibited, minimum 'remove' + if (cp >= 0xD800 && cp <= 0xDFFF) return NCR_LEVEL.remove; + + // 3. XML 1.0 restricted C0 controls + if (this._ncrXmlVersion === 1.0) { + if (cp >= 0x01 && cp <= 0x1F && !XML10_ALLOWED_C0.has(cp)) return NCR_LEVEL.remove; + } + + return -1; // no restriction + } + + /** + * Execute a resolved NCR action. + * + * @param {number} action — NCR_LEVEL value + * @param {string} token — raw token (e.g. '#38') for error messages + * @param {number} cp — codepoint, used only for error messages + * @returns {string|undefined} + * - decoded character string → 'allow' + * - '' → 'remove' + * - undefined → 'leave' (caller must skip past '&' only) + * - throws Error → 'throw' + */ + _applyNCRAction(action, token, cp) { + switch (action) { + case NCR_LEVEL.allow: return String.fromCodePoint(cp); + case NCR_LEVEL.remove: return ''; + case NCR_LEVEL.leave: return undefined; // signal: keep literal + case NCR_LEVEL.throw: + throw new Error( + `[EntityDecoder] Prohibited numeric character reference ` + + `&${token}; (U+${cp.toString(16).toUpperCase().padStart(4, '0')})` + ); + default: return String.fromCodePoint(cp); + } + } + + /** + * Full NCR resolution pipeline for a numeric token. + * + * Steps: + * 1. Parse the codepoint (decimal or hex). + * 2. Validate the raw codepoint range (NaN, <0, >0x10FFFF). + * 3. If numericAllowed is false and no minimum restriction applies → leave as-is. + * 4. Classify the codepoint to find the minimum required action level. + * 5. Resolve effective action = max(onNCR, minimum). + * 6. Apply and return. + * + * @param {string} token — e.g. '#38', '#x26', '#X26' + * @returns {string|undefined} + * - string (incl. '') — replacement ('' = remove) + * - undefined — leave original &token; as-is + */ + _resolveNCR(token) { + // Step 1: parse codepoint + const second = token.charCodeAt(1); + let cp; + if (second === 120 /* x */ || second === 88 /* X */) { + cp = parseInt(token.slice(2), 16); + } else { + cp = parseInt(token.slice(1), 10); + } + + // Step 2: out-of-range → leave as-is unconditionally + if (Number.isNaN(cp) || cp < 0 || cp > 0x10FFFF) return undefined; + + // Step 3: classify to get minimum action level + const minimum = this._classifyNCR(cp); + + // Step 4: if numericAllowed is false and no hard minimum → leave + if (!this._numericAllowed && minimum < NCR_LEVEL.remove) return undefined; + + // Step 5: effective action = max(configured onNCR, range minimum) + const effective = minimum === -1 + ? this._ncrOnLevel + : Math.max(this._ncrOnLevel, minimum); + + // Step 6: apply + return this._applyNCRAction(effective, token, cp); + } +} ;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/xmlparser/OrderedObjParser.js ///@ts-check @@ -54326,6 +56889,8 @@ function ignoreAttributes_getIgnoreAttributesFn(ignoreAttributes) { + + // const regx = // '<((!\\[CDATA\\[([\\s\\S]*?)(]]>))|((NAME:)?(NAME))([^>]*)>|((\\/)(NAME)\\s*>))([^<]*)' // .replace(/NAME/g, util.nameRegexp); @@ -54385,36 +56950,10 @@ function extractNamespace(rawTagName) { } class OrderedObjParser { - constructor(options) { + constructor(options, externalEntities) { this.options = options; this.currentNode = null; this.tagsNodeStack = []; - this.docTypeEntities = {}; - this.lastEntities = { - "apos": { regex: /&(apos|#39|#x27);/g, val: "'" }, - "gt": { regex: /&(gt|#62|#x3E);/g, val: ">" }, - "lt": { regex: /&(lt|#60|#x3C);/g, val: "<" }, - "quot": { regex: /&(quot|#34|#x22);/g, val: "\"" }, - }; - this.ampEntity = { regex: /&(amp|#38|#x26);/g, val: "&" }; - this.htmlEntities = { - "space": { regex: /&(nbsp|#160);/g, val: " " }, - // "lt" : { regex: /&(lt|#60);/g, val: "<" }, - // "gt" : { regex: /&(gt|#62);/g, val: ">" }, - // "amp" : { regex: /&(amp|#38);/g, val: "&" }, - // "quot" : { regex: /&(quot|#34);/g, val: "\"" }, - // "apos" : { regex: /&(apos|#39);/g, val: "'" }, - "cent": { regex: /&(cent|#162);/g, val: "¢" }, - "pound": { regex: /&(pound|#163);/g, val: "£" }, - "yen": { regex: /&(yen|#165);/g, val: "¥" }, - "euro": { regex: /&(euro|#8364);/g, val: "€" }, - "copyright": { regex: /&(copy|#169);/g, val: "©" }, - "reg": { regex: /&(reg|#174);/g, val: "®" }, - "inr": { regex: /&(inr|#8377);/g, val: "₹" }, - "num_dec": { regex: /&#([0-9]{1,7});/g, val: (_, str) => fromCodePoint(str, 10, "&#") }, - "num_hex": { regex: /&#x([0-9a-fA-F]{1,6});/g, val: (_, str) => fromCodePoint(str, 16, "&#x") }, - }; - this.addExternalEntities = addExternalEntities; this.parseXml = parseXml; this.parseTextData = parseTextData; this.resolveNameSpace = resolveNameSpace; @@ -54427,42 +56966,51 @@ class OrderedObjParser { this.ignoreAttributesFn = ignoreAttributes_getIgnoreAttributesFn(this.options.ignoreAttributes) this.entityExpansionCount = 0; this.currentExpandedLength = 0; + let namedEntities = { ...XML }; + if (this.options.entityDecoder) { + this.entityDecoder = this.options.entityDecoder + } else { + if (typeof this.options.htmlEntities === "object") namedEntities = this.options.htmlEntities; + else if (this.options.htmlEntities === true) namedEntities = { ...COMMON_HTML, ...CURRENCY }; + this.entityDecoder = new EntityDecoder({ + namedEntities: { ...namedEntities, ...externalEntities }, + numericAllowed: this.options.htmlEntities, + limit: { + maxTotalExpansions: this.options.processEntities.maxTotalExpansions, + maxExpandedLength: this.options.processEntities.maxExpandedLength, + applyLimitsTo: this.options.processEntities.appliesTo, + } + //postCheck: resolved => resolved + }); + } // Initialize path matcher for path-expression-matcher this.matcher = new Matcher(); + this.readonlyMatcher = this.matcher.readOnly(); // Flag to track if current node is a stop node (optimization) this.isCurrentNodeStopNode = false; // Pre-compile stopNodes expressions - if (this.options.stopNodes && this.options.stopNodes.length > 0) { - this.stopNodeExpressions = []; - for (let i = 0; i < this.options.stopNodes.length; i++) { - const stopNodeExp = this.options.stopNodes[i]; + this.stopNodeExpressionsSet = new ExpressionSet(); + const stopNodesOpts = this.options.stopNodes; + if (stopNodesOpts && stopNodesOpts.length > 0) { + for (let i = 0; i < stopNodesOpts.length; i++) { + const stopNodeExp = stopNodesOpts[i]; if (typeof stopNodeExp === 'string') { // Convert string to Expression object - this.stopNodeExpressions.push(new Expression(stopNodeExp)); + this.stopNodeExpressionsSet.add(new Expression(stopNodeExp)); } else if (stopNodeExp instanceof Expression) { // Already an Expression object - this.stopNodeExpressions.push(stopNodeExp); + this.stopNodeExpressionsSet.add(stopNodeExp); } } + this.stopNodeExpressionsSet.seal(); } } } -function addExternalEntities(externalEntities) { - const entKeys = Object.keys(externalEntities); - for (let i = 0; i < entKeys.length; i++) { - const ent = entKeys[i]; - const escaped = ent.replace(/[.\-+*:]/g, '\\.'); - this.lastEntities[ent] = { - regex: new RegExp("&" + escaped + ";", "g"), - val: externalEntities[ent] - } - } -} /** * @param {string} val @@ -54474,28 +57022,29 @@ function addExternalEntities(externalEntities) { * @param {boolean} escapeEntities */ function parseTextData(val, tagName, jPath, dontTrim, hasAttributes, isLeafNode, escapeEntities) { + const options = this.options; if (val !== undefined) { - if (this.options.trimValues && !dontTrim) { + if (options.trimValues && !dontTrim) { val = val.trim(); } if (val.length > 0) { if (!escapeEntities) val = this.replaceEntitiesValue(val, tagName, jPath); // Pass jPath string or matcher based on options.jPath setting - const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath; - const newval = this.options.tagValueProcessor(tagName, val, jPathOrMatcher, hasAttributes, isLeafNode); + const jPathOrMatcher = options.jPath ? jPath.toString() : jPath; + const newval = options.tagValueProcessor(tagName, val, jPathOrMatcher, hasAttributes, isLeafNode); if (newval === null || newval === undefined) { //don't parse return val; } else if (typeof newval !== typeof val || newval !== val) { //overwrite return newval; - } else if (this.options.trimValues) { - return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions); + } else if (options.trimValues) { + return parseValue(val, options.parseTagValue, options.numberParseOptions); } else { const trimmedVal = val.trim(); if (trimmedVal === val) { - return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions); + return parseValue(val, options.parseTagValue, options.numberParseOptions); } else { return val; } @@ -54522,8 +57071,9 @@ function resolveNameSpace(tagname) { //const attrsRegx = new RegExp("([\\w\\-\\.\\:]+)\\s*=\\s*(['\"])((.|\n)*?)\\2","gm"); const attrsRegx = new RegExp('([^\\s=]+)\\s*(=\\s*([\'"])([\\s\\S]*?)\\3)?', 'gm'); -function buildAttributesMap(attrStr, jPath, tagName) { - if (this.options.ignoreAttributes !== true && typeof attrStr === 'string') { +function buildAttributesMap(attrStr, jPath, tagName, force = false) { + const options = this.options; + if (force === true || (options.ignoreAttributes !== true && typeof attrStr === 'string')) { // attrStr = attrStr.replace(/\r?\n/g, ' '); //attrStr = attrStr || attrStr.trim(); @@ -54531,89 +57081,80 @@ function buildAttributesMap(attrStr, jPath, tagName) { const len = matches.length; //don't make it inline const attrs = {}; - // First pass: parse all attributes and update matcher with raw values - // This ensures the matcher has all attribute values when processors run + // Pre-process values once: trim + entity replacement + // Reused in both matcher update and second pass + const processedVals = new Array(len); + let hasRawAttrs = false; const rawAttrsForMatcher = {}; + for (let i = 0; i < len; i++) { const attrName = this.resolveNameSpace(matches[i][1]); const oldVal = matches[i][4]; if (attrName.length && oldVal !== undefined) { - let parsedVal = oldVal; - if (this.options.trimValues) { - parsedVal = parsedVal.trim(); - } - parsedVal = this.replaceEntitiesValue(parsedVal, tagName, jPath); - rawAttrsForMatcher[attrName] = parsedVal; + let val = oldVal; + if (options.trimValues) val = val.trim(); + val = this.replaceEntitiesValue(val, tagName, this.readonlyMatcher); + processedVals[i] = val; + + rawAttrsForMatcher[attrName] = val; + hasRawAttrs = true; } } - // Update matcher with raw attribute values BEFORE running processors - if (Object.keys(rawAttrsForMatcher).length > 0 && typeof jPath === 'object' && jPath.updateCurrent) { + // Update matcher ONCE before second pass, if applicable + if (hasRawAttrs && typeof jPath === 'object' && jPath.updateCurrent) { jPath.updateCurrent(rawAttrsForMatcher); } - // Second pass: now process attributes with matcher having full attribute context + // Hoist toString() once — path doesn't change during attribute processing + const jPathStr = options.jPath ? jPath.toString() : this.readonlyMatcher; + + // Second pass: apply processors, build final attrs + let hasAttrs = false; for (let i = 0; i < len; i++) { const attrName = this.resolveNameSpace(matches[i][1]); - // Convert jPath to string if needed for ignoreAttributesFn - const jPathStr = this.options.jPath ? jPath.toString() : jPath; - if (this.ignoreAttributesFn(attrName, jPathStr)) { - continue - } + if (this.ignoreAttributesFn(attrName, jPathStr)) continue; - let oldVal = matches[i][4]; - let aName = this.options.attributeNamePrefix + attrName; + let aName = options.attributeNamePrefix + attrName; if (attrName.length) { - if (this.options.transformAttributeName) { - aName = this.options.transformAttributeName(aName); + if (options.transformAttributeName) { + aName = options.transformAttributeName(aName); } - //if (aName === "__proto__") aName = "#__proto__"; - aName = sanitizeName(aName, this.options); + aName = sanitizeName(aName, options); - if (oldVal !== undefined) { - if (this.options.trimValues) { - oldVal = oldVal.trim(); - } - oldVal = this.replaceEntitiesValue(oldVal, tagName, jPath); + if (matches[i][4] !== undefined) { + // Reuse already-processed value — no double entity replacement + const oldVal = processedVals[i]; - // Pass jPath string or matcher based on options.jPath setting - const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath; - const newVal = this.options.attributeValueProcessor(attrName, oldVal, jPathOrMatcher); + const newVal = options.attributeValueProcessor(attrName, oldVal, jPathStr); if (newVal === null || newVal === undefined) { - //don't parse attrs[aName] = oldVal; } else if (typeof newVal !== typeof oldVal || newVal !== oldVal) { - //overwrite attrs[aName] = newVal; } else { - //parse - attrs[aName] = parseValue( - oldVal, - this.options.parseAttributeValue, - this.options.numberParseOptions - ); + attrs[aName] = parseValue(oldVal, options.parseAttributeValue, options.numberParseOptions); } - } else if (this.options.allowBooleanAttributes) { + hasAttrs = true; + } else if (options.allowBooleanAttributes) { attrs[aName] = true; + hasAttrs = true; } } } - if (!Object.keys(attrs).length) { - return; - } - if (this.options.attributesGroupName) { + if (!hasAttrs) return; + + if (options.attributesGroupName && !options.preserveOrder) { const attrCollection = {}; - attrCollection[this.options.attributesGroupName] = attrs; + attrCollection[options.attributesGroupName] = attrs; return attrCollection; } - return attrs + return attrs; } } - const parseXml = function (xmlData) { xmlData = xmlData.replace(/\r\n?/g, "\n"); //TODO: remove this line const xmlObj = new XmlNode('!xml'); @@ -54622,40 +57163,43 @@ const parseXml = function (xmlData) { // Reset matcher for new document this.matcher.reset(); + this.entityDecoder.reset(); // Reset entity expansion counters for this document this.entityExpansionCount = 0; this.currentExpandedLength = 0; - - const docTypeReader = new DocTypeReader(this.options.processEntities); - for (let i = 0; i < xmlData.length; i++) {//for each char in XML data + const options = this.options; + const docTypeReader = new DocTypeReader(options.processEntities); + const xmlLen = xmlData.length; + for (let i = 0; i < xmlLen; i++) {//for each char in XML data const ch = xmlData[i]; if (ch === '<') { // const nextIndex = i+1; // const _2ndChar = xmlData[nextIndex]; - if (xmlData[i + 1] === '/') {//Closing Tag + const c1 = xmlData.charCodeAt(i + 1); + if (c1 === 47) {//Closing Tag '/' const closeIndex = findClosingIndex(xmlData, ">", i, "Closing Tag is not closed.") let tagName = xmlData.substring(i + 2, closeIndex).trim(); - if (this.options.removeNSPrefix) { + if (options.removeNSPrefix) { const colonIndex = tagName.indexOf(":"); if (colonIndex !== -1) { tagName = tagName.substr(colonIndex + 1); } } - tagName = transformTagName(this.options.transformTagName, tagName, "", this.options).tagName; + tagName = transformTagName(options.transformTagName, tagName, "", options).tagName; if (currentNode) { - textData = this.saveTextToParentTag(textData, currentNode, this.matcher); + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher); } //check if last tag of nested tag was unpaired tag const lastTagName = this.matcher.getCurrentTag(); - if (tagName && this.options.unpairedTags.indexOf(tagName) !== -1) { + if (tagName && options.unpairedTagsSet.has(tagName)) { throw new Error(`Unpaired tag can not be used as closing tag: `); } - if (lastTagName && this.options.unpairedTags.indexOf(lastTagName) !== -1) { + if (lastTagName && options.unpairedTagsSet.has(lastTagName)) { // Pop the unpaired tag this.matcher.pop(); this.tagsNodeStack.pop(); @@ -54667,65 +57211,75 @@ const parseXml = function (xmlData) { currentNode = this.tagsNodeStack.pop();//avoid recursion, set the parent tag scope textData = ""; i = closeIndex; - } else if (xmlData[i + 1] === '?') { + } else if (c1 === 63) { //'?' let tagData = readTagExp(xmlData, i, false, "?>"); if (!tagData) throw new Error("Pi Tag is not closed."); - textData = this.saveTextToParentTag(textData, currentNode, this.matcher); - if ((this.options.ignoreDeclaration && tagData.tagName === "?xml") || this.options.ignorePiTags) { + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher); + const attsMap = this.buildAttributesMap(tagData.tagExp, this.matcher, tagData.tagName, true); + if (attsMap) { + const ver = attsMap[this.options.attributeNamePrefix + "version"]; + this.entityDecoder.setXmlVersion(Number(ver) || 1.0); + docTypeReader.setXmlVersion(Number(ver) || 1.0); + } + if ((options.ignoreDeclaration && tagData.tagName === "?xml") || options.ignorePiTags) { //do nothing } else { const childNode = new XmlNode(tagData.tagName); - childNode.add(this.options.textNodeName, ""); + childNode.add(options.textNodeName, ""); - if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent) { - childNode[":@"] = this.buildAttributesMap(tagData.tagExp, this.matcher, tagData.tagName); + if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent && options.ignoreAttributes !== true) { + childNode[":@"] = attsMap } - this.addChild(currentNode, childNode, this.matcher, i); + this.addChild(currentNode, childNode, this.readonlyMatcher, i); } i = tagData.closeIndex + 1; - } else if (xmlData.substr(i + 1, 3) === '!--') { + } else if (c1 === 33 + && xmlData.charCodeAt(i + 2) === 45 + && xmlData.charCodeAt(i + 3) === 45) { //'!--' const endIndex = findClosingIndex(xmlData, "-->", i + 4, "Comment is not closed.") - if (this.options.commentPropName) { + if (options.commentPropName) { const comment = xmlData.substring(i + 4, endIndex - 2); - textData = this.saveTextToParentTag(textData, currentNode, this.matcher); + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher); - currentNode.add(this.options.commentPropName, [{ [this.options.textNodeName]: comment }]); + currentNode.add(options.commentPropName, [{ [options.textNodeName]: comment }]); } i = endIndex; - } else if (xmlData.substr(i + 1, 2) === '!D') { + } else if (c1 === 33 + && xmlData.charCodeAt(i + 2) === 68) { //'!D' const result = docTypeReader.readDocType(xmlData, i); - this.docTypeEntities = result.entities; + this.entityDecoder.addInputEntities(result.entities); i = result.i; - } else if (xmlData.substr(i + 1, 2) === '![') { + } else if (c1 === 33 + && xmlData.charCodeAt(i + 2) === 91) { // '![' const closeIndex = findClosingIndex(xmlData, "]]>", i, "CDATA is not closed.") - 2; const tagExp = xmlData.substring(i + 9, closeIndex); - textData = this.saveTextToParentTag(textData, currentNode, this.matcher); + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher); - let val = this.parseTextData(tagExp, currentNode.tagname, this.matcher, true, false, true, true); + let val = this.parseTextData(tagExp, currentNode.tagname, this.readonlyMatcher, true, false, true, true); if (val == undefined) val = ""; //cdata should be set even if it is 0 length string - if (this.options.cdataPropName) { - currentNode.add(this.options.cdataPropName, [{ [this.options.textNodeName]: tagExp }]); + if (options.cdataPropName) { + currentNode.add(options.cdataPropName, [{ [options.textNodeName]: tagExp }]); } else { - currentNode.add(this.options.textNodeName, val); + currentNode.add(options.textNodeName, val); } i = closeIndex + 2; } else {//Opening tag - let result = readTagExp(xmlData, i, this.options.removeNSPrefix); + let result = readTagExp(xmlData, i, options.removeNSPrefix); // Safety check: readTagExp can return undefined if (!result) { // Log context for debugging - const context = xmlData.substring(Math.max(0, i - 50), Math.min(xmlData.length, i + 50)); + const context = xmlData.substring(Math.max(0, i - 50), Math.min(xmlLen, i + 50)); throw new Error(`readTagExp returned undefined at position ${i}. Context: "${context}"`); } @@ -54735,11 +57289,13 @@ const parseXml = function (xmlData) { let attrExpPresent = result.attrExpPresent; let closeIndex = result.closeIndex; - ({ tagName, tagExp } = transformTagName(this.options.transformTagName, tagName, tagExp, this.options)); + ({ tagName, tagExp } = transformTagName(options.transformTagName, tagName, tagExp, options)); - if (this.options.strictReservedNames && - (tagName === this.options.commentPropName - || tagName === this.options.cdataPropName + if (options.strictReservedNames && + (tagName === options.commentPropName + || tagName === options.cdataPropName + || tagName === options.textNodeName + || tagName === options.attributesGroupName )) { throw new Error(`Invalid tag name: ${tagName}`); } @@ -54748,13 +57304,13 @@ const parseXml = function (xmlData) { if (currentNode && textData) { if (currentNode.tagname !== '!xml') { //when nested tag is found - textData = this.saveTextToParentTag(textData, currentNode, this.matcher, false); + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher, false); } } //check if last tag was unpaired tag const lastTag = currentNode; - if (lastTag && this.options.unpairedTags.indexOf(lastTag.tagname) !== -1) { + if (lastTag && options.unpairedTagsSet.has(lastTag.tagname)) { currentNode = this.tagsNodeStack.pop(); this.matcher.pop(); } @@ -54796,13 +57352,14 @@ const parseXml = function (xmlData) { if (prefixedAttrs) { // Extract raw attributes (without prefix) for our use - rawAttrs = extractRawAttributes(prefixedAttrs, this.options); + //TODO: seems a performance overhead + rawAttrs = extractRawAttributes(prefixedAttrs, options); } } // Now check if this is a stop node (after attributes are set) if (tagName !== xmlObj.tagname) { - this.isCurrentNodeStopNode = this.isItStopNode(this.stopNodeExpressions, this.matcher); + this.isCurrentNodeStopNode = this.isItStopNode(); } const startIndex = i; @@ -54814,7 +57371,7 @@ const parseXml = function (xmlData) { i = result.closeIndex; } //unpaired tag - else if (this.options.unpairedTags.indexOf(tagName) !== -1) { + else if (options.unpairedTagsSet.has(tagName)) { i = result.closeIndex; } //normal tag @@ -54833,31 +57390,31 @@ const parseXml = function (xmlData) { } // For stop nodes, store raw content as-is without any processing - childNode.add(this.options.textNodeName, tagContent); + childNode.add(options.textNodeName, tagContent); this.matcher.pop(); // Pop the stop node tag this.isCurrentNodeStopNode = false; // Reset flag - this.addChild(currentNode, childNode, this.matcher, startIndex); + this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); } else { //selfClosing tag if (isSelfClosing) { - ({ tagName, tagExp } = transformTagName(this.options.transformTagName, tagName, tagExp, this.options)); + ({ tagName, tagExp } = transformTagName(options.transformTagName, tagName, tagExp, options)); const childNode = new XmlNode(tagName); if (prefixedAttrs) { childNode[":@"] = prefixedAttrs; } - this.addChild(currentNode, childNode, this.matcher, startIndex); + this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); this.matcher.pop(); // Pop self-closing tag this.isCurrentNodeStopNode = false; // Reset flag } - else if (this.options.unpairedTags.indexOf(tagName) !== -1) {//unpaired tag + else if (options.unpairedTagsSet.has(tagName)) {//unpaired tag const childNode = new XmlNode(tagName); if (prefixedAttrs) { childNode[":@"] = prefixedAttrs; } - this.addChild(currentNode, childNode, this.matcher, startIndex); + this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); this.matcher.pop(); // Pop unpaired tag this.isCurrentNodeStopNode = false; // Reset flag i = result.closeIndex; @@ -54867,7 +57424,7 @@ const parseXml = function (xmlData) { //opening tag else { const childNode = new XmlNode(tagName); - if (this.tagsNodeStack.length > this.options.maxNestedTags) { + if (this.tagsNodeStack.length > options.maxNestedTags) { throw new Error("Maximum nested tags exceeded"); } this.tagsNodeStack.push(currentNode); @@ -54875,7 +57432,7 @@ const parseXml = function (xmlData) { if (prefixedAttrs) { childNode[":@"] = prefixedAttrs; } - this.addChild(currentNode, childNode, this.matcher, startIndex); + this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); currentNode = childNode; } textData = ""; @@ -54938,79 +57495,7 @@ function OrderedObjParser_replaceEntitiesValue(val, tagName, jPath) { } } - // Replace DOCTYPE entities - for (const entityName of Object.keys(this.docTypeEntities)) { - const entity = this.docTypeEntities[entityName]; - const matches = val.match(entity.regx); - - if (matches) { - // Track expansions - this.entityExpansionCount += matches.length; - - // Check expansion limit - if (entityConfig.maxTotalExpansions && - this.entityExpansionCount > entityConfig.maxTotalExpansions) { - throw new Error( - `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}` - ); - } - - // Store length before replacement - const lengthBefore = val.length; - val = val.replace(entity.regx, entity.val); - - // Check expanded length immediately after replacement - if (entityConfig.maxExpandedLength) { - this.currentExpandedLength += (val.length - lengthBefore); - - if (this.currentExpandedLength > entityConfig.maxExpandedLength) { - throw new Error( - `Total expanded content size exceeded: ${this.currentExpandedLength} > ${entityConfig.maxExpandedLength}` - ); - } - } - } - } - // Replace standard entities - for (const entityName of Object.keys(this.lastEntities)) { - const entity = this.lastEntities[entityName]; - const matches = val.match(entity.regex); - if (matches) { - this.entityExpansionCount += matches.length; - if (entityConfig.maxTotalExpansions && - this.entityExpansionCount > entityConfig.maxTotalExpansions) { - throw new Error( - `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}` - ); - } - } - val = val.replace(entity.regex, entity.val); - } - if (val.indexOf('&') === -1) return val; - - // Replace HTML entities if enabled - if (this.options.htmlEntities) { - for (const entityName of Object.keys(this.htmlEntities)) { - const entity = this.htmlEntities[entityName]; - const matches = val.match(entity.regex); - if (matches) { - //console.log(matches); - this.entityExpansionCount += matches.length; - if (entityConfig.maxTotalExpansions && - this.entityExpansionCount > entityConfig.maxTotalExpansions) { - throw new Error( - `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}` - ); - } - } - val = val.replace(entity.regex, entity.val); - } - } - - // Replace ampersand entity last - val = val.replace(this.ampEntity.regex, this.ampEntity.val); - - return val; + return this.entityDecoder.decode(val); } @@ -55032,20 +57517,14 @@ function saveTextToParentTag(textData, parentNode, matcher, isLeafNode) { return textData; } -//TODO: use jPath to simplify the logic /** * @param {Array} stopNodeExpressions - Array of compiled Expression objects * @param {Matcher} matcher - Current path matcher */ -function isItStopNode(stopNodeExpressions, matcher) { - if (!stopNodeExpressions || stopNodeExpressions.length === 0) return false; +function isItStopNode() { + if (this.stopNodeExpressionsSet.size === 0) return false; - for (let i = 0; i < stopNodeExpressions.length; i++) { - if (matcher.matches(stopNodeExpressions[i])) { - return true; - } - } - return false; + return this.matcher.matchesAny(this.stopNodeExpressionsSet); } /** @@ -55055,32 +57534,38 @@ function isItStopNode(stopNodeExpressions, matcher) { * @returns */ function tagExpWithClosingIndex(xmlData, i, closingChar = ">") { - let attrBoundary; - let tagExp = ""; - for (let index = i; index < xmlData.length; index++) { - let ch = xmlData[index]; + //TODO: ignore boolean attributes in tag expression + //TODO: if ignore attributes, dont read full attribute expression but the end. But read for xml declaration + let attrBoundary = 0; + const len = xmlData.length; + const closeCode0 = closingChar.charCodeAt(0); + const closeCode1 = closingChar.length > 1 ? closingChar.charCodeAt(1) : -1; + + let result = ''; + let segmentStart = i; + + for (let index = i; index < len; index++) { + const code = xmlData.charCodeAt(index); + if (attrBoundary) { - if (ch === attrBoundary) attrBoundary = "";//reset - } else if (ch === '"' || ch === "'") { - attrBoundary = ch; - } else if (ch === closingChar[0]) { - if (closingChar[1]) { - if (xmlData[index + 1] === closingChar[1]) { - return { - data: tagExp, - index: index - } + if (code === attrBoundary) attrBoundary = 0; + } else if (code === 34 || code === 39) { // " or ' + attrBoundary = code; + } else if (code === closeCode0) { + if (closeCode1 !== -1) { + if (xmlData.charCodeAt(index + 1) === closeCode1) { + result += xmlData.substring(segmentStart, index); + return { data: result, index }; } } else { - return { - data: tagExp, - index: index - } + result += xmlData.substring(segmentStart, index); + return { data: result, index }; } - } else if (ch === '\t') { - ch = " " + } else if (code === 9 && !attrBoundary) { // \t - only replace with space outside attribute values + // Flush accumulated segment, add space, start new segment + result += xmlData.substring(segmentStart, index) + ' '; + segmentStart = index + 1; } - tagExp += ch; } } @@ -55093,6 +57578,12 @@ function findClosingIndex(xmlData, str, i, errMsg) { } } +function findClosingChar(xmlData, char, i, errMsg) { + const closingIndex = xmlData.indexOf(char, i); + if (closingIndex === -1) throw new Error(errMsg); + return closingIndex; // no offset needed +} + function readTagExp(xmlData, i, removeNSPrefix, closingChar = ">") { const result = tagExpWithClosingIndex(xmlData, i + 1, closingChar); if (!result) return; @@ -55134,10 +57625,12 @@ function readStopNodeData(xmlData, tagName, i) { // Starting at 1 since we already have an open tag let openTagCount = 1; - for (; i < xmlData.length; i++) { + const xmllen = xmlData.length; + for (; i < xmllen; i++) { if (xmlData[i] === "<") { - if (xmlData[i + 1] === "/") {//close tag - const closeIndex = findClosingIndex(xmlData, ">", i, `${tagName} is not closed`); + const c1 = xmlData.charCodeAt(i + 1); + if (c1 === 47) {//close tag '/' + const closeIndex = findClosingChar(xmlData, ">", i, `${tagName} is not closed`); let closeTagName = xmlData.substring(i + 2, closeIndex).trim(); if (closeTagName === tagName) { openTagCount--; @@ -55149,17 +57642,20 @@ function readStopNodeData(xmlData, tagName, i) { } } i = closeIndex; - } else if (xmlData[i + 1] === '?') { + } else if (c1 === 63) { //? const closeIndex = findClosingIndex(xmlData, "?>", i + 1, "StopNode is not closed.") i = closeIndex; - } else if (xmlData.substr(i + 1, 3) === '!--') { + } else if (c1 === 33 + && xmlData.charCodeAt(i + 2) === 45 + && xmlData.charCodeAt(i + 3) === 45) { // '!--' const closeIndex = findClosingIndex(xmlData, "-->", i + 3, "StopNode is not closed.") i = closeIndex; - } else if (xmlData.substr(i + 1, 2) === '![') { + } else if (c1 === 33 + && xmlData.charCodeAt(i + 2) === 91) { // '![' const closeIndex = findClosingIndex(xmlData, "]]>", i, "StopNode is not closed.") - 2; i = closeIndex; } else { - const tagData = readTagExp(xmlData, i, '>') + const tagData = readTagExp(xmlData, i, false) if (tagData) { const openTagName = tagData && tagData.tagName; @@ -55259,18 +57755,17 @@ function stripAttributePrefix(attrs, prefix) { * @param {Matcher} matcher - Path matcher instance * @returns */ -function prettify(node, options, matcher) { - return compress(node, options, matcher); +function prettify(node, options, matcher, readonlyMatcher) { + return compress(node, options, matcher, readonlyMatcher); } /** - * * @param {array} arr * @param {object} options * @param {Matcher} matcher - Path matcher instance * @returns object */ -function compress(arr, options, matcher) { +function compress(arr, options, matcher, readonlyMatcher) { let text; const compressedObj = {}; //This is intended to be a plain object for (let i = 0; i < arr.length; i++) { @@ -55293,11 +57788,15 @@ function compress(arr, options, matcher) { continue; } else if (tagObj[property]) { - let val = compress(tagObj[property], options, matcher); + let val = compress(tagObj[property], options, matcher, readonlyMatcher); const isLeaf = isLeafTag(val, options); + if (Object.keys(val).length === 0 && options.alwaysCreateTextNode) { + val[options.textNodeName] = ""; + } + if (tagObj[":@"]) { - assignAttributes(val, tagObj[":@"], matcher, options); + assignAttributes(val, tagObj[":@"], readonlyMatcher, options); } else if (Object.keys(val).length === 1 && val[options.textNodeName] !== undefined && !options.alwaysCreateTextNode) { val = val[options.textNodeName]; } else if (Object.keys(val).length === 0) { @@ -55319,8 +57818,8 @@ function compress(arr, options, matcher) { //TODO: if a node is not an array, then check if it should be an array //also determine if it is a leaf node - // Pass jPath string or matcher based on options.jPath setting - const jPathOrMatcher = options.jPath ? matcher.toString() : matcher; + // Pass jPath string or readonlyMatcher based on options.jPath setting + const jPathOrMatcher = options.jPath ? readonlyMatcher.toString() : readonlyMatcher; if (options.isArray(property, jPathOrMatcher, isLeaf)) { compressedObj[property] = [val]; } else { @@ -55352,7 +57851,7 @@ function node2json_propName(obj) { } } -function assignAttributes(obj, attrMap, matcher, options) { +function assignAttributes(obj, attrMap, readonlyMatcher, options) { if (attrMap) { const keys = Object.keys(attrMap); const len = keys.length; //don't make it inline @@ -55367,8 +57866,8 @@ function assignAttributes(obj, attrMap, matcher, options) { // For attributes, we need to create a temporary path // Pass jPath string or matcher based on options.jPath setting const jPathOrMatcher = options.jPath - ? matcher.toString() + "." + rawAttrName - : matcher; + ? readonlyMatcher.toString() + "." + rawAttrName + : readonlyMatcher; if (options.isArray(atrrName, jPathOrMatcher, true, true)) { obj[atrrName] = [attrMap[atrrName]]; @@ -55425,16 +57924,16 @@ 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}`) } } - const orderedObjParser = new OrderedObjParser(this.options); - orderedObjParser.addExternalEntities(this.externalEntities); + const orderedObjParser = new OrderedObjParser(this.options, this.externalEntities); + // orderedObjParser.entityDecoder.setExternalEntities(this.externalEntities); const orderedResult = orderedObjParser.parseXml(xmlData); if (this.options.preserveOrder || orderedResult === undefined) return orderedResult; - else return prettify(orderedResult, this.options, orderedObjParser.matcher); + else return prettify(orderedResult, this.options, orderedObjParser.matcher, orderedObjParser.readonlyMatcher); } /** @@ -55468,7 +57967,6 @@ class XMLParser { return XmlNode.getMetaDataSymbol(); } } - ;// CONCATENATED MODULE: ./node_modules/@azure/core-xml/dist/esm/xml.common.js // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. @@ -55487,20 +57985,34 @@ const xml_common_XML_CHARKEY = "_"; function getCommonOptions(options) { - var _a; return { attributesGroupName: xml_common_XML_ATTRKEY, - textNodeName: (_a = options.xmlCharKey) !== null && _a !== void 0 ? _a : xml_common_XML_CHARKEY, + textNodeName: options.xmlCharKey ?? xml_common_XML_CHARKEY, ignoreAttributes: false, suppressBooleanAttributes: false, }; } function getSerializerOptions(options = {}) { - var _a, _b; - return Object.assign(Object.assign({}, getCommonOptions(options)), { attributeNamePrefix: "@_", format: true, suppressEmptyNode: true, indentBy: "", rootNodeName: (_a = options.rootName) !== null && _a !== void 0 ? _a : "root", cdataPropName: (_b = options.cdataPropName) !== null && _b !== void 0 ? _b : "__cdata" }); + return { + ...getCommonOptions(options), + attributeNamePrefix: "@_", + format: true, + suppressEmptyNode: true, + indentBy: "", + rootNodeName: options.rootName ?? "root", + cdataPropName: options.cdataPropName ?? "__cdata", + }; } function getParserOptions(options = {}) { - return Object.assign(Object.assign({}, getCommonOptions(options)), { parseAttributeValue: false, parseTagValue: false, attributeNamePrefix: "", stopNodes: options.stopNodes, processEntities: true, trimValues: false }); + return { + ...getCommonOptions(options), + parseAttributeValue: false, + parseTagValue: false, + attributeNamePrefix: "", + stopNodes: options.stopNodes, + processEntities: true, + trimValues: false, + }; } /** * Converts given JSON object to XML string @@ -55539,7 +58051,7 @@ async function parseXML(str, opts = {}) { if (!opts.includeRoot) { for (const key of Object.keys(parsedXml)) { const value = parsedXml[key]; - return typeof value === "object" ? Object.assign({}, value) : value; + return typeof value === "object" ? { ...value } : value; } } return parsedXml; @@ -57124,39 +59636,6 @@ class StorageSharedKeyCredential extends Credential { } } //# sourceMappingURL=StorageSharedKeyCredential.js.map -;// CONCATENATED MODULE: ./node_modules/@azure/storage-common/node_modules/@azure/abort-controller/dist/esm/AbortError.js -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -/** - * This error is thrown when an asynchronous operation has been aborted. - * Check for this error by testing the `name` that the name property of the - * error matches `"AbortError"`. - * - * @example - * ```ts - * const controller = new AbortController(); - * controller.abort(); - * try { - * doAsyncWork(controller.signal) - * } catch (e) { - * if (e.name === 'AbortError') { - * // handle abort error here. - * } - * } - * ``` - */ -class esm_AbortError_AbortError extends Error { - constructor(message) { - super(message); - this.name = "AbortError"; - } -} -//# sourceMappingURL=AbortError.js.map -;// CONCATENATED MODULE: ./node_modules/@azure/storage-common/node_modules/@azure/abort-controller/dist/esm/index.js -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -//# sourceMappingURL=index.js.map ;// CONCATENATED MODULE: ./node_modules/@azure/storage-common/dist/esm/log.js // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. @@ -57214,7 +59693,7 @@ const DEFAULT_RETRY_OPTIONS = { secondaryHost: "", tryTimeoutInMs: undefined, // Use server side default timeout strategy }; -const RETRY_ABORT_ERROR = new esm_AbortError_AbortError("The operation was aborted."); +const RETRY_ABORT_ERROR = new AbortError_AbortError("The operation was aborted."); /** * Retry policy with exponential retry and linear retry implemented. */ @@ -57527,7 +60006,7 @@ const retriableErrors = [ "EPIPE", "REQUEST_SEND_ERROR", ]; -const StorageRetryPolicyV2_RETRY_ABORT_ERROR = new esm_AbortError_AbortError("The operation was aborted."); +const StorageRetryPolicyV2_RETRY_ABORT_ERROR = new AbortError_AbortError("The operation was aborted."); /** * Retry policy with exponential retry and linear retry implemented. */ @@ -57890,15 +60369,12 @@ class UserDelegationKeyCredential { - - - //# sourceMappingURL=index.js.map ;// CONCATENATED MODULE: ./node_modules/@azure/storage-blob/dist/esm/utils/constants.js // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -const esm_utils_constants_SDK_VERSION = "12.30.0"; +const esm_utils_constants_SDK_VERSION = "12.31.0"; const SERVICE_VERSION = "2026-02-06"; const BLOCK_BLOB_MAX_UPLOAD_BLOB_BYTES = 256 * 1024 * 1024; // 256MB const BLOCK_BLOB_MAX_STAGE_BLOCK_BYTES = 4000 * 1024 * 1024; // 4000MB @@ -74396,39 +76872,6 @@ class BlobLeaseClient { } } //# sourceMappingURL=BlobLeaseClient.js.map -;// CONCATENATED MODULE: ./node_modules/@azure/storage-blob/node_modules/@azure/abort-controller/dist/esm/AbortError.js -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -/** - * This error is thrown when an asynchronous operation has been aborted. - * Check for this error by testing the `name` that the name property of the - * error matches `"AbortError"`. - * - * @example - * ```ts - * const controller = new AbortController(); - * controller.abort(); - * try { - * doAsyncWork(controller.signal) - * } catch (e) { - * if (e.name === 'AbortError') { - * // handle abort error here. - * } - * } - * ``` - */ -class dist_esm_AbortError_AbortError extends Error { - constructor(message) { - super(message); - this.name = "AbortError"; - } -} -//# sourceMappingURL=AbortError.js.map -;// CONCATENATED MODULE: ./node_modules/@azure/storage-blob/node_modules/@azure/abort-controller/dist/esm/index.js -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -//# sourceMappingURL=index.js.map ;// CONCATENATED MODULE: ./node_modules/@azure/storage-blob/dist/esm/utils/RetriableReadableStream.js // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. @@ -74508,7 +76951,7 @@ class RetriableReadableStream extends external_node_stream_.Readable { } }; sourceAbortedHandler = () => { - const abortError = new dist_esm_AbortError_AbortError("The operation was aborted."); + const abortError = new AbortError_AbortError("The operation was aborted."); this.destroy(abortError); }; sourceErrorOrEndHandler = (err) => { @@ -75503,7 +77946,7 @@ const external_buffer_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import. -const ABORT_ERROR = new dist_esm_AbortError_AbortError("Reading from the avro stream was aborted."); +const ABORT_ERROR = new AbortError_AbortError("Reading from the avro stream was aborted."); class AvroReadableFromStream extends AvroReadable { _position; _readable; @@ -78123,7 +80566,7 @@ class Clients_BlobClient extends StorageClient_StorageClient { options.conditions = options.conditions || {}; ensureCpkIfSpecified(options.customerProvidedKey, this.isHttps); return tracingClient.withSpan("BlobClient-download", options, async (updatedOptions) => { - const res = utils_common_assertResponse(await this.blobContext.download({ + const res = utils_common_assertResponse((await this.blobContext.download({ abortSignal: options.abortSignal, leaseAccessConditions: options.conditions, modifiedAccessConditions: { @@ -78139,7 +80582,7 @@ class Clients_BlobClient extends StorageClient_StorageClient { snapshot: options.snapshot, cpkInfo: options.customerProvidedKey, tracingOptions: updatedOptions.tracingOptions, - })); + }))); const wrappedRes = { ...res, _response: res._response, // _response is made non-enumerable @@ -79461,7 +81904,7 @@ class BlockBlobClient extends Clients_BlobClient { throw new Error("This operation currently is only supported in Node.js."); } return tracingClient.withSpan("BlockBlobClient-query", options, async (updatedOptions) => { - const response = utils_common_assertResponse(await this._blobContext.query({ + const response = utils_common_assertResponse((await this._blobContext.query({ abortSignal: options.abortSignal, queryRequest: { queryType: "SQL", @@ -79476,7 +81919,7 @@ class BlockBlobClient extends Clients_BlobClient { }, cpkInfo: options.customerProvidedKey, tracingOptions: updatedOptions.tracingOptions, - })); + }))); return new BlobQueryResponse(response, { abortSignal: options.abortSignal, onProgress: options.onProgress, @@ -81372,9 +83815,9 @@ class BlobBatchClient { return tracingClient.withSpan("BlobBatchClient-submitBatch", options, async (updatedOptions) => { const batchRequestBody = batchRequest.getHttpRequestBody(); // ServiceSubmitBatchResponseModel and ContainerSubmitBatchResponse are compatible for now. - const rawBatchResponse = utils_common_assertResponse(await this.serviceOrContainerContext.submitBatch(utf8ByteLength(batchRequestBody), batchRequest.getMultiPartContentType(), batchRequestBody, { + const rawBatchResponse = utils_common_assertResponse((await this.serviceOrContainerContext.submitBatch(utf8ByteLength(batchRequestBody), batchRequest.getMultiPartContentType(), batchRequestBody, { ...updatedOptions, - })); + }))); // Parse the sub responses result, if logic reaches here(i.e. the batch request succeeded with status code 202). const batchResponseParser = new BatchResponseParser(rawBatchResponse, batchRequest.getSubRequests()); const responseSummary = await batchResponseParser.parseBatchResponse(); @@ -83954,7 +86397,7 @@ NetworkError.isNetworkErrorCode = (code) => { }; class UsageError extends Error { constructor() { - const message = `Cache storage quota has been hit. Unable to upload any new cache entries. Usage is recalculated every 6-12 hours.\nMore info on storage limits: https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#calculating-minute-and-storage-spending`; + const message = `Cache storage quota has been hit. Unable to upload any new cache entries.\nMore info on storage limits: https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#calculating-minute-and-storage-spending`; super(message); this.name = 'UsageError'; } diff --git a/dist/restore/index.js b/dist/restore/index.js index 0f0b37a..936b3f9 100644 --- a/dist/restore/index.js +++ b/dist/restore/index.js @@ -38848,7 +38848,7 @@ module.exports = { version: packageJson.version } /***/ 4012: /***/ ((module) => { -module.exports = /*#__PURE__*/JSON.parse('{"name":"@actions/cache","version":"6.0.0","description":"Actions cache lib","keywords":["github","actions","cache"],"homepage":"https://github.com/actions/toolkit/tree/main/packages/cache","license":"MIT","type":"module","main":"lib/cache.js","types":"lib/cache.d.ts","exports":{".":{"types":"./lib/cache.d.ts","import":"./lib/cache.js"}},"directories":{"lib":"lib","test":"__tests__"},"files":["lib","!.DS_Store"],"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/actions/toolkit.git","directory":"packages/cache"},"scripts":{"audit-moderate":"npm install && npm audit --json --audit-level=moderate > audit.json","test":"echo \\"Error: run tests from root\\" && exit 1","tsc":"tsc && cp src/internal/shared/package-version.cjs lib/internal/shared/"},"bugs":{"url":"https://github.com/actions/toolkit/issues"},"dependencies":{"@actions/core":"^3.0.0","@actions/exec":"^3.0.0","@actions/glob":"^0.6.1","@actions/http-client":"^4.0.0","@actions/io":"^3.0.0","@azure/core-rest-pipeline":"^1.22.0","@azure/storage-blob":"^12.30.0","@protobuf-ts/runtime-rpc":"^2.11.1","semver":"^7.7.3"},"devDependencies":{"@protobuf-ts/plugin":"^2.9.4","@types/node":"^25.1.0","@types/semver":"^7.7.1","typescript":"^5.2.2"},"overrides":{"uri-js":"npm:uri-js-replace@^1.0.1","node-fetch":"^3.3.2"}}'); +module.exports = /*#__PURE__*/JSON.parse('{"name":"@actions/cache","version":"6.0.1","description":"Actions cache lib","keywords":["github","actions","cache"],"homepage":"https://github.com/actions/toolkit/tree/main/packages/cache","license":"MIT","type":"module","main":"lib/cache.js","types":"lib/cache.d.ts","exports":{".":{"types":"./lib/cache.d.ts","import":"./lib/cache.js"}},"directories":{"lib":"lib","test":"__tests__"},"files":["lib","!.DS_Store"],"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/actions/toolkit.git","directory":"packages/cache"},"scripts":{"audit-moderate":"npm install && npm audit --json --audit-level=moderate > audit.json","test":"echo \\"Error: run tests from root\\" && exit 1","tsc":"tsc && cp src/internal/shared/package-version.cjs lib/internal/shared/"},"bugs":{"url":"https://github.com/actions/toolkit/issues"},"dependencies":{"@actions/core":"^3.0.1","@actions/exec":"^3.0.0","@actions/glob":"^0.6.1","@actions/http-client":"^4.0.1","@actions/io":"^3.0.2","@azure/core-rest-pipeline":"^1.23.0","@azure/storage-blob":"^12.31.0","@protobuf-ts/runtime-rpc":"^2.11.1","semver":"^7.7.4"},"devDependencies":{"@protobuf-ts/plugin":"^2.11.1","@types/node":"^25.6.0","@types/semver":"^7.7.1","typescript":"^5.9.3"},"overrides":{"uri-js":"npm:uri-js-replace@^1.0.1","node-fetch":"^3.3.2"}}'); /***/ }) @@ -46846,7 +46846,7 @@ async function util_userAgentPlatform_setPlatformSpecificData(map) { ;// CONCATENATED MODULE: ./node_modules/@azure/core-rest-pipeline/dist/esm/constants.js // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -const esm_constants_SDK_VERSION = "1.22.2"; +const esm_constants_SDK_VERSION = "1.22.3"; const esm_constants_DEFAULT_RETRY_POLICY_COUNT = 3; //# sourceMappingURL=constants.js.map ;// CONCATENATED MODULE: ./node_modules/@azure/core-rest-pipeline/dist/esm/util/userAgent.js @@ -46945,7 +46945,7 @@ async function computeSha256Hash(content, encoding) { //# sourceMappingURL=internal.js.map -;// CONCATENATED MODULE: ./node_modules/@azure/core-util/node_modules/@azure/abort-controller/dist/esm/AbortError.js +;// CONCATENATED MODULE: ./node_modules/@azure/abort-controller/dist/esm/AbortError.js // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. /** @@ -46973,7 +46973,7 @@ class AbortError_AbortError extends Error { } } //# sourceMappingURL=AbortError.js.map -;// CONCATENATED MODULE: ./node_modules/@azure/core-util/node_modules/@azure/abort-controller/dist/esm/index.js +;// CONCATENATED MODULE: ./node_modules/@azure/abort-controller/dist/esm/index.js // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. @@ -51564,6 +51564,7 @@ function convertHttpClient(requestPolicyClient) { + //# sourceMappingURL=index.js.map ;// CONCATENATED MODULE: ./node_modules/path-expression-matcher/src/Expression.js /** @@ -51583,11 +51584,11 @@ class Expression { * @param {Object} options - Configuration options * @param {string} options.separator - Path separator (default: '.') */ - constructor(pattern, options = {}) { + constructor(pattern, options = {}, data) { this.pattern = pattern; this.separator = options.separator || '.'; this.segments = this._parse(pattern); - + this.data = data; // Cache expensive checks for performance (O(1) instead of O(n)) this._hasDeepWildcard = this.segments.some(seg => seg.type === 'deep-wildcard'); this._hasAttributeCondition = this.segments.some(seg => seg.attrName !== undefined); @@ -51799,48 +51800,206 @@ class Expression { } } ;// CONCATENATED MODULE: ./node_modules/path-expression-matcher/src/Matcher.js + + /** - * Matcher - Tracks current path in XML/JSON tree and matches against Expressions - * + * MatcherView - A lightweight read-only view over a Matcher's internal state. + * + * Created once by Matcher and reused across all callbacks. Holds a direct + * reference to the parent Matcher so it always reflects current parser state + * with zero copying or freezing overhead. + * + * Users receive this via {@link Matcher#readOnly} or directly from parser + * callbacks. It exposes all query and matching methods but has no mutation + * methods — misuse is caught at the TypeScript level rather than at runtime. + * + * @example + * const matcher = new Matcher(); + * const view = matcher.readOnly(); + * + * matcher.push("root", {}); + * view.getCurrentTag(); // "root" + * view.getDepth(); // 1 + */ +class MatcherView { + /** + * @param {Matcher} matcher - The parent Matcher instance to read from. + */ + constructor(matcher) { + this._matcher = matcher; + } + + /** + * Get the path separator used by the parent matcher. + * @returns {string} + */ + get separator() { + return this._matcher.separator; + } + + /** + * Get current tag name. + * @returns {string|undefined} + */ + getCurrentTag() { + const path = this._matcher.path; + return path.length > 0 ? path[path.length - 1].tag : undefined; + } + + /** + * Get current namespace. + * @returns {string|undefined} + */ + getCurrentNamespace() { + const path = this._matcher.path; + return path.length > 0 ? path[path.length - 1].namespace : undefined; + } + + /** + * Get current node's attribute value. + * @param {string} attrName + * @returns {*} + */ + getAttrValue(attrName) { + const path = this._matcher.path; + if (path.length === 0) return undefined; + return path[path.length - 1].values?.[attrName]; + } + + /** + * Check if current node has an attribute. + * @param {string} attrName + * @returns {boolean} + */ + hasAttr(attrName) { + const path = this._matcher.path; + if (path.length === 0) return false; + const current = path[path.length - 1]; + return current.values !== undefined && attrName in current.values; + } + + /** + * Get current node's sibling position (child index in parent). + * @returns {number} + */ + getPosition() { + const path = this._matcher.path; + if (path.length === 0) return -1; + return path[path.length - 1].position ?? 0; + } + + /** + * Get current node's repeat counter (occurrence count of this tag name). + * @returns {number} + */ + getCounter() { + const path = this._matcher.path; + if (path.length === 0) return -1; + return path[path.length - 1].counter ?? 0; + } + + /** + * Get current node's sibling index (alias for getPosition). + * @returns {number} + * @deprecated Use getPosition() or getCounter() instead + */ + getIndex() { + return this.getPosition(); + } + + /** + * Get current path depth. + * @returns {number} + */ + getDepth() { + return this._matcher.path.length; + } + + /** + * Get path as string. + * @param {string} [separator] - Optional separator (uses default if not provided) + * @param {boolean} [includeNamespace=true] + * @returns {string} + */ + toString(separator, includeNamespace = true) { + return this._matcher.toString(separator, includeNamespace); + } + + /** + * Get path as array of tag names. + * @returns {string[]} + */ + toArray() { + return this._matcher.path.map(n => n.tag); + } + + /** + * Match current path against an Expression. + * @param {Expression} expression + * @returns {boolean} + */ + matches(expression) { + return this._matcher.matches(expression); + } + + /** + * Match any expression in the given set against the current path. + * @param {ExpressionSet} exprSet + * @returns {boolean} + */ + matchesAny(exprSet) { + return exprSet.matchesAny(this._matcher); + } +} + +/** + * Matcher - Tracks current path in XML/JSON tree and matches against Expressions. + * * The matcher maintains a stack of nodes representing the current path from root to * current tag. It only stores attribute values for the current (top) node to minimize * memory usage. Sibling tracking is used to auto-calculate position and counter. - * + * + * Use {@link Matcher#readOnly} to obtain a {@link MatcherView} safe to pass to + * user callbacks — it always reflects current state with no Proxy overhead. + * * @example * const matcher = new Matcher(); * matcher.push("root", {}); * matcher.push("users", {}); * matcher.push("user", { id: "123", type: "admin" }); - * + * * const expr = new Expression("root.users.user"); * matcher.matches(expr); // true */ class Matcher { /** - * Create a new Matcher - * @param {Object} options - Configuration options - * @param {string} options.separator - Default path separator (default: '.') + * Create a new Matcher. + * @param {Object} [options={}] + * @param {string} [options.separator='.'] - Default path separator */ constructor(options = {}) { this.separator = options.separator || '.'; this.path = []; this.siblingStacks = []; - // Each path node: { tag: string, values: object, position: number, counter: number } + // Each path node: { tag, values, position, counter, namespace? } // values only present for current (last) node // Each siblingStacks entry: Map tracking occurrences at each level + this._pathStringCache = null; + this._view = new MatcherView(this); } /** - * Push a new tag onto the path - * @param {string} tagName - Name of the tag - * @param {Object} attrValues - Attribute key-value pairs for current node (optional) - * @param {string} namespace - Namespace for the tag (optional) + * Push a new tag onto the path. + * @param {string} tagName + * @param {Object|null} [attrValues=null] + * @param {string|null} [namespace=null] */ push(tagName, attrValues = null, namespace = null) { + this._pathStringCache = null; + // Remove values from previous current node (now becoming ancestor) if (this.path.length > 0) { - const prev = this.path[this.path.length - 1]; - prev.values = undefined; + this.path[this.path.length - 1].values = undefined; } // Get or create sibling tracking for current level @@ -51873,12 +52032,10 @@ class Matcher { counter: counter }; - // Store namespace if provided if (namespace !== null && namespace !== undefined) { node.namespace = namespace; } - // Store values only for current node if (attrValues !== null && attrValues !== undefined) { node.values = attrValues; } @@ -51887,19 +52044,15 @@ class Matcher { } /** - * Pop the last tag from the path + * Pop the last tag from the path. * @returns {Object|undefined} The popped node */ pop() { - if (this.path.length === 0) { - return undefined; - } + if (this.path.length === 0) return undefined; + this._pathStringCache = null; const node = this.path.pop(); - // Clean up sibling tracking for levels deeper than current - // After pop, path.length is the new depth - // We need to clean up siblingStacks[path.length + 1] and beyond if (this.siblingStacks.length > this.path.length + 1) { this.siblingStacks.length = this.path.length + 1; } @@ -51908,9 +52061,9 @@ class Matcher { } /** - * Update current node's attribute values - * Useful when attributes are parsed after push - * @param {Object} attrValues - Attribute values + * Update current node's attribute values. + * Useful when attributes are parsed after push. + * @param {Object} attrValues */ updateCurrent(attrValues) { if (this.path.length > 0) { @@ -51922,7 +52075,7 @@ class Matcher { } /** - * Get current tag name + * Get current tag name. * @returns {string|undefined} */ getCurrentTag() { @@ -51930,7 +52083,7 @@ class Matcher { } /** - * Get current namespace + * Get current namespace. * @returns {string|undefined} */ getCurrentNamespace() { @@ -51938,19 +52091,18 @@ class Matcher { } /** - * Get current node's attribute value - * @param {string} attrName - Attribute name - * @returns {*} Attribute value or undefined + * Get current node's attribute value. + * @param {string} attrName + * @returns {*} */ getAttrValue(attrName) { if (this.path.length === 0) return undefined; - const current = this.path[this.path.length - 1]; - return current.values?.[attrName]; + return this.path[this.path.length - 1].values?.[attrName]; } /** - * Check if current node has an attribute - * @param {string} attrName - Attribute name + * Check if current node has an attribute. + * @param {string} attrName * @returns {boolean} */ hasAttr(attrName) { @@ -51960,7 +52112,7 @@ class Matcher { } /** - * Get current node's sibling position (child index in parent) + * Get current node's sibling position (child index in parent). * @returns {number} */ getPosition() { @@ -51969,7 +52121,7 @@ class Matcher { } /** - * Get current node's repeat counter (occurrence count of this tag name) + * Get current node's repeat counter (occurrence count of this tag name). * @returns {number} */ getCounter() { @@ -51978,7 +52130,7 @@ class Matcher { } /** - * Get current node's sibling index (alias for getPosition for backward compatibility) + * Get current node's sibling index (alias for getPosition). * @returns {number} * @deprecated Use getPosition() or getCounter() instead */ @@ -51987,7 +52139,7 @@ class Matcher { } /** - * Get current path depth + * Get current path depth. * @returns {number} */ getDepth() { @@ -51995,23 +52147,33 @@ class Matcher { } /** - * Get path as string - * @param {string} separator - Optional separator (uses default if not provided) - * @param {boolean} includeNamespace - Whether to include namespace in output (default: true) + * Get path as string. + * @param {string} [separator] - Optional separator (uses default if not provided) + * @param {boolean} [includeNamespace=true] * @returns {string} */ toString(separator, includeNamespace = true) { const sep = separator || this.separator; - return this.path.map(n => { - if (includeNamespace && n.namespace) { - return `${n.namespace}:${n.tag}`; + const isDefault = (sep === this.separator && includeNamespace === true); + + if (isDefault) { + if (this._pathStringCache !== null) { + return this._pathStringCache; } - return n.tag; - }).join(sep); + const result = this.path.map(n => + (n.namespace) ? `${n.namespace}:${n.tag}` : n.tag + ).join(sep); + this._pathStringCache = result; + return result; + } + + return this.path.map(n => + (includeNamespace && n.namespace) ? `${n.namespace}:${n.tag}` : n.tag + ).join(sep); } /** - * Get path as array of tag names + * Get path as array of tag names. * @returns {string[]} */ toArray() { @@ -52019,17 +52181,18 @@ class Matcher { } /** - * Reset the path to empty + * Reset the path to empty. */ reset() { + this._pathStringCache = null; this.path = []; this.siblingStacks = []; } /** - * Match current path against an Expression - * @param {Expression} expression - The expression to match against - * @returns {boolean} True if current path matches the expression + * Match current path against an Expression. + * @param {Expression} expression + * @returns {boolean} */ matches(expression) { const segments = expression.segments; @@ -52038,32 +52201,23 @@ class Matcher { return false; } - // Handle deep wildcard patterns if (expression.hasDeepWildcard()) { return this._matchWithDeepWildcard(segments); } - // Simple path matching (no deep wildcards) return this._matchSimple(segments); } /** - * Match simple path (no deep wildcards) * @private */ _matchSimple(segments) { - // Path must be same length as segments if (this.path.length !== segments.length) { return false; } - // Match each segment bottom-to-top for (let i = 0; i < segments.length; i++) { - const segment = segments[i]; - const node = this.path[i]; - const isCurrentNode = (i === this.path.length - 1); - - if (!this._matchSegment(segment, node, isCurrentNode)) { + if (!this._matchSegment(segments[i], this.path[i], i === this.path.length - 1)) { return false; } } @@ -52072,32 +52226,27 @@ class Matcher { } /** - * Match path with deep wildcards * @private */ _matchWithDeepWildcard(segments) { - let pathIdx = this.path.length - 1; // Start from current node (bottom) - let segIdx = segments.length - 1; // Start from last segment + let pathIdx = this.path.length - 1; + let segIdx = segments.length - 1; while (segIdx >= 0 && pathIdx >= 0) { const segment = segments[segIdx]; if (segment.type === 'deep-wildcard') { - // ".." matches zero or more levels segIdx--; if (segIdx < 0) { - // Pattern ends with "..", always matches return true; } - // Find where next segment matches in the path const nextSeg = segments[segIdx]; let found = false; for (let i = pathIdx; i >= 0; i--) { - const isCurrentNode = (i === this.path.length - 1); - if (this._matchSegment(nextSeg, this.path[i], isCurrentNode)) { + if (this._matchSegment(nextSeg, this.path[i], i === this.path.length - 1)) { pathIdx = i - 1; segIdx--; found = true; @@ -52109,9 +52258,7 @@ class Matcher { return false; } } else { - // Regular segment - const isCurrentNode = (pathIdx === this.path.length - 1); - if (!this._matchSegment(segment, this.path[pathIdx], isCurrentNode)) { + if (!this._matchSegment(segment, this.path[pathIdx], pathIdx === this.path.length - 1)) { return false; } pathIdx--; @@ -52119,38 +52266,25 @@ class Matcher { } } - // All segments must be consumed return segIdx < 0; } /** - * Match a single segment against a node * @private - * @param {Object} segment - Segment from Expression - * @param {Object} node - Node from path - * @param {boolean} isCurrentNode - Whether this is the current (last) node - * @returns {boolean} */ _matchSegment(segment, node, isCurrentNode) { - // Match tag name (* is wildcard) if (segment.tag !== '*' && segment.tag !== node.tag) { return false; } - // Match namespace if specified in segment if (segment.namespace !== undefined) { - // Segment has namespace - node must match it if (segment.namespace !== '*' && segment.namespace !== node.namespace) { return false; } } - // If segment has no namespace, it matches nodes with or without namespace - // Match attribute name (check if node has this attribute) - // Can only check for current node since ancestors don't have values if (segment.attrName !== undefined) { if (!isCurrentNode) { - // Can't check attributes for ancestor nodes (values not stored) return false; } @@ -52158,20 +52292,15 @@ class Matcher { return false; } - // Match attribute value (only possible for current node) if (segment.attrValue !== undefined) { - const actualValue = node.values[segment.attrName]; - // Both should be strings - if (String(actualValue) !== String(segment.attrValue)) { + if (String(node.values[segment.attrName]) !== String(segment.attrValue)) { return false; } } } - // Match position (only for current node) if (segment.position !== undefined) { if (!isCurrentNode) { - // Can't check position for ancestor nodes return false; } @@ -52183,10 +52312,8 @@ class Matcher { return false; } else if (segment.position === 'even' && counter % 2 !== 0) { return false; - } else if (segment.position === 'nth') { - if (counter !== segment.positionValue) { - return false; - } + } else if (segment.position === 'nth' && counter !== segment.positionValue) { + return false; } } @@ -52194,8 +52321,17 @@ class Matcher { } /** - * Create a snapshot of current state - * @returns {Object} State snapshot + * Match any expression in the given set against the current path. + * @param {ExpressionSet} exprSet + * @returns {boolean} + */ + matchesAny(exprSet) { + return exprSet.matchesAny(this); + } + + /** + * Create a snapshot of current state. + * @returns {Object} */ snapshot() { return { @@ -52205,28 +52341,378 @@ class Matcher { } /** - * Restore state from snapshot - * @param {Object} snapshot - State snapshot + * Restore state from snapshot. + * @param {Object} snapshot */ restore(snapshot) { + this._pathStringCache = null; this.path = snapshot.path.map(node => ({ ...node })); this.siblingStacks = snapshot.siblingStacks.map(map => new Map(map)); } + + /** + * Return the read-only {@link MatcherView} for this matcher. + * + * The same instance is returned on every call — no allocation occurs. + * It always reflects the current parser state and is safe to pass to + * user callbacks without risk of accidental mutation. + * + * @returns {MatcherView} + * + * @example + * const view = matcher.readOnly(); + * // pass view to callbacks — it stays in sync automatically + * view.matches(expr); // ✓ + * view.getCurrentTag(); // ✓ + * // view.push(...) // ✗ method does not exist — caught by TypeScript + */ + readOnly() { + 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; } @@ -52243,13 +52729,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; @@ -52269,20 +52758,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); @@ -52298,21 +52799,25 @@ function arrToStr(arr, options, indentation, matcher, stopNodeExpressions) { if (isPreviousElementTag) { xmlStr += indentation; } - xmlStr += ``; + const val = tagObj[rawTagName][0][options.textNodeName]; + const safeVal = safeCdata(val); + xmlStr += ``; isPreviousElementTag = false; matcher.pop(); continue; } else if (tagName === options.commentPropName) { - xmlStr += indentation + ``; + 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; @@ -52324,16 +52829,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) { @@ -52377,7 +52881,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; } @@ -52415,9 +52919,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); @@ -52444,7 +52946,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)}"`; } } } @@ -52460,13 +52962,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]; @@ -52477,9 +52989,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)}"`; } } } @@ -52532,6 +53044,8 @@ function getIgnoreAttributesFn(ignoreAttributes) { + + const defaultOptions = { attributeNamePrefix: '@_', attributesGroupName: false, @@ -52565,7 +53079,11 @@ const 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) { @@ -52622,6 +53140,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); @@ -52633,11 +53189,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) { @@ -52651,6 +53208,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)) { @@ -52660,21 +53233,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) { @@ -52682,7 +53256,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(); @@ -52690,12 +53264,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 + '` + this.newLine; + const safeVal = safeCdata(val); + return this.indentate(level) + `` + this.newLine; } else if (this.options.commentPropName !== false && key === this.options.commentPropName) { - return this.indentate(level) + `` + this.newLine; + const safeVal = safeComment(val); + return this.indentate(level) + `` + this.newLine; } else if (key[0] === "?") {//PI tag return this.indentate(level) + '<' + key + attrStr + '?' + this.tagEndChar; } else { @@ -53135,7 +53717,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 @@ -53559,12 +54141,13 @@ function getPositionFromMatch(match) { const XMLValidator = { - validate: validate + validate: validator_validate } ;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/xmlparser/OptionsBuilder.js + const defaultOnDangerousProperty = (name) => { if (DANGEROUS_PROPERTY_NAMES.includes(name)) { return "__" + name; @@ -53604,6 +54187,7 @@ const OptionsBuilder_defaultOptions = { unpairedTags: [], processEntities: true, htmlEntities: false, + entityDecoder: null, ignoreDeclaration: false, ignorePiTags: false, transformTagName: false, @@ -53650,32 +54234,34 @@ function validatePropertyName(propertyName, optionName) { * @param {boolean|object} value * @returns {object} Always returns normalized object */ -function normalizeProcessEntities(value) { +function normalizeProcessEntities(value, htmlEntities) { // Boolean backward compatibility if (typeof value === 'boolean') { return { enabled: value, // true or false maxEntitySize: 10000, - maxExpansionDepth: 10, - maxTotalExpansions: 1000, + maxExpansionDepth: 10000, + maxTotalExpansions: Infinity, maxExpandedLength: 100000, - maxEntityCount: 100, + maxEntityCount: 1000, allowedTags: null, - tagFilter: null + tagFilter: null, + appliesTo: "all", }; } // Object config - merge with defaults if (typeof value === 'object' && value !== null) { return { - enabled: value.enabled !== false, // default true if not specified - maxEntitySize: value.maxEntitySize ?? 10000, - maxExpansionDepth: value.maxExpansionDepth ?? 10, - maxTotalExpansions: value.maxTotalExpansions ?? 1000, - maxExpandedLength: value.maxExpandedLength ?? 100000, - maxEntityCount: value.maxEntityCount ?? 100, + enabled: value.enabled !== false, + maxEntitySize: Math.max(1, value.maxEntitySize ?? 10000), + maxExpansionDepth: Math.max(1, value.maxExpansionDepth ?? 10000), + maxTotalExpansions: Math.max(1, value.maxTotalExpansions ?? Infinity), + maxExpandedLength: Math.max(1, value.maxExpandedLength ?? 100000), + maxEntityCount: Math.max(1, value.maxEntityCount ?? 1000), allowedTags: value.allowedTags ?? null, - tagFilter: value.tagFilter ?? null + tagFilter: value.tagFilter ?? null, + appliesTo: value.appliesTo ?? "all", }; } @@ -53706,8 +54292,8 @@ const buildOptions = function (options) { } // Always normalize processEntities for backward compatibility and validation - built.processEntities = normalizeProcessEntities(built.processEntities); - + built.processEntities = normalizeProcessEntities(built.processEntities, built.htmlEntities); + built.unpairedTagsSet = new Set(built.unpairedTags); // Convert old-style stopNodes for backward compatibility if (built.stopNodes && Array.isArray(built.stopNodes)) { built.stopNodes = built.stopNodes.map(node => { @@ -53768,11 +54354,15 @@ class XmlNode { class DocTypeReader { - constructor(options) { + constructor(options, xmlVersion) { this.suppressValidationErr = !options; this.options = options; + this.xmlVersion = xmlVersion || 1.0; } + setXmlVersion(xmlVersion = 1.0) { + this.xmlVersion = xmlVersion; + } readDocType(xmlData, i) { const entities = Object.create(null); let entityCount = 0; @@ -53795,18 +54385,15 @@ class DocTypeReader { [entityName, val, i] = this.readEntityExp(xmlData, i + 1, this.suppressValidationErr); if (val.indexOf("&") === -1) { //Parameter entities are not supported if (this.options.enabled !== false && - this.options.maxEntityCount && + this.options.maxEntityCount != null && entityCount >= this.options.maxEntityCount) { throw new Error( `Entity count (${entityCount + 1}) exceeds maximum allowed (${this.options.maxEntityCount})` ); } //const escaped = entityName.replace(/[.\-+*:]/g, '\\.'); - const escaped = entityName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - entities[entityName] = { - regx: RegExp(`&${escaped};`, "g"), - val: val - }; + //const escaped = entityName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + entities[entityName] = val; entityCount++; } } @@ -53867,12 +54454,13 @@ class DocTypeReader { i = skipWhitespace(xmlData, i); // Read entity name - let entityName = ""; + const startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i]) && xmlData[i] !== '"' && xmlData[i] !== "'") { - entityName += xmlData[i]; i++; } - validateEntityName(entityName); + let entityName = xmlData.substring(startIndex, i); + + validateEntityName(entityName, { xmlVersion: this.xmlVersion }); // Skip whitespace after entity name i = skipWhitespace(xmlData, i); @@ -53892,7 +54480,7 @@ class DocTypeReader { // Validate entity size if (this.options.enabled !== false && - this.options.maxEntitySize && + this.options.maxEntitySize != null && entityValue.length > this.options.maxEntitySize) { throw new Error( `Entity "${entityName}" size (${entityValue.length}) exceeds maximum allowed size (${this.options.maxEntitySize})` @@ -53908,12 +54496,14 @@ class DocTypeReader { i = skipWhitespace(xmlData, i); // Read notation name - let notationName = ""; + + const startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - notationName += xmlData[i]; i++; } - !this.suppressValidationErr && validateEntityName(notationName); + let notationName = xmlData.substring(startIndex, i); + + !this.suppressValidationErr && validateEntityName(notationName, { xmlVersion: this.xmlVersion }); // Skip whitespace after notation name i = skipWhitespace(xmlData, i); @@ -53962,10 +54552,11 @@ class DocTypeReader { } i++; + const startIndex = i; while (i < xmlData.length && xmlData[i] !== startChar) { - identifierVal += xmlData[i]; i++; } + identifierVal = xmlData.substring(startIndex, i); if (xmlData[i] !== startChar) { throw new Error(`Unterminated ${type} value`); @@ -53985,14 +54576,14 @@ class DocTypeReader { i = skipWhitespace(xmlData, i); // Read element name - let elementName = ""; + const startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - elementName += xmlData[i]; i++; } + let elementName = xmlData.substring(startIndex, i); // Validate element name - if (!this.suppressValidationErr && !isName(elementName)) { + if (!this.suppressValidationErr && !qName(elementName, { xmlVersion: this.xmlVersion })) { throw new Error(`Invalid element name: "${elementName}"`); } @@ -54006,10 +54597,12 @@ class DocTypeReader { i++; // Move past '(' // Read content model + const startIndex = i; while (i < xmlData.length && xmlData[i] !== ")") { - contentModel += xmlData[i]; i++; } + contentModel = xmlData.substring(startIndex, i); + if (xmlData[i] !== ")") { throw new Error("Unterminated content model"); } @@ -54030,27 +54623,27 @@ class DocTypeReader { i = skipWhitespace(xmlData, i); // Read element name - let elementName = ""; + let startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - elementName += xmlData[i]; i++; } + let elementName = xmlData.substring(startIndex, i); // Validate element name - validateEntityName(elementName) + validateEntityName(elementName, { xmlVersion: this.xmlVersion }) // Skip whitespace after element name i = skipWhitespace(xmlData, i); // Read attribute name - let attributeName = ""; + startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - attributeName += xmlData[i]; i++; } + let attributeName = xmlData.substring(startIndex, i); // Validate attribute name - if (!validateEntityName(attributeName)) { + if (!validateEntityName(attributeName, { xmlVersion: this.xmlVersion })) { throw new Error(`Invalid attribute name: "${attributeName}"`); } @@ -54075,15 +54668,17 @@ class DocTypeReader { // Read the list of allowed notations let allowedNotations = []; while (i < xmlData.length && xmlData[i] !== ")") { - let notation = ""; + + + const startIndex = i; while (i < xmlData.length && xmlData[i] !== "|" && xmlData[i] !== ")") { - notation += xmlData[i]; i++; } + let notation = xmlData.substring(startIndex, i); // Validate notation name notation = notation.trim(); - if (!validateEntityName(notation)) { + if (!validateEntityName(notation, { xmlVersion: this.xmlVersion })) { throw new Error(`Invalid notation name: "${notation}"`); } @@ -54105,10 +54700,11 @@ class DocTypeReader { attributeType += " (" + allowedNotations.join("|") + ")"; } else { // Handle simple types (e.g., CDATA, ID, IDREF, etc.) + const startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - attributeType += xmlData[i]; i++; } + attributeType += xmlData.substring(startIndex, i); // Validate simple attribute type const validTypes = ["CDATA", "ID", "IDREF", "IDREFS", "ENTITY", "ENTITIES", "NMTOKEN", "NMTOKENS"]; @@ -54160,115 +54756,124 @@ function hasSeq(data, seq, i) { return true; } -function validateEntityName(name) { - if (isName(name)) +function validateEntityName(name, xmlVersion) { + if (qName(name, { xmlVersion: xmlVersion })) return name; else throw new Error(`Invalid entity name ${name}`); } ;// CONCATENATED MODULE: ./node_modules/strnum/strnum.js const hexRegex = /^[-+]?0x[a-fA-F0-9]+$/; +const binRegex = /^0b[01]+$/; +const octRegex = /^0o[0-7]+$/; const numRegex = /^([\-\+])?(0*)([0-9]*(\.[0-9]*)?)$/; -// const octRegex = /^0x[a-z0-9]+/; -// const binRegex = /0x[a-z0-9]+/; - const consider = { - hex : true, - // oct: false, + hex: true, + binary: false, + octal: false, leadingZeros: true, decimalPoint: "\.", eNotation: true, - //skipLike: /regex/ + //skipLike: /regex/, + infinity: "original", // "null", "infinity" (Infinity type), "string" ("Infinity" (the string literal)) }; -function toNumber(str, options = {}){ - options = Object.assign({}, consider, options ); - if(!str || typeof str !== "string" ) return str; - - let trimmedStr = str.trim(); - - if(options.skipLike !== undefined && options.skipLike.test(trimmedStr)) return str; - else if(str==="0") return 0; +function toNumber(str, options = {}) { + options = Object.assign({}, consider, options); + if (!str || typeof str !== "string") return str; + + let trimmedStr = str.trim(); + + if (trimmedStr.length === 0) return str; + else if (options.skipLike !== undefined && options.skipLike.test(trimmedStr)) return str; + else if (trimmedStr === "0") return 0; else if (options.hex && hexRegex.test(trimmedStr)) { return parse_int(trimmedStr, 16); - // }else if (options.oct && octRegex.test(str)) { - // return Number.parseInt(val, 8); - }else if (trimmedStr.includes('e') || trimmedStr.includes('E')) { //eNotation - return resolveEnotation(str,trimmedStr,options); - // }else if (options.parseBin && binRegex.test(str)) { - // return Number.parseInt(val, 2); - }else{ + } else if (options.binary && binRegex.test(trimmedStr)) { + return parse_int(trimmedStr, 2); + } else if (options.octal && octRegex.test(trimmedStr)) { + return parse_int(trimmedStr, 8); + } else if (!isFinite(trimmedStr)) { //Infinity + return handleInfinity(str, Number(trimmedStr), options); + } else if (trimmedStr.includes('e') || trimmedStr.includes('E')) { //eNotation + return resolveEnotation(str, trimmedStr, options); + } else { //separate negative sign, leading zeros, and rest number const match = numRegex.exec(trimmedStr); // +00.123 => [ , '+', '00', '.123', .. - if(match){ + if (match) { const sign = match[1] || ""; const leadingZeros = match[2]; let numTrimmedByZeros = trimZeros(match[3]); //complete num without leading zeros const decimalAdjacentToLeadingZeros = sign ? // 0., -00., 000. - str[leadingZeros.length+1] === "." + str[leadingZeros.length + 1] === "." : str[leadingZeros.length] === "."; //trim ending zeros for floating number - if(!options.leadingZeros //leading zeros are not allowed - && (leadingZeros.length > 1 - || (leadingZeros.length === 1 && !decimalAdjacentToLeadingZeros))){ + if (!options.leadingZeros //leading zeros are not allowed + && (leadingZeros.length > 1 + || (leadingZeros.length === 1 && !decimalAdjacentToLeadingZeros))) { // 00, 00.3, +03.24, 03, 03.24 return str; } - else{//no leading zeros or leading zeros are allowed + else {//no leading zeros or leading zeros are allowed const num = Number(trimmedStr); const parsedStr = String(num); - if( num === 0) return num; - if(parsedStr.search(/[eE]/) !== -1){ //given number is long and parsed to eNotation - if(options.eNotation) return num; + if (num === 0) return num; + if (parsedStr.search(/[eE]/) !== -1) { //given number is long and parsed to eNotation + if (options.eNotation) return num; else return str; - }else if(trimmedStr.indexOf(".") !== -1){ //floating number - if(parsedStr === "0") return num; //0.0 - else if(parsedStr === numTrimmedByZeros) return num; //0.456. 0.79000 - else if( parsedStr === `${sign}${numTrimmedByZeros}`) return num; + } else if (trimmedStr.indexOf(".") !== -1) { //floating number + if (parsedStr === "0") return num; //0.0 + else if (parsedStr === numTrimmedByZeros) return num; //0.456. 0.79000 + else if (parsedStr === `${sign}${numTrimmedByZeros}`) return num; else return str; } - - let n = leadingZeros? numTrimmedByZeros : trimmedStr; - if(leadingZeros){ + + let n = leadingZeros ? numTrimmedByZeros : trimmedStr; + if (leadingZeros) { // -009 => -9 - return (n === parsedStr) || (sign+n === parsedStr) ? num : str - }else { + return (n === parsedStr) || (sign + n === parsedStr) ? num : str + } else { // +9 - return (n === parsedStr) || (n === sign+parsedStr) ? num : str + return (n === parsedStr) || (n === sign + parsedStr) ? num : str } } - }else{ //non-numeric string + } else { //non-numeric string return str; } } } const eNotationRegx = /^([-+])?(0*)(\d*(\.\d*)?[eE][-\+]?\d+)$/; -function resolveEnotation(str,trimmedStr,options){ - if(!options.eNotation) return str; - const notation = trimmedStr.match(eNotationRegx); - if(notation){ +function resolveEnotation(str, trimmedStr, options) { + if (!options.eNotation) return str; + const notation = trimmedStr.match(eNotationRegx); + if (notation) { let sign = notation[1] || ""; const eChar = notation[3].indexOf("e") === -1 ? "E" : "e"; const leadingZeros = notation[2]; const eAdjacentToLeadingZeros = sign ? // 0E. - str[leadingZeros.length+1] === eChar + str[leadingZeros.length + 1] === eChar : str[leadingZeros.length] === eChar; - if(leadingZeros.length > 1 && eAdjacentToLeadingZeros) return str; - else if(leadingZeros.length === 1 - && (notation[3].startsWith(`.${eChar}`) || notation[3][0] === eChar)){ - return Number(trimmedStr); - }else if(options.leadingZeros && !eAdjacentToLeadingZeros){ //accept with leading zeros - //remove leading 0s - trimmedStr = (notation[1] || "") + notation[3]; + if (leadingZeros.length > 1 && eAdjacentToLeadingZeros) return str; + else if (leadingZeros.length === 1 + && (notation[3].startsWith(`.${eChar}`) || notation[3][0] === eChar)) { return Number(trimmedStr); - }else return str; - }else{ + } else if (leadingZeros.length > 0) { + // Has leading zeros — only accept if leadingZeros option allows it + if (options.leadingZeros && !eAdjacentToLeadingZeros) { + trimmedStr = (notation[1] || "") + notation[3]; + return Number(trimmedStr); + } else return str; + } else { + // No leading zeros — always valid e-notation, parse it + return Number(trimmedStr); + } + } else { return str; } } @@ -54278,23 +54883,48 @@ function resolveEnotation(str,trimmedStr,options){ * @param {string} numStr without leading zeros * @returns */ -function trimZeros(numStr){ - if(numStr && numStr.indexOf(".") !== -1){//float +function trimZeros(numStr) { + if (numStr && numStr.indexOf(".") !== -1) {//float numStr = numStr.replace(/0+$/, ""); //remove ending zeros - if(numStr === ".") numStr = "0"; - else if(numStr[0] === ".") numStr = "0"+numStr; - else if(numStr[numStr.length-1] === ".") numStr = numStr.substring(0,numStr.length-1); + if (numStr === ".") numStr = "0"; + else if (numStr[0] === ".") numStr = "0" + numStr; + else if (numStr[numStr.length - 1] === ".") numStr = numStr.substring(0, numStr.length - 1); return numStr; } return numStr; } -function parse_int(numStr, base){ - //polyfill - if(parseInt) return parseInt(numStr, base); - else if(Number.parseInt) return Number.parseInt(numStr, base); - else if(window && window.parseInt) return window.parseInt(numStr, base); - else throw new Error("parseInt, Number.parseInt, window.parseInt are not supported") +function parse_int(numStr, base) { + const str = numStr.trim(); + if (base === 2 || base === 8) numStr = str.substring(2); + + if (parseInt) return parseInt(numStr, base); + else if (Number.parseInt) return Number.parseInt(numStr, base); + else if (window && window.parseInt) return window.parseInt(numStr, base); + else throw new Error("parseInt, Number.parseInt, window.parseInt are not supported"); +} + +/** + * Handle infinite values based on user option + * @param {string} str - original input string + * @param {number} num - parsed number (Infinity or -Infinity) + * @param {object} options - user options + * @returns {string|number|null} based on infinity option + */ +function handleInfinity(str, num, options) { + const isPositive = num === Infinity; + + switch (options.infinity.toLowerCase()) { + case "null": + return null; + case "infinity": + return num; // Return Infinity or -Infinity + case "string": + return isPositive ? "Infinity" : "-Infinity"; + case "original": + default: + return str; // Return original string like "1e1000" + } } ;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/ignoreAttributes.js function ignoreAttributes_getIgnoreAttributesFn(ignoreAttributes) { @@ -54315,6 +54945,1939 @@ function ignoreAttributes_getIgnoreAttributesFn(ignoreAttributes) { } return () => false } +;// CONCATENATED MODULE: ./node_modules/path-expression-matcher/src/ExpressionSet.js +/** + * ExpressionSet - An indexed collection of Expressions for efficient bulk matching + * + * Instead of iterating all expressions on every tag, ExpressionSet pre-indexes + * them at insertion time by depth and terminal tag name. At match time, only + * the relevant bucket is evaluated — typically reducing checks from O(E) to O(1) + * lookup plus O(small bucket) matches. + * + * Three buckets are maintained: + * - `_byDepthAndTag` — exact depth + exact tag name (tightest, used first) + * - `_wildcardByDepth` — exact depth + wildcard tag `*` (depth-matched only) + * - `_deepWildcards` — expressions containing `..` (cannot be depth-indexed) + * + * @example + * import { Expression, ExpressionSet } from 'fast-xml-tagger'; + * + * // Build once at config time + * const stopNodes = new ExpressionSet(); + * stopNodes.add(new Expression('root.users.user')); + * stopNodes.add(new Expression('root.config.setting')); + * stopNodes.add(new Expression('..script')); + * + * // Query on every tag — hot path + * if (stopNodes.matchesAny(matcher)) { ... } + */ +class ExpressionSet { + constructor() { + /** @type {Map} depth:tag → expressions */ + this._byDepthAndTag = new Map(); + + /** @type {Map} depth → wildcard-tag expressions */ + this._wildcardByDepth = new Map(); + + /** @type {import('./Expression.js').default[]} expressions containing deep wildcard (..) */ + this._deepWildcards = []; + + /** @type {Set} pattern strings already added — used for deduplication */ + this._patterns = new Set(); + + /** @type {boolean} whether the set is sealed against further additions */ + this._sealed = false; + } + + /** + * Add an Expression to the set. + * Duplicate patterns (same pattern string) are silently ignored. + * + * @param {import('./Expression.js').default} expression - A pre-constructed Expression instance + * @returns {this} for chaining + * @throws {TypeError} if called after seal() + * + * @example + * set.add(new Expression('root.users.user')); + * set.add(new Expression('..script')); + */ + add(expression) { + if (this._sealed) { + throw new TypeError( + 'ExpressionSet is sealed. Create a new ExpressionSet to add more expressions.' + ); + } + + // Deduplicate by pattern string + if (this._patterns.has(expression.pattern)) return this; + this._patterns.add(expression.pattern); + + if (expression.hasDeepWildcard()) { + this._deepWildcards.push(expression); + return this; + } + + const depth = expression.length; + const lastSeg = expression.segments[expression.segments.length - 1]; + const tag = lastSeg?.tag; + + if (!tag || tag === '*') { + // Can index by depth but not by tag + if (!this._wildcardByDepth.has(depth)) this._wildcardByDepth.set(depth, []); + this._wildcardByDepth.get(depth).push(expression); + } else { + // Tightest bucket: depth + tag + const key = `${depth}:${tag}`; + if (!this._byDepthAndTag.has(key)) this._byDepthAndTag.set(key, []); + this._byDepthAndTag.get(key).push(expression); + } + + return this; + } + + /** + * Add multiple expressions at once. + * + * @param {import('./Expression.js').default[]} expressions - Array of Expression instances + * @returns {this} for chaining + * + * @example + * set.addAll([ + * new Expression('root.users.user'), + * new Expression('root.config.setting'), + * ]); + */ + addAll(expressions) { + for (const expr of expressions) this.add(expr); + return this; + } + + /** + * Check whether a pattern string is already present in the set. + * + * @param {import('./Expression.js').default} expression + * @returns {boolean} + */ + has(expression) { + return this._patterns.has(expression.pattern); + } + + /** + * Number of expressions in the set. + * @type {number} + */ + get size() { + return this._patterns.size; + } + + /** + * Seal the set against further modifications. + * Useful to prevent accidental mutations after config is built. + * Calling add() or addAll() on a sealed set throws a TypeError. + * + * @returns {this} + */ + seal() { + this._sealed = true; + return this; + } + + /** + * Whether the set has been sealed. + * @type {boolean} + */ + get isSealed() { + return this._sealed; + } + + /** + * Test whether the matcher's current path matches any expression in the set. + * + * Evaluation order (cheapest → most expensive): + * 1. Exact depth + tag bucket — O(1) lookup, typically 0–2 expressions + * 2. Depth-only wildcard bucket — O(1) lookup, rare + * 3. Deep-wildcard list — always checked, but usually small + * + * @param {import('./Matcher.js').default} matcher - Matcher instance (or readOnly view) + * @returns {boolean} true if any expression matches the current path + * + * @example + * if (stopNodes.matchesAny(matcher)) { + * // handle stop node + * } + */ + matchesAny(matcher) { + return this.findMatch(matcher) !== null; + } + /** + * Find and return the first Expression that matches the matcher's current path. + * + * Uses the same evaluation order as matchesAny (cheapest → most expensive): + * 1. Exact depth + tag bucket + * 2. Depth-only wildcard bucket + * 3. Deep-wildcard list + * + * @param {import('./Matcher.js').default} matcher - Matcher instance (or readOnly view) + * @returns {import('./Expression.js').default | null} the first matching Expression, or null + * + * @example + * const expr = stopNodes.findMatch(matcher); + * if (expr) { + * // access expr.config, expr.pattern, etc. + * } + */ + findMatch(matcher) { + const depth = matcher.getDepth(); + const tag = matcher.getCurrentTag(); + + // 1. Tightest bucket — most expressions live here + const exactKey = `${depth}:${tag}`; + const exactBucket = this._byDepthAndTag.get(exactKey); + if (exactBucket) { + for (let i = 0; i < exactBucket.length; i++) { + if (matcher.matches(exactBucket[i])) return exactBucket[i]; + } + } + + // 2. Depth-matched wildcard-tag expressions + const wildcardBucket = this._wildcardByDepth.get(depth); + if (wildcardBucket) { + for (let i = 0; i < wildcardBucket.length; i++) { + if (matcher.matches(wildcardBucket[i])) return wildcardBucket[i]; + } + } + + // 3. Deep wildcards — cannot be pre-filtered by depth or tag + for (let i = 0; i < this._deepWildcards.length; i++) { + if (matcher.matches(this._deepWildcards[i])) return this._deepWildcards[i]; + } + + return null; + } +} + +;// CONCATENATED MODULE: ./node_modules/@nodable/entities/src/entities.js +// --------------------------------------------------------------------------- +// Complete HTML5 named entity reference +// Organized by logical categories for easy maintenance and selective importing +// --------------------------------------------------------------------------- + +/** + * Basic Latin & Special Characters + * @type {Record} + */ +const BASIC_LATIN = { + amp: '&', + AMP: '&', + lt: '<', + LT: '<', + gt: '>', + GT: '>', + quot: '"', + QUOT: '"', + apos: "'", + lsquo: '‘', + rsquo: '’', + ldquo: '“', + rdquo: '”', + lsquor: '‚', + rsquor: '’', + ldquor: '„', + bdquo: '„', + comma: ',', + period: '.', + colon: ':', + semi: ';', + excl: '!', + quest: '?', + num: '#', + dollar: '$', + percent: '%', + amp: '&', + ast: '*', + commat: '@', + lowbar: '_', + verbar: '|', + vert: '|', + sol: '/', + bsol: '\\', + lbrace: '{', + rbrace: '}', + lbrack: '[', + rbrack: ']', + lpar: '(', + rpar: ')', + nbsp: '\u00a0', + iexcl: '¡', + cent: '¢', + pound: '£', + curren: '¤', + yen: '¥', + brvbar: '¦', + sect: '§', + uml: '¨', + copy: '©', + COPY: '©', + ordf: 'ª', + laquo: '«', + not: '¬', + shy: '\u00ad', + reg: '®', + REG: '®', + macr: '¯', + deg: '°', + plusmn: '±', + sup2: '²', + sup3: '³', + acute: '´', + micro: 'µ', + para: '¶', + middot: '·', + cedil: '¸', + sup1: '¹', + ordm: 'º', + raquo: '»', + frac14: '¼', + frac12: '½', + half: '½', + frac34: '¾', + iquest: '¿', + times: '×', + div: '÷', + divide: '÷', +}; + +/** + * Latin Extended & Accented Letters (A-Z) + * @type {Record} + */ +const LATIN_ACCENTS = { + Agrave: 'À', + agrave: 'à', + Aacute: 'Á', + aacute: 'á', + Acirc: 'Â', + acirc: 'â', + Atilde: 'Ã', + atilde: 'ã', + Auml: 'Ä', + auml: 'ä', + Aring: 'Å', + aring: 'å', + AElig: 'Æ', + aelig: 'æ', + Ccedil: 'Ç', + ccedil: 'ç', + Egrave: 'È', + egrave: 'è', + Eacute: 'É', + eacute: 'é', + Ecirc: 'Ê', + ecirc: 'ê', + Euml: 'Ë', + euml: 'ë', + Igrave: 'Ì', + igrave: 'ì', + Iacute: 'Í', + iacute: 'í', + Icirc: 'Î', + icirc: 'î', + Iuml: 'Ï', + iuml: 'ï', + ETH: 'Ð', + eth: 'ð', + Ntilde: 'Ñ', + ntilde: 'ñ', + Ograve: 'Ò', + ograve: 'ò', + Oacute: 'Ó', + oacute: 'ó', + Ocirc: 'Ô', + ocirc: 'ô', + Otilde: 'Õ', + otilde: 'õ', + Ouml: 'Ö', + ouml: 'ö', + Oslash: 'Ø', + oslash: 'ø', + Ugrave: 'Ù', + ugrave: 'ù', + Uacute: 'Ú', + uacute: 'ú', + Ucirc: 'Û', + ucirc: 'û', + Uuml: 'Ü', + uuml: 'ü', + Yacute: 'Ý', + yacute: 'ý', + THORN: 'Þ', + thorn: 'þ', + szlig: 'ß', + yuml: 'ÿ', + Yuml: 'Ÿ', +}; + +/** + * Latin Extended (Letters with diacritics) + * @type {Record} + */ +const LATIN_EXTENDED = { + Amacr: 'Ā', + amacr: 'ā', + Abreve: 'Ă', + abreve: 'ă', + Aogon: 'Ą', + aogon: 'ą', + Cacute: 'Ć', + cacute: 'ć', + Ccirc: 'Ĉ', + ccirc: 'ĉ', + Cdot: 'Ċ', + cdot: 'ċ', + Ccaron: 'Č', + ccaron: 'č', + Dcaron: 'Ď', + dcaron: 'ď', + Dstrok: 'Đ', + dstrok: 'đ', + Emacr: 'Ē', + emacr: 'ē', + Ecaron: 'Ě', + ecaron: 'ě', + Edot: 'Ė', + edot: 'ė', + Eogon: 'Ę', + eogon: 'ę', + Gcirc: 'Ĝ', + gcirc: 'ĝ', + Gbreve: 'Ğ', + gbreve: 'ğ', + Gdot: 'Ġ', + gdot: 'ġ', + Gcedil: 'Ģ', + Hcirc: 'Ĥ', + hcirc: 'ĥ', + Hstrok: 'Ħ', + hstrok: 'ħ', + Itilde: 'Ĩ', + itilde: 'ĩ', + Imacr: 'Ī', + imacr: 'ī', + Iogon: 'Į', + iogon: 'į', + Idot: 'İ', + IJlig: 'IJ', + ijlig: 'ij', + Jcirc: 'Ĵ', + jcirc: 'ĵ', + Kcedil: 'Ķ', + kcedil: 'ķ', + kgreen: 'ĸ', + Lacute: 'Ĺ', + lacute: 'ĺ', + Lcedil: 'Ļ', + lcedil: 'ļ', + Lcaron: 'Ľ', + lcaron: 'ľ', + Lmidot: 'Ŀ', + lmidot: 'ŀ', + Lstrok: 'Ł', + lstrok: 'ł', + Nacute: 'Ń', + nacute: 'ń', + Ncaron: 'Ň', + ncaron: 'ň', + Ncedil: 'Ņ', + ncedil: 'ņ', + ENG: 'Ŋ', + eng: 'ŋ', + Omacr: 'Ō', + omacr: 'ō', + Odblac: 'Ő', + odblac: 'ő', + OElig: 'Œ', + oelig: 'œ', + Racute: 'Ŕ', + racute: 'ŕ', + Rcaron: 'Ř', + rcaron: 'ř', + Rcedil: 'Ŗ', + rcedil: 'ŗ', + Sacute: 'Ś', + sacute: 'ś', + Scirc: 'Ŝ', + scirc: 'ŝ', + Scedil: 'Ş', + scedil: 'ş', + Scaron: 'Š', + scaron: 'š', + Tcedil: 'Ţ', + tcedil: 'ţ', + Tcaron: 'Ť', + tcaron: 'ť', + Tstrok: 'Ŧ', + tstrok: 'ŧ', + Utilde: 'Ũ', + utilde: 'ũ', + Umacr: 'Ū', + umacr: 'ū', + Ubreve: 'Ŭ', + ubreve: 'ŭ', + Uring: 'Ů', + uring: 'ů', + Udblac: 'Ű', + udblac: 'ű', + Uogon: 'Ų', + uogon: 'ų', + Wcirc: 'Ŵ', + wcirc: 'ŵ', + Ycirc: 'Ŷ', + ycirc: 'ŷ', + Zacute: 'Ź', + zacute: 'ź', + Zdot: 'Ż', + zdot: 'ż', + Zcaron: 'Ž', + zcaron: 'ž', +}; + +/** + * Greek Letters + * @type {Record} + */ +const GREEK = { + Alpha: 'Α', + alpha: 'α', + Beta: 'Β', + beta: 'β', + Gamma: 'Γ', + gamma: 'γ', + Delta: 'Δ', + delta: 'δ', + Epsilon: 'Ε', + epsilon: 'ε', + epsiv: 'ϵ', + varepsilon: 'ϵ', + Zeta: 'Ζ', + zeta: 'ζ', + Eta: 'Η', + eta: 'η', + Theta: 'Θ', + theta: 'θ', + thetasym: 'ϑ', + vartheta: 'ϑ', + Iota: 'Ι', + iota: 'ι', + Kappa: 'Κ', + kappa: 'κ', + kappav: 'ϰ', + varkappa: 'ϰ', + Lambda: 'Λ', + lambda: 'λ', + Mu: 'Μ', + mu: 'μ', + Nu: 'Ν', + nu: 'ν', + Xi: 'Ξ', + xi: 'ξ', + Omicron: 'Ο', + omicron: 'ο', + Pi: 'Π', + pi: 'π', + piv: 'ϖ', + varpi: 'ϖ', + Rho: 'Ρ', + rho: 'ρ', + rhov: 'ϱ', + varrho: 'ϱ', + Sigma: 'Σ', + sigma: 'σ', + sigmaf: 'ς', + sigmav: 'ς', + varsigma: 'ς', + Tau: 'Τ', + tau: 'τ', + Upsilon: 'Υ', + upsilon: 'υ', + upsi: 'υ', + Upsi: 'ϒ', + upsih: 'ϒ', + Phi: 'Φ', + phi: 'φ', + phiv: 'ϕ', + varphi: 'ϕ', + Chi: 'Χ', + chi: 'χ', + Psi: 'Ψ', + psi: 'ψ', + Omega: 'Ω', + omega: 'ω', + ohm: 'Ω', + Gammad: 'Ϝ', + gammad: 'ϝ', + digamma: 'ϝ', +}; + +/** + * Cyrillic Letters + * @type {Record} + */ +const CYRILLIC = { + Afr: '𝔄', + afr: '𝔞', + Acy: 'А', + acy: 'а', + Bcy: 'Б', + bcy: 'б', + Vcy: 'В', + vcy: 'в', + Gcy: 'Г', + gcy: 'г', + Dcy: 'Д', + dcy: 'д', + IEcy: 'Е', + iecy: 'е', + IOcy: 'Ё', + iocy: 'ё', + ZHcy: 'Ж', + zhcy: 'ж', + Zcy: 'З', + zcy: 'з', + Icy: 'И', + icy: 'и', + Jcy: 'Й', + jcy: 'й', + Kcy: 'К', + kcy: 'к', + Lcy: 'Л', + lcy: 'л', + Mcy: 'М', + mcy: 'м', + Ncy: 'Н', + ncy: 'н', + Ocy: 'О', + ocy: 'о', + Pcy: 'П', + pcy: 'п', + Rcy: 'Р', + rcy: 'р', + Scy: 'С', + scy: 'с', + Tcy: 'Т', + tcy: 'т', + Ucy: 'У', + ucy: 'у', + Fcy: 'Ф', + fcy: 'ф', + KHcy: 'Х', + khcy: 'х', + TScy: 'Ц', + tscy: 'ц', + CHcy: 'Ч', + chcy: 'ч', + SHcy: 'Ш', + shcy: 'ш', + SHCHcy: 'Щ', + shchcy: 'щ', + HARDcy: 'Ъ', + hardcy: 'ъ', + Ycy: 'Ы', + ycy: 'ы', + SOFTcy: 'Ь', + softcy: 'ь', + Ecy: 'Э', + ecy: 'э', + YUcy: 'Ю', + yucy: 'ю', + YAcy: 'Я', + yacy: 'я', + DJcy: 'Ђ', + djcy: 'ђ', + GJcy: 'Ѓ', + gjcy: 'ѓ', + Jukcy: 'Є', + jukcy: 'є', + DScy: 'Ѕ', + dscy: 'ѕ', + Iukcy: 'І', + iukcy: 'і', + YIcy: 'Ї', + yicy: 'ї', + Jsercy: 'Ј', + jsercy: 'ј', + LJcy: 'Љ', + ljcy: 'љ', + NJcy: 'Њ', + njcy: 'њ', + TSHcy: 'Ћ', + tshcy: 'ћ', + KJcy: 'Ќ', + kjcy: 'ќ', + Ubrcy: 'Ў', + ubrcy: 'ў', + DZcy: 'Џ', + dzcy: 'џ', +}; + +/** + * Mathematical Operators & Relations + * @type {Record} + */ +const MATH = { + plus: '+', + minus: '−', + mnplus: '∓', + mp: '∓', + pm: '±', + times: '×', + div: '÷', + divide: '÷', + sdot: '⋅', + star: '☆', + starf: '★', + bigstar: '★', + lowast: '∗', + ast: '*', + midast: '*', + compfn: '∘', + smallcircle: '∘', + bullet: '•', + bull: '•', + nbsp: '\u00a0', + hellip: '…', + mldr: '…', + prime: '′', + Prime: '″', + tprime: '‴', + bprime: '‵', + backprime: '‵', + minus: '−', + minusd: '∸', + dotminus: '∸', + plusdo: '∔', + dotplus: '∔', + plusmn: '±', + minusplus: '∓', + mnplus: '∓', + mp: '∓', + setminus: '∖', + smallsetminus: '∖', + Backslash: '∖', + setmn: '∖', + ssetmn: '∖', + lowbar: '_', + verbar: '|', + vert: '|', + VerticalLine: '|', + colon: ':', + Colon: '∷', + Proportion: '∷', + ratio: '∶', + equals: '=', + ne: '≠', + nequiv: '≢', + equiv: '≡', + Congruent: '≡', + sim: '∼', + thicksim: '∼', + thksim: '∼', + sime: '≃', + simeq: '≃', + TildeEqual: '≃', + asymp: '≈', + approx: '≈', + thickapprox: '≈', + thkap: '≈', + TildeTilde: '≈', + ncong: '≇', + cong: '≅', + TildeFullEqual: '≅', + asympeq: '≍', + CupCap: '≍', + bump: '≎', + Bumpeq: '≎', + HumpDownHump: '≎', + bumpe: '≏', + bumpeq: '≏', + HumpEqual: '≏', + dotminus: '∸', + minusd: '∸', + plusdo: '∔', + dotplus: '∔', + le: '≤', + LessEqual: '≤', + ge: '≥', + GreaterEqual: '≥', + lesseqgtr: '⋚', + lesseqqgtr: '⪋', + greater: '>', + less: '<', +}; + +/** + * Mathematical Operators (Advanced) + * @type {Record} + */ +const MATH_ADVANCED = { + alefsym: 'ℵ', + aleph: 'ℵ', + beth: 'ℶ', + gimel: 'ℷ', + daleth: 'ℸ', + forall: '∀', + ForAll: '∀', + part: '∂', + PartialD: '∂', + exist: '∃', + Exists: '∃', + nexist: '∄', + nexists: '∄', + empty: '∅', + emptyset: '∅', + emptyv: '∅', + varnothing: '∅', + nabla: '∇', + Del: '∇', + isin: '∈', + isinv: '∈', + in: '∈', + Element: '∈', + notin: '∉', + notinva: '∉', + ni: '∋', + niv: '∋', + SuchThat: '∋', + ReverseElement: '∋', + notni: '∌', + notniva: '∌', + prod: '∏', + Product: '∏', + coprod: '∐', + Coproduct: '∐', + sum: '∑', + Sum: '∑', + minus: '−', + mp: '∓', + plusdo: '∔', + dotplus: '∔', + setminus: '∖', + lowast: '∗', + radic: '√', + Sqrt: '√', + prop: '∝', + propto: '∝', + Proportional: '∝', + varpropto: '∝', + infin: '∞', + infintie: '⧝', + ang: '∠', + angle: '∠', + angmsd: '∡', + measuredangle: '∡', + angsph: '∢', + mid: '∣', + VerticalBar: '∣', + nmid: '∤', + nsmid: '∤', + npar: '∦', + parallel: '∥', + spar: '∥', + nparallel: '∦', + nspar: '∦', + and: '∧', + wedge: '∧', + or: '∨', + vee: '∨', + cap: '∩', + cup: '∪', + int: '∫', + Integral: '∫', + conint: '∮', + ContourIntegral: '∮', + Conint: '∯', + DoubleContourIntegral: '∯', + Cconint: '∰', + there4: '∴', + therefore: '∴', + Therefore: '∴', + becaus: '∵', + because: '∵', + Because: '∵', + ratio: '∶', + Proportion: '∷', + minusd: '∸', + dotminus: '∸', + mDDot: '∺', + homtht: '∻', + sim: '∼', + bsimg: '∽', + backsim: '∽', + ac: '∾', + mstpos: '∾', + acd: '∿', + VerticalTilde: '≀', + wr: '≀', + wreath: '≀', + nsime: '≄', + nsimeq: '≄', + nsimeq: '≄', + ncong: '≇', + simne: '≆', + ncongdot: '⩭̸', + ngsim: '≵', + nsim: '≁', + napprox: '≉', + nap: '≉', + ngeq: '≱', + nge: '≱', + nleq: '≰', + nle: '≰', + ngtr: '≯', + ngt: '≯', + nless: '≮', + nlt: '≮', + nprec: '⊀', + npr: '⊀', + nsucc: '⊁', + nsc: '⊁', +}; + +/** + * Arrows + * @type {Record} + */ +const ARROWS = { + larr: '←', + leftarrow: '←', + LeftArrow: '←', + uarr: '↑', + uparrow: '↑', + UpArrow: '↑', + rarr: '→', + rightarrow: '→', + RightArrow: '→', + darr: '↓', + downarrow: '↓', + DownArrow: '↓', + harr: '↔', + leftrightarrow: '↔', + LeftRightArrow: '↔', + varr: '↕', + updownarrow: '↕', + UpDownArrow: '↕', + nwarr: '↖', + nwarrow: '↖', + UpperLeftArrow: '↖', + nearr: '↗', + nearrow: '↗', + UpperRightArrow: '↗', + searr: '↘', + searrow: '↘', + LowerRightArrow: '↘', + swarr: '↙', + swarrow: '↙', + LowerLeftArrow: '↙', + lArr: '⇐', + Leftarrow: '⇐', + uArr: '⇑', + Uparrow: '⇑', + rArr: '⇒', + Rightarrow: '⇒', + dArr: '⇓', + Downarrow: '⇓', + hArr: '⇔', + Leftrightarrow: '⇔', + iff: '⇔', + vArr: '⇕', + Updownarrow: '⇕', + lAarr: '⇚', + Lleftarrow: '⇚', + rAarr: '⇛', + Rrightarrow: '⇛', + lrarr: '⇆', + leftrightarrows: '⇆', + rlarr: '⇄', + rightleftarrows: '⇄', + lrhar: '⇋', + leftrightharpoons: '⇋', + ReverseEquilibrium: '⇋', + rlhar: '⇌', + rightleftharpoons: '⇌', + Equilibrium: '⇌', + udarr: '⇅', + UpArrowDownArrow: '⇅', + duarr: '⇵', + DownArrowUpArrow: '⇵', + llarr: '⇇', + leftleftarrows: '⇇', + rrarr: '⇉', + rightrightarrows: '⇉', + ddarr: '⇊', + downdownarrows: '⇊', + har: '↽', + lhard: '↽', + leftharpoondown: '↽', + lharu: '↼', + leftharpoonup: '↼', + rhard: '⇁', + rightharpoondown: '⇁', + rharu: '⇀', + rightharpoonup: '⇀', + lsh: '↰', + Lsh: '↰', + rsh: '↱', + Rsh: '↱', + ldsh: '↲', + rdsh: '↳', + hookleftarrow: '↩', + hookrightarrow: '↪', + mapstoleft: '↤', + mapstoup: '↥', + map: '↦', + mapsto: '↦', + mapstodown: '↧', + crarr: '↵', + nwarrow: '↖', + nearrow: '↗', + searrow: '↘', + swarrow: '↙', + nleftarrow: '↚', + nleftrightarrow: '↮', + nrightarrow: '↛', + nrarr: '↛', + larrtl: '↢', + rarrtl: '↣', + leftarrowtail: '↢', + rightarrowtail: '↣', + twoheadleftarrow: '↞', + twoheadrightarrow: '↠', + Larr: '↞', + Rarr: '↠', + larrhk: '↩', + rarrhk: '↪', + larrlp: '↫', + looparrowleft: '↫', + rarrlp: '↬', + looparrowright: '↬', + harrw: '↭', + leftrightsquigarrow: '↭', + nrarrw: '↝̸', + rarrw: '↝', + rightsquigarrow: '↝', + larrbfs: '⤟', + rarrbfs: '⤠', + nvHarr: '⤄', + nvlArr: '⤂', + nvrArr: '⤃', + larrfs: '⤝', + rarrfs: '⤞', + Map: '⤅', + larrsim: '⥳', + rarrsim: '⥴', + harrcir: '⥈', + Uarrocir: '⥉', + lurdshar: '⥊', + ldrdhar: '⥧', + ldrushar: '⥋', + rdldhar: '⥩', + lrhard: '⥭', + rlhar: '⇌', + uharr: '↾', + uharl: '↿', + dharr: '⇂', + dharl: '⇃', + Uarr: '↟', + Darr: '↡', + zigrarr: '⇝', + nwArr: '⇖', + neArr: '⇗', + seArr: '⇘', + swArr: '⇙', + nharr: '↮', + nhArr: '⇎', + nlarr: '↚', + nlArr: '⇍', + nrarr: '↛', + nrArr: '⇏', + larrb: '⇤', + LeftArrowBar: '⇤', + rarrb: '⇥', + RightArrowBar: '⇥', +}; + +/** + * Geometric Shapes + * @type {Record} + */ +const SHAPES = { + square: '□', + Square: '□', + squ: '□', + squf: '▪', + squarf: '▪', + blacksquar: '▪', + blacksquare: '▪', + FilledVerySmallSquare: '▪', + blk34: '▓', + blk12: '▒', + blk14: '░', + block: '█', + srect: '▭', + rect: '▭', + sdot: '⋅', + sdotb: '⊡', + dotsquare: '⊡', + triangle: '▵', + tri: '▵', + trine: '▵', + utri: '▵', + triangledown: '▿', + dtri: '▿', + tridown: '▿', + triangleleft: '◃', + ltri: '◃', + triangleright: '▹', + rtri: '▹', + blacktriangle: '▴', + utrif: '▴', + blacktriangledown: '▾', + dtrif: '▾', + blacktriangleleft: '◂', + ltrif: '◂', + blacktriangleright: '▸', + rtrif: '▸', + loz: '◊', + lozenge: '◊', + blacklozenge: '⧫', + lozf: '⧫', + bigcirc: '◯', + xcirc: '◯', + circ: 'ˆ', + Circle: '○', + cir: '○', + o: '○', + bullet: '•', + bull: '•', + hellip: '…', + mldr: '…', + nldr: '‥', + boxh: '─', + HorizontalLine: '─', + boxv: '│', + boxdr: '┌', + boxdl: '┐', + boxur: '└', + boxul: '┘', + boxvr: '├', + boxvl: '┤', + boxhd: '┬', + boxhu: '┴', + boxvh: '┼', + boxH: '═', + boxV: '║', + boxdR: '╒', + boxDr: '╓', + boxDR: '╔', + boxDl: '╕', + boxdL: '╖', + boxDL: '╗', + boxuR: '╘', + boxUr: '╙', + boxUR: '╚', + boxUl: '╜', + boxuL: '╛', + boxUL: '╝', + boxvR: '╞', + boxVr: '╟', + boxVR: '╠', + boxVl: '╢', + boxvL: '╡', + boxVL: '╣', + boxHd: '╤', + boxhD: '╥', + boxHD: '╦', + boxHu: '╧', + boxhU: '╨', + boxHU: '╩', + boxvH: '╪', + boxVh: '╫', + boxVH: '╬', +}; + +/** + * Punctuation & Diacritics + * @type {Record} + */ +const PUNCTUATION = { + excl: '!', + iexcl: '¡', + brvbar: '¦', + sect: '§', + uml: '¨', + copy: '©', + ordf: 'ª', + laquo: '«', + not: '¬', + shy: '\u00ad', + reg: '®', + macr: '¯', + deg: '°', + plusmn: '±', + sup2: '²', + sup3: '³', + acute: '´', + micro: 'µ', + para: '¶', + middot: '·', + cedil: '¸', + sup1: '¹', + ordm: 'º', + raquo: '»', + frac14: '¼', + frac12: '½', + frac34: '¾', + iquest: '¿', + nbsp: '\u00a0', + comma: ',', + period: '.', + colon: ':', + semi: ';', + vert: '|', + Verbar: '‖', + verbar: '|', + dblac: '˝', + circ: 'ˆ', + caron: 'ˇ', + breve: '˘', + dot: '˙', + ring: '˚', + ogon: '˛', + tilde: '˜', + DiacriticalGrave: '`', + DiacriticalAcute: '´', + DiacriticalTilde: '˜', + DiacriticalDot: '˙', + DiacriticalDoubleAcute: '˝', + grave: '`', + acute: '´', +}; + +/** + * Currency Symbols + * @type {Record} + */ +const CURRENCY = { + cent: '¢', + pound: '£', + curren: '¤', + yen: '¥', + euro: '€', + dollar: '$', + euro: '€', + fnof: 'ƒ', + inr: '₹', + af: '؋', + birr: 'ብር', + peso: '₱', + rub: '₽', + won: '₩', + yuan: '¥', + cedil: '¸', +}; + +/** + * Fractions + * @type {Record} + */ +const FRACTIONS = { + frac12: '½', + half: '½', + frac13: '⅓', + frac14: '¼', + frac15: '⅕', + frac16: '⅙', + frac18: '⅛', + frac23: '⅔', + frac25: '⅖', + frac34: '¾', + frac35: '⅗', + frac38: '⅜', + frac45: '⅘', + frac56: '⅚', + frac58: '⅝', + frac78: '⅞', + frasl: '⁄', +}; + +/** + * Miscellaneous Symbols + * @type {Record} + */ +const MISC_SYMBOLS = { + trade: '™', + TRADE: '™', + telrec: '⌕', + target: '⌖', + ulcorn: '⌜', + ulcorner: '⌜', + urcorn: '⌝', + urcorner: '⌝', + dlcorn: '⌞', + llcorner: '⌞', + drcorn: '⌟', + lrcorner: '⌟', + intercal: '⊺', + intcal: '⊺', + oplus: '⊕', + CirclePlus: '⊕', + ominus: '⊖', + CircleMinus: '⊖', + otimes: '⊗', + CircleTimes: '⊗', + osol: '⊘', + odot: '⊙', + CircleDot: '⊙', + oast: '⊛', + circledast: '⊛', + odash: '⊝', + circleddash: '⊝', + ocirc: '⊚', + circledcirc: '⊚', + boxplus: '⊞', + plusb: '⊞', + boxminus: '⊟', + minusb: '⊟', + boxtimes: '⊠', + timesb: '⊠', + boxdot: '⊡', + sdotb: '⊡', + veebar: '⊻', + vee: '∨', + barvee: '⊽', + and: '∧', + wedge: '∧', + Cap: '⋒', + Cup: '⋓', + Fork: '⋔', + pitchfork: '⋔', + epar: '⋕', + ltlarr: '⥶', + nvap: '≍⃒', + nvsim: '∼⃒', + nvge: '≥⃒', + nvle: '≤⃒', + nvlt: '<⃒', + nvgt: '>⃒', + nvltrie: '⊴⃒', + nvrtrie: '⊵⃒', + Vdash: '⊩', + dashv: '⊣', + vDash: '⊨', + Vdash: '⊩', + Vvdash: '⊪', + nvdash: '⊬', + nvDash: '⊭', + nVdash: '⊮', + nVDash: '⊯', +}; + +/** + * All entities combined (if you need everything) + * @type {Record} + */ +const ALL_ENTITIES = { + ...BASIC_LATIN, + ...LATIN_ACCENTS, + ...LATIN_EXTENDED, + ...GREEK, + ...CYRILLIC, + ...MATH, + ...MATH_ADVANCED, + ...ARROWS, + ...SHAPES, + ...PUNCTUATION, + ...CURRENCY, + ...FRACTIONS, + ...MISC_SYMBOLS, +}; + +const XML = { + amp: "&", + apos: "'", + gt: ">", + lt: "<", + quot: "\"" +} +const COMMON_HTML = { + nbsp: '\u00a0', + copy: '\u00a9', + reg: '\u00ae', + trade: '\u2122', + mdash: '\u2014', + ndash: '\u2013', + hellip: '\u2026', + laquo: '\u00ab', + raquo: '\u00bb', + lsquo: '\u2018', + rsquo: '\u2019', + ldquo: '\u201c', + rdquo: '\u201d', + bull: '\u2022', + para: '\u00b6', + sect: '\u00a7', + deg: '\u00b0', + frac12: '\u00bd', + frac14: '\u00bc', + frac34: '\u00be', +} +// --------------------------------------------------------------------------- +// Note: NUMERIC_ENTITIES (&#NNN; / &#xHH;) are handled by the scanner directly +// via String.fromCodePoint() without any map lookup. +// --------------------------------------------------------------------------- +;// CONCATENATED MODULE: ./node_modules/@nodable/entities/src/EntityDecoder.js +// --------------------------------------------------------------------------- +// Built-in named entity map (name → replacement string) +// No regex, no {regex,val} objects — just flat key/value pairs. +// --------------------------------------------------------------------------- + + + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +const SPECIAL_CHARS = new Set('!?\\\\/[]$%{}^&*()<>|+'); + +/** + * Validate that an entity name contains no dangerous characters. + * @param {string} name + * @returns {string} the name, unchanged + * @throws {Error} on invalid characters + */ +function EntityDecoder_validateEntityName(name) { + if (name[0] === '#') { + throw new Error(`[EntityReplacer] Invalid character '#' in entity name: "${name}"`); + } + for (const ch of name) { + if (SPECIAL_CHARS.has(ch)) { + throw new Error(`[EntityReplacer] Invalid character '${ch}' in entity name: "${name}"`); + } + } + return name; +} + +/** + * Merge one or more entity maps into a flat name→string map. + * Accepts either: + * - plain string values: { amp: '&' } + * - legacy {regex,val} / {regx,val}: { lt: { regex: /.../, val: '<' } } + * + * Values containing '&' are skipped (recursive expansion risk). + * + * @param {...object} maps + * @returns {Record} + */ +function mergeEntityMaps(...maps) { + const out = Object.create(null); + for (const map of maps) { + if (!map) continue; + for (const key of Object.keys(map)) { + const raw = map[key]; + if (typeof raw === 'string') { + out[key] = raw; + } else if (raw && typeof raw === 'object' && raw.val !== undefined) { + // Legacy {regex,val} or {regx,val} — extract the string val only + const val = raw.val; + if (typeof val === 'string') { + out[key] = val; + } + // function vals are not supported in the scanner — skip + } + } + } + return out; +} + +// --------------------------------------------------------------------------- +// applyLimitsTo helpers +// --------------------------------------------------------------------------- + +const LIMIT_TIER_EXTERNAL = 'external'; // input/runtime + persistent external maps +const LIMIT_TIER_BASE = 'base'; // DEFAULT_XML_ENTITIES + namedEntities (system) maps +const LIMIT_TIER_ALL = 'all'; // every entity regardless of tier + +/** + * Resolve `applyLimitsTo` option into a normalised Set of tier strings. + * Accepted values: 'external' | 'base' | 'all' | string[] + * Default: 'external' (only untrusted injected entities are counted). + * @param {string|string[]|undefined} raw + * @returns {Set} + */ +function parseLimitTiers(raw) { + if (!raw || raw === LIMIT_TIER_EXTERNAL) return new Set([LIMIT_TIER_EXTERNAL]); + if (raw === LIMIT_TIER_ALL) return new Set([LIMIT_TIER_ALL]); + if (raw === LIMIT_TIER_BASE) return new Set([LIMIT_TIER_BASE]); + if (Array.isArray(raw)) return new Set(raw); + return new Set([LIMIT_TIER_EXTERNAL]); // safe default for unrecognised values +} + +// --------------------------------------------------------------------------- +// NCR (Numeric Character Reference) classification +// --------------------------------------------------------------------------- + +// Severity order — higher number = stricter action. +// Used to enforce minimum action levels for specific codepoint ranges. +const NCR_LEVEL = Object.freeze({ allow: 0, leave: 1, remove: 2, throw: 3 }); + +// XML 1.0 §2.2: allowed chars are #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] +// Restricted C0: U+0001–U+001F excluding U+0009, U+000A, U+000D +const XML10_ALLOWED_C0 = new Set([0x09, 0x0A, 0x0D]); + +/** + * Parse the `ncr` constructor option into flat, hot-path-friendly fields. + * @param {object|undefined} ncr + * @returns {{ xmlVersion: number, onLevel: number, nullLevel: number }} + */ +function parseNCRConfig(ncr) { + if (!ncr) { + return { xmlVersion: 1.0, onLevel: NCR_LEVEL.allow, nullLevel: NCR_LEVEL.remove }; + } + const xmlVersion = ncr.xmlVersion === 1.1 ? 1.1 : 1.0; + const onLevel = NCR_LEVEL[ncr.onNCR] ?? NCR_LEVEL.allow; + const nullLevel = NCR_LEVEL[ncr.nullNCR] ?? NCR_LEVEL.remove; + // 'allow' is not meaningful for null — clamp to at least 'remove' + const clampedNull = Math.max(nullLevel, NCR_LEVEL.remove); + return { xmlVersion, onLevel, nullLevel: clampedNull }; +} + +// --------------------------------------------------------------------------- +// EntityReplacer +// --------------------------------------------------------------------------- + +/** + * Single-pass, zero-regex entity replacer for XML/HTML content. + * + * Algorithm: scan the string once for '&', read to ';', resolve via map + * or direct codepoint conversion, build output chunks, join once at the end. + * + * Entity lookup priority (highest → lowest): + * 1. input / runtime (DOCTYPE entities for current document) + * 2. persistent external (survive across documents) + * 3. base named map (DEFAULT_XML_ENTITIES + user-supplied namedEntities) + * + * Both input and external resolve as the 'external' tier for limit purposes. + * Base map entities resolve as the 'base' tier. + * + * Numeric / hex references (&#NNN; / &#xHH;) are resolved directly via + * String.fromCodePoint() — no map needed. They count as 'base' tier. + * + * @example + * const replacer = new EntityReplacer({ namedEntities: COMMON_HTML }); + * replacer.setExternalEntities({ brand: 'Acme' }); + * + * const instance = replacer.reset(); + * instance.addInputEntities({ version: '1.0' }); + * instance.encode('&brand; v&version; <'); // 'Acme v1.0 <' + */ +class EntityDecoder { + /** + * @param {object} [options] + * @param {object|null} [options.namedEntities] — extra named entities merged into base map + * @param {object} [options.limit] — security limits + * @param {number} [options.limit.maxTotalExpansions=0] — 0 = unlimited + * @param {number} [options.limit.maxExpandedLength=0] — 0 = unlimited + * @param {'external'|'base'|'all'|string[]} [options.limit.applyLimitsTo='external'] + * Which entity tiers count against the security limits: + * - 'external' (default) — only input/runtime + persistent external entities + * - 'base' — only DEFAULT_XML_ENTITIES + namedEntities + * - 'all' — every entity regardless of tier + * - string[] — explicit combination, e.g. ['external', 'base'] + * @param {((resolved: string, original: string) => string)|null} [options.postCheck=null] + * @param {string[]} [options.remove=[]] — entity names (e.g. ['nbsp', '#13']) to delete (replace with empty string) + * @param {string[]} [options.leave=[]] — entity names to keep as literal (unchanged in output) + * @param {object} [options.ncr] — Numeric Character Reference controls + * @param {1.0|1.1} [options.ncr.xmlVersion=1.0] + * XML version governing which codepoint ranges are restricted: + * - 1.0 — C0 controls U+0001–U+001F (except U+0009/000A/000D) are prohibited + * - 1.1 — C0 controls are allowed when written as NCRs; C1 (U+007F–U+009F) decoded as-is + * @param {'allow'|'leave'|'remove'|'throw'} [options.ncr.onNCR='allow'] + * Base action for numeric references. Severity order: allow < leave < remove < throw. + * For codepoint ranges that carry a minimum level (surrogates → remove, XML 1.0 C0 → remove), + * the effective action is max(onNCR, rangeMinimum). + * @param {'remove'|'throw'} [options.ncr.nullNCR='remove'] + * Action for U+0000 (null). 'allow' and 'leave' are clamped to 'remove' since null is never safe. + */ + constructor(options = {}) { + this._limit = options.limit || {}; + this._maxTotalExpansions = this._limit.maxTotalExpansions || 0; + this._maxExpandedLength = this._limit.maxExpandedLength || 0; + this._postCheck = typeof options.postCheck === 'function' ? options.postCheck : r => r; + this._limitTiers = parseLimitTiers(this._limit.applyLimitsTo ?? LIMIT_TIER_EXTERNAL); + this._numericAllowed = options.numericAllowed ?? true; + // Base map: DEFAULT_XML_ENTITIES + user-supplied extras. Immutable after construction. + this._baseMap = mergeEntityMaps(XML, options.namedEntities || null); + + // Persistent external entities — survive across documents. + // Stored as a separate map so reset() never touches them. + /** @type {Record} */ + this._externalMap = Object.create(null); + + // Input / runtime entities — current document only, wiped on reset(). + /** @type {Record} */ + this._inputMap = Object.create(null); + + // Per-document counters + this._totalExpansions = 0; + this._expandedLength = 0; + + // --- New: remove / leave sets --- + /** @type {Set} */ + this._removeSet = new Set(options.remove && Array.isArray(options.remove) ? options.remove : []); + /** @type {Set} */ + this._leaveSet = new Set(options.leave && Array.isArray(options.leave) ? options.leave : []); + + // --- NCR config (parsed into flat fields for hot-path speed) --- + const ncrCfg = parseNCRConfig(options.ncr); + this._ncrXmlVersion = ncrCfg.xmlVersion; + this._ncrOnLevel = ncrCfg.onLevel; + this._ncrNullLevel = ncrCfg.nullLevel; + } + + // ------------------------------------------------------------------------- + // Persistent external entity registration + // ------------------------------------------------------------------------- + + /** + * Replace the full set of persistent external entities. + * All keys are validated — throws on invalid characters. + * @param {Record} map + */ + setExternalEntities(map) { + if (map) { + for (const key of Object.keys(map)) { + EntityDecoder_validateEntityName(key); + } + } + this._externalMap = mergeEntityMaps(map); + } + + /** + * Add a single persistent external entity. + * @param {string} key + * @param {string} value + */ + addExternalEntity(key, value) { + EntityDecoder_validateEntityName(key); + if (typeof value === 'string' && value.indexOf('&') === -1) { + this._externalMap[key] = value; + } + } + + // ------------------------------------------------------------------------- + // Input / runtime entity registration (per document) + // ------------------------------------------------------------------------- + + /** + * Inject DOCTYPE entities for the current document. + * Also resets per-document expansion counters. + * @param {Record} map + */ + addInputEntities(map) { + this._totalExpansions = 0; + this._expandedLength = 0; + this._inputMap = mergeEntityMaps(map); + } + + // ------------------------------------------------------------------------- + // Per-document reset + // ------------------------------------------------------------------------- + + /** + * Wipe input/runtime entities and reset counters. + * Call this before processing each new document. + * @returns {this} + */ + reset() { + this._inputMap = Object.create(null); + this._totalExpansions = 0; + this._expandedLength = 0; + return this; + } + + // ------------------------------------------------------------------------- + // XML version (can be set after construction, e.g. once parser reads ) + // ------------------------------------------------------------------------- + + /** + * Update the XML version used for NCR classification. + * Call this as soon as the document's `` declaration is parsed. + * @param {1.0|1.1|number} version + */ + setXmlVersion(version) { + this._ncrXmlVersion = version === 1.1 ? 1.1 : 1.0; + } + + // ------------------------------------------------------------------------- + // Primary API + // ------------------------------------------------------------------------- + + /** + * Replace all entity references in `str` in a single pass. + * + * @param {string} str + * @returns {string} + */ + decode(str) { + if (typeof str !== 'string' || str.length === 0) return str; + //TODO: check if needed + //if (str.indexOf('&') === -1) return str; // fast path — no entities at all + + const original = str; + const chunks = []; + const len = str.length; + let last = 0; // start of next unprocessed literal chunk + let i = 0; + + const limitExpansions = this._maxTotalExpansions > 0; + const limitLength = this._maxExpandedLength > 0; + const checkLimits = limitExpansions || limitLength; + + while (i < len) { + // Scan forward to next '&' + if (str.charCodeAt(i) !== 38 /* '&' */) { i++; continue; } + + // --- Found '&' at position i --- + + // Scan forward to ';' + let j = i + 1; + while (j < len && str.charCodeAt(j) !== 59 /* ';' */ && (j - i) <= 32) j++; + + if (j >= len || str.charCodeAt(j) !== 59) { + // No closing ';' within window — treat '&' as literal + i++; + continue; + } + + // Raw token between '&' and ';' (exclusive) + const token = str.slice(i + 1, j); + if (token.length === 0) { i++; continue; } + + let replacement; + let tier; // which limit tier this entity belongs to + + if (this._removeSet.has(token)) { + // Remove entity: replace with empty string + replacement = ''; + // If entity was unknown (replacement undefined), we still need a tier for limits. + // Treat as external tier because it's user-directed removal of an unknown reference. + if (tier === undefined) { + tier = LIMIT_TIER_EXTERNAL; + } + } else if (this._leaveSet.has(token)) { + // Do not replace — keep original &token; as literal + i++; + continue; + } else if (token.charCodeAt(0) === 35 /* '#' */) { + // ---- Numeric / NCR reference ---- + // NCR classification always runs first — prohibited codepoints must be + // caught regardless of numericAllowed. + const ncrResult = this._resolveNCR(token); + if (ncrResult === undefined) { + // 'leave' action — keep original &token; as-is + i++; + continue; + } + replacement = ncrResult; // '' for remove, char string for allow + tier = LIMIT_TIER_BASE; + } else { + // ---- Named reference ---- + const resolved = this._resolveName(token); + replacement = resolved?.value; + tier = resolved?.tier; + } + + if (replacement === undefined) { + // Unknown entity — leave as-is, advance past '&' only + i++; + continue; + } + + // Flush literal chunk before this entity + if (i > last) chunks.push(str.slice(last, i)); + chunks.push(replacement); + last = j + 1; // skip past ';' + i = last; + + // Apply expansion limits only if this tier is being tracked + if (checkLimits && this._tierCounts(tier)) { + if (limitExpansions) { + this._totalExpansions++; + if (this._totalExpansions > this._maxTotalExpansions) { + throw new Error( + `[EntityReplacer] Entity expansion count limit exceeded: ` + + `${this._totalExpansions} > ${this._maxTotalExpansions}` + ); + } + } + if (limitLength) { + // delta: replacement.length minus the raw &token; length (token.length + 2 for '&' and ';') + const delta = replacement.length - (token.length + 2); + if (delta > 0) { + this._expandedLength += delta; + if (this._expandedLength > this._maxExpandedLength) { + throw new Error( + `[EntityReplacer] Expanded content length limit exceeded: ` + + `${this._expandedLength} > ${this._maxExpandedLength}` + ); + } + } + } + } + } + + // Flush trailing literal + if (last < len) chunks.push(str.slice(last)); + + // If nothing was replaced, chunks is empty — return original + const result = chunks.length === 0 ? str : chunks.join(''); + + return this._postCheck(result, original); + } + + // ------------------------------------------------------------------------- + // Private: limit tier check + // ------------------------------------------------------------------------- + + /** + * Returns true if a resolved entity of the given tier should count + * against the expansion/length limits. + * @param {string} tier — LIMIT_TIER_EXTERNAL | LIMIT_TIER_BASE + * @returns {boolean} + */ + _tierCounts(tier) { + if (this._limitTiers.has(LIMIT_TIER_ALL)) return true; + return this._limitTiers.has(tier); + } + + // ------------------------------------------------------------------------- + // Private: entity resolution + // ------------------------------------------------------------------------- + + /** + * Resolve a named entity token (without & and ;). + * Priority: inputMap > externalMap > baseMap + * Returns the resolved value tagged with its limit tier. + * + * @param {string} name + * @returns {{ value: string, tier: string }|undefined} + */ + _resolveName(name) { + // input and external both count as 'external' tier for limit purposes — + // they are injected at runtime and are the untrusted surface. + if (name in this._inputMap) return { value: this._inputMap[name], tier: LIMIT_TIER_EXTERNAL }; + if (name in this._externalMap) return { value: this._externalMap[name], tier: LIMIT_TIER_EXTERNAL }; + if (name in this._baseMap) return { value: this._baseMap[name], tier: LIMIT_TIER_BASE }; + return undefined; + } + + /** + * Classify a codepoint and return the minimum action level that must be applied. + * Returns -1 when no minimum is imposed (normal allow path). + * + * Ranges checked (in priority order): + * 1. U+0000 — null, governed by nullNCR (always ≥ remove) + * 2. U+D800–U+DFFF — surrogates, always prohibited (min: remove) + * 3. U+0001–U+001F \ {0x09,0x0A,0x0D} — XML 1.0 restricted C0 (min: remove) + * (skipped in XML 1.1 — C0 controls are allowed when written as NCRs) + * + * @param {number} cp — codepoint + * @returns {number} — minimum NCR_LEVEL value, or -1 for no restriction + */ + _classifyNCR(cp) { + // 1. Null + if (cp === 0) return this._ncrNullLevel; + + // 2. Surrogates — always prohibited, minimum 'remove' + if (cp >= 0xD800 && cp <= 0xDFFF) return NCR_LEVEL.remove; + + // 3. XML 1.0 restricted C0 controls + if (this._ncrXmlVersion === 1.0) { + if (cp >= 0x01 && cp <= 0x1F && !XML10_ALLOWED_C0.has(cp)) return NCR_LEVEL.remove; + } + + return -1; // no restriction + } + + /** + * Execute a resolved NCR action. + * + * @param {number} action — NCR_LEVEL value + * @param {string} token — raw token (e.g. '#38') for error messages + * @param {number} cp — codepoint, used only for error messages + * @returns {string|undefined} + * - decoded character string → 'allow' + * - '' → 'remove' + * - undefined → 'leave' (caller must skip past '&' only) + * - throws Error → 'throw' + */ + _applyNCRAction(action, token, cp) { + switch (action) { + case NCR_LEVEL.allow: return String.fromCodePoint(cp); + case NCR_LEVEL.remove: return ''; + case NCR_LEVEL.leave: return undefined; // signal: keep literal + case NCR_LEVEL.throw: + throw new Error( + `[EntityDecoder] Prohibited numeric character reference ` + + `&${token}; (U+${cp.toString(16).toUpperCase().padStart(4, '0')})` + ); + default: return String.fromCodePoint(cp); + } + } + + /** + * Full NCR resolution pipeline for a numeric token. + * + * Steps: + * 1. Parse the codepoint (decimal or hex). + * 2. Validate the raw codepoint range (NaN, <0, >0x10FFFF). + * 3. If numericAllowed is false and no minimum restriction applies → leave as-is. + * 4. Classify the codepoint to find the minimum required action level. + * 5. Resolve effective action = max(onNCR, minimum). + * 6. Apply and return. + * + * @param {string} token — e.g. '#38', '#x26', '#X26' + * @returns {string|undefined} + * - string (incl. '') — replacement ('' = remove) + * - undefined — leave original &token; as-is + */ + _resolveNCR(token) { + // Step 1: parse codepoint + const second = token.charCodeAt(1); + let cp; + if (second === 120 /* x */ || second === 88 /* X */) { + cp = parseInt(token.slice(2), 16); + } else { + cp = parseInt(token.slice(1), 10); + } + + // Step 2: out-of-range → leave as-is unconditionally + if (Number.isNaN(cp) || cp < 0 || cp > 0x10FFFF) return undefined; + + // Step 3: classify to get minimum action level + const minimum = this._classifyNCR(cp); + + // Step 4: if numericAllowed is false and no hard minimum → leave + if (!this._numericAllowed && minimum < NCR_LEVEL.remove) return undefined; + + // Step 5: effective action = max(configured onNCR, range minimum) + const effective = minimum === -1 + ? this._ncrOnLevel + : Math.max(this._ncrOnLevel, minimum); + + // Step 6: apply + return this._applyNCRAction(effective, token, cp); + } +} ;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/xmlparser/OrderedObjParser.js ///@ts-check @@ -54326,6 +56889,8 @@ function ignoreAttributes_getIgnoreAttributesFn(ignoreAttributes) { + + // const regx = // '<((!\\[CDATA\\[([\\s\\S]*?)(]]>))|((NAME:)?(NAME))([^>]*)>|((\\/)(NAME)\\s*>))([^<]*)' // .replace(/NAME/g, util.nameRegexp); @@ -54385,36 +56950,10 @@ function extractNamespace(rawTagName) { } class OrderedObjParser { - constructor(options) { + constructor(options, externalEntities) { this.options = options; this.currentNode = null; this.tagsNodeStack = []; - this.docTypeEntities = {}; - this.lastEntities = { - "apos": { regex: /&(apos|#39|#x27);/g, val: "'" }, - "gt": { regex: /&(gt|#62|#x3E);/g, val: ">" }, - "lt": { regex: /&(lt|#60|#x3C);/g, val: "<" }, - "quot": { regex: /&(quot|#34|#x22);/g, val: "\"" }, - }; - this.ampEntity = { regex: /&(amp|#38|#x26);/g, val: "&" }; - this.htmlEntities = { - "space": { regex: /&(nbsp|#160);/g, val: " " }, - // "lt" : { regex: /&(lt|#60);/g, val: "<" }, - // "gt" : { regex: /&(gt|#62);/g, val: ">" }, - // "amp" : { regex: /&(amp|#38);/g, val: "&" }, - // "quot" : { regex: /&(quot|#34);/g, val: "\"" }, - // "apos" : { regex: /&(apos|#39);/g, val: "'" }, - "cent": { regex: /&(cent|#162);/g, val: "¢" }, - "pound": { regex: /&(pound|#163);/g, val: "£" }, - "yen": { regex: /&(yen|#165);/g, val: "¥" }, - "euro": { regex: /&(euro|#8364);/g, val: "€" }, - "copyright": { regex: /&(copy|#169);/g, val: "©" }, - "reg": { regex: /&(reg|#174);/g, val: "®" }, - "inr": { regex: /&(inr|#8377);/g, val: "₹" }, - "num_dec": { regex: /&#([0-9]{1,7});/g, val: (_, str) => fromCodePoint(str, 10, "&#") }, - "num_hex": { regex: /&#x([0-9a-fA-F]{1,6});/g, val: (_, str) => fromCodePoint(str, 16, "&#x") }, - }; - this.addExternalEntities = addExternalEntities; this.parseXml = parseXml; this.parseTextData = parseTextData; this.resolveNameSpace = resolveNameSpace; @@ -54427,42 +56966,51 @@ class OrderedObjParser { this.ignoreAttributesFn = ignoreAttributes_getIgnoreAttributesFn(this.options.ignoreAttributes) this.entityExpansionCount = 0; this.currentExpandedLength = 0; + let namedEntities = { ...XML }; + if (this.options.entityDecoder) { + this.entityDecoder = this.options.entityDecoder + } else { + if (typeof this.options.htmlEntities === "object") namedEntities = this.options.htmlEntities; + else if (this.options.htmlEntities === true) namedEntities = { ...COMMON_HTML, ...CURRENCY }; + this.entityDecoder = new EntityDecoder({ + namedEntities: { ...namedEntities, ...externalEntities }, + numericAllowed: this.options.htmlEntities, + limit: { + maxTotalExpansions: this.options.processEntities.maxTotalExpansions, + maxExpandedLength: this.options.processEntities.maxExpandedLength, + applyLimitsTo: this.options.processEntities.appliesTo, + } + //postCheck: resolved => resolved + }); + } // Initialize path matcher for path-expression-matcher this.matcher = new Matcher(); + this.readonlyMatcher = this.matcher.readOnly(); // Flag to track if current node is a stop node (optimization) this.isCurrentNodeStopNode = false; // Pre-compile stopNodes expressions - if (this.options.stopNodes && this.options.stopNodes.length > 0) { - this.stopNodeExpressions = []; - for (let i = 0; i < this.options.stopNodes.length; i++) { - const stopNodeExp = this.options.stopNodes[i]; + this.stopNodeExpressionsSet = new ExpressionSet(); + const stopNodesOpts = this.options.stopNodes; + if (stopNodesOpts && stopNodesOpts.length > 0) { + for (let i = 0; i < stopNodesOpts.length; i++) { + const stopNodeExp = stopNodesOpts[i]; if (typeof stopNodeExp === 'string') { // Convert string to Expression object - this.stopNodeExpressions.push(new Expression(stopNodeExp)); + this.stopNodeExpressionsSet.add(new Expression(stopNodeExp)); } else if (stopNodeExp instanceof Expression) { // Already an Expression object - this.stopNodeExpressions.push(stopNodeExp); + this.stopNodeExpressionsSet.add(stopNodeExp); } } + this.stopNodeExpressionsSet.seal(); } } } -function addExternalEntities(externalEntities) { - const entKeys = Object.keys(externalEntities); - for (let i = 0; i < entKeys.length; i++) { - const ent = entKeys[i]; - const escaped = ent.replace(/[.\-+*:]/g, '\\.'); - this.lastEntities[ent] = { - regex: new RegExp("&" + escaped + ";", "g"), - val: externalEntities[ent] - } - } -} /** * @param {string} val @@ -54474,28 +57022,29 @@ function addExternalEntities(externalEntities) { * @param {boolean} escapeEntities */ function parseTextData(val, tagName, jPath, dontTrim, hasAttributes, isLeafNode, escapeEntities) { + const options = this.options; if (val !== undefined) { - if (this.options.trimValues && !dontTrim) { + if (options.trimValues && !dontTrim) { val = val.trim(); } if (val.length > 0) { if (!escapeEntities) val = this.replaceEntitiesValue(val, tagName, jPath); // Pass jPath string or matcher based on options.jPath setting - const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath; - const newval = this.options.tagValueProcessor(tagName, val, jPathOrMatcher, hasAttributes, isLeafNode); + const jPathOrMatcher = options.jPath ? jPath.toString() : jPath; + const newval = options.tagValueProcessor(tagName, val, jPathOrMatcher, hasAttributes, isLeafNode); if (newval === null || newval === undefined) { //don't parse return val; } else if (typeof newval !== typeof val || newval !== val) { //overwrite return newval; - } else if (this.options.trimValues) { - return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions); + } else if (options.trimValues) { + return parseValue(val, options.parseTagValue, options.numberParseOptions); } else { const trimmedVal = val.trim(); if (trimmedVal === val) { - return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions); + return parseValue(val, options.parseTagValue, options.numberParseOptions); } else { return val; } @@ -54522,8 +57071,9 @@ function resolveNameSpace(tagname) { //const attrsRegx = new RegExp("([\\w\\-\\.\\:]+)\\s*=\\s*(['\"])((.|\n)*?)\\2","gm"); const attrsRegx = new RegExp('([^\\s=]+)\\s*(=\\s*([\'"])([\\s\\S]*?)\\3)?', 'gm'); -function buildAttributesMap(attrStr, jPath, tagName) { - if (this.options.ignoreAttributes !== true && typeof attrStr === 'string') { +function buildAttributesMap(attrStr, jPath, tagName, force = false) { + const options = this.options; + if (force === true || (options.ignoreAttributes !== true && typeof attrStr === 'string')) { // attrStr = attrStr.replace(/\r?\n/g, ' '); //attrStr = attrStr || attrStr.trim(); @@ -54531,89 +57081,80 @@ function buildAttributesMap(attrStr, jPath, tagName) { const len = matches.length; //don't make it inline const attrs = {}; - // First pass: parse all attributes and update matcher with raw values - // This ensures the matcher has all attribute values when processors run + // Pre-process values once: trim + entity replacement + // Reused in both matcher update and second pass + const processedVals = new Array(len); + let hasRawAttrs = false; const rawAttrsForMatcher = {}; + for (let i = 0; i < len; i++) { const attrName = this.resolveNameSpace(matches[i][1]); const oldVal = matches[i][4]; if (attrName.length && oldVal !== undefined) { - let parsedVal = oldVal; - if (this.options.trimValues) { - parsedVal = parsedVal.trim(); - } - parsedVal = this.replaceEntitiesValue(parsedVal, tagName, jPath); - rawAttrsForMatcher[attrName] = parsedVal; + let val = oldVal; + if (options.trimValues) val = val.trim(); + val = this.replaceEntitiesValue(val, tagName, this.readonlyMatcher); + processedVals[i] = val; + + rawAttrsForMatcher[attrName] = val; + hasRawAttrs = true; } } - // Update matcher with raw attribute values BEFORE running processors - if (Object.keys(rawAttrsForMatcher).length > 0 && typeof jPath === 'object' && jPath.updateCurrent) { + // Update matcher ONCE before second pass, if applicable + if (hasRawAttrs && typeof jPath === 'object' && jPath.updateCurrent) { jPath.updateCurrent(rawAttrsForMatcher); } - // Second pass: now process attributes with matcher having full attribute context + // Hoist toString() once — path doesn't change during attribute processing + const jPathStr = options.jPath ? jPath.toString() : this.readonlyMatcher; + + // Second pass: apply processors, build final attrs + let hasAttrs = false; for (let i = 0; i < len; i++) { const attrName = this.resolveNameSpace(matches[i][1]); - // Convert jPath to string if needed for ignoreAttributesFn - const jPathStr = this.options.jPath ? jPath.toString() : jPath; - if (this.ignoreAttributesFn(attrName, jPathStr)) { - continue - } + if (this.ignoreAttributesFn(attrName, jPathStr)) continue; - let oldVal = matches[i][4]; - let aName = this.options.attributeNamePrefix + attrName; + let aName = options.attributeNamePrefix + attrName; if (attrName.length) { - if (this.options.transformAttributeName) { - aName = this.options.transformAttributeName(aName); + if (options.transformAttributeName) { + aName = options.transformAttributeName(aName); } - //if (aName === "__proto__") aName = "#__proto__"; - aName = sanitizeName(aName, this.options); + aName = sanitizeName(aName, options); - if (oldVal !== undefined) { - if (this.options.trimValues) { - oldVal = oldVal.trim(); - } - oldVal = this.replaceEntitiesValue(oldVal, tagName, jPath); + if (matches[i][4] !== undefined) { + // Reuse already-processed value — no double entity replacement + const oldVal = processedVals[i]; - // Pass jPath string or matcher based on options.jPath setting - const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath; - const newVal = this.options.attributeValueProcessor(attrName, oldVal, jPathOrMatcher); + const newVal = options.attributeValueProcessor(attrName, oldVal, jPathStr); if (newVal === null || newVal === undefined) { - //don't parse attrs[aName] = oldVal; } else if (typeof newVal !== typeof oldVal || newVal !== oldVal) { - //overwrite attrs[aName] = newVal; } else { - //parse - attrs[aName] = parseValue( - oldVal, - this.options.parseAttributeValue, - this.options.numberParseOptions - ); + attrs[aName] = parseValue(oldVal, options.parseAttributeValue, options.numberParseOptions); } - } else if (this.options.allowBooleanAttributes) { + hasAttrs = true; + } else if (options.allowBooleanAttributes) { attrs[aName] = true; + hasAttrs = true; } } } - if (!Object.keys(attrs).length) { - return; - } - if (this.options.attributesGroupName) { + if (!hasAttrs) return; + + if (options.attributesGroupName && !options.preserveOrder) { const attrCollection = {}; - attrCollection[this.options.attributesGroupName] = attrs; + attrCollection[options.attributesGroupName] = attrs; return attrCollection; } - return attrs + return attrs; } } - const parseXml = function (xmlData) { xmlData = xmlData.replace(/\r\n?/g, "\n"); //TODO: remove this line const xmlObj = new XmlNode('!xml'); @@ -54622,40 +57163,43 @@ const parseXml = function (xmlData) { // Reset matcher for new document this.matcher.reset(); + this.entityDecoder.reset(); // Reset entity expansion counters for this document this.entityExpansionCount = 0; this.currentExpandedLength = 0; - - const docTypeReader = new DocTypeReader(this.options.processEntities); - for (let i = 0; i < xmlData.length; i++) {//for each char in XML data + const options = this.options; + const docTypeReader = new DocTypeReader(options.processEntities); + const xmlLen = xmlData.length; + for (let i = 0; i < xmlLen; i++) {//for each char in XML data const ch = xmlData[i]; if (ch === '<') { // const nextIndex = i+1; // const _2ndChar = xmlData[nextIndex]; - if (xmlData[i + 1] === '/') {//Closing Tag + const c1 = xmlData.charCodeAt(i + 1); + if (c1 === 47) {//Closing Tag '/' const closeIndex = findClosingIndex(xmlData, ">", i, "Closing Tag is not closed.") let tagName = xmlData.substring(i + 2, closeIndex).trim(); - if (this.options.removeNSPrefix) { + if (options.removeNSPrefix) { const colonIndex = tagName.indexOf(":"); if (colonIndex !== -1) { tagName = tagName.substr(colonIndex + 1); } } - tagName = transformTagName(this.options.transformTagName, tagName, "", this.options).tagName; + tagName = transformTagName(options.transformTagName, tagName, "", options).tagName; if (currentNode) { - textData = this.saveTextToParentTag(textData, currentNode, this.matcher); + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher); } //check if last tag of nested tag was unpaired tag const lastTagName = this.matcher.getCurrentTag(); - if (tagName && this.options.unpairedTags.indexOf(tagName) !== -1) { + if (tagName && options.unpairedTagsSet.has(tagName)) { throw new Error(`Unpaired tag can not be used as closing tag: `); } - if (lastTagName && this.options.unpairedTags.indexOf(lastTagName) !== -1) { + if (lastTagName && options.unpairedTagsSet.has(lastTagName)) { // Pop the unpaired tag this.matcher.pop(); this.tagsNodeStack.pop(); @@ -54667,65 +57211,75 @@ const parseXml = function (xmlData) { currentNode = this.tagsNodeStack.pop();//avoid recursion, set the parent tag scope textData = ""; i = closeIndex; - } else if (xmlData[i + 1] === '?') { + } else if (c1 === 63) { //'?' let tagData = readTagExp(xmlData, i, false, "?>"); if (!tagData) throw new Error("Pi Tag is not closed."); - textData = this.saveTextToParentTag(textData, currentNode, this.matcher); - if ((this.options.ignoreDeclaration && tagData.tagName === "?xml") || this.options.ignorePiTags) { + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher); + const attsMap = this.buildAttributesMap(tagData.tagExp, this.matcher, tagData.tagName, true); + if (attsMap) { + const ver = attsMap[this.options.attributeNamePrefix + "version"]; + this.entityDecoder.setXmlVersion(Number(ver) || 1.0); + docTypeReader.setXmlVersion(Number(ver) || 1.0); + } + if ((options.ignoreDeclaration && tagData.tagName === "?xml") || options.ignorePiTags) { //do nothing } else { const childNode = new XmlNode(tagData.tagName); - childNode.add(this.options.textNodeName, ""); + childNode.add(options.textNodeName, ""); - if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent) { - childNode[":@"] = this.buildAttributesMap(tagData.tagExp, this.matcher, tagData.tagName); + if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent && options.ignoreAttributes !== true) { + childNode[":@"] = attsMap } - this.addChild(currentNode, childNode, this.matcher, i); + this.addChild(currentNode, childNode, this.readonlyMatcher, i); } i = tagData.closeIndex + 1; - } else if (xmlData.substr(i + 1, 3) === '!--') { + } else if (c1 === 33 + && xmlData.charCodeAt(i + 2) === 45 + && xmlData.charCodeAt(i + 3) === 45) { //'!--' const endIndex = findClosingIndex(xmlData, "-->", i + 4, "Comment is not closed.") - if (this.options.commentPropName) { + if (options.commentPropName) { const comment = xmlData.substring(i + 4, endIndex - 2); - textData = this.saveTextToParentTag(textData, currentNode, this.matcher); + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher); - currentNode.add(this.options.commentPropName, [{ [this.options.textNodeName]: comment }]); + currentNode.add(options.commentPropName, [{ [options.textNodeName]: comment }]); } i = endIndex; - } else if (xmlData.substr(i + 1, 2) === '!D') { + } else if (c1 === 33 + && xmlData.charCodeAt(i + 2) === 68) { //'!D' const result = docTypeReader.readDocType(xmlData, i); - this.docTypeEntities = result.entities; + this.entityDecoder.addInputEntities(result.entities); i = result.i; - } else if (xmlData.substr(i + 1, 2) === '![') { + } else if (c1 === 33 + && xmlData.charCodeAt(i + 2) === 91) { // '![' const closeIndex = findClosingIndex(xmlData, "]]>", i, "CDATA is not closed.") - 2; const tagExp = xmlData.substring(i + 9, closeIndex); - textData = this.saveTextToParentTag(textData, currentNode, this.matcher); + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher); - let val = this.parseTextData(tagExp, currentNode.tagname, this.matcher, true, false, true, true); + let val = this.parseTextData(tagExp, currentNode.tagname, this.readonlyMatcher, true, false, true, true); if (val == undefined) val = ""; //cdata should be set even if it is 0 length string - if (this.options.cdataPropName) { - currentNode.add(this.options.cdataPropName, [{ [this.options.textNodeName]: tagExp }]); + if (options.cdataPropName) { + currentNode.add(options.cdataPropName, [{ [options.textNodeName]: tagExp }]); } else { - currentNode.add(this.options.textNodeName, val); + currentNode.add(options.textNodeName, val); } i = closeIndex + 2; } else {//Opening tag - let result = readTagExp(xmlData, i, this.options.removeNSPrefix); + let result = readTagExp(xmlData, i, options.removeNSPrefix); // Safety check: readTagExp can return undefined if (!result) { // Log context for debugging - const context = xmlData.substring(Math.max(0, i - 50), Math.min(xmlData.length, i + 50)); + const context = xmlData.substring(Math.max(0, i - 50), Math.min(xmlLen, i + 50)); throw new Error(`readTagExp returned undefined at position ${i}. Context: "${context}"`); } @@ -54735,11 +57289,13 @@ const parseXml = function (xmlData) { let attrExpPresent = result.attrExpPresent; let closeIndex = result.closeIndex; - ({ tagName, tagExp } = transformTagName(this.options.transformTagName, tagName, tagExp, this.options)); + ({ tagName, tagExp } = transformTagName(options.transformTagName, tagName, tagExp, options)); - if (this.options.strictReservedNames && - (tagName === this.options.commentPropName - || tagName === this.options.cdataPropName + if (options.strictReservedNames && + (tagName === options.commentPropName + || tagName === options.cdataPropName + || tagName === options.textNodeName + || tagName === options.attributesGroupName )) { throw new Error(`Invalid tag name: ${tagName}`); } @@ -54748,13 +57304,13 @@ const parseXml = function (xmlData) { if (currentNode && textData) { if (currentNode.tagname !== '!xml') { //when nested tag is found - textData = this.saveTextToParentTag(textData, currentNode, this.matcher, false); + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher, false); } } //check if last tag was unpaired tag const lastTag = currentNode; - if (lastTag && this.options.unpairedTags.indexOf(lastTag.tagname) !== -1) { + if (lastTag && options.unpairedTagsSet.has(lastTag.tagname)) { currentNode = this.tagsNodeStack.pop(); this.matcher.pop(); } @@ -54796,13 +57352,14 @@ const parseXml = function (xmlData) { if (prefixedAttrs) { // Extract raw attributes (without prefix) for our use - rawAttrs = extractRawAttributes(prefixedAttrs, this.options); + //TODO: seems a performance overhead + rawAttrs = extractRawAttributes(prefixedAttrs, options); } } // Now check if this is a stop node (after attributes are set) if (tagName !== xmlObj.tagname) { - this.isCurrentNodeStopNode = this.isItStopNode(this.stopNodeExpressions, this.matcher); + this.isCurrentNodeStopNode = this.isItStopNode(); } const startIndex = i; @@ -54814,7 +57371,7 @@ const parseXml = function (xmlData) { i = result.closeIndex; } //unpaired tag - else if (this.options.unpairedTags.indexOf(tagName) !== -1) { + else if (options.unpairedTagsSet.has(tagName)) { i = result.closeIndex; } //normal tag @@ -54833,31 +57390,31 @@ const parseXml = function (xmlData) { } // For stop nodes, store raw content as-is without any processing - childNode.add(this.options.textNodeName, tagContent); + childNode.add(options.textNodeName, tagContent); this.matcher.pop(); // Pop the stop node tag this.isCurrentNodeStopNode = false; // Reset flag - this.addChild(currentNode, childNode, this.matcher, startIndex); + this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); } else { //selfClosing tag if (isSelfClosing) { - ({ tagName, tagExp } = transformTagName(this.options.transformTagName, tagName, tagExp, this.options)); + ({ tagName, tagExp } = transformTagName(options.transformTagName, tagName, tagExp, options)); const childNode = new XmlNode(tagName); if (prefixedAttrs) { childNode[":@"] = prefixedAttrs; } - this.addChild(currentNode, childNode, this.matcher, startIndex); + this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); this.matcher.pop(); // Pop self-closing tag this.isCurrentNodeStopNode = false; // Reset flag } - else if (this.options.unpairedTags.indexOf(tagName) !== -1) {//unpaired tag + else if (options.unpairedTagsSet.has(tagName)) {//unpaired tag const childNode = new XmlNode(tagName); if (prefixedAttrs) { childNode[":@"] = prefixedAttrs; } - this.addChild(currentNode, childNode, this.matcher, startIndex); + this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); this.matcher.pop(); // Pop unpaired tag this.isCurrentNodeStopNode = false; // Reset flag i = result.closeIndex; @@ -54867,7 +57424,7 @@ const parseXml = function (xmlData) { //opening tag else { const childNode = new XmlNode(tagName); - if (this.tagsNodeStack.length > this.options.maxNestedTags) { + if (this.tagsNodeStack.length > options.maxNestedTags) { throw new Error("Maximum nested tags exceeded"); } this.tagsNodeStack.push(currentNode); @@ -54875,7 +57432,7 @@ const parseXml = function (xmlData) { if (prefixedAttrs) { childNode[":@"] = prefixedAttrs; } - this.addChild(currentNode, childNode, this.matcher, startIndex); + this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); currentNode = childNode; } textData = ""; @@ -54938,79 +57495,7 @@ function OrderedObjParser_replaceEntitiesValue(val, tagName, jPath) { } } - // Replace DOCTYPE entities - for (const entityName of Object.keys(this.docTypeEntities)) { - const entity = this.docTypeEntities[entityName]; - const matches = val.match(entity.regx); - - if (matches) { - // Track expansions - this.entityExpansionCount += matches.length; - - // Check expansion limit - if (entityConfig.maxTotalExpansions && - this.entityExpansionCount > entityConfig.maxTotalExpansions) { - throw new Error( - `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}` - ); - } - - // Store length before replacement - const lengthBefore = val.length; - val = val.replace(entity.regx, entity.val); - - // Check expanded length immediately after replacement - if (entityConfig.maxExpandedLength) { - this.currentExpandedLength += (val.length - lengthBefore); - - if (this.currentExpandedLength > entityConfig.maxExpandedLength) { - throw new Error( - `Total expanded content size exceeded: ${this.currentExpandedLength} > ${entityConfig.maxExpandedLength}` - ); - } - } - } - } - // Replace standard entities - for (const entityName of Object.keys(this.lastEntities)) { - const entity = this.lastEntities[entityName]; - const matches = val.match(entity.regex); - if (matches) { - this.entityExpansionCount += matches.length; - if (entityConfig.maxTotalExpansions && - this.entityExpansionCount > entityConfig.maxTotalExpansions) { - throw new Error( - `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}` - ); - } - } - val = val.replace(entity.regex, entity.val); - } - if (val.indexOf('&') === -1) return val; - - // Replace HTML entities if enabled - if (this.options.htmlEntities) { - for (const entityName of Object.keys(this.htmlEntities)) { - const entity = this.htmlEntities[entityName]; - const matches = val.match(entity.regex); - if (matches) { - //console.log(matches); - this.entityExpansionCount += matches.length; - if (entityConfig.maxTotalExpansions && - this.entityExpansionCount > entityConfig.maxTotalExpansions) { - throw new Error( - `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}` - ); - } - } - val = val.replace(entity.regex, entity.val); - } - } - - // Replace ampersand entity last - val = val.replace(this.ampEntity.regex, this.ampEntity.val); - - return val; + return this.entityDecoder.decode(val); } @@ -55032,20 +57517,14 @@ function saveTextToParentTag(textData, parentNode, matcher, isLeafNode) { return textData; } -//TODO: use jPath to simplify the logic /** * @param {Array} stopNodeExpressions - Array of compiled Expression objects * @param {Matcher} matcher - Current path matcher */ -function isItStopNode(stopNodeExpressions, matcher) { - if (!stopNodeExpressions || stopNodeExpressions.length === 0) return false; +function isItStopNode() { + if (this.stopNodeExpressionsSet.size === 0) return false; - for (let i = 0; i < stopNodeExpressions.length; i++) { - if (matcher.matches(stopNodeExpressions[i])) { - return true; - } - } - return false; + return this.matcher.matchesAny(this.stopNodeExpressionsSet); } /** @@ -55055,32 +57534,38 @@ function isItStopNode(stopNodeExpressions, matcher) { * @returns */ function tagExpWithClosingIndex(xmlData, i, closingChar = ">") { - let attrBoundary; - let tagExp = ""; - for (let index = i; index < xmlData.length; index++) { - let ch = xmlData[index]; + //TODO: ignore boolean attributes in tag expression + //TODO: if ignore attributes, dont read full attribute expression but the end. But read for xml declaration + let attrBoundary = 0; + const len = xmlData.length; + const closeCode0 = closingChar.charCodeAt(0); + const closeCode1 = closingChar.length > 1 ? closingChar.charCodeAt(1) : -1; + + let result = ''; + let segmentStart = i; + + for (let index = i; index < len; index++) { + const code = xmlData.charCodeAt(index); + if (attrBoundary) { - if (ch === attrBoundary) attrBoundary = "";//reset - } else if (ch === '"' || ch === "'") { - attrBoundary = ch; - } else if (ch === closingChar[0]) { - if (closingChar[1]) { - if (xmlData[index + 1] === closingChar[1]) { - return { - data: tagExp, - index: index - } + if (code === attrBoundary) attrBoundary = 0; + } else if (code === 34 || code === 39) { // " or ' + attrBoundary = code; + } else if (code === closeCode0) { + if (closeCode1 !== -1) { + if (xmlData.charCodeAt(index + 1) === closeCode1) { + result += xmlData.substring(segmentStart, index); + return { data: result, index }; } } else { - return { - data: tagExp, - index: index - } + result += xmlData.substring(segmentStart, index); + return { data: result, index }; } - } else if (ch === '\t') { - ch = " " + } else if (code === 9 && !attrBoundary) { // \t - only replace with space outside attribute values + // Flush accumulated segment, add space, start new segment + result += xmlData.substring(segmentStart, index) + ' '; + segmentStart = index + 1; } - tagExp += ch; } } @@ -55093,6 +57578,12 @@ function findClosingIndex(xmlData, str, i, errMsg) { } } +function findClosingChar(xmlData, char, i, errMsg) { + const closingIndex = xmlData.indexOf(char, i); + if (closingIndex === -1) throw new Error(errMsg); + return closingIndex; // no offset needed +} + function readTagExp(xmlData, i, removeNSPrefix, closingChar = ">") { const result = tagExpWithClosingIndex(xmlData, i + 1, closingChar); if (!result) return; @@ -55134,10 +57625,12 @@ function readStopNodeData(xmlData, tagName, i) { // Starting at 1 since we already have an open tag let openTagCount = 1; - for (; i < xmlData.length; i++) { + const xmllen = xmlData.length; + for (; i < xmllen; i++) { if (xmlData[i] === "<") { - if (xmlData[i + 1] === "/") {//close tag - const closeIndex = findClosingIndex(xmlData, ">", i, `${tagName} is not closed`); + const c1 = xmlData.charCodeAt(i + 1); + if (c1 === 47) {//close tag '/' + const closeIndex = findClosingChar(xmlData, ">", i, `${tagName} is not closed`); let closeTagName = xmlData.substring(i + 2, closeIndex).trim(); if (closeTagName === tagName) { openTagCount--; @@ -55149,17 +57642,20 @@ function readStopNodeData(xmlData, tagName, i) { } } i = closeIndex; - } else if (xmlData[i + 1] === '?') { + } else if (c1 === 63) { //? const closeIndex = findClosingIndex(xmlData, "?>", i + 1, "StopNode is not closed.") i = closeIndex; - } else if (xmlData.substr(i + 1, 3) === '!--') { + } else if (c1 === 33 + && xmlData.charCodeAt(i + 2) === 45 + && xmlData.charCodeAt(i + 3) === 45) { // '!--' const closeIndex = findClosingIndex(xmlData, "-->", i + 3, "StopNode is not closed.") i = closeIndex; - } else if (xmlData.substr(i + 1, 2) === '![') { + } else if (c1 === 33 + && xmlData.charCodeAt(i + 2) === 91) { // '![' const closeIndex = findClosingIndex(xmlData, "]]>", i, "StopNode is not closed.") - 2; i = closeIndex; } else { - const tagData = readTagExp(xmlData, i, '>') + const tagData = readTagExp(xmlData, i, false) if (tagData) { const openTagName = tagData && tagData.tagName; @@ -55259,18 +57755,17 @@ function stripAttributePrefix(attrs, prefix) { * @param {Matcher} matcher - Path matcher instance * @returns */ -function prettify(node, options, matcher) { - return compress(node, options, matcher); +function prettify(node, options, matcher, readonlyMatcher) { + return compress(node, options, matcher, readonlyMatcher); } /** - * * @param {array} arr * @param {object} options * @param {Matcher} matcher - Path matcher instance * @returns object */ -function compress(arr, options, matcher) { +function compress(arr, options, matcher, readonlyMatcher) { let text; const compressedObj = {}; //This is intended to be a plain object for (let i = 0; i < arr.length; i++) { @@ -55293,11 +57788,15 @@ function compress(arr, options, matcher) { continue; } else if (tagObj[property]) { - let val = compress(tagObj[property], options, matcher); + let val = compress(tagObj[property], options, matcher, readonlyMatcher); const isLeaf = isLeafTag(val, options); + if (Object.keys(val).length === 0 && options.alwaysCreateTextNode) { + val[options.textNodeName] = ""; + } + if (tagObj[":@"]) { - assignAttributes(val, tagObj[":@"], matcher, options); + assignAttributes(val, tagObj[":@"], readonlyMatcher, options); } else if (Object.keys(val).length === 1 && val[options.textNodeName] !== undefined && !options.alwaysCreateTextNode) { val = val[options.textNodeName]; } else if (Object.keys(val).length === 0) { @@ -55319,8 +57818,8 @@ function compress(arr, options, matcher) { //TODO: if a node is not an array, then check if it should be an array //also determine if it is a leaf node - // Pass jPath string or matcher based on options.jPath setting - const jPathOrMatcher = options.jPath ? matcher.toString() : matcher; + // Pass jPath string or readonlyMatcher based on options.jPath setting + const jPathOrMatcher = options.jPath ? readonlyMatcher.toString() : readonlyMatcher; if (options.isArray(property, jPathOrMatcher, isLeaf)) { compressedObj[property] = [val]; } else { @@ -55352,7 +57851,7 @@ function node2json_propName(obj) { } } -function assignAttributes(obj, attrMap, matcher, options) { +function assignAttributes(obj, attrMap, readonlyMatcher, options) { if (attrMap) { const keys = Object.keys(attrMap); const len = keys.length; //don't make it inline @@ -55367,8 +57866,8 @@ function assignAttributes(obj, attrMap, matcher, options) { // For attributes, we need to create a temporary path // Pass jPath string or matcher based on options.jPath setting const jPathOrMatcher = options.jPath - ? matcher.toString() + "." + rawAttrName - : matcher; + ? readonlyMatcher.toString() + "." + rawAttrName + : readonlyMatcher; if (options.isArray(atrrName, jPathOrMatcher, true, true)) { obj[atrrName] = [attrMap[atrrName]]; @@ -55425,16 +57924,16 @@ 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}`) } } - const orderedObjParser = new OrderedObjParser(this.options); - orderedObjParser.addExternalEntities(this.externalEntities); + const orderedObjParser = new OrderedObjParser(this.options, this.externalEntities); + // orderedObjParser.entityDecoder.setExternalEntities(this.externalEntities); const orderedResult = orderedObjParser.parseXml(xmlData); if (this.options.preserveOrder || orderedResult === undefined) return orderedResult; - else return prettify(orderedResult, this.options, orderedObjParser.matcher); + else return prettify(orderedResult, this.options, orderedObjParser.matcher, orderedObjParser.readonlyMatcher); } /** @@ -55468,7 +57967,6 @@ class XMLParser { return XmlNode.getMetaDataSymbol(); } } - ;// CONCATENATED MODULE: ./node_modules/@azure/core-xml/dist/esm/xml.common.js // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. @@ -55487,20 +57985,34 @@ const xml_common_XML_CHARKEY = "_"; function getCommonOptions(options) { - var _a; return { attributesGroupName: xml_common_XML_ATTRKEY, - textNodeName: (_a = options.xmlCharKey) !== null && _a !== void 0 ? _a : xml_common_XML_CHARKEY, + textNodeName: options.xmlCharKey ?? xml_common_XML_CHARKEY, ignoreAttributes: false, suppressBooleanAttributes: false, }; } function getSerializerOptions(options = {}) { - var _a, _b; - return Object.assign(Object.assign({}, getCommonOptions(options)), { attributeNamePrefix: "@_", format: true, suppressEmptyNode: true, indentBy: "", rootNodeName: (_a = options.rootName) !== null && _a !== void 0 ? _a : "root", cdataPropName: (_b = options.cdataPropName) !== null && _b !== void 0 ? _b : "__cdata" }); + return { + ...getCommonOptions(options), + attributeNamePrefix: "@_", + format: true, + suppressEmptyNode: true, + indentBy: "", + rootNodeName: options.rootName ?? "root", + cdataPropName: options.cdataPropName ?? "__cdata", + }; } function getParserOptions(options = {}) { - return Object.assign(Object.assign({}, getCommonOptions(options)), { parseAttributeValue: false, parseTagValue: false, attributeNamePrefix: "", stopNodes: options.stopNodes, processEntities: true, trimValues: false }); + return { + ...getCommonOptions(options), + parseAttributeValue: false, + parseTagValue: false, + attributeNamePrefix: "", + stopNodes: options.stopNodes, + processEntities: true, + trimValues: false, + }; } /** * Converts given JSON object to XML string @@ -55539,7 +58051,7 @@ async function parseXML(str, opts = {}) { if (!opts.includeRoot) { for (const key of Object.keys(parsedXml)) { const value = parsedXml[key]; - return typeof value === "object" ? Object.assign({}, value) : value; + return typeof value === "object" ? { ...value } : value; } } return parsedXml; @@ -57124,39 +59636,6 @@ class StorageSharedKeyCredential extends Credential { } } //# sourceMappingURL=StorageSharedKeyCredential.js.map -;// CONCATENATED MODULE: ./node_modules/@azure/storage-common/node_modules/@azure/abort-controller/dist/esm/AbortError.js -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -/** - * This error is thrown when an asynchronous operation has been aborted. - * Check for this error by testing the `name` that the name property of the - * error matches `"AbortError"`. - * - * @example - * ```ts - * const controller = new AbortController(); - * controller.abort(); - * try { - * doAsyncWork(controller.signal) - * } catch (e) { - * if (e.name === 'AbortError') { - * // handle abort error here. - * } - * } - * ``` - */ -class esm_AbortError_AbortError extends Error { - constructor(message) { - super(message); - this.name = "AbortError"; - } -} -//# sourceMappingURL=AbortError.js.map -;// CONCATENATED MODULE: ./node_modules/@azure/storage-common/node_modules/@azure/abort-controller/dist/esm/index.js -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -//# sourceMappingURL=index.js.map ;// CONCATENATED MODULE: ./node_modules/@azure/storage-common/dist/esm/log.js // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. @@ -57214,7 +59693,7 @@ const DEFAULT_RETRY_OPTIONS = { secondaryHost: "", tryTimeoutInMs: undefined, // Use server side default timeout strategy }; -const RETRY_ABORT_ERROR = new esm_AbortError_AbortError("The operation was aborted."); +const RETRY_ABORT_ERROR = new AbortError_AbortError("The operation was aborted."); /** * Retry policy with exponential retry and linear retry implemented. */ @@ -57527,7 +60006,7 @@ const retriableErrors = [ "EPIPE", "REQUEST_SEND_ERROR", ]; -const StorageRetryPolicyV2_RETRY_ABORT_ERROR = new esm_AbortError_AbortError("The operation was aborted."); +const StorageRetryPolicyV2_RETRY_ABORT_ERROR = new AbortError_AbortError("The operation was aborted."); /** * Retry policy with exponential retry and linear retry implemented. */ @@ -57890,15 +60369,12 @@ class UserDelegationKeyCredential { - - - //# sourceMappingURL=index.js.map ;// CONCATENATED MODULE: ./node_modules/@azure/storage-blob/dist/esm/utils/constants.js // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -const esm_utils_constants_SDK_VERSION = "12.30.0"; +const esm_utils_constants_SDK_VERSION = "12.31.0"; const SERVICE_VERSION = "2026-02-06"; const BLOCK_BLOB_MAX_UPLOAD_BLOB_BYTES = 256 * 1024 * 1024; // 256MB const BLOCK_BLOB_MAX_STAGE_BLOCK_BYTES = 4000 * 1024 * 1024; // 4000MB @@ -74396,39 +76872,6 @@ class BlobLeaseClient { } } //# sourceMappingURL=BlobLeaseClient.js.map -;// CONCATENATED MODULE: ./node_modules/@azure/storage-blob/node_modules/@azure/abort-controller/dist/esm/AbortError.js -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -/** - * This error is thrown when an asynchronous operation has been aborted. - * Check for this error by testing the `name` that the name property of the - * error matches `"AbortError"`. - * - * @example - * ```ts - * const controller = new AbortController(); - * controller.abort(); - * try { - * doAsyncWork(controller.signal) - * } catch (e) { - * if (e.name === 'AbortError') { - * // handle abort error here. - * } - * } - * ``` - */ -class dist_esm_AbortError_AbortError extends Error { - constructor(message) { - super(message); - this.name = "AbortError"; - } -} -//# sourceMappingURL=AbortError.js.map -;// CONCATENATED MODULE: ./node_modules/@azure/storage-blob/node_modules/@azure/abort-controller/dist/esm/index.js -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -//# sourceMappingURL=index.js.map ;// CONCATENATED MODULE: ./node_modules/@azure/storage-blob/dist/esm/utils/RetriableReadableStream.js // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. @@ -74508,7 +76951,7 @@ class RetriableReadableStream extends external_node_stream_.Readable { } }; sourceAbortedHandler = () => { - const abortError = new dist_esm_AbortError_AbortError("The operation was aborted."); + const abortError = new AbortError_AbortError("The operation was aborted."); this.destroy(abortError); }; sourceErrorOrEndHandler = (err) => { @@ -75503,7 +77946,7 @@ const external_buffer_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import. -const ABORT_ERROR = new dist_esm_AbortError_AbortError("Reading from the avro stream was aborted."); +const ABORT_ERROR = new AbortError_AbortError("Reading from the avro stream was aborted."); class AvroReadableFromStream extends AvroReadable { _position; _readable; @@ -78123,7 +80566,7 @@ class Clients_BlobClient extends StorageClient_StorageClient { options.conditions = options.conditions || {}; ensureCpkIfSpecified(options.customerProvidedKey, this.isHttps); return tracingClient.withSpan("BlobClient-download", options, async (updatedOptions) => { - const res = utils_common_assertResponse(await this.blobContext.download({ + const res = utils_common_assertResponse((await this.blobContext.download({ abortSignal: options.abortSignal, leaseAccessConditions: options.conditions, modifiedAccessConditions: { @@ -78139,7 +80582,7 @@ class Clients_BlobClient extends StorageClient_StorageClient { snapshot: options.snapshot, cpkInfo: options.customerProvidedKey, tracingOptions: updatedOptions.tracingOptions, - })); + }))); const wrappedRes = { ...res, _response: res._response, // _response is made non-enumerable @@ -79461,7 +81904,7 @@ class BlockBlobClient extends Clients_BlobClient { throw new Error("This operation currently is only supported in Node.js."); } return tracingClient.withSpan("BlockBlobClient-query", options, async (updatedOptions) => { - const response = utils_common_assertResponse(await this._blobContext.query({ + const response = utils_common_assertResponse((await this._blobContext.query({ abortSignal: options.abortSignal, queryRequest: { queryType: "SQL", @@ -79476,7 +81919,7 @@ class BlockBlobClient extends Clients_BlobClient { }, cpkInfo: options.customerProvidedKey, tracingOptions: updatedOptions.tracingOptions, - })); + }))); return new BlobQueryResponse(response, { abortSignal: options.abortSignal, onProgress: options.onProgress, @@ -81372,9 +83815,9 @@ class BlobBatchClient { return tracingClient.withSpan("BlobBatchClient-submitBatch", options, async (updatedOptions) => { const batchRequestBody = batchRequest.getHttpRequestBody(); // ServiceSubmitBatchResponseModel and ContainerSubmitBatchResponse are compatible for now. - const rawBatchResponse = utils_common_assertResponse(await this.serviceOrContainerContext.submitBatch(utf8ByteLength(batchRequestBody), batchRequest.getMultiPartContentType(), batchRequestBody, { + const rawBatchResponse = utils_common_assertResponse((await this.serviceOrContainerContext.submitBatch(utf8ByteLength(batchRequestBody), batchRequest.getMultiPartContentType(), batchRequestBody, { ...updatedOptions, - })); + }))); // Parse the sub responses result, if logic reaches here(i.e. the batch request succeeded with status code 202). const batchResponseParser = new BatchResponseParser(rawBatchResponse, batchRequest.getSubRequests()); const responseSummary = await batchResponseParser.parseBatchResponse(); @@ -83954,7 +86397,7 @@ NetworkError.isNetworkErrorCode = (code) => { }; class UsageError extends Error { constructor() { - const message = `Cache storage quota has been hit. Unable to upload any new cache entries. Usage is recalculated every 6-12 hours.\nMore info on storage limits: https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#calculating-minute-and-storage-spending`; + const message = `Cache storage quota has been hit. Unable to upload any new cache entries.\nMore info on storage limits: https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#calculating-minute-and-storage-spending`; super(message); this.name = 'UsageError'; } diff --git a/dist/save-only/index.js b/dist/save-only/index.js index bc5394f..898a9be 100644 --- a/dist/save-only/index.js +++ b/dist/save-only/index.js @@ -38848,7 +38848,7 @@ module.exports = { version: packageJson.version } /***/ 4012: /***/ ((module) => { -module.exports = /*#__PURE__*/JSON.parse('{"name":"@actions/cache","version":"6.0.0","description":"Actions cache lib","keywords":["github","actions","cache"],"homepage":"https://github.com/actions/toolkit/tree/main/packages/cache","license":"MIT","type":"module","main":"lib/cache.js","types":"lib/cache.d.ts","exports":{".":{"types":"./lib/cache.d.ts","import":"./lib/cache.js"}},"directories":{"lib":"lib","test":"__tests__"},"files":["lib","!.DS_Store"],"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/actions/toolkit.git","directory":"packages/cache"},"scripts":{"audit-moderate":"npm install && npm audit --json --audit-level=moderate > audit.json","test":"echo \\"Error: run tests from root\\" && exit 1","tsc":"tsc && cp src/internal/shared/package-version.cjs lib/internal/shared/"},"bugs":{"url":"https://github.com/actions/toolkit/issues"},"dependencies":{"@actions/core":"^3.0.0","@actions/exec":"^3.0.0","@actions/glob":"^0.6.1","@actions/http-client":"^4.0.0","@actions/io":"^3.0.0","@azure/core-rest-pipeline":"^1.22.0","@azure/storage-blob":"^12.30.0","@protobuf-ts/runtime-rpc":"^2.11.1","semver":"^7.7.3"},"devDependencies":{"@protobuf-ts/plugin":"^2.9.4","@types/node":"^25.1.0","@types/semver":"^7.7.1","typescript":"^5.2.2"},"overrides":{"uri-js":"npm:uri-js-replace@^1.0.1","node-fetch":"^3.3.2"}}'); +module.exports = /*#__PURE__*/JSON.parse('{"name":"@actions/cache","version":"6.0.1","description":"Actions cache lib","keywords":["github","actions","cache"],"homepage":"https://github.com/actions/toolkit/tree/main/packages/cache","license":"MIT","type":"module","main":"lib/cache.js","types":"lib/cache.d.ts","exports":{".":{"types":"./lib/cache.d.ts","import":"./lib/cache.js"}},"directories":{"lib":"lib","test":"__tests__"},"files":["lib","!.DS_Store"],"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/actions/toolkit.git","directory":"packages/cache"},"scripts":{"audit-moderate":"npm install && npm audit --json --audit-level=moderate > audit.json","test":"echo \\"Error: run tests from root\\" && exit 1","tsc":"tsc && cp src/internal/shared/package-version.cjs lib/internal/shared/"},"bugs":{"url":"https://github.com/actions/toolkit/issues"},"dependencies":{"@actions/core":"^3.0.1","@actions/exec":"^3.0.0","@actions/glob":"^0.6.1","@actions/http-client":"^4.0.1","@actions/io":"^3.0.2","@azure/core-rest-pipeline":"^1.23.0","@azure/storage-blob":"^12.31.0","@protobuf-ts/runtime-rpc":"^2.11.1","semver":"^7.7.4"},"devDependencies":{"@protobuf-ts/plugin":"^2.11.1","@types/node":"^25.6.0","@types/semver":"^7.7.1","typescript":"^5.9.3"},"overrides":{"uri-js":"npm:uri-js-replace@^1.0.1","node-fetch":"^3.3.2"}}'); /***/ }) @@ -46852,7 +46852,7 @@ async function util_userAgentPlatform_setPlatformSpecificData(map) { ;// CONCATENATED MODULE: ./node_modules/@azure/core-rest-pipeline/dist/esm/constants.js // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -const esm_constants_SDK_VERSION = "1.22.2"; +const esm_constants_SDK_VERSION = "1.22.3"; const esm_constants_DEFAULT_RETRY_POLICY_COUNT = 3; //# sourceMappingURL=constants.js.map ;// CONCATENATED MODULE: ./node_modules/@azure/core-rest-pipeline/dist/esm/util/userAgent.js @@ -46951,7 +46951,7 @@ async function computeSha256Hash(content, encoding) { //# sourceMappingURL=internal.js.map -;// CONCATENATED MODULE: ./node_modules/@azure/core-util/node_modules/@azure/abort-controller/dist/esm/AbortError.js +;// CONCATENATED MODULE: ./node_modules/@azure/abort-controller/dist/esm/AbortError.js // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. /** @@ -46979,7 +46979,7 @@ class AbortError_AbortError extends Error { } } //# sourceMappingURL=AbortError.js.map -;// CONCATENATED MODULE: ./node_modules/@azure/core-util/node_modules/@azure/abort-controller/dist/esm/index.js +;// CONCATENATED MODULE: ./node_modules/@azure/abort-controller/dist/esm/index.js // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. @@ -51570,6 +51570,7 @@ function convertHttpClient(requestPolicyClient) { + //# sourceMappingURL=index.js.map ;// CONCATENATED MODULE: ./node_modules/path-expression-matcher/src/Expression.js /** @@ -51589,11 +51590,11 @@ class Expression { * @param {Object} options - Configuration options * @param {string} options.separator - Path separator (default: '.') */ - constructor(pattern, options = {}) { + constructor(pattern, options = {}, data) { this.pattern = pattern; this.separator = options.separator || '.'; this.segments = this._parse(pattern); - + this.data = data; // Cache expensive checks for performance (O(1) instead of O(n)) this._hasDeepWildcard = this.segments.some(seg => seg.type === 'deep-wildcard'); this._hasAttributeCondition = this.segments.some(seg => seg.attrName !== undefined); @@ -51805,48 +51806,206 @@ class Expression { } } ;// CONCATENATED MODULE: ./node_modules/path-expression-matcher/src/Matcher.js + + /** - * Matcher - Tracks current path in XML/JSON tree and matches against Expressions - * + * MatcherView - A lightweight read-only view over a Matcher's internal state. + * + * Created once by Matcher and reused across all callbacks. Holds a direct + * reference to the parent Matcher so it always reflects current parser state + * with zero copying or freezing overhead. + * + * Users receive this via {@link Matcher#readOnly} or directly from parser + * callbacks. It exposes all query and matching methods but has no mutation + * methods — misuse is caught at the TypeScript level rather than at runtime. + * + * @example + * const matcher = new Matcher(); + * const view = matcher.readOnly(); + * + * matcher.push("root", {}); + * view.getCurrentTag(); // "root" + * view.getDepth(); // 1 + */ +class MatcherView { + /** + * @param {Matcher} matcher - The parent Matcher instance to read from. + */ + constructor(matcher) { + this._matcher = matcher; + } + + /** + * Get the path separator used by the parent matcher. + * @returns {string} + */ + get separator() { + return this._matcher.separator; + } + + /** + * Get current tag name. + * @returns {string|undefined} + */ + getCurrentTag() { + const path = this._matcher.path; + return path.length > 0 ? path[path.length - 1].tag : undefined; + } + + /** + * Get current namespace. + * @returns {string|undefined} + */ + getCurrentNamespace() { + const path = this._matcher.path; + return path.length > 0 ? path[path.length - 1].namespace : undefined; + } + + /** + * Get current node's attribute value. + * @param {string} attrName + * @returns {*} + */ + getAttrValue(attrName) { + const path = this._matcher.path; + if (path.length === 0) return undefined; + return path[path.length - 1].values?.[attrName]; + } + + /** + * Check if current node has an attribute. + * @param {string} attrName + * @returns {boolean} + */ + hasAttr(attrName) { + const path = this._matcher.path; + if (path.length === 0) return false; + const current = path[path.length - 1]; + return current.values !== undefined && attrName in current.values; + } + + /** + * Get current node's sibling position (child index in parent). + * @returns {number} + */ + getPosition() { + const path = this._matcher.path; + if (path.length === 0) return -1; + return path[path.length - 1].position ?? 0; + } + + /** + * Get current node's repeat counter (occurrence count of this tag name). + * @returns {number} + */ + getCounter() { + const path = this._matcher.path; + if (path.length === 0) return -1; + return path[path.length - 1].counter ?? 0; + } + + /** + * Get current node's sibling index (alias for getPosition). + * @returns {number} + * @deprecated Use getPosition() or getCounter() instead + */ + getIndex() { + return this.getPosition(); + } + + /** + * Get current path depth. + * @returns {number} + */ + getDepth() { + return this._matcher.path.length; + } + + /** + * Get path as string. + * @param {string} [separator] - Optional separator (uses default if not provided) + * @param {boolean} [includeNamespace=true] + * @returns {string} + */ + toString(separator, includeNamespace = true) { + return this._matcher.toString(separator, includeNamespace); + } + + /** + * Get path as array of tag names. + * @returns {string[]} + */ + toArray() { + return this._matcher.path.map(n => n.tag); + } + + /** + * Match current path against an Expression. + * @param {Expression} expression + * @returns {boolean} + */ + matches(expression) { + return this._matcher.matches(expression); + } + + /** + * Match any expression in the given set against the current path. + * @param {ExpressionSet} exprSet + * @returns {boolean} + */ + matchesAny(exprSet) { + return exprSet.matchesAny(this._matcher); + } +} + +/** + * Matcher - Tracks current path in XML/JSON tree and matches against Expressions. + * * The matcher maintains a stack of nodes representing the current path from root to * current tag. It only stores attribute values for the current (top) node to minimize * memory usage. Sibling tracking is used to auto-calculate position and counter. - * + * + * Use {@link Matcher#readOnly} to obtain a {@link MatcherView} safe to pass to + * user callbacks — it always reflects current state with no Proxy overhead. + * * @example * const matcher = new Matcher(); * matcher.push("root", {}); * matcher.push("users", {}); * matcher.push("user", { id: "123", type: "admin" }); - * + * * const expr = new Expression("root.users.user"); * matcher.matches(expr); // true */ class Matcher { /** - * Create a new Matcher - * @param {Object} options - Configuration options - * @param {string} options.separator - Default path separator (default: '.') + * Create a new Matcher. + * @param {Object} [options={}] + * @param {string} [options.separator='.'] - Default path separator */ constructor(options = {}) { this.separator = options.separator || '.'; this.path = []; this.siblingStacks = []; - // Each path node: { tag: string, values: object, position: number, counter: number } + // Each path node: { tag, values, position, counter, namespace? } // values only present for current (last) node // Each siblingStacks entry: Map tracking occurrences at each level + this._pathStringCache = null; + this._view = new MatcherView(this); } /** - * Push a new tag onto the path - * @param {string} tagName - Name of the tag - * @param {Object} attrValues - Attribute key-value pairs for current node (optional) - * @param {string} namespace - Namespace for the tag (optional) + * Push a new tag onto the path. + * @param {string} tagName + * @param {Object|null} [attrValues=null] + * @param {string|null} [namespace=null] */ push(tagName, attrValues = null, namespace = null) { + this._pathStringCache = null; + // Remove values from previous current node (now becoming ancestor) if (this.path.length > 0) { - const prev = this.path[this.path.length - 1]; - prev.values = undefined; + this.path[this.path.length - 1].values = undefined; } // Get or create sibling tracking for current level @@ -51879,12 +52038,10 @@ class Matcher { counter: counter }; - // Store namespace if provided if (namespace !== null && namespace !== undefined) { node.namespace = namespace; } - // Store values only for current node if (attrValues !== null && attrValues !== undefined) { node.values = attrValues; } @@ -51893,19 +52050,15 @@ class Matcher { } /** - * Pop the last tag from the path + * Pop the last tag from the path. * @returns {Object|undefined} The popped node */ pop() { - if (this.path.length === 0) { - return undefined; - } + if (this.path.length === 0) return undefined; + this._pathStringCache = null; const node = this.path.pop(); - // Clean up sibling tracking for levels deeper than current - // After pop, path.length is the new depth - // We need to clean up siblingStacks[path.length + 1] and beyond if (this.siblingStacks.length > this.path.length + 1) { this.siblingStacks.length = this.path.length + 1; } @@ -51914,9 +52067,9 @@ class Matcher { } /** - * Update current node's attribute values - * Useful when attributes are parsed after push - * @param {Object} attrValues - Attribute values + * Update current node's attribute values. + * Useful when attributes are parsed after push. + * @param {Object} attrValues */ updateCurrent(attrValues) { if (this.path.length > 0) { @@ -51928,7 +52081,7 @@ class Matcher { } /** - * Get current tag name + * Get current tag name. * @returns {string|undefined} */ getCurrentTag() { @@ -51936,7 +52089,7 @@ class Matcher { } /** - * Get current namespace + * Get current namespace. * @returns {string|undefined} */ getCurrentNamespace() { @@ -51944,19 +52097,18 @@ class Matcher { } /** - * Get current node's attribute value - * @param {string} attrName - Attribute name - * @returns {*} Attribute value or undefined + * Get current node's attribute value. + * @param {string} attrName + * @returns {*} */ getAttrValue(attrName) { if (this.path.length === 0) return undefined; - const current = this.path[this.path.length - 1]; - return current.values?.[attrName]; + return this.path[this.path.length - 1].values?.[attrName]; } /** - * Check if current node has an attribute - * @param {string} attrName - Attribute name + * Check if current node has an attribute. + * @param {string} attrName * @returns {boolean} */ hasAttr(attrName) { @@ -51966,7 +52118,7 @@ class Matcher { } /** - * Get current node's sibling position (child index in parent) + * Get current node's sibling position (child index in parent). * @returns {number} */ getPosition() { @@ -51975,7 +52127,7 @@ class Matcher { } /** - * Get current node's repeat counter (occurrence count of this tag name) + * Get current node's repeat counter (occurrence count of this tag name). * @returns {number} */ getCounter() { @@ -51984,7 +52136,7 @@ class Matcher { } /** - * Get current node's sibling index (alias for getPosition for backward compatibility) + * Get current node's sibling index (alias for getPosition). * @returns {number} * @deprecated Use getPosition() or getCounter() instead */ @@ -51993,7 +52145,7 @@ class Matcher { } /** - * Get current path depth + * Get current path depth. * @returns {number} */ getDepth() { @@ -52001,23 +52153,33 @@ class Matcher { } /** - * Get path as string - * @param {string} separator - Optional separator (uses default if not provided) - * @param {boolean} includeNamespace - Whether to include namespace in output (default: true) + * Get path as string. + * @param {string} [separator] - Optional separator (uses default if not provided) + * @param {boolean} [includeNamespace=true] * @returns {string} */ toString(separator, includeNamespace = true) { const sep = separator || this.separator; - return this.path.map(n => { - if (includeNamespace && n.namespace) { - return `${n.namespace}:${n.tag}`; + const isDefault = (sep === this.separator && includeNamespace === true); + + if (isDefault) { + if (this._pathStringCache !== null) { + return this._pathStringCache; } - return n.tag; - }).join(sep); + const result = this.path.map(n => + (n.namespace) ? `${n.namespace}:${n.tag}` : n.tag + ).join(sep); + this._pathStringCache = result; + return result; + } + + return this.path.map(n => + (includeNamespace && n.namespace) ? `${n.namespace}:${n.tag}` : n.tag + ).join(sep); } /** - * Get path as array of tag names + * Get path as array of tag names. * @returns {string[]} */ toArray() { @@ -52025,17 +52187,18 @@ class Matcher { } /** - * Reset the path to empty + * Reset the path to empty. */ reset() { + this._pathStringCache = null; this.path = []; this.siblingStacks = []; } /** - * Match current path against an Expression - * @param {Expression} expression - The expression to match against - * @returns {boolean} True if current path matches the expression + * Match current path against an Expression. + * @param {Expression} expression + * @returns {boolean} */ matches(expression) { const segments = expression.segments; @@ -52044,32 +52207,23 @@ class Matcher { return false; } - // Handle deep wildcard patterns if (expression.hasDeepWildcard()) { return this._matchWithDeepWildcard(segments); } - // Simple path matching (no deep wildcards) return this._matchSimple(segments); } /** - * Match simple path (no deep wildcards) * @private */ _matchSimple(segments) { - // Path must be same length as segments if (this.path.length !== segments.length) { return false; } - // Match each segment bottom-to-top for (let i = 0; i < segments.length; i++) { - const segment = segments[i]; - const node = this.path[i]; - const isCurrentNode = (i === this.path.length - 1); - - if (!this._matchSegment(segment, node, isCurrentNode)) { + if (!this._matchSegment(segments[i], this.path[i], i === this.path.length - 1)) { return false; } } @@ -52078,32 +52232,27 @@ class Matcher { } /** - * Match path with deep wildcards * @private */ _matchWithDeepWildcard(segments) { - let pathIdx = this.path.length - 1; // Start from current node (bottom) - let segIdx = segments.length - 1; // Start from last segment + let pathIdx = this.path.length - 1; + let segIdx = segments.length - 1; while (segIdx >= 0 && pathIdx >= 0) { const segment = segments[segIdx]; if (segment.type === 'deep-wildcard') { - // ".." matches zero or more levels segIdx--; if (segIdx < 0) { - // Pattern ends with "..", always matches return true; } - // Find where next segment matches in the path const nextSeg = segments[segIdx]; let found = false; for (let i = pathIdx; i >= 0; i--) { - const isCurrentNode = (i === this.path.length - 1); - if (this._matchSegment(nextSeg, this.path[i], isCurrentNode)) { + if (this._matchSegment(nextSeg, this.path[i], i === this.path.length - 1)) { pathIdx = i - 1; segIdx--; found = true; @@ -52115,9 +52264,7 @@ class Matcher { return false; } } else { - // Regular segment - const isCurrentNode = (pathIdx === this.path.length - 1); - if (!this._matchSegment(segment, this.path[pathIdx], isCurrentNode)) { + if (!this._matchSegment(segment, this.path[pathIdx], pathIdx === this.path.length - 1)) { return false; } pathIdx--; @@ -52125,38 +52272,25 @@ class Matcher { } } - // All segments must be consumed return segIdx < 0; } /** - * Match a single segment against a node * @private - * @param {Object} segment - Segment from Expression - * @param {Object} node - Node from path - * @param {boolean} isCurrentNode - Whether this is the current (last) node - * @returns {boolean} */ _matchSegment(segment, node, isCurrentNode) { - // Match tag name (* is wildcard) if (segment.tag !== '*' && segment.tag !== node.tag) { return false; } - // Match namespace if specified in segment if (segment.namespace !== undefined) { - // Segment has namespace - node must match it if (segment.namespace !== '*' && segment.namespace !== node.namespace) { return false; } } - // If segment has no namespace, it matches nodes with or without namespace - // Match attribute name (check if node has this attribute) - // Can only check for current node since ancestors don't have values if (segment.attrName !== undefined) { if (!isCurrentNode) { - // Can't check attributes for ancestor nodes (values not stored) return false; } @@ -52164,20 +52298,15 @@ class Matcher { return false; } - // Match attribute value (only possible for current node) if (segment.attrValue !== undefined) { - const actualValue = node.values[segment.attrName]; - // Both should be strings - if (String(actualValue) !== String(segment.attrValue)) { + if (String(node.values[segment.attrName]) !== String(segment.attrValue)) { return false; } } } - // Match position (only for current node) if (segment.position !== undefined) { if (!isCurrentNode) { - // Can't check position for ancestor nodes return false; } @@ -52189,10 +52318,8 @@ class Matcher { return false; } else if (segment.position === 'even' && counter % 2 !== 0) { return false; - } else if (segment.position === 'nth') { - if (counter !== segment.positionValue) { - return false; - } + } else if (segment.position === 'nth' && counter !== segment.positionValue) { + return false; } } @@ -52200,8 +52327,17 @@ class Matcher { } /** - * Create a snapshot of current state - * @returns {Object} State snapshot + * Match any expression in the given set against the current path. + * @param {ExpressionSet} exprSet + * @returns {boolean} + */ + matchesAny(exprSet) { + return exprSet.matchesAny(this); + } + + /** + * Create a snapshot of current state. + * @returns {Object} */ snapshot() { return { @@ -52211,28 +52347,378 @@ class Matcher { } /** - * Restore state from snapshot - * @param {Object} snapshot - State snapshot + * Restore state from snapshot. + * @param {Object} snapshot */ restore(snapshot) { + this._pathStringCache = null; this.path = snapshot.path.map(node => ({ ...node })); this.siblingStacks = snapshot.siblingStacks.map(map => new Map(map)); } + + /** + * Return the read-only {@link MatcherView} for this matcher. + * + * The same instance is returned on every call — no allocation occurs. + * It always reflects the current parser state and is safe to pass to + * user callbacks without risk of accidental mutation. + * + * @returns {MatcherView} + * + * @example + * const view = matcher.readOnly(); + * // pass view to callbacks — it stays in sync automatically + * view.matches(expr); // ✓ + * view.getCurrentTag(); // ✓ + * // view.push(...) // ✗ method does not exist — caught by TypeScript + */ + readOnly() { + 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; } @@ -52249,13 +52735,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; @@ -52275,20 +52764,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); @@ -52304,21 +52805,25 @@ function arrToStr(arr, options, indentation, matcher, stopNodeExpressions) { if (isPreviousElementTag) { xmlStr += indentation; } - xmlStr += ``; + const val = tagObj[rawTagName][0][options.textNodeName]; + const safeVal = safeCdata(val); + xmlStr += ``; isPreviousElementTag = false; matcher.pop(); continue; } else if (tagName === options.commentPropName) { - xmlStr += indentation + ``; + 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; @@ -52330,16 +52835,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) { @@ -52383,7 +52887,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; } @@ -52421,9 +52925,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); @@ -52450,7 +52952,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)}"`; } } } @@ -52466,13 +52968,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]; @@ -52483,9 +52995,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)}"`; } } } @@ -52538,6 +53050,8 @@ function getIgnoreAttributesFn(ignoreAttributes) { + + const defaultOptions = { attributeNamePrefix: '@_', attributesGroupName: false, @@ -52571,7 +53085,11 @@ const 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) { @@ -52628,6 +53146,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); @@ -52639,11 +53195,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) { @@ -52657,6 +53214,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)) { @@ -52666,21 +53239,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) { @@ -52688,7 +53262,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(); @@ -52696,12 +53270,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 + '` + this.newLine; + const safeVal = safeCdata(val); + return this.indentate(level) + `` + this.newLine; } else if (this.options.commentPropName !== false && key === this.options.commentPropName) { - return this.indentate(level) + `` + this.newLine; + const safeVal = safeComment(val); + return this.indentate(level) + `` + this.newLine; } else if (key[0] === "?") {//PI tag return this.indentate(level) + '<' + key + attrStr + '?' + this.tagEndChar; } else { @@ -53141,7 +53723,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 @@ -53565,12 +54147,13 @@ function getPositionFromMatch(match) { const XMLValidator = { - validate: validate + validate: validator_validate } ;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/xmlparser/OptionsBuilder.js + const defaultOnDangerousProperty = (name) => { if (DANGEROUS_PROPERTY_NAMES.includes(name)) { return "__" + name; @@ -53610,6 +54193,7 @@ const OptionsBuilder_defaultOptions = { unpairedTags: [], processEntities: true, htmlEntities: false, + entityDecoder: null, ignoreDeclaration: false, ignorePiTags: false, transformTagName: false, @@ -53656,32 +54240,34 @@ function validatePropertyName(propertyName, optionName) { * @param {boolean|object} value * @returns {object} Always returns normalized object */ -function normalizeProcessEntities(value) { +function normalizeProcessEntities(value, htmlEntities) { // Boolean backward compatibility if (typeof value === 'boolean') { return { enabled: value, // true or false maxEntitySize: 10000, - maxExpansionDepth: 10, - maxTotalExpansions: 1000, + maxExpansionDepth: 10000, + maxTotalExpansions: Infinity, maxExpandedLength: 100000, - maxEntityCount: 100, + maxEntityCount: 1000, allowedTags: null, - tagFilter: null + tagFilter: null, + appliesTo: "all", }; } // Object config - merge with defaults if (typeof value === 'object' && value !== null) { return { - enabled: value.enabled !== false, // default true if not specified - maxEntitySize: value.maxEntitySize ?? 10000, - maxExpansionDepth: value.maxExpansionDepth ?? 10, - maxTotalExpansions: value.maxTotalExpansions ?? 1000, - maxExpandedLength: value.maxExpandedLength ?? 100000, - maxEntityCount: value.maxEntityCount ?? 100, + enabled: value.enabled !== false, + maxEntitySize: Math.max(1, value.maxEntitySize ?? 10000), + maxExpansionDepth: Math.max(1, value.maxExpansionDepth ?? 10000), + maxTotalExpansions: Math.max(1, value.maxTotalExpansions ?? Infinity), + maxExpandedLength: Math.max(1, value.maxExpandedLength ?? 100000), + maxEntityCount: Math.max(1, value.maxEntityCount ?? 1000), allowedTags: value.allowedTags ?? null, - tagFilter: value.tagFilter ?? null + tagFilter: value.tagFilter ?? null, + appliesTo: value.appliesTo ?? "all", }; } @@ -53712,8 +54298,8 @@ const buildOptions = function (options) { } // Always normalize processEntities for backward compatibility and validation - built.processEntities = normalizeProcessEntities(built.processEntities); - + built.processEntities = normalizeProcessEntities(built.processEntities, built.htmlEntities); + built.unpairedTagsSet = new Set(built.unpairedTags); // Convert old-style stopNodes for backward compatibility if (built.stopNodes && Array.isArray(built.stopNodes)) { built.stopNodes = built.stopNodes.map(node => { @@ -53774,11 +54360,15 @@ class XmlNode { class DocTypeReader { - constructor(options) { + constructor(options, xmlVersion) { this.suppressValidationErr = !options; this.options = options; + this.xmlVersion = xmlVersion || 1.0; } + setXmlVersion(xmlVersion = 1.0) { + this.xmlVersion = xmlVersion; + } readDocType(xmlData, i) { const entities = Object.create(null); let entityCount = 0; @@ -53801,18 +54391,15 @@ class DocTypeReader { [entityName, val, i] = this.readEntityExp(xmlData, i + 1, this.suppressValidationErr); if (val.indexOf("&") === -1) { //Parameter entities are not supported if (this.options.enabled !== false && - this.options.maxEntityCount && + this.options.maxEntityCount != null && entityCount >= this.options.maxEntityCount) { throw new Error( `Entity count (${entityCount + 1}) exceeds maximum allowed (${this.options.maxEntityCount})` ); } //const escaped = entityName.replace(/[.\-+*:]/g, '\\.'); - const escaped = entityName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - entities[entityName] = { - regx: RegExp(`&${escaped};`, "g"), - val: val - }; + //const escaped = entityName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + entities[entityName] = val; entityCount++; } } @@ -53873,12 +54460,13 @@ class DocTypeReader { i = skipWhitespace(xmlData, i); // Read entity name - let entityName = ""; + const startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i]) && xmlData[i] !== '"' && xmlData[i] !== "'") { - entityName += xmlData[i]; i++; } - validateEntityName(entityName); + let entityName = xmlData.substring(startIndex, i); + + validateEntityName(entityName, { xmlVersion: this.xmlVersion }); // Skip whitespace after entity name i = skipWhitespace(xmlData, i); @@ -53898,7 +54486,7 @@ class DocTypeReader { // Validate entity size if (this.options.enabled !== false && - this.options.maxEntitySize && + this.options.maxEntitySize != null && entityValue.length > this.options.maxEntitySize) { throw new Error( `Entity "${entityName}" size (${entityValue.length}) exceeds maximum allowed size (${this.options.maxEntitySize})` @@ -53914,12 +54502,14 @@ class DocTypeReader { i = skipWhitespace(xmlData, i); // Read notation name - let notationName = ""; + + const startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - notationName += xmlData[i]; i++; } - !this.suppressValidationErr && validateEntityName(notationName); + let notationName = xmlData.substring(startIndex, i); + + !this.suppressValidationErr && validateEntityName(notationName, { xmlVersion: this.xmlVersion }); // Skip whitespace after notation name i = skipWhitespace(xmlData, i); @@ -53968,10 +54558,11 @@ class DocTypeReader { } i++; + const startIndex = i; while (i < xmlData.length && xmlData[i] !== startChar) { - identifierVal += xmlData[i]; i++; } + identifierVal = xmlData.substring(startIndex, i); if (xmlData[i] !== startChar) { throw new Error(`Unterminated ${type} value`); @@ -53991,14 +54582,14 @@ class DocTypeReader { i = skipWhitespace(xmlData, i); // Read element name - let elementName = ""; + const startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - elementName += xmlData[i]; i++; } + let elementName = xmlData.substring(startIndex, i); // Validate element name - if (!this.suppressValidationErr && !isName(elementName)) { + if (!this.suppressValidationErr && !qName(elementName, { xmlVersion: this.xmlVersion })) { throw new Error(`Invalid element name: "${elementName}"`); } @@ -54012,10 +54603,12 @@ class DocTypeReader { i++; // Move past '(' // Read content model + const startIndex = i; while (i < xmlData.length && xmlData[i] !== ")") { - contentModel += xmlData[i]; i++; } + contentModel = xmlData.substring(startIndex, i); + if (xmlData[i] !== ")") { throw new Error("Unterminated content model"); } @@ -54036,27 +54629,27 @@ class DocTypeReader { i = skipWhitespace(xmlData, i); // Read element name - let elementName = ""; + let startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - elementName += xmlData[i]; i++; } + let elementName = xmlData.substring(startIndex, i); // Validate element name - validateEntityName(elementName) + validateEntityName(elementName, { xmlVersion: this.xmlVersion }) // Skip whitespace after element name i = skipWhitespace(xmlData, i); // Read attribute name - let attributeName = ""; + startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - attributeName += xmlData[i]; i++; } + let attributeName = xmlData.substring(startIndex, i); // Validate attribute name - if (!validateEntityName(attributeName)) { + if (!validateEntityName(attributeName, { xmlVersion: this.xmlVersion })) { throw new Error(`Invalid attribute name: "${attributeName}"`); } @@ -54081,15 +54674,17 @@ class DocTypeReader { // Read the list of allowed notations let allowedNotations = []; while (i < xmlData.length && xmlData[i] !== ")") { - let notation = ""; + + + const startIndex = i; while (i < xmlData.length && xmlData[i] !== "|" && xmlData[i] !== ")") { - notation += xmlData[i]; i++; } + let notation = xmlData.substring(startIndex, i); // Validate notation name notation = notation.trim(); - if (!validateEntityName(notation)) { + if (!validateEntityName(notation, { xmlVersion: this.xmlVersion })) { throw new Error(`Invalid notation name: "${notation}"`); } @@ -54111,10 +54706,11 @@ class DocTypeReader { attributeType += " (" + allowedNotations.join("|") + ")"; } else { // Handle simple types (e.g., CDATA, ID, IDREF, etc.) + const startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - attributeType += xmlData[i]; i++; } + attributeType += xmlData.substring(startIndex, i); // Validate simple attribute type const validTypes = ["CDATA", "ID", "IDREF", "IDREFS", "ENTITY", "ENTITIES", "NMTOKEN", "NMTOKENS"]; @@ -54166,115 +54762,124 @@ function hasSeq(data, seq, i) { return true; } -function validateEntityName(name) { - if (isName(name)) +function validateEntityName(name, xmlVersion) { + if (qName(name, { xmlVersion: xmlVersion })) return name; else throw new Error(`Invalid entity name ${name}`); } ;// CONCATENATED MODULE: ./node_modules/strnum/strnum.js const hexRegex = /^[-+]?0x[a-fA-F0-9]+$/; +const binRegex = /^0b[01]+$/; +const octRegex = /^0o[0-7]+$/; const numRegex = /^([\-\+])?(0*)([0-9]*(\.[0-9]*)?)$/; -// const octRegex = /^0x[a-z0-9]+/; -// const binRegex = /0x[a-z0-9]+/; - const consider = { - hex : true, - // oct: false, + hex: true, + binary: false, + octal: false, leadingZeros: true, decimalPoint: "\.", eNotation: true, - //skipLike: /regex/ + //skipLike: /regex/, + infinity: "original", // "null", "infinity" (Infinity type), "string" ("Infinity" (the string literal)) }; -function toNumber(str, options = {}){ - options = Object.assign({}, consider, options ); - if(!str || typeof str !== "string" ) return str; - - let trimmedStr = str.trim(); - - if(options.skipLike !== undefined && options.skipLike.test(trimmedStr)) return str; - else if(str==="0") return 0; +function toNumber(str, options = {}) { + options = Object.assign({}, consider, options); + if (!str || typeof str !== "string") return str; + + let trimmedStr = str.trim(); + + if (trimmedStr.length === 0) return str; + else if (options.skipLike !== undefined && options.skipLike.test(trimmedStr)) return str; + else if (trimmedStr === "0") return 0; else if (options.hex && hexRegex.test(trimmedStr)) { return parse_int(trimmedStr, 16); - // }else if (options.oct && octRegex.test(str)) { - // return Number.parseInt(val, 8); - }else if (trimmedStr.includes('e') || trimmedStr.includes('E')) { //eNotation - return resolveEnotation(str,trimmedStr,options); - // }else if (options.parseBin && binRegex.test(str)) { - // return Number.parseInt(val, 2); - }else{ + } else if (options.binary && binRegex.test(trimmedStr)) { + return parse_int(trimmedStr, 2); + } else if (options.octal && octRegex.test(trimmedStr)) { + return parse_int(trimmedStr, 8); + } else if (!isFinite(trimmedStr)) { //Infinity + return handleInfinity(str, Number(trimmedStr), options); + } else if (trimmedStr.includes('e') || trimmedStr.includes('E')) { //eNotation + return resolveEnotation(str, trimmedStr, options); + } else { //separate negative sign, leading zeros, and rest number const match = numRegex.exec(trimmedStr); // +00.123 => [ , '+', '00', '.123', .. - if(match){ + if (match) { const sign = match[1] || ""; const leadingZeros = match[2]; let numTrimmedByZeros = trimZeros(match[3]); //complete num without leading zeros const decimalAdjacentToLeadingZeros = sign ? // 0., -00., 000. - str[leadingZeros.length+1] === "." + str[leadingZeros.length + 1] === "." : str[leadingZeros.length] === "."; //trim ending zeros for floating number - if(!options.leadingZeros //leading zeros are not allowed - && (leadingZeros.length > 1 - || (leadingZeros.length === 1 && !decimalAdjacentToLeadingZeros))){ + if (!options.leadingZeros //leading zeros are not allowed + && (leadingZeros.length > 1 + || (leadingZeros.length === 1 && !decimalAdjacentToLeadingZeros))) { // 00, 00.3, +03.24, 03, 03.24 return str; } - else{//no leading zeros or leading zeros are allowed + else {//no leading zeros or leading zeros are allowed const num = Number(trimmedStr); const parsedStr = String(num); - if( num === 0) return num; - if(parsedStr.search(/[eE]/) !== -1){ //given number is long and parsed to eNotation - if(options.eNotation) return num; + if (num === 0) return num; + if (parsedStr.search(/[eE]/) !== -1) { //given number is long and parsed to eNotation + if (options.eNotation) return num; else return str; - }else if(trimmedStr.indexOf(".") !== -1){ //floating number - if(parsedStr === "0") return num; //0.0 - else if(parsedStr === numTrimmedByZeros) return num; //0.456. 0.79000 - else if( parsedStr === `${sign}${numTrimmedByZeros}`) return num; + } else if (trimmedStr.indexOf(".") !== -1) { //floating number + if (parsedStr === "0") return num; //0.0 + else if (parsedStr === numTrimmedByZeros) return num; //0.456. 0.79000 + else if (parsedStr === `${sign}${numTrimmedByZeros}`) return num; else return str; } - - let n = leadingZeros? numTrimmedByZeros : trimmedStr; - if(leadingZeros){ + + let n = leadingZeros ? numTrimmedByZeros : trimmedStr; + if (leadingZeros) { // -009 => -9 - return (n === parsedStr) || (sign+n === parsedStr) ? num : str - }else { + return (n === parsedStr) || (sign + n === parsedStr) ? num : str + } else { // +9 - return (n === parsedStr) || (n === sign+parsedStr) ? num : str + return (n === parsedStr) || (n === sign + parsedStr) ? num : str } } - }else{ //non-numeric string + } else { //non-numeric string return str; } } } const eNotationRegx = /^([-+])?(0*)(\d*(\.\d*)?[eE][-\+]?\d+)$/; -function resolveEnotation(str,trimmedStr,options){ - if(!options.eNotation) return str; - const notation = trimmedStr.match(eNotationRegx); - if(notation){ +function resolveEnotation(str, trimmedStr, options) { + if (!options.eNotation) return str; + const notation = trimmedStr.match(eNotationRegx); + if (notation) { let sign = notation[1] || ""; const eChar = notation[3].indexOf("e") === -1 ? "E" : "e"; const leadingZeros = notation[2]; const eAdjacentToLeadingZeros = sign ? // 0E. - str[leadingZeros.length+1] === eChar + str[leadingZeros.length + 1] === eChar : str[leadingZeros.length] === eChar; - if(leadingZeros.length > 1 && eAdjacentToLeadingZeros) return str; - else if(leadingZeros.length === 1 - && (notation[3].startsWith(`.${eChar}`) || notation[3][0] === eChar)){ - return Number(trimmedStr); - }else if(options.leadingZeros && !eAdjacentToLeadingZeros){ //accept with leading zeros - //remove leading 0s - trimmedStr = (notation[1] || "") + notation[3]; + if (leadingZeros.length > 1 && eAdjacentToLeadingZeros) return str; + else if (leadingZeros.length === 1 + && (notation[3].startsWith(`.${eChar}`) || notation[3][0] === eChar)) { return Number(trimmedStr); - }else return str; - }else{ + } else if (leadingZeros.length > 0) { + // Has leading zeros — only accept if leadingZeros option allows it + if (options.leadingZeros && !eAdjacentToLeadingZeros) { + trimmedStr = (notation[1] || "") + notation[3]; + return Number(trimmedStr); + } else return str; + } else { + // No leading zeros — always valid e-notation, parse it + return Number(trimmedStr); + } + } else { return str; } } @@ -54284,23 +54889,48 @@ function resolveEnotation(str,trimmedStr,options){ * @param {string} numStr without leading zeros * @returns */ -function trimZeros(numStr){ - if(numStr && numStr.indexOf(".") !== -1){//float +function trimZeros(numStr) { + if (numStr && numStr.indexOf(".") !== -1) {//float numStr = numStr.replace(/0+$/, ""); //remove ending zeros - if(numStr === ".") numStr = "0"; - else if(numStr[0] === ".") numStr = "0"+numStr; - else if(numStr[numStr.length-1] === ".") numStr = numStr.substring(0,numStr.length-1); + if (numStr === ".") numStr = "0"; + else if (numStr[0] === ".") numStr = "0" + numStr; + else if (numStr[numStr.length - 1] === ".") numStr = numStr.substring(0, numStr.length - 1); return numStr; } return numStr; } -function parse_int(numStr, base){ - //polyfill - if(parseInt) return parseInt(numStr, base); - else if(Number.parseInt) return Number.parseInt(numStr, base); - else if(window && window.parseInt) return window.parseInt(numStr, base); - else throw new Error("parseInt, Number.parseInt, window.parseInt are not supported") +function parse_int(numStr, base) { + const str = numStr.trim(); + if (base === 2 || base === 8) numStr = str.substring(2); + + if (parseInt) return parseInt(numStr, base); + else if (Number.parseInt) return Number.parseInt(numStr, base); + else if (window && window.parseInt) return window.parseInt(numStr, base); + else throw new Error("parseInt, Number.parseInt, window.parseInt are not supported"); +} + +/** + * Handle infinite values based on user option + * @param {string} str - original input string + * @param {number} num - parsed number (Infinity or -Infinity) + * @param {object} options - user options + * @returns {string|number|null} based on infinity option + */ +function handleInfinity(str, num, options) { + const isPositive = num === Infinity; + + switch (options.infinity.toLowerCase()) { + case "null": + return null; + case "infinity": + return num; // Return Infinity or -Infinity + case "string": + return isPositive ? "Infinity" : "-Infinity"; + case "original": + default: + return str; // Return original string like "1e1000" + } } ;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/ignoreAttributes.js function ignoreAttributes_getIgnoreAttributesFn(ignoreAttributes) { @@ -54321,6 +54951,1939 @@ function ignoreAttributes_getIgnoreAttributesFn(ignoreAttributes) { } return () => false } +;// CONCATENATED MODULE: ./node_modules/path-expression-matcher/src/ExpressionSet.js +/** + * ExpressionSet - An indexed collection of Expressions for efficient bulk matching + * + * Instead of iterating all expressions on every tag, ExpressionSet pre-indexes + * them at insertion time by depth and terminal tag name. At match time, only + * the relevant bucket is evaluated — typically reducing checks from O(E) to O(1) + * lookup plus O(small bucket) matches. + * + * Three buckets are maintained: + * - `_byDepthAndTag` — exact depth + exact tag name (tightest, used first) + * - `_wildcardByDepth` — exact depth + wildcard tag `*` (depth-matched only) + * - `_deepWildcards` — expressions containing `..` (cannot be depth-indexed) + * + * @example + * import { Expression, ExpressionSet } from 'fast-xml-tagger'; + * + * // Build once at config time + * const stopNodes = new ExpressionSet(); + * stopNodes.add(new Expression('root.users.user')); + * stopNodes.add(new Expression('root.config.setting')); + * stopNodes.add(new Expression('..script')); + * + * // Query on every tag — hot path + * if (stopNodes.matchesAny(matcher)) { ... } + */ +class ExpressionSet { + constructor() { + /** @type {Map} depth:tag → expressions */ + this._byDepthAndTag = new Map(); + + /** @type {Map} depth → wildcard-tag expressions */ + this._wildcardByDepth = new Map(); + + /** @type {import('./Expression.js').default[]} expressions containing deep wildcard (..) */ + this._deepWildcards = []; + + /** @type {Set} pattern strings already added — used for deduplication */ + this._patterns = new Set(); + + /** @type {boolean} whether the set is sealed against further additions */ + this._sealed = false; + } + + /** + * Add an Expression to the set. + * Duplicate patterns (same pattern string) are silently ignored. + * + * @param {import('./Expression.js').default} expression - A pre-constructed Expression instance + * @returns {this} for chaining + * @throws {TypeError} if called after seal() + * + * @example + * set.add(new Expression('root.users.user')); + * set.add(new Expression('..script')); + */ + add(expression) { + if (this._sealed) { + throw new TypeError( + 'ExpressionSet is sealed. Create a new ExpressionSet to add more expressions.' + ); + } + + // Deduplicate by pattern string + if (this._patterns.has(expression.pattern)) return this; + this._patterns.add(expression.pattern); + + if (expression.hasDeepWildcard()) { + this._deepWildcards.push(expression); + return this; + } + + const depth = expression.length; + const lastSeg = expression.segments[expression.segments.length - 1]; + const tag = lastSeg?.tag; + + if (!tag || tag === '*') { + // Can index by depth but not by tag + if (!this._wildcardByDepth.has(depth)) this._wildcardByDepth.set(depth, []); + this._wildcardByDepth.get(depth).push(expression); + } else { + // Tightest bucket: depth + tag + const key = `${depth}:${tag}`; + if (!this._byDepthAndTag.has(key)) this._byDepthAndTag.set(key, []); + this._byDepthAndTag.get(key).push(expression); + } + + return this; + } + + /** + * Add multiple expressions at once. + * + * @param {import('./Expression.js').default[]} expressions - Array of Expression instances + * @returns {this} for chaining + * + * @example + * set.addAll([ + * new Expression('root.users.user'), + * new Expression('root.config.setting'), + * ]); + */ + addAll(expressions) { + for (const expr of expressions) this.add(expr); + return this; + } + + /** + * Check whether a pattern string is already present in the set. + * + * @param {import('./Expression.js').default} expression + * @returns {boolean} + */ + has(expression) { + return this._patterns.has(expression.pattern); + } + + /** + * Number of expressions in the set. + * @type {number} + */ + get size() { + return this._patterns.size; + } + + /** + * Seal the set against further modifications. + * Useful to prevent accidental mutations after config is built. + * Calling add() or addAll() on a sealed set throws a TypeError. + * + * @returns {this} + */ + seal() { + this._sealed = true; + return this; + } + + /** + * Whether the set has been sealed. + * @type {boolean} + */ + get isSealed() { + return this._sealed; + } + + /** + * Test whether the matcher's current path matches any expression in the set. + * + * Evaluation order (cheapest → most expensive): + * 1. Exact depth + tag bucket — O(1) lookup, typically 0–2 expressions + * 2. Depth-only wildcard bucket — O(1) lookup, rare + * 3. Deep-wildcard list — always checked, but usually small + * + * @param {import('./Matcher.js').default} matcher - Matcher instance (or readOnly view) + * @returns {boolean} true if any expression matches the current path + * + * @example + * if (stopNodes.matchesAny(matcher)) { + * // handle stop node + * } + */ + matchesAny(matcher) { + return this.findMatch(matcher) !== null; + } + /** + * Find and return the first Expression that matches the matcher's current path. + * + * Uses the same evaluation order as matchesAny (cheapest → most expensive): + * 1. Exact depth + tag bucket + * 2. Depth-only wildcard bucket + * 3. Deep-wildcard list + * + * @param {import('./Matcher.js').default} matcher - Matcher instance (or readOnly view) + * @returns {import('./Expression.js').default | null} the first matching Expression, or null + * + * @example + * const expr = stopNodes.findMatch(matcher); + * if (expr) { + * // access expr.config, expr.pattern, etc. + * } + */ + findMatch(matcher) { + const depth = matcher.getDepth(); + const tag = matcher.getCurrentTag(); + + // 1. Tightest bucket — most expressions live here + const exactKey = `${depth}:${tag}`; + const exactBucket = this._byDepthAndTag.get(exactKey); + if (exactBucket) { + for (let i = 0; i < exactBucket.length; i++) { + if (matcher.matches(exactBucket[i])) return exactBucket[i]; + } + } + + // 2. Depth-matched wildcard-tag expressions + const wildcardBucket = this._wildcardByDepth.get(depth); + if (wildcardBucket) { + for (let i = 0; i < wildcardBucket.length; i++) { + if (matcher.matches(wildcardBucket[i])) return wildcardBucket[i]; + } + } + + // 3. Deep wildcards — cannot be pre-filtered by depth or tag + for (let i = 0; i < this._deepWildcards.length; i++) { + if (matcher.matches(this._deepWildcards[i])) return this._deepWildcards[i]; + } + + return null; + } +} + +;// CONCATENATED MODULE: ./node_modules/@nodable/entities/src/entities.js +// --------------------------------------------------------------------------- +// Complete HTML5 named entity reference +// Organized by logical categories for easy maintenance and selective importing +// --------------------------------------------------------------------------- + +/** + * Basic Latin & Special Characters + * @type {Record} + */ +const BASIC_LATIN = { + amp: '&', + AMP: '&', + lt: '<', + LT: '<', + gt: '>', + GT: '>', + quot: '"', + QUOT: '"', + apos: "'", + lsquo: '‘', + rsquo: '’', + ldquo: '“', + rdquo: '”', + lsquor: '‚', + rsquor: '’', + ldquor: '„', + bdquo: '„', + comma: ',', + period: '.', + colon: ':', + semi: ';', + excl: '!', + quest: '?', + num: '#', + dollar: '$', + percent: '%', + amp: '&', + ast: '*', + commat: '@', + lowbar: '_', + verbar: '|', + vert: '|', + sol: '/', + bsol: '\\', + lbrace: '{', + rbrace: '}', + lbrack: '[', + rbrack: ']', + lpar: '(', + rpar: ')', + nbsp: '\u00a0', + iexcl: '¡', + cent: '¢', + pound: '£', + curren: '¤', + yen: '¥', + brvbar: '¦', + sect: '§', + uml: '¨', + copy: '©', + COPY: '©', + ordf: 'ª', + laquo: '«', + not: '¬', + shy: '\u00ad', + reg: '®', + REG: '®', + macr: '¯', + deg: '°', + plusmn: '±', + sup2: '²', + sup3: '³', + acute: '´', + micro: 'µ', + para: '¶', + middot: '·', + cedil: '¸', + sup1: '¹', + ordm: 'º', + raquo: '»', + frac14: '¼', + frac12: '½', + half: '½', + frac34: '¾', + iquest: '¿', + times: '×', + div: '÷', + divide: '÷', +}; + +/** + * Latin Extended & Accented Letters (A-Z) + * @type {Record} + */ +const LATIN_ACCENTS = { + Agrave: 'À', + agrave: 'à', + Aacute: 'Á', + aacute: 'á', + Acirc: 'Â', + acirc: 'â', + Atilde: 'Ã', + atilde: 'ã', + Auml: 'Ä', + auml: 'ä', + Aring: 'Å', + aring: 'å', + AElig: 'Æ', + aelig: 'æ', + Ccedil: 'Ç', + ccedil: 'ç', + Egrave: 'È', + egrave: 'è', + Eacute: 'É', + eacute: 'é', + Ecirc: 'Ê', + ecirc: 'ê', + Euml: 'Ë', + euml: 'ë', + Igrave: 'Ì', + igrave: 'ì', + Iacute: 'Í', + iacute: 'í', + Icirc: 'Î', + icirc: 'î', + Iuml: 'Ï', + iuml: 'ï', + ETH: 'Ð', + eth: 'ð', + Ntilde: 'Ñ', + ntilde: 'ñ', + Ograve: 'Ò', + ograve: 'ò', + Oacute: 'Ó', + oacute: 'ó', + Ocirc: 'Ô', + ocirc: 'ô', + Otilde: 'Õ', + otilde: 'õ', + Ouml: 'Ö', + ouml: 'ö', + Oslash: 'Ø', + oslash: 'ø', + Ugrave: 'Ù', + ugrave: 'ù', + Uacute: 'Ú', + uacute: 'ú', + Ucirc: 'Û', + ucirc: 'û', + Uuml: 'Ü', + uuml: 'ü', + Yacute: 'Ý', + yacute: 'ý', + THORN: 'Þ', + thorn: 'þ', + szlig: 'ß', + yuml: 'ÿ', + Yuml: 'Ÿ', +}; + +/** + * Latin Extended (Letters with diacritics) + * @type {Record} + */ +const LATIN_EXTENDED = { + Amacr: 'Ā', + amacr: 'ā', + Abreve: 'Ă', + abreve: 'ă', + Aogon: 'Ą', + aogon: 'ą', + Cacute: 'Ć', + cacute: 'ć', + Ccirc: 'Ĉ', + ccirc: 'ĉ', + Cdot: 'Ċ', + cdot: 'ċ', + Ccaron: 'Č', + ccaron: 'č', + Dcaron: 'Ď', + dcaron: 'ď', + Dstrok: 'Đ', + dstrok: 'đ', + Emacr: 'Ē', + emacr: 'ē', + Ecaron: 'Ě', + ecaron: 'ě', + Edot: 'Ė', + edot: 'ė', + Eogon: 'Ę', + eogon: 'ę', + Gcirc: 'Ĝ', + gcirc: 'ĝ', + Gbreve: 'Ğ', + gbreve: 'ğ', + Gdot: 'Ġ', + gdot: 'ġ', + Gcedil: 'Ģ', + Hcirc: 'Ĥ', + hcirc: 'ĥ', + Hstrok: 'Ħ', + hstrok: 'ħ', + Itilde: 'Ĩ', + itilde: 'ĩ', + Imacr: 'Ī', + imacr: 'ī', + Iogon: 'Į', + iogon: 'į', + Idot: 'İ', + IJlig: 'IJ', + ijlig: 'ij', + Jcirc: 'Ĵ', + jcirc: 'ĵ', + Kcedil: 'Ķ', + kcedil: 'ķ', + kgreen: 'ĸ', + Lacute: 'Ĺ', + lacute: 'ĺ', + Lcedil: 'Ļ', + lcedil: 'ļ', + Lcaron: 'Ľ', + lcaron: 'ľ', + Lmidot: 'Ŀ', + lmidot: 'ŀ', + Lstrok: 'Ł', + lstrok: 'ł', + Nacute: 'Ń', + nacute: 'ń', + Ncaron: 'Ň', + ncaron: 'ň', + Ncedil: 'Ņ', + ncedil: 'ņ', + ENG: 'Ŋ', + eng: 'ŋ', + Omacr: 'Ō', + omacr: 'ō', + Odblac: 'Ő', + odblac: 'ő', + OElig: 'Œ', + oelig: 'œ', + Racute: 'Ŕ', + racute: 'ŕ', + Rcaron: 'Ř', + rcaron: 'ř', + Rcedil: 'Ŗ', + rcedil: 'ŗ', + Sacute: 'Ś', + sacute: 'ś', + Scirc: 'Ŝ', + scirc: 'ŝ', + Scedil: 'Ş', + scedil: 'ş', + Scaron: 'Š', + scaron: 'š', + Tcedil: 'Ţ', + tcedil: 'ţ', + Tcaron: 'Ť', + tcaron: 'ť', + Tstrok: 'Ŧ', + tstrok: 'ŧ', + Utilde: 'Ũ', + utilde: 'ũ', + Umacr: 'Ū', + umacr: 'ū', + Ubreve: 'Ŭ', + ubreve: 'ŭ', + Uring: 'Ů', + uring: 'ů', + Udblac: 'Ű', + udblac: 'ű', + Uogon: 'Ų', + uogon: 'ų', + Wcirc: 'Ŵ', + wcirc: 'ŵ', + Ycirc: 'Ŷ', + ycirc: 'ŷ', + Zacute: 'Ź', + zacute: 'ź', + Zdot: 'Ż', + zdot: 'ż', + Zcaron: 'Ž', + zcaron: 'ž', +}; + +/** + * Greek Letters + * @type {Record} + */ +const GREEK = { + Alpha: 'Α', + alpha: 'α', + Beta: 'Β', + beta: 'β', + Gamma: 'Γ', + gamma: 'γ', + Delta: 'Δ', + delta: 'δ', + Epsilon: 'Ε', + epsilon: 'ε', + epsiv: 'ϵ', + varepsilon: 'ϵ', + Zeta: 'Ζ', + zeta: 'ζ', + Eta: 'Η', + eta: 'η', + Theta: 'Θ', + theta: 'θ', + thetasym: 'ϑ', + vartheta: 'ϑ', + Iota: 'Ι', + iota: 'ι', + Kappa: 'Κ', + kappa: 'κ', + kappav: 'ϰ', + varkappa: 'ϰ', + Lambda: 'Λ', + lambda: 'λ', + Mu: 'Μ', + mu: 'μ', + Nu: 'Ν', + nu: 'ν', + Xi: 'Ξ', + xi: 'ξ', + Omicron: 'Ο', + omicron: 'ο', + Pi: 'Π', + pi: 'π', + piv: 'ϖ', + varpi: 'ϖ', + Rho: 'Ρ', + rho: 'ρ', + rhov: 'ϱ', + varrho: 'ϱ', + Sigma: 'Σ', + sigma: 'σ', + sigmaf: 'ς', + sigmav: 'ς', + varsigma: 'ς', + Tau: 'Τ', + tau: 'τ', + Upsilon: 'Υ', + upsilon: 'υ', + upsi: 'υ', + Upsi: 'ϒ', + upsih: 'ϒ', + Phi: 'Φ', + phi: 'φ', + phiv: 'ϕ', + varphi: 'ϕ', + Chi: 'Χ', + chi: 'χ', + Psi: 'Ψ', + psi: 'ψ', + Omega: 'Ω', + omega: 'ω', + ohm: 'Ω', + Gammad: 'Ϝ', + gammad: 'ϝ', + digamma: 'ϝ', +}; + +/** + * Cyrillic Letters + * @type {Record} + */ +const CYRILLIC = { + Afr: '𝔄', + afr: '𝔞', + Acy: 'А', + acy: 'а', + Bcy: 'Б', + bcy: 'б', + Vcy: 'В', + vcy: 'в', + Gcy: 'Г', + gcy: 'г', + Dcy: 'Д', + dcy: 'д', + IEcy: 'Е', + iecy: 'е', + IOcy: 'Ё', + iocy: 'ё', + ZHcy: 'Ж', + zhcy: 'ж', + Zcy: 'З', + zcy: 'з', + Icy: 'И', + icy: 'и', + Jcy: 'Й', + jcy: 'й', + Kcy: 'К', + kcy: 'к', + Lcy: 'Л', + lcy: 'л', + Mcy: 'М', + mcy: 'м', + Ncy: 'Н', + ncy: 'н', + Ocy: 'О', + ocy: 'о', + Pcy: 'П', + pcy: 'п', + Rcy: 'Р', + rcy: 'р', + Scy: 'С', + scy: 'с', + Tcy: 'Т', + tcy: 'т', + Ucy: 'У', + ucy: 'у', + Fcy: 'Ф', + fcy: 'ф', + KHcy: 'Х', + khcy: 'х', + TScy: 'Ц', + tscy: 'ц', + CHcy: 'Ч', + chcy: 'ч', + SHcy: 'Ш', + shcy: 'ш', + SHCHcy: 'Щ', + shchcy: 'щ', + HARDcy: 'Ъ', + hardcy: 'ъ', + Ycy: 'Ы', + ycy: 'ы', + SOFTcy: 'Ь', + softcy: 'ь', + Ecy: 'Э', + ecy: 'э', + YUcy: 'Ю', + yucy: 'ю', + YAcy: 'Я', + yacy: 'я', + DJcy: 'Ђ', + djcy: 'ђ', + GJcy: 'Ѓ', + gjcy: 'ѓ', + Jukcy: 'Є', + jukcy: 'є', + DScy: 'Ѕ', + dscy: 'ѕ', + Iukcy: 'І', + iukcy: 'і', + YIcy: 'Ї', + yicy: 'ї', + Jsercy: 'Ј', + jsercy: 'ј', + LJcy: 'Љ', + ljcy: 'љ', + NJcy: 'Њ', + njcy: 'њ', + TSHcy: 'Ћ', + tshcy: 'ћ', + KJcy: 'Ќ', + kjcy: 'ќ', + Ubrcy: 'Ў', + ubrcy: 'ў', + DZcy: 'Џ', + dzcy: 'џ', +}; + +/** + * Mathematical Operators & Relations + * @type {Record} + */ +const MATH = { + plus: '+', + minus: '−', + mnplus: '∓', + mp: '∓', + pm: '±', + times: '×', + div: '÷', + divide: '÷', + sdot: '⋅', + star: '☆', + starf: '★', + bigstar: '★', + lowast: '∗', + ast: '*', + midast: '*', + compfn: '∘', + smallcircle: '∘', + bullet: '•', + bull: '•', + nbsp: '\u00a0', + hellip: '…', + mldr: '…', + prime: '′', + Prime: '″', + tprime: '‴', + bprime: '‵', + backprime: '‵', + minus: '−', + minusd: '∸', + dotminus: '∸', + plusdo: '∔', + dotplus: '∔', + plusmn: '±', + minusplus: '∓', + mnplus: '∓', + mp: '∓', + setminus: '∖', + smallsetminus: '∖', + Backslash: '∖', + setmn: '∖', + ssetmn: '∖', + lowbar: '_', + verbar: '|', + vert: '|', + VerticalLine: '|', + colon: ':', + Colon: '∷', + Proportion: '∷', + ratio: '∶', + equals: '=', + ne: '≠', + nequiv: '≢', + equiv: '≡', + Congruent: '≡', + sim: '∼', + thicksim: '∼', + thksim: '∼', + sime: '≃', + simeq: '≃', + TildeEqual: '≃', + asymp: '≈', + approx: '≈', + thickapprox: '≈', + thkap: '≈', + TildeTilde: '≈', + ncong: '≇', + cong: '≅', + TildeFullEqual: '≅', + asympeq: '≍', + CupCap: '≍', + bump: '≎', + Bumpeq: '≎', + HumpDownHump: '≎', + bumpe: '≏', + bumpeq: '≏', + HumpEqual: '≏', + dotminus: '∸', + minusd: '∸', + plusdo: '∔', + dotplus: '∔', + le: '≤', + LessEqual: '≤', + ge: '≥', + GreaterEqual: '≥', + lesseqgtr: '⋚', + lesseqqgtr: '⪋', + greater: '>', + less: '<', +}; + +/** + * Mathematical Operators (Advanced) + * @type {Record} + */ +const MATH_ADVANCED = { + alefsym: 'ℵ', + aleph: 'ℵ', + beth: 'ℶ', + gimel: 'ℷ', + daleth: 'ℸ', + forall: '∀', + ForAll: '∀', + part: '∂', + PartialD: '∂', + exist: '∃', + Exists: '∃', + nexist: '∄', + nexists: '∄', + empty: '∅', + emptyset: '∅', + emptyv: '∅', + varnothing: '∅', + nabla: '∇', + Del: '∇', + isin: '∈', + isinv: '∈', + in: '∈', + Element: '∈', + notin: '∉', + notinva: '∉', + ni: '∋', + niv: '∋', + SuchThat: '∋', + ReverseElement: '∋', + notni: '∌', + notniva: '∌', + prod: '∏', + Product: '∏', + coprod: '∐', + Coproduct: '∐', + sum: '∑', + Sum: '∑', + minus: '−', + mp: '∓', + plusdo: '∔', + dotplus: '∔', + setminus: '∖', + lowast: '∗', + radic: '√', + Sqrt: '√', + prop: '∝', + propto: '∝', + Proportional: '∝', + varpropto: '∝', + infin: '∞', + infintie: '⧝', + ang: '∠', + angle: '∠', + angmsd: '∡', + measuredangle: '∡', + angsph: '∢', + mid: '∣', + VerticalBar: '∣', + nmid: '∤', + nsmid: '∤', + npar: '∦', + parallel: '∥', + spar: '∥', + nparallel: '∦', + nspar: '∦', + and: '∧', + wedge: '∧', + or: '∨', + vee: '∨', + cap: '∩', + cup: '∪', + int: '∫', + Integral: '∫', + conint: '∮', + ContourIntegral: '∮', + Conint: '∯', + DoubleContourIntegral: '∯', + Cconint: '∰', + there4: '∴', + therefore: '∴', + Therefore: '∴', + becaus: '∵', + because: '∵', + Because: '∵', + ratio: '∶', + Proportion: '∷', + minusd: '∸', + dotminus: '∸', + mDDot: '∺', + homtht: '∻', + sim: '∼', + bsimg: '∽', + backsim: '∽', + ac: '∾', + mstpos: '∾', + acd: '∿', + VerticalTilde: '≀', + wr: '≀', + wreath: '≀', + nsime: '≄', + nsimeq: '≄', + nsimeq: '≄', + ncong: '≇', + simne: '≆', + ncongdot: '⩭̸', + ngsim: '≵', + nsim: '≁', + napprox: '≉', + nap: '≉', + ngeq: '≱', + nge: '≱', + nleq: '≰', + nle: '≰', + ngtr: '≯', + ngt: '≯', + nless: '≮', + nlt: '≮', + nprec: '⊀', + npr: '⊀', + nsucc: '⊁', + nsc: '⊁', +}; + +/** + * Arrows + * @type {Record} + */ +const ARROWS = { + larr: '←', + leftarrow: '←', + LeftArrow: '←', + uarr: '↑', + uparrow: '↑', + UpArrow: '↑', + rarr: '→', + rightarrow: '→', + RightArrow: '→', + darr: '↓', + downarrow: '↓', + DownArrow: '↓', + harr: '↔', + leftrightarrow: '↔', + LeftRightArrow: '↔', + varr: '↕', + updownarrow: '↕', + UpDownArrow: '↕', + nwarr: '↖', + nwarrow: '↖', + UpperLeftArrow: '↖', + nearr: '↗', + nearrow: '↗', + UpperRightArrow: '↗', + searr: '↘', + searrow: '↘', + LowerRightArrow: '↘', + swarr: '↙', + swarrow: '↙', + LowerLeftArrow: '↙', + lArr: '⇐', + Leftarrow: '⇐', + uArr: '⇑', + Uparrow: '⇑', + rArr: '⇒', + Rightarrow: '⇒', + dArr: '⇓', + Downarrow: '⇓', + hArr: '⇔', + Leftrightarrow: '⇔', + iff: '⇔', + vArr: '⇕', + Updownarrow: '⇕', + lAarr: '⇚', + Lleftarrow: '⇚', + rAarr: '⇛', + Rrightarrow: '⇛', + lrarr: '⇆', + leftrightarrows: '⇆', + rlarr: '⇄', + rightleftarrows: '⇄', + lrhar: '⇋', + leftrightharpoons: '⇋', + ReverseEquilibrium: '⇋', + rlhar: '⇌', + rightleftharpoons: '⇌', + Equilibrium: '⇌', + udarr: '⇅', + UpArrowDownArrow: '⇅', + duarr: '⇵', + DownArrowUpArrow: '⇵', + llarr: '⇇', + leftleftarrows: '⇇', + rrarr: '⇉', + rightrightarrows: '⇉', + ddarr: '⇊', + downdownarrows: '⇊', + har: '↽', + lhard: '↽', + leftharpoondown: '↽', + lharu: '↼', + leftharpoonup: '↼', + rhard: '⇁', + rightharpoondown: '⇁', + rharu: '⇀', + rightharpoonup: '⇀', + lsh: '↰', + Lsh: '↰', + rsh: '↱', + Rsh: '↱', + ldsh: '↲', + rdsh: '↳', + hookleftarrow: '↩', + hookrightarrow: '↪', + mapstoleft: '↤', + mapstoup: '↥', + map: '↦', + mapsto: '↦', + mapstodown: '↧', + crarr: '↵', + nwarrow: '↖', + nearrow: '↗', + searrow: '↘', + swarrow: '↙', + nleftarrow: '↚', + nleftrightarrow: '↮', + nrightarrow: '↛', + nrarr: '↛', + larrtl: '↢', + rarrtl: '↣', + leftarrowtail: '↢', + rightarrowtail: '↣', + twoheadleftarrow: '↞', + twoheadrightarrow: '↠', + Larr: '↞', + Rarr: '↠', + larrhk: '↩', + rarrhk: '↪', + larrlp: '↫', + looparrowleft: '↫', + rarrlp: '↬', + looparrowright: '↬', + harrw: '↭', + leftrightsquigarrow: '↭', + nrarrw: '↝̸', + rarrw: '↝', + rightsquigarrow: '↝', + larrbfs: '⤟', + rarrbfs: '⤠', + nvHarr: '⤄', + nvlArr: '⤂', + nvrArr: '⤃', + larrfs: '⤝', + rarrfs: '⤞', + Map: '⤅', + larrsim: '⥳', + rarrsim: '⥴', + harrcir: '⥈', + Uarrocir: '⥉', + lurdshar: '⥊', + ldrdhar: '⥧', + ldrushar: '⥋', + rdldhar: '⥩', + lrhard: '⥭', + rlhar: '⇌', + uharr: '↾', + uharl: '↿', + dharr: '⇂', + dharl: '⇃', + Uarr: '↟', + Darr: '↡', + zigrarr: '⇝', + nwArr: '⇖', + neArr: '⇗', + seArr: '⇘', + swArr: '⇙', + nharr: '↮', + nhArr: '⇎', + nlarr: '↚', + nlArr: '⇍', + nrarr: '↛', + nrArr: '⇏', + larrb: '⇤', + LeftArrowBar: '⇤', + rarrb: '⇥', + RightArrowBar: '⇥', +}; + +/** + * Geometric Shapes + * @type {Record} + */ +const SHAPES = { + square: '□', + Square: '□', + squ: '□', + squf: '▪', + squarf: '▪', + blacksquar: '▪', + blacksquare: '▪', + FilledVerySmallSquare: '▪', + blk34: '▓', + blk12: '▒', + blk14: '░', + block: '█', + srect: '▭', + rect: '▭', + sdot: '⋅', + sdotb: '⊡', + dotsquare: '⊡', + triangle: '▵', + tri: '▵', + trine: '▵', + utri: '▵', + triangledown: '▿', + dtri: '▿', + tridown: '▿', + triangleleft: '◃', + ltri: '◃', + triangleright: '▹', + rtri: '▹', + blacktriangle: '▴', + utrif: '▴', + blacktriangledown: '▾', + dtrif: '▾', + blacktriangleleft: '◂', + ltrif: '◂', + blacktriangleright: '▸', + rtrif: '▸', + loz: '◊', + lozenge: '◊', + blacklozenge: '⧫', + lozf: '⧫', + bigcirc: '◯', + xcirc: '◯', + circ: 'ˆ', + Circle: '○', + cir: '○', + o: '○', + bullet: '•', + bull: '•', + hellip: '…', + mldr: '…', + nldr: '‥', + boxh: '─', + HorizontalLine: '─', + boxv: '│', + boxdr: '┌', + boxdl: '┐', + boxur: '└', + boxul: '┘', + boxvr: '├', + boxvl: '┤', + boxhd: '┬', + boxhu: '┴', + boxvh: '┼', + boxH: '═', + boxV: '║', + boxdR: '╒', + boxDr: '╓', + boxDR: '╔', + boxDl: '╕', + boxdL: '╖', + boxDL: '╗', + boxuR: '╘', + boxUr: '╙', + boxUR: '╚', + boxUl: '╜', + boxuL: '╛', + boxUL: '╝', + boxvR: '╞', + boxVr: '╟', + boxVR: '╠', + boxVl: '╢', + boxvL: '╡', + boxVL: '╣', + boxHd: '╤', + boxhD: '╥', + boxHD: '╦', + boxHu: '╧', + boxhU: '╨', + boxHU: '╩', + boxvH: '╪', + boxVh: '╫', + boxVH: '╬', +}; + +/** + * Punctuation & Diacritics + * @type {Record} + */ +const PUNCTUATION = { + excl: '!', + iexcl: '¡', + brvbar: '¦', + sect: '§', + uml: '¨', + copy: '©', + ordf: 'ª', + laquo: '«', + not: '¬', + shy: '\u00ad', + reg: '®', + macr: '¯', + deg: '°', + plusmn: '±', + sup2: '²', + sup3: '³', + acute: '´', + micro: 'µ', + para: '¶', + middot: '·', + cedil: '¸', + sup1: '¹', + ordm: 'º', + raquo: '»', + frac14: '¼', + frac12: '½', + frac34: '¾', + iquest: '¿', + nbsp: '\u00a0', + comma: ',', + period: '.', + colon: ':', + semi: ';', + vert: '|', + Verbar: '‖', + verbar: '|', + dblac: '˝', + circ: 'ˆ', + caron: 'ˇ', + breve: '˘', + dot: '˙', + ring: '˚', + ogon: '˛', + tilde: '˜', + DiacriticalGrave: '`', + DiacriticalAcute: '´', + DiacriticalTilde: '˜', + DiacriticalDot: '˙', + DiacriticalDoubleAcute: '˝', + grave: '`', + acute: '´', +}; + +/** + * Currency Symbols + * @type {Record} + */ +const CURRENCY = { + cent: '¢', + pound: '£', + curren: '¤', + yen: '¥', + euro: '€', + dollar: '$', + euro: '€', + fnof: 'ƒ', + inr: '₹', + af: '؋', + birr: 'ብር', + peso: '₱', + rub: '₽', + won: '₩', + yuan: '¥', + cedil: '¸', +}; + +/** + * Fractions + * @type {Record} + */ +const FRACTIONS = { + frac12: '½', + half: '½', + frac13: '⅓', + frac14: '¼', + frac15: '⅕', + frac16: '⅙', + frac18: '⅛', + frac23: '⅔', + frac25: '⅖', + frac34: '¾', + frac35: '⅗', + frac38: '⅜', + frac45: '⅘', + frac56: '⅚', + frac58: '⅝', + frac78: '⅞', + frasl: '⁄', +}; + +/** + * Miscellaneous Symbols + * @type {Record} + */ +const MISC_SYMBOLS = { + trade: '™', + TRADE: '™', + telrec: '⌕', + target: '⌖', + ulcorn: '⌜', + ulcorner: '⌜', + urcorn: '⌝', + urcorner: '⌝', + dlcorn: '⌞', + llcorner: '⌞', + drcorn: '⌟', + lrcorner: '⌟', + intercal: '⊺', + intcal: '⊺', + oplus: '⊕', + CirclePlus: '⊕', + ominus: '⊖', + CircleMinus: '⊖', + otimes: '⊗', + CircleTimes: '⊗', + osol: '⊘', + odot: '⊙', + CircleDot: '⊙', + oast: '⊛', + circledast: '⊛', + odash: '⊝', + circleddash: '⊝', + ocirc: '⊚', + circledcirc: '⊚', + boxplus: '⊞', + plusb: '⊞', + boxminus: '⊟', + minusb: '⊟', + boxtimes: '⊠', + timesb: '⊠', + boxdot: '⊡', + sdotb: '⊡', + veebar: '⊻', + vee: '∨', + barvee: '⊽', + and: '∧', + wedge: '∧', + Cap: '⋒', + Cup: '⋓', + Fork: '⋔', + pitchfork: '⋔', + epar: '⋕', + ltlarr: '⥶', + nvap: '≍⃒', + nvsim: '∼⃒', + nvge: '≥⃒', + nvle: '≤⃒', + nvlt: '<⃒', + nvgt: '>⃒', + nvltrie: '⊴⃒', + nvrtrie: '⊵⃒', + Vdash: '⊩', + dashv: '⊣', + vDash: '⊨', + Vdash: '⊩', + Vvdash: '⊪', + nvdash: '⊬', + nvDash: '⊭', + nVdash: '⊮', + nVDash: '⊯', +}; + +/** + * All entities combined (if you need everything) + * @type {Record} + */ +const ALL_ENTITIES = { + ...BASIC_LATIN, + ...LATIN_ACCENTS, + ...LATIN_EXTENDED, + ...GREEK, + ...CYRILLIC, + ...MATH, + ...MATH_ADVANCED, + ...ARROWS, + ...SHAPES, + ...PUNCTUATION, + ...CURRENCY, + ...FRACTIONS, + ...MISC_SYMBOLS, +}; + +const XML = { + amp: "&", + apos: "'", + gt: ">", + lt: "<", + quot: "\"" +} +const COMMON_HTML = { + nbsp: '\u00a0', + copy: '\u00a9', + reg: '\u00ae', + trade: '\u2122', + mdash: '\u2014', + ndash: '\u2013', + hellip: '\u2026', + laquo: '\u00ab', + raquo: '\u00bb', + lsquo: '\u2018', + rsquo: '\u2019', + ldquo: '\u201c', + rdquo: '\u201d', + bull: '\u2022', + para: '\u00b6', + sect: '\u00a7', + deg: '\u00b0', + frac12: '\u00bd', + frac14: '\u00bc', + frac34: '\u00be', +} +// --------------------------------------------------------------------------- +// Note: NUMERIC_ENTITIES (&#NNN; / &#xHH;) are handled by the scanner directly +// via String.fromCodePoint() without any map lookup. +// --------------------------------------------------------------------------- +;// CONCATENATED MODULE: ./node_modules/@nodable/entities/src/EntityDecoder.js +// --------------------------------------------------------------------------- +// Built-in named entity map (name → replacement string) +// No regex, no {regex,val} objects — just flat key/value pairs. +// --------------------------------------------------------------------------- + + + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +const SPECIAL_CHARS = new Set('!?\\\\/[]$%{}^&*()<>|+'); + +/** + * Validate that an entity name contains no dangerous characters. + * @param {string} name + * @returns {string} the name, unchanged + * @throws {Error} on invalid characters + */ +function EntityDecoder_validateEntityName(name) { + if (name[0] === '#') { + throw new Error(`[EntityReplacer] Invalid character '#' in entity name: "${name}"`); + } + for (const ch of name) { + if (SPECIAL_CHARS.has(ch)) { + throw new Error(`[EntityReplacer] Invalid character '${ch}' in entity name: "${name}"`); + } + } + return name; +} + +/** + * Merge one or more entity maps into a flat name→string map. + * Accepts either: + * - plain string values: { amp: '&' } + * - legacy {regex,val} / {regx,val}: { lt: { regex: /.../, val: '<' } } + * + * Values containing '&' are skipped (recursive expansion risk). + * + * @param {...object} maps + * @returns {Record} + */ +function mergeEntityMaps(...maps) { + const out = Object.create(null); + for (const map of maps) { + if (!map) continue; + for (const key of Object.keys(map)) { + const raw = map[key]; + if (typeof raw === 'string') { + out[key] = raw; + } else if (raw && typeof raw === 'object' && raw.val !== undefined) { + // Legacy {regex,val} or {regx,val} — extract the string val only + const val = raw.val; + if (typeof val === 'string') { + out[key] = val; + } + // function vals are not supported in the scanner — skip + } + } + } + return out; +} + +// --------------------------------------------------------------------------- +// applyLimitsTo helpers +// --------------------------------------------------------------------------- + +const LIMIT_TIER_EXTERNAL = 'external'; // input/runtime + persistent external maps +const LIMIT_TIER_BASE = 'base'; // DEFAULT_XML_ENTITIES + namedEntities (system) maps +const LIMIT_TIER_ALL = 'all'; // every entity regardless of tier + +/** + * Resolve `applyLimitsTo` option into a normalised Set of tier strings. + * Accepted values: 'external' | 'base' | 'all' | string[] + * Default: 'external' (only untrusted injected entities are counted). + * @param {string|string[]|undefined} raw + * @returns {Set} + */ +function parseLimitTiers(raw) { + if (!raw || raw === LIMIT_TIER_EXTERNAL) return new Set([LIMIT_TIER_EXTERNAL]); + if (raw === LIMIT_TIER_ALL) return new Set([LIMIT_TIER_ALL]); + if (raw === LIMIT_TIER_BASE) return new Set([LIMIT_TIER_BASE]); + if (Array.isArray(raw)) return new Set(raw); + return new Set([LIMIT_TIER_EXTERNAL]); // safe default for unrecognised values +} + +// --------------------------------------------------------------------------- +// NCR (Numeric Character Reference) classification +// --------------------------------------------------------------------------- + +// Severity order — higher number = stricter action. +// Used to enforce minimum action levels for specific codepoint ranges. +const NCR_LEVEL = Object.freeze({ allow: 0, leave: 1, remove: 2, throw: 3 }); + +// XML 1.0 §2.2: allowed chars are #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] +// Restricted C0: U+0001–U+001F excluding U+0009, U+000A, U+000D +const XML10_ALLOWED_C0 = new Set([0x09, 0x0A, 0x0D]); + +/** + * Parse the `ncr` constructor option into flat, hot-path-friendly fields. + * @param {object|undefined} ncr + * @returns {{ xmlVersion: number, onLevel: number, nullLevel: number }} + */ +function parseNCRConfig(ncr) { + if (!ncr) { + return { xmlVersion: 1.0, onLevel: NCR_LEVEL.allow, nullLevel: NCR_LEVEL.remove }; + } + const xmlVersion = ncr.xmlVersion === 1.1 ? 1.1 : 1.0; + const onLevel = NCR_LEVEL[ncr.onNCR] ?? NCR_LEVEL.allow; + const nullLevel = NCR_LEVEL[ncr.nullNCR] ?? NCR_LEVEL.remove; + // 'allow' is not meaningful for null — clamp to at least 'remove' + const clampedNull = Math.max(nullLevel, NCR_LEVEL.remove); + return { xmlVersion, onLevel, nullLevel: clampedNull }; +} + +// --------------------------------------------------------------------------- +// EntityReplacer +// --------------------------------------------------------------------------- + +/** + * Single-pass, zero-regex entity replacer for XML/HTML content. + * + * Algorithm: scan the string once for '&', read to ';', resolve via map + * or direct codepoint conversion, build output chunks, join once at the end. + * + * Entity lookup priority (highest → lowest): + * 1. input / runtime (DOCTYPE entities for current document) + * 2. persistent external (survive across documents) + * 3. base named map (DEFAULT_XML_ENTITIES + user-supplied namedEntities) + * + * Both input and external resolve as the 'external' tier for limit purposes. + * Base map entities resolve as the 'base' tier. + * + * Numeric / hex references (&#NNN; / &#xHH;) are resolved directly via + * String.fromCodePoint() — no map needed. They count as 'base' tier. + * + * @example + * const replacer = new EntityReplacer({ namedEntities: COMMON_HTML }); + * replacer.setExternalEntities({ brand: 'Acme' }); + * + * const instance = replacer.reset(); + * instance.addInputEntities({ version: '1.0' }); + * instance.encode('&brand; v&version; <'); // 'Acme v1.0 <' + */ +class EntityDecoder { + /** + * @param {object} [options] + * @param {object|null} [options.namedEntities] — extra named entities merged into base map + * @param {object} [options.limit] — security limits + * @param {number} [options.limit.maxTotalExpansions=0] — 0 = unlimited + * @param {number} [options.limit.maxExpandedLength=0] — 0 = unlimited + * @param {'external'|'base'|'all'|string[]} [options.limit.applyLimitsTo='external'] + * Which entity tiers count against the security limits: + * - 'external' (default) — only input/runtime + persistent external entities + * - 'base' — only DEFAULT_XML_ENTITIES + namedEntities + * - 'all' — every entity regardless of tier + * - string[] — explicit combination, e.g. ['external', 'base'] + * @param {((resolved: string, original: string) => string)|null} [options.postCheck=null] + * @param {string[]} [options.remove=[]] — entity names (e.g. ['nbsp', '#13']) to delete (replace with empty string) + * @param {string[]} [options.leave=[]] — entity names to keep as literal (unchanged in output) + * @param {object} [options.ncr] — Numeric Character Reference controls + * @param {1.0|1.1} [options.ncr.xmlVersion=1.0] + * XML version governing which codepoint ranges are restricted: + * - 1.0 — C0 controls U+0001–U+001F (except U+0009/000A/000D) are prohibited + * - 1.1 — C0 controls are allowed when written as NCRs; C1 (U+007F–U+009F) decoded as-is + * @param {'allow'|'leave'|'remove'|'throw'} [options.ncr.onNCR='allow'] + * Base action for numeric references. Severity order: allow < leave < remove < throw. + * For codepoint ranges that carry a minimum level (surrogates → remove, XML 1.0 C0 → remove), + * the effective action is max(onNCR, rangeMinimum). + * @param {'remove'|'throw'} [options.ncr.nullNCR='remove'] + * Action for U+0000 (null). 'allow' and 'leave' are clamped to 'remove' since null is never safe. + */ + constructor(options = {}) { + this._limit = options.limit || {}; + this._maxTotalExpansions = this._limit.maxTotalExpansions || 0; + this._maxExpandedLength = this._limit.maxExpandedLength || 0; + this._postCheck = typeof options.postCheck === 'function' ? options.postCheck : r => r; + this._limitTiers = parseLimitTiers(this._limit.applyLimitsTo ?? LIMIT_TIER_EXTERNAL); + this._numericAllowed = options.numericAllowed ?? true; + // Base map: DEFAULT_XML_ENTITIES + user-supplied extras. Immutable after construction. + this._baseMap = mergeEntityMaps(XML, options.namedEntities || null); + + // Persistent external entities — survive across documents. + // Stored as a separate map so reset() never touches them. + /** @type {Record} */ + this._externalMap = Object.create(null); + + // Input / runtime entities — current document only, wiped on reset(). + /** @type {Record} */ + this._inputMap = Object.create(null); + + // Per-document counters + this._totalExpansions = 0; + this._expandedLength = 0; + + // --- New: remove / leave sets --- + /** @type {Set} */ + this._removeSet = new Set(options.remove && Array.isArray(options.remove) ? options.remove : []); + /** @type {Set} */ + this._leaveSet = new Set(options.leave && Array.isArray(options.leave) ? options.leave : []); + + // --- NCR config (parsed into flat fields for hot-path speed) --- + const ncrCfg = parseNCRConfig(options.ncr); + this._ncrXmlVersion = ncrCfg.xmlVersion; + this._ncrOnLevel = ncrCfg.onLevel; + this._ncrNullLevel = ncrCfg.nullLevel; + } + + // ------------------------------------------------------------------------- + // Persistent external entity registration + // ------------------------------------------------------------------------- + + /** + * Replace the full set of persistent external entities. + * All keys are validated — throws on invalid characters. + * @param {Record} map + */ + setExternalEntities(map) { + if (map) { + for (const key of Object.keys(map)) { + EntityDecoder_validateEntityName(key); + } + } + this._externalMap = mergeEntityMaps(map); + } + + /** + * Add a single persistent external entity. + * @param {string} key + * @param {string} value + */ + addExternalEntity(key, value) { + EntityDecoder_validateEntityName(key); + if (typeof value === 'string' && value.indexOf('&') === -1) { + this._externalMap[key] = value; + } + } + + // ------------------------------------------------------------------------- + // Input / runtime entity registration (per document) + // ------------------------------------------------------------------------- + + /** + * Inject DOCTYPE entities for the current document. + * Also resets per-document expansion counters. + * @param {Record} map + */ + addInputEntities(map) { + this._totalExpansions = 0; + this._expandedLength = 0; + this._inputMap = mergeEntityMaps(map); + } + + // ------------------------------------------------------------------------- + // Per-document reset + // ------------------------------------------------------------------------- + + /** + * Wipe input/runtime entities and reset counters. + * Call this before processing each new document. + * @returns {this} + */ + reset() { + this._inputMap = Object.create(null); + this._totalExpansions = 0; + this._expandedLength = 0; + return this; + } + + // ------------------------------------------------------------------------- + // XML version (can be set after construction, e.g. once parser reads ) + // ------------------------------------------------------------------------- + + /** + * Update the XML version used for NCR classification. + * Call this as soon as the document's `` declaration is parsed. + * @param {1.0|1.1|number} version + */ + setXmlVersion(version) { + this._ncrXmlVersion = version === 1.1 ? 1.1 : 1.0; + } + + // ------------------------------------------------------------------------- + // Primary API + // ------------------------------------------------------------------------- + + /** + * Replace all entity references in `str` in a single pass. + * + * @param {string} str + * @returns {string} + */ + decode(str) { + if (typeof str !== 'string' || str.length === 0) return str; + //TODO: check if needed + //if (str.indexOf('&') === -1) return str; // fast path — no entities at all + + const original = str; + const chunks = []; + const len = str.length; + let last = 0; // start of next unprocessed literal chunk + let i = 0; + + const limitExpansions = this._maxTotalExpansions > 0; + const limitLength = this._maxExpandedLength > 0; + const checkLimits = limitExpansions || limitLength; + + while (i < len) { + // Scan forward to next '&' + if (str.charCodeAt(i) !== 38 /* '&' */) { i++; continue; } + + // --- Found '&' at position i --- + + // Scan forward to ';' + let j = i + 1; + while (j < len && str.charCodeAt(j) !== 59 /* ';' */ && (j - i) <= 32) j++; + + if (j >= len || str.charCodeAt(j) !== 59) { + // No closing ';' within window — treat '&' as literal + i++; + continue; + } + + // Raw token between '&' and ';' (exclusive) + const token = str.slice(i + 1, j); + if (token.length === 0) { i++; continue; } + + let replacement; + let tier; // which limit tier this entity belongs to + + if (this._removeSet.has(token)) { + // Remove entity: replace with empty string + replacement = ''; + // If entity was unknown (replacement undefined), we still need a tier for limits. + // Treat as external tier because it's user-directed removal of an unknown reference. + if (tier === undefined) { + tier = LIMIT_TIER_EXTERNAL; + } + } else if (this._leaveSet.has(token)) { + // Do not replace — keep original &token; as literal + i++; + continue; + } else if (token.charCodeAt(0) === 35 /* '#' */) { + // ---- Numeric / NCR reference ---- + // NCR classification always runs first — prohibited codepoints must be + // caught regardless of numericAllowed. + const ncrResult = this._resolveNCR(token); + if (ncrResult === undefined) { + // 'leave' action — keep original &token; as-is + i++; + continue; + } + replacement = ncrResult; // '' for remove, char string for allow + tier = LIMIT_TIER_BASE; + } else { + // ---- Named reference ---- + const resolved = this._resolveName(token); + replacement = resolved?.value; + tier = resolved?.tier; + } + + if (replacement === undefined) { + // Unknown entity — leave as-is, advance past '&' only + i++; + continue; + } + + // Flush literal chunk before this entity + if (i > last) chunks.push(str.slice(last, i)); + chunks.push(replacement); + last = j + 1; // skip past ';' + i = last; + + // Apply expansion limits only if this tier is being tracked + if (checkLimits && this._tierCounts(tier)) { + if (limitExpansions) { + this._totalExpansions++; + if (this._totalExpansions > this._maxTotalExpansions) { + throw new Error( + `[EntityReplacer] Entity expansion count limit exceeded: ` + + `${this._totalExpansions} > ${this._maxTotalExpansions}` + ); + } + } + if (limitLength) { + // delta: replacement.length minus the raw &token; length (token.length + 2 for '&' and ';') + const delta = replacement.length - (token.length + 2); + if (delta > 0) { + this._expandedLength += delta; + if (this._expandedLength > this._maxExpandedLength) { + throw new Error( + `[EntityReplacer] Expanded content length limit exceeded: ` + + `${this._expandedLength} > ${this._maxExpandedLength}` + ); + } + } + } + } + } + + // Flush trailing literal + if (last < len) chunks.push(str.slice(last)); + + // If nothing was replaced, chunks is empty — return original + const result = chunks.length === 0 ? str : chunks.join(''); + + return this._postCheck(result, original); + } + + // ------------------------------------------------------------------------- + // Private: limit tier check + // ------------------------------------------------------------------------- + + /** + * Returns true if a resolved entity of the given tier should count + * against the expansion/length limits. + * @param {string} tier — LIMIT_TIER_EXTERNAL | LIMIT_TIER_BASE + * @returns {boolean} + */ + _tierCounts(tier) { + if (this._limitTiers.has(LIMIT_TIER_ALL)) return true; + return this._limitTiers.has(tier); + } + + // ------------------------------------------------------------------------- + // Private: entity resolution + // ------------------------------------------------------------------------- + + /** + * Resolve a named entity token (without & and ;). + * Priority: inputMap > externalMap > baseMap + * Returns the resolved value tagged with its limit tier. + * + * @param {string} name + * @returns {{ value: string, tier: string }|undefined} + */ + _resolveName(name) { + // input and external both count as 'external' tier for limit purposes — + // they are injected at runtime and are the untrusted surface. + if (name in this._inputMap) return { value: this._inputMap[name], tier: LIMIT_TIER_EXTERNAL }; + if (name in this._externalMap) return { value: this._externalMap[name], tier: LIMIT_TIER_EXTERNAL }; + if (name in this._baseMap) return { value: this._baseMap[name], tier: LIMIT_TIER_BASE }; + return undefined; + } + + /** + * Classify a codepoint and return the minimum action level that must be applied. + * Returns -1 when no minimum is imposed (normal allow path). + * + * Ranges checked (in priority order): + * 1. U+0000 — null, governed by nullNCR (always ≥ remove) + * 2. U+D800–U+DFFF — surrogates, always prohibited (min: remove) + * 3. U+0001–U+001F \ {0x09,0x0A,0x0D} — XML 1.0 restricted C0 (min: remove) + * (skipped in XML 1.1 — C0 controls are allowed when written as NCRs) + * + * @param {number} cp — codepoint + * @returns {number} — minimum NCR_LEVEL value, or -1 for no restriction + */ + _classifyNCR(cp) { + // 1. Null + if (cp === 0) return this._ncrNullLevel; + + // 2. Surrogates — always prohibited, minimum 'remove' + if (cp >= 0xD800 && cp <= 0xDFFF) return NCR_LEVEL.remove; + + // 3. XML 1.0 restricted C0 controls + if (this._ncrXmlVersion === 1.0) { + if (cp >= 0x01 && cp <= 0x1F && !XML10_ALLOWED_C0.has(cp)) return NCR_LEVEL.remove; + } + + return -1; // no restriction + } + + /** + * Execute a resolved NCR action. + * + * @param {number} action — NCR_LEVEL value + * @param {string} token — raw token (e.g. '#38') for error messages + * @param {number} cp — codepoint, used only for error messages + * @returns {string|undefined} + * - decoded character string → 'allow' + * - '' → 'remove' + * - undefined → 'leave' (caller must skip past '&' only) + * - throws Error → 'throw' + */ + _applyNCRAction(action, token, cp) { + switch (action) { + case NCR_LEVEL.allow: return String.fromCodePoint(cp); + case NCR_LEVEL.remove: return ''; + case NCR_LEVEL.leave: return undefined; // signal: keep literal + case NCR_LEVEL.throw: + throw new Error( + `[EntityDecoder] Prohibited numeric character reference ` + + `&${token}; (U+${cp.toString(16).toUpperCase().padStart(4, '0')})` + ); + default: return String.fromCodePoint(cp); + } + } + + /** + * Full NCR resolution pipeline for a numeric token. + * + * Steps: + * 1. Parse the codepoint (decimal or hex). + * 2. Validate the raw codepoint range (NaN, <0, >0x10FFFF). + * 3. If numericAllowed is false and no minimum restriction applies → leave as-is. + * 4. Classify the codepoint to find the minimum required action level. + * 5. Resolve effective action = max(onNCR, minimum). + * 6. Apply and return. + * + * @param {string} token — e.g. '#38', '#x26', '#X26' + * @returns {string|undefined} + * - string (incl. '') — replacement ('' = remove) + * - undefined — leave original &token; as-is + */ + _resolveNCR(token) { + // Step 1: parse codepoint + const second = token.charCodeAt(1); + let cp; + if (second === 120 /* x */ || second === 88 /* X */) { + cp = parseInt(token.slice(2), 16); + } else { + cp = parseInt(token.slice(1), 10); + } + + // Step 2: out-of-range → leave as-is unconditionally + if (Number.isNaN(cp) || cp < 0 || cp > 0x10FFFF) return undefined; + + // Step 3: classify to get minimum action level + const minimum = this._classifyNCR(cp); + + // Step 4: if numericAllowed is false and no hard minimum → leave + if (!this._numericAllowed && minimum < NCR_LEVEL.remove) return undefined; + + // Step 5: effective action = max(configured onNCR, range minimum) + const effective = minimum === -1 + ? this._ncrOnLevel + : Math.max(this._ncrOnLevel, minimum); + + // Step 6: apply + return this._applyNCRAction(effective, token, cp); + } +} ;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/xmlparser/OrderedObjParser.js ///@ts-check @@ -54332,6 +56895,8 @@ function ignoreAttributes_getIgnoreAttributesFn(ignoreAttributes) { + + // const regx = // '<((!\\[CDATA\\[([\\s\\S]*?)(]]>))|((NAME:)?(NAME))([^>]*)>|((\\/)(NAME)\\s*>))([^<]*)' // .replace(/NAME/g, util.nameRegexp); @@ -54391,36 +56956,10 @@ function extractNamespace(rawTagName) { } class OrderedObjParser { - constructor(options) { + constructor(options, externalEntities) { this.options = options; this.currentNode = null; this.tagsNodeStack = []; - this.docTypeEntities = {}; - this.lastEntities = { - "apos": { regex: /&(apos|#39|#x27);/g, val: "'" }, - "gt": { regex: /&(gt|#62|#x3E);/g, val: ">" }, - "lt": { regex: /&(lt|#60|#x3C);/g, val: "<" }, - "quot": { regex: /&(quot|#34|#x22);/g, val: "\"" }, - }; - this.ampEntity = { regex: /&(amp|#38|#x26);/g, val: "&" }; - this.htmlEntities = { - "space": { regex: /&(nbsp|#160);/g, val: " " }, - // "lt" : { regex: /&(lt|#60);/g, val: "<" }, - // "gt" : { regex: /&(gt|#62);/g, val: ">" }, - // "amp" : { regex: /&(amp|#38);/g, val: "&" }, - // "quot" : { regex: /&(quot|#34);/g, val: "\"" }, - // "apos" : { regex: /&(apos|#39);/g, val: "'" }, - "cent": { regex: /&(cent|#162);/g, val: "¢" }, - "pound": { regex: /&(pound|#163);/g, val: "£" }, - "yen": { regex: /&(yen|#165);/g, val: "¥" }, - "euro": { regex: /&(euro|#8364);/g, val: "€" }, - "copyright": { regex: /&(copy|#169);/g, val: "©" }, - "reg": { regex: /&(reg|#174);/g, val: "®" }, - "inr": { regex: /&(inr|#8377);/g, val: "₹" }, - "num_dec": { regex: /&#([0-9]{1,7});/g, val: (_, str) => fromCodePoint(str, 10, "&#") }, - "num_hex": { regex: /&#x([0-9a-fA-F]{1,6});/g, val: (_, str) => fromCodePoint(str, 16, "&#x") }, - }; - this.addExternalEntities = addExternalEntities; this.parseXml = parseXml; this.parseTextData = parseTextData; this.resolveNameSpace = resolveNameSpace; @@ -54433,42 +56972,51 @@ class OrderedObjParser { this.ignoreAttributesFn = ignoreAttributes_getIgnoreAttributesFn(this.options.ignoreAttributes) this.entityExpansionCount = 0; this.currentExpandedLength = 0; + let namedEntities = { ...XML }; + if (this.options.entityDecoder) { + this.entityDecoder = this.options.entityDecoder + } else { + if (typeof this.options.htmlEntities === "object") namedEntities = this.options.htmlEntities; + else if (this.options.htmlEntities === true) namedEntities = { ...COMMON_HTML, ...CURRENCY }; + this.entityDecoder = new EntityDecoder({ + namedEntities: { ...namedEntities, ...externalEntities }, + numericAllowed: this.options.htmlEntities, + limit: { + maxTotalExpansions: this.options.processEntities.maxTotalExpansions, + maxExpandedLength: this.options.processEntities.maxExpandedLength, + applyLimitsTo: this.options.processEntities.appliesTo, + } + //postCheck: resolved => resolved + }); + } // Initialize path matcher for path-expression-matcher this.matcher = new Matcher(); + this.readonlyMatcher = this.matcher.readOnly(); // Flag to track if current node is a stop node (optimization) this.isCurrentNodeStopNode = false; // Pre-compile stopNodes expressions - if (this.options.stopNodes && this.options.stopNodes.length > 0) { - this.stopNodeExpressions = []; - for (let i = 0; i < this.options.stopNodes.length; i++) { - const stopNodeExp = this.options.stopNodes[i]; + this.stopNodeExpressionsSet = new ExpressionSet(); + const stopNodesOpts = this.options.stopNodes; + if (stopNodesOpts && stopNodesOpts.length > 0) { + for (let i = 0; i < stopNodesOpts.length; i++) { + const stopNodeExp = stopNodesOpts[i]; if (typeof stopNodeExp === 'string') { // Convert string to Expression object - this.stopNodeExpressions.push(new Expression(stopNodeExp)); + this.stopNodeExpressionsSet.add(new Expression(stopNodeExp)); } else if (stopNodeExp instanceof Expression) { // Already an Expression object - this.stopNodeExpressions.push(stopNodeExp); + this.stopNodeExpressionsSet.add(stopNodeExp); } } + this.stopNodeExpressionsSet.seal(); } } } -function addExternalEntities(externalEntities) { - const entKeys = Object.keys(externalEntities); - for (let i = 0; i < entKeys.length; i++) { - const ent = entKeys[i]; - const escaped = ent.replace(/[.\-+*:]/g, '\\.'); - this.lastEntities[ent] = { - regex: new RegExp("&" + escaped + ";", "g"), - val: externalEntities[ent] - } - } -} /** * @param {string} val @@ -54480,28 +57028,29 @@ function addExternalEntities(externalEntities) { * @param {boolean} escapeEntities */ function parseTextData(val, tagName, jPath, dontTrim, hasAttributes, isLeafNode, escapeEntities) { + const options = this.options; if (val !== undefined) { - if (this.options.trimValues && !dontTrim) { + if (options.trimValues && !dontTrim) { val = val.trim(); } if (val.length > 0) { if (!escapeEntities) val = this.replaceEntitiesValue(val, tagName, jPath); // Pass jPath string or matcher based on options.jPath setting - const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath; - const newval = this.options.tagValueProcessor(tagName, val, jPathOrMatcher, hasAttributes, isLeafNode); + const jPathOrMatcher = options.jPath ? jPath.toString() : jPath; + const newval = options.tagValueProcessor(tagName, val, jPathOrMatcher, hasAttributes, isLeafNode); if (newval === null || newval === undefined) { //don't parse return val; } else if (typeof newval !== typeof val || newval !== val) { //overwrite return newval; - } else if (this.options.trimValues) { - return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions); + } else if (options.trimValues) { + return parseValue(val, options.parseTagValue, options.numberParseOptions); } else { const trimmedVal = val.trim(); if (trimmedVal === val) { - return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions); + return parseValue(val, options.parseTagValue, options.numberParseOptions); } else { return val; } @@ -54528,8 +57077,9 @@ function resolveNameSpace(tagname) { //const attrsRegx = new RegExp("([\\w\\-\\.\\:]+)\\s*=\\s*(['\"])((.|\n)*?)\\2","gm"); const attrsRegx = new RegExp('([^\\s=]+)\\s*(=\\s*([\'"])([\\s\\S]*?)\\3)?', 'gm'); -function buildAttributesMap(attrStr, jPath, tagName) { - if (this.options.ignoreAttributes !== true && typeof attrStr === 'string') { +function buildAttributesMap(attrStr, jPath, tagName, force = false) { + const options = this.options; + if (force === true || (options.ignoreAttributes !== true && typeof attrStr === 'string')) { // attrStr = attrStr.replace(/\r?\n/g, ' '); //attrStr = attrStr || attrStr.trim(); @@ -54537,89 +57087,80 @@ function buildAttributesMap(attrStr, jPath, tagName) { const len = matches.length; //don't make it inline const attrs = {}; - // First pass: parse all attributes and update matcher with raw values - // This ensures the matcher has all attribute values when processors run + // Pre-process values once: trim + entity replacement + // Reused in both matcher update and second pass + const processedVals = new Array(len); + let hasRawAttrs = false; const rawAttrsForMatcher = {}; + for (let i = 0; i < len; i++) { const attrName = this.resolveNameSpace(matches[i][1]); const oldVal = matches[i][4]; if (attrName.length && oldVal !== undefined) { - let parsedVal = oldVal; - if (this.options.trimValues) { - parsedVal = parsedVal.trim(); - } - parsedVal = this.replaceEntitiesValue(parsedVal, tagName, jPath); - rawAttrsForMatcher[attrName] = parsedVal; + let val = oldVal; + if (options.trimValues) val = val.trim(); + val = this.replaceEntitiesValue(val, tagName, this.readonlyMatcher); + processedVals[i] = val; + + rawAttrsForMatcher[attrName] = val; + hasRawAttrs = true; } } - // Update matcher with raw attribute values BEFORE running processors - if (Object.keys(rawAttrsForMatcher).length > 0 && typeof jPath === 'object' && jPath.updateCurrent) { + // Update matcher ONCE before second pass, if applicable + if (hasRawAttrs && typeof jPath === 'object' && jPath.updateCurrent) { jPath.updateCurrent(rawAttrsForMatcher); } - // Second pass: now process attributes with matcher having full attribute context + // Hoist toString() once — path doesn't change during attribute processing + const jPathStr = options.jPath ? jPath.toString() : this.readonlyMatcher; + + // Second pass: apply processors, build final attrs + let hasAttrs = false; for (let i = 0; i < len; i++) { const attrName = this.resolveNameSpace(matches[i][1]); - // Convert jPath to string if needed for ignoreAttributesFn - const jPathStr = this.options.jPath ? jPath.toString() : jPath; - if (this.ignoreAttributesFn(attrName, jPathStr)) { - continue - } + if (this.ignoreAttributesFn(attrName, jPathStr)) continue; - let oldVal = matches[i][4]; - let aName = this.options.attributeNamePrefix + attrName; + let aName = options.attributeNamePrefix + attrName; if (attrName.length) { - if (this.options.transformAttributeName) { - aName = this.options.transformAttributeName(aName); + if (options.transformAttributeName) { + aName = options.transformAttributeName(aName); } - //if (aName === "__proto__") aName = "#__proto__"; - aName = sanitizeName(aName, this.options); + aName = sanitizeName(aName, options); - if (oldVal !== undefined) { - if (this.options.trimValues) { - oldVal = oldVal.trim(); - } - oldVal = this.replaceEntitiesValue(oldVal, tagName, jPath); + if (matches[i][4] !== undefined) { + // Reuse already-processed value — no double entity replacement + const oldVal = processedVals[i]; - // Pass jPath string or matcher based on options.jPath setting - const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath; - const newVal = this.options.attributeValueProcessor(attrName, oldVal, jPathOrMatcher); + const newVal = options.attributeValueProcessor(attrName, oldVal, jPathStr); if (newVal === null || newVal === undefined) { - //don't parse attrs[aName] = oldVal; } else if (typeof newVal !== typeof oldVal || newVal !== oldVal) { - //overwrite attrs[aName] = newVal; } else { - //parse - attrs[aName] = parseValue( - oldVal, - this.options.parseAttributeValue, - this.options.numberParseOptions - ); + attrs[aName] = parseValue(oldVal, options.parseAttributeValue, options.numberParseOptions); } - } else if (this.options.allowBooleanAttributes) { + hasAttrs = true; + } else if (options.allowBooleanAttributes) { attrs[aName] = true; + hasAttrs = true; } } } - if (!Object.keys(attrs).length) { - return; - } - if (this.options.attributesGroupName) { + if (!hasAttrs) return; + + if (options.attributesGroupName && !options.preserveOrder) { const attrCollection = {}; - attrCollection[this.options.attributesGroupName] = attrs; + attrCollection[options.attributesGroupName] = attrs; return attrCollection; } - return attrs + return attrs; } } - const parseXml = function (xmlData) { xmlData = xmlData.replace(/\r\n?/g, "\n"); //TODO: remove this line const xmlObj = new XmlNode('!xml'); @@ -54628,40 +57169,43 @@ const parseXml = function (xmlData) { // Reset matcher for new document this.matcher.reset(); + this.entityDecoder.reset(); // Reset entity expansion counters for this document this.entityExpansionCount = 0; this.currentExpandedLength = 0; - - const docTypeReader = new DocTypeReader(this.options.processEntities); - for (let i = 0; i < xmlData.length; i++) {//for each char in XML data + const options = this.options; + const docTypeReader = new DocTypeReader(options.processEntities); + const xmlLen = xmlData.length; + for (let i = 0; i < xmlLen; i++) {//for each char in XML data const ch = xmlData[i]; if (ch === '<') { // const nextIndex = i+1; // const _2ndChar = xmlData[nextIndex]; - if (xmlData[i + 1] === '/') {//Closing Tag + const c1 = xmlData.charCodeAt(i + 1); + if (c1 === 47) {//Closing Tag '/' const closeIndex = findClosingIndex(xmlData, ">", i, "Closing Tag is not closed.") let tagName = xmlData.substring(i + 2, closeIndex).trim(); - if (this.options.removeNSPrefix) { + if (options.removeNSPrefix) { const colonIndex = tagName.indexOf(":"); if (colonIndex !== -1) { tagName = tagName.substr(colonIndex + 1); } } - tagName = transformTagName(this.options.transformTagName, tagName, "", this.options).tagName; + tagName = transformTagName(options.transformTagName, tagName, "", options).tagName; if (currentNode) { - textData = this.saveTextToParentTag(textData, currentNode, this.matcher); + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher); } //check if last tag of nested tag was unpaired tag const lastTagName = this.matcher.getCurrentTag(); - if (tagName && this.options.unpairedTags.indexOf(tagName) !== -1) { + if (tagName && options.unpairedTagsSet.has(tagName)) { throw new Error(`Unpaired tag can not be used as closing tag: `); } - if (lastTagName && this.options.unpairedTags.indexOf(lastTagName) !== -1) { + if (lastTagName && options.unpairedTagsSet.has(lastTagName)) { // Pop the unpaired tag this.matcher.pop(); this.tagsNodeStack.pop(); @@ -54673,65 +57217,75 @@ const parseXml = function (xmlData) { currentNode = this.tagsNodeStack.pop();//avoid recursion, set the parent tag scope textData = ""; i = closeIndex; - } else if (xmlData[i + 1] === '?') { + } else if (c1 === 63) { //'?' let tagData = readTagExp(xmlData, i, false, "?>"); if (!tagData) throw new Error("Pi Tag is not closed."); - textData = this.saveTextToParentTag(textData, currentNode, this.matcher); - if ((this.options.ignoreDeclaration && tagData.tagName === "?xml") || this.options.ignorePiTags) { + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher); + const attsMap = this.buildAttributesMap(tagData.tagExp, this.matcher, tagData.tagName, true); + if (attsMap) { + const ver = attsMap[this.options.attributeNamePrefix + "version"]; + this.entityDecoder.setXmlVersion(Number(ver) || 1.0); + docTypeReader.setXmlVersion(Number(ver) || 1.0); + } + if ((options.ignoreDeclaration && tagData.tagName === "?xml") || options.ignorePiTags) { //do nothing } else { const childNode = new XmlNode(tagData.tagName); - childNode.add(this.options.textNodeName, ""); + childNode.add(options.textNodeName, ""); - if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent) { - childNode[":@"] = this.buildAttributesMap(tagData.tagExp, this.matcher, tagData.tagName); + if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent && options.ignoreAttributes !== true) { + childNode[":@"] = attsMap } - this.addChild(currentNode, childNode, this.matcher, i); + this.addChild(currentNode, childNode, this.readonlyMatcher, i); } i = tagData.closeIndex + 1; - } else if (xmlData.substr(i + 1, 3) === '!--') { + } else if (c1 === 33 + && xmlData.charCodeAt(i + 2) === 45 + && xmlData.charCodeAt(i + 3) === 45) { //'!--' const endIndex = findClosingIndex(xmlData, "-->", i + 4, "Comment is not closed.") - if (this.options.commentPropName) { + if (options.commentPropName) { const comment = xmlData.substring(i + 4, endIndex - 2); - textData = this.saveTextToParentTag(textData, currentNode, this.matcher); + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher); - currentNode.add(this.options.commentPropName, [{ [this.options.textNodeName]: comment }]); + currentNode.add(options.commentPropName, [{ [options.textNodeName]: comment }]); } i = endIndex; - } else if (xmlData.substr(i + 1, 2) === '!D') { + } else if (c1 === 33 + && xmlData.charCodeAt(i + 2) === 68) { //'!D' const result = docTypeReader.readDocType(xmlData, i); - this.docTypeEntities = result.entities; + this.entityDecoder.addInputEntities(result.entities); i = result.i; - } else if (xmlData.substr(i + 1, 2) === '![') { + } else if (c1 === 33 + && xmlData.charCodeAt(i + 2) === 91) { // '![' const closeIndex = findClosingIndex(xmlData, "]]>", i, "CDATA is not closed.") - 2; const tagExp = xmlData.substring(i + 9, closeIndex); - textData = this.saveTextToParentTag(textData, currentNode, this.matcher); + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher); - let val = this.parseTextData(tagExp, currentNode.tagname, this.matcher, true, false, true, true); + let val = this.parseTextData(tagExp, currentNode.tagname, this.readonlyMatcher, true, false, true, true); if (val == undefined) val = ""; //cdata should be set even if it is 0 length string - if (this.options.cdataPropName) { - currentNode.add(this.options.cdataPropName, [{ [this.options.textNodeName]: tagExp }]); + if (options.cdataPropName) { + currentNode.add(options.cdataPropName, [{ [options.textNodeName]: tagExp }]); } else { - currentNode.add(this.options.textNodeName, val); + currentNode.add(options.textNodeName, val); } i = closeIndex + 2; } else {//Opening tag - let result = readTagExp(xmlData, i, this.options.removeNSPrefix); + let result = readTagExp(xmlData, i, options.removeNSPrefix); // Safety check: readTagExp can return undefined if (!result) { // Log context for debugging - const context = xmlData.substring(Math.max(0, i - 50), Math.min(xmlData.length, i + 50)); + const context = xmlData.substring(Math.max(0, i - 50), Math.min(xmlLen, i + 50)); throw new Error(`readTagExp returned undefined at position ${i}. Context: "${context}"`); } @@ -54741,11 +57295,13 @@ const parseXml = function (xmlData) { let attrExpPresent = result.attrExpPresent; let closeIndex = result.closeIndex; - ({ tagName, tagExp } = transformTagName(this.options.transformTagName, tagName, tagExp, this.options)); + ({ tagName, tagExp } = transformTagName(options.transformTagName, tagName, tagExp, options)); - if (this.options.strictReservedNames && - (tagName === this.options.commentPropName - || tagName === this.options.cdataPropName + if (options.strictReservedNames && + (tagName === options.commentPropName + || tagName === options.cdataPropName + || tagName === options.textNodeName + || tagName === options.attributesGroupName )) { throw new Error(`Invalid tag name: ${tagName}`); } @@ -54754,13 +57310,13 @@ const parseXml = function (xmlData) { if (currentNode && textData) { if (currentNode.tagname !== '!xml') { //when nested tag is found - textData = this.saveTextToParentTag(textData, currentNode, this.matcher, false); + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher, false); } } //check if last tag was unpaired tag const lastTag = currentNode; - if (lastTag && this.options.unpairedTags.indexOf(lastTag.tagname) !== -1) { + if (lastTag && options.unpairedTagsSet.has(lastTag.tagname)) { currentNode = this.tagsNodeStack.pop(); this.matcher.pop(); } @@ -54802,13 +57358,14 @@ const parseXml = function (xmlData) { if (prefixedAttrs) { // Extract raw attributes (without prefix) for our use - rawAttrs = extractRawAttributes(prefixedAttrs, this.options); + //TODO: seems a performance overhead + rawAttrs = extractRawAttributes(prefixedAttrs, options); } } // Now check if this is a stop node (after attributes are set) if (tagName !== xmlObj.tagname) { - this.isCurrentNodeStopNode = this.isItStopNode(this.stopNodeExpressions, this.matcher); + this.isCurrentNodeStopNode = this.isItStopNode(); } const startIndex = i; @@ -54820,7 +57377,7 @@ const parseXml = function (xmlData) { i = result.closeIndex; } //unpaired tag - else if (this.options.unpairedTags.indexOf(tagName) !== -1) { + else if (options.unpairedTagsSet.has(tagName)) { i = result.closeIndex; } //normal tag @@ -54839,31 +57396,31 @@ const parseXml = function (xmlData) { } // For stop nodes, store raw content as-is without any processing - childNode.add(this.options.textNodeName, tagContent); + childNode.add(options.textNodeName, tagContent); this.matcher.pop(); // Pop the stop node tag this.isCurrentNodeStopNode = false; // Reset flag - this.addChild(currentNode, childNode, this.matcher, startIndex); + this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); } else { //selfClosing tag if (isSelfClosing) { - ({ tagName, tagExp } = transformTagName(this.options.transformTagName, tagName, tagExp, this.options)); + ({ tagName, tagExp } = transformTagName(options.transformTagName, tagName, tagExp, options)); const childNode = new XmlNode(tagName); if (prefixedAttrs) { childNode[":@"] = prefixedAttrs; } - this.addChild(currentNode, childNode, this.matcher, startIndex); + this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); this.matcher.pop(); // Pop self-closing tag this.isCurrentNodeStopNode = false; // Reset flag } - else if (this.options.unpairedTags.indexOf(tagName) !== -1) {//unpaired tag + else if (options.unpairedTagsSet.has(tagName)) {//unpaired tag const childNode = new XmlNode(tagName); if (prefixedAttrs) { childNode[":@"] = prefixedAttrs; } - this.addChild(currentNode, childNode, this.matcher, startIndex); + this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); this.matcher.pop(); // Pop unpaired tag this.isCurrentNodeStopNode = false; // Reset flag i = result.closeIndex; @@ -54873,7 +57430,7 @@ const parseXml = function (xmlData) { //opening tag else { const childNode = new XmlNode(tagName); - if (this.tagsNodeStack.length > this.options.maxNestedTags) { + if (this.tagsNodeStack.length > options.maxNestedTags) { throw new Error("Maximum nested tags exceeded"); } this.tagsNodeStack.push(currentNode); @@ -54881,7 +57438,7 @@ const parseXml = function (xmlData) { if (prefixedAttrs) { childNode[":@"] = prefixedAttrs; } - this.addChild(currentNode, childNode, this.matcher, startIndex); + this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); currentNode = childNode; } textData = ""; @@ -54944,79 +57501,7 @@ function OrderedObjParser_replaceEntitiesValue(val, tagName, jPath) { } } - // Replace DOCTYPE entities - for (const entityName of Object.keys(this.docTypeEntities)) { - const entity = this.docTypeEntities[entityName]; - const matches = val.match(entity.regx); - - if (matches) { - // Track expansions - this.entityExpansionCount += matches.length; - - // Check expansion limit - if (entityConfig.maxTotalExpansions && - this.entityExpansionCount > entityConfig.maxTotalExpansions) { - throw new Error( - `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}` - ); - } - - // Store length before replacement - const lengthBefore = val.length; - val = val.replace(entity.regx, entity.val); - - // Check expanded length immediately after replacement - if (entityConfig.maxExpandedLength) { - this.currentExpandedLength += (val.length - lengthBefore); - - if (this.currentExpandedLength > entityConfig.maxExpandedLength) { - throw new Error( - `Total expanded content size exceeded: ${this.currentExpandedLength} > ${entityConfig.maxExpandedLength}` - ); - } - } - } - } - // Replace standard entities - for (const entityName of Object.keys(this.lastEntities)) { - const entity = this.lastEntities[entityName]; - const matches = val.match(entity.regex); - if (matches) { - this.entityExpansionCount += matches.length; - if (entityConfig.maxTotalExpansions && - this.entityExpansionCount > entityConfig.maxTotalExpansions) { - throw new Error( - `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}` - ); - } - } - val = val.replace(entity.regex, entity.val); - } - if (val.indexOf('&') === -1) return val; - - // Replace HTML entities if enabled - if (this.options.htmlEntities) { - for (const entityName of Object.keys(this.htmlEntities)) { - const entity = this.htmlEntities[entityName]; - const matches = val.match(entity.regex); - if (matches) { - //console.log(matches); - this.entityExpansionCount += matches.length; - if (entityConfig.maxTotalExpansions && - this.entityExpansionCount > entityConfig.maxTotalExpansions) { - throw new Error( - `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}` - ); - } - } - val = val.replace(entity.regex, entity.val); - } - } - - // Replace ampersand entity last - val = val.replace(this.ampEntity.regex, this.ampEntity.val); - - return val; + return this.entityDecoder.decode(val); } @@ -55038,20 +57523,14 @@ function saveTextToParentTag(textData, parentNode, matcher, isLeafNode) { return textData; } -//TODO: use jPath to simplify the logic /** * @param {Array} stopNodeExpressions - Array of compiled Expression objects * @param {Matcher} matcher - Current path matcher */ -function isItStopNode(stopNodeExpressions, matcher) { - if (!stopNodeExpressions || stopNodeExpressions.length === 0) return false; +function isItStopNode() { + if (this.stopNodeExpressionsSet.size === 0) return false; - for (let i = 0; i < stopNodeExpressions.length; i++) { - if (matcher.matches(stopNodeExpressions[i])) { - return true; - } - } - return false; + return this.matcher.matchesAny(this.stopNodeExpressionsSet); } /** @@ -55061,32 +57540,38 @@ function isItStopNode(stopNodeExpressions, matcher) { * @returns */ function tagExpWithClosingIndex(xmlData, i, closingChar = ">") { - let attrBoundary; - let tagExp = ""; - for (let index = i; index < xmlData.length; index++) { - let ch = xmlData[index]; + //TODO: ignore boolean attributes in tag expression + //TODO: if ignore attributes, dont read full attribute expression but the end. But read for xml declaration + let attrBoundary = 0; + const len = xmlData.length; + const closeCode0 = closingChar.charCodeAt(0); + const closeCode1 = closingChar.length > 1 ? closingChar.charCodeAt(1) : -1; + + let result = ''; + let segmentStart = i; + + for (let index = i; index < len; index++) { + const code = xmlData.charCodeAt(index); + if (attrBoundary) { - if (ch === attrBoundary) attrBoundary = "";//reset - } else if (ch === '"' || ch === "'") { - attrBoundary = ch; - } else if (ch === closingChar[0]) { - if (closingChar[1]) { - if (xmlData[index + 1] === closingChar[1]) { - return { - data: tagExp, - index: index - } + if (code === attrBoundary) attrBoundary = 0; + } else if (code === 34 || code === 39) { // " or ' + attrBoundary = code; + } else if (code === closeCode0) { + if (closeCode1 !== -1) { + if (xmlData.charCodeAt(index + 1) === closeCode1) { + result += xmlData.substring(segmentStart, index); + return { data: result, index }; } } else { - return { - data: tagExp, - index: index - } + result += xmlData.substring(segmentStart, index); + return { data: result, index }; } - } else if (ch === '\t') { - ch = " " + } else if (code === 9 && !attrBoundary) { // \t - only replace with space outside attribute values + // Flush accumulated segment, add space, start new segment + result += xmlData.substring(segmentStart, index) + ' '; + segmentStart = index + 1; } - tagExp += ch; } } @@ -55099,6 +57584,12 @@ function findClosingIndex(xmlData, str, i, errMsg) { } } +function findClosingChar(xmlData, char, i, errMsg) { + const closingIndex = xmlData.indexOf(char, i); + if (closingIndex === -1) throw new Error(errMsg); + return closingIndex; // no offset needed +} + function readTagExp(xmlData, i, removeNSPrefix, closingChar = ">") { const result = tagExpWithClosingIndex(xmlData, i + 1, closingChar); if (!result) return; @@ -55140,10 +57631,12 @@ function readStopNodeData(xmlData, tagName, i) { // Starting at 1 since we already have an open tag let openTagCount = 1; - for (; i < xmlData.length; i++) { + const xmllen = xmlData.length; + for (; i < xmllen; i++) { if (xmlData[i] === "<") { - if (xmlData[i + 1] === "/") {//close tag - const closeIndex = findClosingIndex(xmlData, ">", i, `${tagName} is not closed`); + const c1 = xmlData.charCodeAt(i + 1); + if (c1 === 47) {//close tag '/' + const closeIndex = findClosingChar(xmlData, ">", i, `${tagName} is not closed`); let closeTagName = xmlData.substring(i + 2, closeIndex).trim(); if (closeTagName === tagName) { openTagCount--; @@ -55155,17 +57648,20 @@ function readStopNodeData(xmlData, tagName, i) { } } i = closeIndex; - } else if (xmlData[i + 1] === '?') { + } else if (c1 === 63) { //? const closeIndex = findClosingIndex(xmlData, "?>", i + 1, "StopNode is not closed.") i = closeIndex; - } else if (xmlData.substr(i + 1, 3) === '!--') { + } else if (c1 === 33 + && xmlData.charCodeAt(i + 2) === 45 + && xmlData.charCodeAt(i + 3) === 45) { // '!--' const closeIndex = findClosingIndex(xmlData, "-->", i + 3, "StopNode is not closed.") i = closeIndex; - } else if (xmlData.substr(i + 1, 2) === '![') { + } else if (c1 === 33 + && xmlData.charCodeAt(i + 2) === 91) { // '![' const closeIndex = findClosingIndex(xmlData, "]]>", i, "StopNode is not closed.") - 2; i = closeIndex; } else { - const tagData = readTagExp(xmlData, i, '>') + const tagData = readTagExp(xmlData, i, false) if (tagData) { const openTagName = tagData && tagData.tagName; @@ -55265,18 +57761,17 @@ function stripAttributePrefix(attrs, prefix) { * @param {Matcher} matcher - Path matcher instance * @returns */ -function prettify(node, options, matcher) { - return compress(node, options, matcher); +function prettify(node, options, matcher, readonlyMatcher) { + return compress(node, options, matcher, readonlyMatcher); } /** - * * @param {array} arr * @param {object} options * @param {Matcher} matcher - Path matcher instance * @returns object */ -function compress(arr, options, matcher) { +function compress(arr, options, matcher, readonlyMatcher) { let text; const compressedObj = {}; //This is intended to be a plain object for (let i = 0; i < arr.length; i++) { @@ -55299,11 +57794,15 @@ function compress(arr, options, matcher) { continue; } else if (tagObj[property]) { - let val = compress(tagObj[property], options, matcher); + let val = compress(tagObj[property], options, matcher, readonlyMatcher); const isLeaf = isLeafTag(val, options); + if (Object.keys(val).length === 0 && options.alwaysCreateTextNode) { + val[options.textNodeName] = ""; + } + if (tagObj[":@"]) { - assignAttributes(val, tagObj[":@"], matcher, options); + assignAttributes(val, tagObj[":@"], readonlyMatcher, options); } else if (Object.keys(val).length === 1 && val[options.textNodeName] !== undefined && !options.alwaysCreateTextNode) { val = val[options.textNodeName]; } else if (Object.keys(val).length === 0) { @@ -55325,8 +57824,8 @@ function compress(arr, options, matcher) { //TODO: if a node is not an array, then check if it should be an array //also determine if it is a leaf node - // Pass jPath string or matcher based on options.jPath setting - const jPathOrMatcher = options.jPath ? matcher.toString() : matcher; + // Pass jPath string or readonlyMatcher based on options.jPath setting + const jPathOrMatcher = options.jPath ? readonlyMatcher.toString() : readonlyMatcher; if (options.isArray(property, jPathOrMatcher, isLeaf)) { compressedObj[property] = [val]; } else { @@ -55358,7 +57857,7 @@ function node2json_propName(obj) { } } -function assignAttributes(obj, attrMap, matcher, options) { +function assignAttributes(obj, attrMap, readonlyMatcher, options) { if (attrMap) { const keys = Object.keys(attrMap); const len = keys.length; //don't make it inline @@ -55373,8 +57872,8 @@ function assignAttributes(obj, attrMap, matcher, options) { // For attributes, we need to create a temporary path // Pass jPath string or matcher based on options.jPath setting const jPathOrMatcher = options.jPath - ? matcher.toString() + "." + rawAttrName - : matcher; + ? readonlyMatcher.toString() + "." + rawAttrName + : readonlyMatcher; if (options.isArray(atrrName, jPathOrMatcher, true, true)) { obj[atrrName] = [attrMap[atrrName]]; @@ -55431,16 +57930,16 @@ 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}`) } } - const orderedObjParser = new OrderedObjParser(this.options); - orderedObjParser.addExternalEntities(this.externalEntities); + const orderedObjParser = new OrderedObjParser(this.options, this.externalEntities); + // orderedObjParser.entityDecoder.setExternalEntities(this.externalEntities); const orderedResult = orderedObjParser.parseXml(xmlData); if (this.options.preserveOrder || orderedResult === undefined) return orderedResult; - else return prettify(orderedResult, this.options, orderedObjParser.matcher); + else return prettify(orderedResult, this.options, orderedObjParser.matcher, orderedObjParser.readonlyMatcher); } /** @@ -55474,7 +57973,6 @@ class XMLParser { return XmlNode.getMetaDataSymbol(); } } - ;// CONCATENATED MODULE: ./node_modules/@azure/core-xml/dist/esm/xml.common.js // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. @@ -55493,20 +57991,34 @@ const xml_common_XML_CHARKEY = "_"; function getCommonOptions(options) { - var _a; return { attributesGroupName: xml_common_XML_ATTRKEY, - textNodeName: (_a = options.xmlCharKey) !== null && _a !== void 0 ? _a : xml_common_XML_CHARKEY, + textNodeName: options.xmlCharKey ?? xml_common_XML_CHARKEY, ignoreAttributes: false, suppressBooleanAttributes: false, }; } function getSerializerOptions(options = {}) { - var _a, _b; - return Object.assign(Object.assign({}, getCommonOptions(options)), { attributeNamePrefix: "@_", format: true, suppressEmptyNode: true, indentBy: "", rootNodeName: (_a = options.rootName) !== null && _a !== void 0 ? _a : "root", cdataPropName: (_b = options.cdataPropName) !== null && _b !== void 0 ? _b : "__cdata" }); + return { + ...getCommonOptions(options), + attributeNamePrefix: "@_", + format: true, + suppressEmptyNode: true, + indentBy: "", + rootNodeName: options.rootName ?? "root", + cdataPropName: options.cdataPropName ?? "__cdata", + }; } function getParserOptions(options = {}) { - return Object.assign(Object.assign({}, getCommonOptions(options)), { parseAttributeValue: false, parseTagValue: false, attributeNamePrefix: "", stopNodes: options.stopNodes, processEntities: true, trimValues: false }); + return { + ...getCommonOptions(options), + parseAttributeValue: false, + parseTagValue: false, + attributeNamePrefix: "", + stopNodes: options.stopNodes, + processEntities: true, + trimValues: false, + }; } /** * Converts given JSON object to XML string @@ -55545,7 +58057,7 @@ async function parseXML(str, opts = {}) { if (!opts.includeRoot) { for (const key of Object.keys(parsedXml)) { const value = parsedXml[key]; - return typeof value === "object" ? Object.assign({}, value) : value; + return typeof value === "object" ? { ...value } : value; } } return parsedXml; @@ -57130,39 +59642,6 @@ class StorageSharedKeyCredential extends Credential { } } //# sourceMappingURL=StorageSharedKeyCredential.js.map -;// CONCATENATED MODULE: ./node_modules/@azure/storage-common/node_modules/@azure/abort-controller/dist/esm/AbortError.js -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -/** - * This error is thrown when an asynchronous operation has been aborted. - * Check for this error by testing the `name` that the name property of the - * error matches `"AbortError"`. - * - * @example - * ```ts - * const controller = new AbortController(); - * controller.abort(); - * try { - * doAsyncWork(controller.signal) - * } catch (e) { - * if (e.name === 'AbortError') { - * // handle abort error here. - * } - * } - * ``` - */ -class esm_AbortError_AbortError extends Error { - constructor(message) { - super(message); - this.name = "AbortError"; - } -} -//# sourceMappingURL=AbortError.js.map -;// CONCATENATED MODULE: ./node_modules/@azure/storage-common/node_modules/@azure/abort-controller/dist/esm/index.js -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -//# sourceMappingURL=index.js.map ;// CONCATENATED MODULE: ./node_modules/@azure/storage-common/dist/esm/log.js // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. @@ -57220,7 +59699,7 @@ const DEFAULT_RETRY_OPTIONS = { secondaryHost: "", tryTimeoutInMs: undefined, // Use server side default timeout strategy }; -const RETRY_ABORT_ERROR = new esm_AbortError_AbortError("The operation was aborted."); +const RETRY_ABORT_ERROR = new AbortError_AbortError("The operation was aborted."); /** * Retry policy with exponential retry and linear retry implemented. */ @@ -57533,7 +60012,7 @@ const retriableErrors = [ "EPIPE", "REQUEST_SEND_ERROR", ]; -const StorageRetryPolicyV2_RETRY_ABORT_ERROR = new esm_AbortError_AbortError("The operation was aborted."); +const StorageRetryPolicyV2_RETRY_ABORT_ERROR = new AbortError_AbortError("The operation was aborted."); /** * Retry policy with exponential retry and linear retry implemented. */ @@ -57896,15 +60375,12 @@ class UserDelegationKeyCredential { - - - //# sourceMappingURL=index.js.map ;// CONCATENATED MODULE: ./node_modules/@azure/storage-blob/dist/esm/utils/constants.js // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -const esm_utils_constants_SDK_VERSION = "12.30.0"; +const esm_utils_constants_SDK_VERSION = "12.31.0"; const SERVICE_VERSION = "2026-02-06"; const BLOCK_BLOB_MAX_UPLOAD_BLOB_BYTES = 256 * 1024 * 1024; // 256MB const BLOCK_BLOB_MAX_STAGE_BLOCK_BYTES = 4000 * 1024 * 1024; // 4000MB @@ -74402,39 +76878,6 @@ class BlobLeaseClient { } } //# sourceMappingURL=BlobLeaseClient.js.map -;// CONCATENATED MODULE: ./node_modules/@azure/storage-blob/node_modules/@azure/abort-controller/dist/esm/AbortError.js -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -/** - * This error is thrown when an asynchronous operation has been aborted. - * Check for this error by testing the `name` that the name property of the - * error matches `"AbortError"`. - * - * @example - * ```ts - * const controller = new AbortController(); - * controller.abort(); - * try { - * doAsyncWork(controller.signal) - * } catch (e) { - * if (e.name === 'AbortError') { - * // handle abort error here. - * } - * } - * ``` - */ -class dist_esm_AbortError_AbortError extends Error { - constructor(message) { - super(message); - this.name = "AbortError"; - } -} -//# sourceMappingURL=AbortError.js.map -;// CONCATENATED MODULE: ./node_modules/@azure/storage-blob/node_modules/@azure/abort-controller/dist/esm/index.js -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -//# sourceMappingURL=index.js.map ;// CONCATENATED MODULE: ./node_modules/@azure/storage-blob/dist/esm/utils/RetriableReadableStream.js // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. @@ -74514,7 +76957,7 @@ class RetriableReadableStream extends external_node_stream_.Readable { } }; sourceAbortedHandler = () => { - const abortError = new dist_esm_AbortError_AbortError("The operation was aborted."); + const abortError = new AbortError_AbortError("The operation was aborted."); this.destroy(abortError); }; sourceErrorOrEndHandler = (err) => { @@ -75509,7 +77952,7 @@ const external_buffer_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import. -const ABORT_ERROR = new dist_esm_AbortError_AbortError("Reading from the avro stream was aborted."); +const ABORT_ERROR = new AbortError_AbortError("Reading from the avro stream was aborted."); class AvroReadableFromStream extends AvroReadable { _position; _readable; @@ -78129,7 +80572,7 @@ class BlobClient extends StorageClient_StorageClient { options.conditions = options.conditions || {}; ensureCpkIfSpecified(options.customerProvidedKey, this.isHttps); return tracingClient.withSpan("BlobClient-download", options, async (updatedOptions) => { - const res = utils_common_assertResponse(await this.blobContext.download({ + const res = utils_common_assertResponse((await this.blobContext.download({ abortSignal: options.abortSignal, leaseAccessConditions: options.conditions, modifiedAccessConditions: { @@ -78145,7 +80588,7 @@ class BlobClient extends StorageClient_StorageClient { snapshot: options.snapshot, cpkInfo: options.customerProvidedKey, tracingOptions: updatedOptions.tracingOptions, - })); + }))); const wrappedRes = { ...res, _response: res._response, // _response is made non-enumerable @@ -79467,7 +81910,7 @@ class Clients_BlockBlobClient extends BlobClient { throw new Error("This operation currently is only supported in Node.js."); } return tracingClient.withSpan("BlockBlobClient-query", options, async (updatedOptions) => { - const response = utils_common_assertResponse(await this._blobContext.query({ + const response = utils_common_assertResponse((await this._blobContext.query({ abortSignal: options.abortSignal, queryRequest: { queryType: "SQL", @@ -79482,7 +81925,7 @@ class Clients_BlockBlobClient extends BlobClient { }, cpkInfo: options.customerProvidedKey, tracingOptions: updatedOptions.tracingOptions, - })); + }))); return new BlobQueryResponse(response, { abortSignal: options.abortSignal, onProgress: options.onProgress, @@ -81378,9 +83821,9 @@ class BlobBatchClient { return tracingClient.withSpan("BlobBatchClient-submitBatch", options, async (updatedOptions) => { const batchRequestBody = batchRequest.getHttpRequestBody(); // ServiceSubmitBatchResponseModel and ContainerSubmitBatchResponse are compatible for now. - const rawBatchResponse = utils_common_assertResponse(await this.serviceOrContainerContext.submitBatch(utf8ByteLength(batchRequestBody), batchRequest.getMultiPartContentType(), batchRequestBody, { + const rawBatchResponse = utils_common_assertResponse((await this.serviceOrContainerContext.submitBatch(utf8ByteLength(batchRequestBody), batchRequest.getMultiPartContentType(), batchRequestBody, { ...updatedOptions, - })); + }))); // Parse the sub responses result, if logic reaches here(i.e. the batch request succeeded with status code 202). const batchResponseParser = new BatchResponseParser(rawBatchResponse, batchRequest.getSubRequests()); const responseSummary = await batchResponseParser.parseBatchResponse(); @@ -83960,7 +86403,7 @@ NetworkError.isNetworkErrorCode = (code) => { }; class UsageError extends Error { constructor() { - const message = `Cache storage quota has been hit. Unable to upload any new cache entries. Usage is recalculated every 6-12 hours.\nMore info on storage limits: https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#calculating-minute-and-storage-spending`; + const message = `Cache storage quota has been hit. Unable to upload any new cache entries.\nMore info on storage limits: https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#calculating-minute-and-storage-spending`; super(message); this.name = 'UsageError'; } diff --git a/dist/save/index.js b/dist/save/index.js index cc9a0ba..0500ade 100644 --- a/dist/save/index.js +++ b/dist/save/index.js @@ -38848,7 +38848,7 @@ module.exports = { version: packageJson.version } /***/ 4012: /***/ ((module) => { -module.exports = /*#__PURE__*/JSON.parse('{"name":"@actions/cache","version":"6.0.0","description":"Actions cache lib","keywords":["github","actions","cache"],"homepage":"https://github.com/actions/toolkit/tree/main/packages/cache","license":"MIT","type":"module","main":"lib/cache.js","types":"lib/cache.d.ts","exports":{".":{"types":"./lib/cache.d.ts","import":"./lib/cache.js"}},"directories":{"lib":"lib","test":"__tests__"},"files":["lib","!.DS_Store"],"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/actions/toolkit.git","directory":"packages/cache"},"scripts":{"audit-moderate":"npm install && npm audit --json --audit-level=moderate > audit.json","test":"echo \\"Error: run tests from root\\" && exit 1","tsc":"tsc && cp src/internal/shared/package-version.cjs lib/internal/shared/"},"bugs":{"url":"https://github.com/actions/toolkit/issues"},"dependencies":{"@actions/core":"^3.0.0","@actions/exec":"^3.0.0","@actions/glob":"^0.6.1","@actions/http-client":"^4.0.0","@actions/io":"^3.0.0","@azure/core-rest-pipeline":"^1.22.0","@azure/storage-blob":"^12.30.0","@protobuf-ts/runtime-rpc":"^2.11.1","semver":"^7.7.3"},"devDependencies":{"@protobuf-ts/plugin":"^2.9.4","@types/node":"^25.1.0","@types/semver":"^7.7.1","typescript":"^5.2.2"},"overrides":{"uri-js":"npm:uri-js-replace@^1.0.1","node-fetch":"^3.3.2"}}'); +module.exports = /*#__PURE__*/JSON.parse('{"name":"@actions/cache","version":"6.0.1","description":"Actions cache lib","keywords":["github","actions","cache"],"homepage":"https://github.com/actions/toolkit/tree/main/packages/cache","license":"MIT","type":"module","main":"lib/cache.js","types":"lib/cache.d.ts","exports":{".":{"types":"./lib/cache.d.ts","import":"./lib/cache.js"}},"directories":{"lib":"lib","test":"__tests__"},"files":["lib","!.DS_Store"],"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/actions/toolkit.git","directory":"packages/cache"},"scripts":{"audit-moderate":"npm install && npm audit --json --audit-level=moderate > audit.json","test":"echo \\"Error: run tests from root\\" && exit 1","tsc":"tsc && cp src/internal/shared/package-version.cjs lib/internal/shared/"},"bugs":{"url":"https://github.com/actions/toolkit/issues"},"dependencies":{"@actions/core":"^3.0.1","@actions/exec":"^3.0.0","@actions/glob":"^0.6.1","@actions/http-client":"^4.0.1","@actions/io":"^3.0.2","@azure/core-rest-pipeline":"^1.23.0","@azure/storage-blob":"^12.31.0","@protobuf-ts/runtime-rpc":"^2.11.1","semver":"^7.7.4"},"devDependencies":{"@protobuf-ts/plugin":"^2.11.1","@types/node":"^25.6.0","@types/semver":"^7.7.1","typescript":"^5.9.3"},"overrides":{"uri-js":"npm:uri-js-replace@^1.0.1","node-fetch":"^3.3.2"}}'); /***/ }) @@ -46852,7 +46852,7 @@ async function util_userAgentPlatform_setPlatformSpecificData(map) { ;// CONCATENATED MODULE: ./node_modules/@azure/core-rest-pipeline/dist/esm/constants.js // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -const esm_constants_SDK_VERSION = "1.22.2"; +const esm_constants_SDK_VERSION = "1.22.3"; const esm_constants_DEFAULT_RETRY_POLICY_COUNT = 3; //# sourceMappingURL=constants.js.map ;// CONCATENATED MODULE: ./node_modules/@azure/core-rest-pipeline/dist/esm/util/userAgent.js @@ -46951,7 +46951,7 @@ async function computeSha256Hash(content, encoding) { //# sourceMappingURL=internal.js.map -;// CONCATENATED MODULE: ./node_modules/@azure/core-util/node_modules/@azure/abort-controller/dist/esm/AbortError.js +;// CONCATENATED MODULE: ./node_modules/@azure/abort-controller/dist/esm/AbortError.js // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. /** @@ -46979,7 +46979,7 @@ class AbortError_AbortError extends Error { } } //# sourceMappingURL=AbortError.js.map -;// CONCATENATED MODULE: ./node_modules/@azure/core-util/node_modules/@azure/abort-controller/dist/esm/index.js +;// CONCATENATED MODULE: ./node_modules/@azure/abort-controller/dist/esm/index.js // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. @@ -51570,6 +51570,7 @@ function convertHttpClient(requestPolicyClient) { + //# sourceMappingURL=index.js.map ;// CONCATENATED MODULE: ./node_modules/path-expression-matcher/src/Expression.js /** @@ -51589,11 +51590,11 @@ class Expression { * @param {Object} options - Configuration options * @param {string} options.separator - Path separator (default: '.') */ - constructor(pattern, options = {}) { + constructor(pattern, options = {}, data) { this.pattern = pattern; this.separator = options.separator || '.'; this.segments = this._parse(pattern); - + this.data = data; // Cache expensive checks for performance (O(1) instead of O(n)) this._hasDeepWildcard = this.segments.some(seg => seg.type === 'deep-wildcard'); this._hasAttributeCondition = this.segments.some(seg => seg.attrName !== undefined); @@ -51805,48 +51806,206 @@ class Expression { } } ;// CONCATENATED MODULE: ./node_modules/path-expression-matcher/src/Matcher.js + + /** - * Matcher - Tracks current path in XML/JSON tree and matches against Expressions - * + * MatcherView - A lightweight read-only view over a Matcher's internal state. + * + * Created once by Matcher and reused across all callbacks. Holds a direct + * reference to the parent Matcher so it always reflects current parser state + * with zero copying or freezing overhead. + * + * Users receive this via {@link Matcher#readOnly} or directly from parser + * callbacks. It exposes all query and matching methods but has no mutation + * methods — misuse is caught at the TypeScript level rather than at runtime. + * + * @example + * const matcher = new Matcher(); + * const view = matcher.readOnly(); + * + * matcher.push("root", {}); + * view.getCurrentTag(); // "root" + * view.getDepth(); // 1 + */ +class MatcherView { + /** + * @param {Matcher} matcher - The parent Matcher instance to read from. + */ + constructor(matcher) { + this._matcher = matcher; + } + + /** + * Get the path separator used by the parent matcher. + * @returns {string} + */ + get separator() { + return this._matcher.separator; + } + + /** + * Get current tag name. + * @returns {string|undefined} + */ + getCurrentTag() { + const path = this._matcher.path; + return path.length > 0 ? path[path.length - 1].tag : undefined; + } + + /** + * Get current namespace. + * @returns {string|undefined} + */ + getCurrentNamespace() { + const path = this._matcher.path; + return path.length > 0 ? path[path.length - 1].namespace : undefined; + } + + /** + * Get current node's attribute value. + * @param {string} attrName + * @returns {*} + */ + getAttrValue(attrName) { + const path = this._matcher.path; + if (path.length === 0) return undefined; + return path[path.length - 1].values?.[attrName]; + } + + /** + * Check if current node has an attribute. + * @param {string} attrName + * @returns {boolean} + */ + hasAttr(attrName) { + const path = this._matcher.path; + if (path.length === 0) return false; + const current = path[path.length - 1]; + return current.values !== undefined && attrName in current.values; + } + + /** + * Get current node's sibling position (child index in parent). + * @returns {number} + */ + getPosition() { + const path = this._matcher.path; + if (path.length === 0) return -1; + return path[path.length - 1].position ?? 0; + } + + /** + * Get current node's repeat counter (occurrence count of this tag name). + * @returns {number} + */ + getCounter() { + const path = this._matcher.path; + if (path.length === 0) return -1; + return path[path.length - 1].counter ?? 0; + } + + /** + * Get current node's sibling index (alias for getPosition). + * @returns {number} + * @deprecated Use getPosition() or getCounter() instead + */ + getIndex() { + return this.getPosition(); + } + + /** + * Get current path depth. + * @returns {number} + */ + getDepth() { + return this._matcher.path.length; + } + + /** + * Get path as string. + * @param {string} [separator] - Optional separator (uses default if not provided) + * @param {boolean} [includeNamespace=true] + * @returns {string} + */ + toString(separator, includeNamespace = true) { + return this._matcher.toString(separator, includeNamespace); + } + + /** + * Get path as array of tag names. + * @returns {string[]} + */ + toArray() { + return this._matcher.path.map(n => n.tag); + } + + /** + * Match current path against an Expression. + * @param {Expression} expression + * @returns {boolean} + */ + matches(expression) { + return this._matcher.matches(expression); + } + + /** + * Match any expression in the given set against the current path. + * @param {ExpressionSet} exprSet + * @returns {boolean} + */ + matchesAny(exprSet) { + return exprSet.matchesAny(this._matcher); + } +} + +/** + * Matcher - Tracks current path in XML/JSON tree and matches against Expressions. + * * The matcher maintains a stack of nodes representing the current path from root to * current tag. It only stores attribute values for the current (top) node to minimize * memory usage. Sibling tracking is used to auto-calculate position and counter. - * + * + * Use {@link Matcher#readOnly} to obtain a {@link MatcherView} safe to pass to + * user callbacks — it always reflects current state with no Proxy overhead. + * * @example * const matcher = new Matcher(); * matcher.push("root", {}); * matcher.push("users", {}); * matcher.push("user", { id: "123", type: "admin" }); - * + * * const expr = new Expression("root.users.user"); * matcher.matches(expr); // true */ class Matcher { /** - * Create a new Matcher - * @param {Object} options - Configuration options - * @param {string} options.separator - Default path separator (default: '.') + * Create a new Matcher. + * @param {Object} [options={}] + * @param {string} [options.separator='.'] - Default path separator */ constructor(options = {}) { this.separator = options.separator || '.'; this.path = []; this.siblingStacks = []; - // Each path node: { tag: string, values: object, position: number, counter: number } + // Each path node: { tag, values, position, counter, namespace? } // values only present for current (last) node // Each siblingStacks entry: Map tracking occurrences at each level + this._pathStringCache = null; + this._view = new MatcherView(this); } /** - * Push a new tag onto the path - * @param {string} tagName - Name of the tag - * @param {Object} attrValues - Attribute key-value pairs for current node (optional) - * @param {string} namespace - Namespace for the tag (optional) + * Push a new tag onto the path. + * @param {string} tagName + * @param {Object|null} [attrValues=null] + * @param {string|null} [namespace=null] */ push(tagName, attrValues = null, namespace = null) { + this._pathStringCache = null; + // Remove values from previous current node (now becoming ancestor) if (this.path.length > 0) { - const prev = this.path[this.path.length - 1]; - prev.values = undefined; + this.path[this.path.length - 1].values = undefined; } // Get or create sibling tracking for current level @@ -51879,12 +52038,10 @@ class Matcher { counter: counter }; - // Store namespace if provided if (namespace !== null && namespace !== undefined) { node.namespace = namespace; } - // Store values only for current node if (attrValues !== null && attrValues !== undefined) { node.values = attrValues; } @@ -51893,19 +52050,15 @@ class Matcher { } /** - * Pop the last tag from the path + * Pop the last tag from the path. * @returns {Object|undefined} The popped node */ pop() { - if (this.path.length === 0) { - return undefined; - } + if (this.path.length === 0) return undefined; + this._pathStringCache = null; const node = this.path.pop(); - // Clean up sibling tracking for levels deeper than current - // After pop, path.length is the new depth - // We need to clean up siblingStacks[path.length + 1] and beyond if (this.siblingStacks.length > this.path.length + 1) { this.siblingStacks.length = this.path.length + 1; } @@ -51914,9 +52067,9 @@ class Matcher { } /** - * Update current node's attribute values - * Useful when attributes are parsed after push - * @param {Object} attrValues - Attribute values + * Update current node's attribute values. + * Useful when attributes are parsed after push. + * @param {Object} attrValues */ updateCurrent(attrValues) { if (this.path.length > 0) { @@ -51928,7 +52081,7 @@ class Matcher { } /** - * Get current tag name + * Get current tag name. * @returns {string|undefined} */ getCurrentTag() { @@ -51936,7 +52089,7 @@ class Matcher { } /** - * Get current namespace + * Get current namespace. * @returns {string|undefined} */ getCurrentNamespace() { @@ -51944,19 +52097,18 @@ class Matcher { } /** - * Get current node's attribute value - * @param {string} attrName - Attribute name - * @returns {*} Attribute value or undefined + * Get current node's attribute value. + * @param {string} attrName + * @returns {*} */ getAttrValue(attrName) { if (this.path.length === 0) return undefined; - const current = this.path[this.path.length - 1]; - return current.values?.[attrName]; + return this.path[this.path.length - 1].values?.[attrName]; } /** - * Check if current node has an attribute - * @param {string} attrName - Attribute name + * Check if current node has an attribute. + * @param {string} attrName * @returns {boolean} */ hasAttr(attrName) { @@ -51966,7 +52118,7 @@ class Matcher { } /** - * Get current node's sibling position (child index in parent) + * Get current node's sibling position (child index in parent). * @returns {number} */ getPosition() { @@ -51975,7 +52127,7 @@ class Matcher { } /** - * Get current node's repeat counter (occurrence count of this tag name) + * Get current node's repeat counter (occurrence count of this tag name). * @returns {number} */ getCounter() { @@ -51984,7 +52136,7 @@ class Matcher { } /** - * Get current node's sibling index (alias for getPosition for backward compatibility) + * Get current node's sibling index (alias for getPosition). * @returns {number} * @deprecated Use getPosition() or getCounter() instead */ @@ -51993,7 +52145,7 @@ class Matcher { } /** - * Get current path depth + * Get current path depth. * @returns {number} */ getDepth() { @@ -52001,23 +52153,33 @@ class Matcher { } /** - * Get path as string - * @param {string} separator - Optional separator (uses default if not provided) - * @param {boolean} includeNamespace - Whether to include namespace in output (default: true) + * Get path as string. + * @param {string} [separator] - Optional separator (uses default if not provided) + * @param {boolean} [includeNamespace=true] * @returns {string} */ toString(separator, includeNamespace = true) { const sep = separator || this.separator; - return this.path.map(n => { - if (includeNamespace && n.namespace) { - return `${n.namespace}:${n.tag}`; + const isDefault = (sep === this.separator && includeNamespace === true); + + if (isDefault) { + if (this._pathStringCache !== null) { + return this._pathStringCache; } - return n.tag; - }).join(sep); + const result = this.path.map(n => + (n.namespace) ? `${n.namespace}:${n.tag}` : n.tag + ).join(sep); + this._pathStringCache = result; + return result; + } + + return this.path.map(n => + (includeNamespace && n.namespace) ? `${n.namespace}:${n.tag}` : n.tag + ).join(sep); } /** - * Get path as array of tag names + * Get path as array of tag names. * @returns {string[]} */ toArray() { @@ -52025,17 +52187,18 @@ class Matcher { } /** - * Reset the path to empty + * Reset the path to empty. */ reset() { + this._pathStringCache = null; this.path = []; this.siblingStacks = []; } /** - * Match current path against an Expression - * @param {Expression} expression - The expression to match against - * @returns {boolean} True if current path matches the expression + * Match current path against an Expression. + * @param {Expression} expression + * @returns {boolean} */ matches(expression) { const segments = expression.segments; @@ -52044,32 +52207,23 @@ class Matcher { return false; } - // Handle deep wildcard patterns if (expression.hasDeepWildcard()) { return this._matchWithDeepWildcard(segments); } - // Simple path matching (no deep wildcards) return this._matchSimple(segments); } /** - * Match simple path (no deep wildcards) * @private */ _matchSimple(segments) { - // Path must be same length as segments if (this.path.length !== segments.length) { return false; } - // Match each segment bottom-to-top for (let i = 0; i < segments.length; i++) { - const segment = segments[i]; - const node = this.path[i]; - const isCurrentNode = (i === this.path.length - 1); - - if (!this._matchSegment(segment, node, isCurrentNode)) { + if (!this._matchSegment(segments[i], this.path[i], i === this.path.length - 1)) { return false; } } @@ -52078,32 +52232,27 @@ class Matcher { } /** - * Match path with deep wildcards * @private */ _matchWithDeepWildcard(segments) { - let pathIdx = this.path.length - 1; // Start from current node (bottom) - let segIdx = segments.length - 1; // Start from last segment + let pathIdx = this.path.length - 1; + let segIdx = segments.length - 1; while (segIdx >= 0 && pathIdx >= 0) { const segment = segments[segIdx]; if (segment.type === 'deep-wildcard') { - // ".." matches zero or more levels segIdx--; if (segIdx < 0) { - // Pattern ends with "..", always matches return true; } - // Find where next segment matches in the path const nextSeg = segments[segIdx]; let found = false; for (let i = pathIdx; i >= 0; i--) { - const isCurrentNode = (i === this.path.length - 1); - if (this._matchSegment(nextSeg, this.path[i], isCurrentNode)) { + if (this._matchSegment(nextSeg, this.path[i], i === this.path.length - 1)) { pathIdx = i - 1; segIdx--; found = true; @@ -52115,9 +52264,7 @@ class Matcher { return false; } } else { - // Regular segment - const isCurrentNode = (pathIdx === this.path.length - 1); - if (!this._matchSegment(segment, this.path[pathIdx], isCurrentNode)) { + if (!this._matchSegment(segment, this.path[pathIdx], pathIdx === this.path.length - 1)) { return false; } pathIdx--; @@ -52125,38 +52272,25 @@ class Matcher { } } - // All segments must be consumed return segIdx < 0; } /** - * Match a single segment against a node * @private - * @param {Object} segment - Segment from Expression - * @param {Object} node - Node from path - * @param {boolean} isCurrentNode - Whether this is the current (last) node - * @returns {boolean} */ _matchSegment(segment, node, isCurrentNode) { - // Match tag name (* is wildcard) if (segment.tag !== '*' && segment.tag !== node.tag) { return false; } - // Match namespace if specified in segment if (segment.namespace !== undefined) { - // Segment has namespace - node must match it if (segment.namespace !== '*' && segment.namespace !== node.namespace) { return false; } } - // If segment has no namespace, it matches nodes with or without namespace - // Match attribute name (check if node has this attribute) - // Can only check for current node since ancestors don't have values if (segment.attrName !== undefined) { if (!isCurrentNode) { - // Can't check attributes for ancestor nodes (values not stored) return false; } @@ -52164,20 +52298,15 @@ class Matcher { return false; } - // Match attribute value (only possible for current node) if (segment.attrValue !== undefined) { - const actualValue = node.values[segment.attrName]; - // Both should be strings - if (String(actualValue) !== String(segment.attrValue)) { + if (String(node.values[segment.attrName]) !== String(segment.attrValue)) { return false; } } } - // Match position (only for current node) if (segment.position !== undefined) { if (!isCurrentNode) { - // Can't check position for ancestor nodes return false; } @@ -52189,10 +52318,8 @@ class Matcher { return false; } else if (segment.position === 'even' && counter % 2 !== 0) { return false; - } else if (segment.position === 'nth') { - if (counter !== segment.positionValue) { - return false; - } + } else if (segment.position === 'nth' && counter !== segment.positionValue) { + return false; } } @@ -52200,8 +52327,17 @@ class Matcher { } /** - * Create a snapshot of current state - * @returns {Object} State snapshot + * Match any expression in the given set against the current path. + * @param {ExpressionSet} exprSet + * @returns {boolean} + */ + matchesAny(exprSet) { + return exprSet.matchesAny(this); + } + + /** + * Create a snapshot of current state. + * @returns {Object} */ snapshot() { return { @@ -52211,28 +52347,378 @@ class Matcher { } /** - * Restore state from snapshot - * @param {Object} snapshot - State snapshot + * Restore state from snapshot. + * @param {Object} snapshot */ restore(snapshot) { + this._pathStringCache = null; this.path = snapshot.path.map(node => ({ ...node })); this.siblingStacks = snapshot.siblingStacks.map(map => new Map(map)); } + + /** + * Return the read-only {@link MatcherView} for this matcher. + * + * The same instance is returned on every call — no allocation occurs. + * It always reflects the current parser state and is safe to pass to + * user callbacks without risk of accidental mutation. + * + * @returns {MatcherView} + * + * @example + * const view = matcher.readOnly(); + * // pass view to callbacks — it stays in sync automatically + * view.matches(expr); // ✓ + * view.getCurrentTag(); // ✓ + * // view.push(...) // ✗ method does not exist — caught by TypeScript + */ + readOnly() { + 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; } @@ -52249,13 +52735,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; @@ -52275,20 +52764,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); @@ -52304,21 +52805,25 @@ function arrToStr(arr, options, indentation, matcher, stopNodeExpressions) { if (isPreviousElementTag) { xmlStr += indentation; } - xmlStr += ``; + const val = tagObj[rawTagName][0][options.textNodeName]; + const safeVal = safeCdata(val); + xmlStr += ``; isPreviousElementTag = false; matcher.pop(); continue; } else if (tagName === options.commentPropName) { - xmlStr += indentation + ``; + 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; @@ -52330,16 +52835,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) { @@ -52383,7 +52887,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; } @@ -52421,9 +52925,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); @@ -52450,7 +52952,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)}"`; } } } @@ -52466,13 +52968,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]; @@ -52483,9 +52995,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)}"`; } } } @@ -52538,6 +53050,8 @@ function getIgnoreAttributesFn(ignoreAttributes) { + + const defaultOptions = { attributeNamePrefix: '@_', attributesGroupName: false, @@ -52571,7 +53085,11 @@ const 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) { @@ -52628,6 +53146,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); @@ -52639,11 +53195,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) { @@ -52657,6 +53214,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)) { @@ -52666,21 +53239,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) { @@ -52688,7 +53262,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(); @@ -52696,12 +53270,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 + '` + this.newLine; + const safeVal = safeCdata(val); + return this.indentate(level) + `` + this.newLine; } else if (this.options.commentPropName !== false && key === this.options.commentPropName) { - return this.indentate(level) + `` + this.newLine; + const safeVal = safeComment(val); + return this.indentate(level) + `` + this.newLine; } else if (key[0] === "?") {//PI tag return this.indentate(level) + '<' + key + attrStr + '?' + this.tagEndChar; } else { @@ -53141,7 +53723,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 @@ -53565,12 +54147,13 @@ function getPositionFromMatch(match) { const XMLValidator = { - validate: validate + validate: validator_validate } ;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/xmlparser/OptionsBuilder.js + const defaultOnDangerousProperty = (name) => { if (DANGEROUS_PROPERTY_NAMES.includes(name)) { return "__" + name; @@ -53610,6 +54193,7 @@ const OptionsBuilder_defaultOptions = { unpairedTags: [], processEntities: true, htmlEntities: false, + entityDecoder: null, ignoreDeclaration: false, ignorePiTags: false, transformTagName: false, @@ -53656,32 +54240,34 @@ function validatePropertyName(propertyName, optionName) { * @param {boolean|object} value * @returns {object} Always returns normalized object */ -function normalizeProcessEntities(value) { +function normalizeProcessEntities(value, htmlEntities) { // Boolean backward compatibility if (typeof value === 'boolean') { return { enabled: value, // true or false maxEntitySize: 10000, - maxExpansionDepth: 10, - maxTotalExpansions: 1000, + maxExpansionDepth: 10000, + maxTotalExpansions: Infinity, maxExpandedLength: 100000, - maxEntityCount: 100, + maxEntityCount: 1000, allowedTags: null, - tagFilter: null + tagFilter: null, + appliesTo: "all", }; } // Object config - merge with defaults if (typeof value === 'object' && value !== null) { return { - enabled: value.enabled !== false, // default true if not specified - maxEntitySize: value.maxEntitySize ?? 10000, - maxExpansionDepth: value.maxExpansionDepth ?? 10, - maxTotalExpansions: value.maxTotalExpansions ?? 1000, - maxExpandedLength: value.maxExpandedLength ?? 100000, - maxEntityCount: value.maxEntityCount ?? 100, + enabled: value.enabled !== false, + maxEntitySize: Math.max(1, value.maxEntitySize ?? 10000), + maxExpansionDepth: Math.max(1, value.maxExpansionDepth ?? 10000), + maxTotalExpansions: Math.max(1, value.maxTotalExpansions ?? Infinity), + maxExpandedLength: Math.max(1, value.maxExpandedLength ?? 100000), + maxEntityCount: Math.max(1, value.maxEntityCount ?? 1000), allowedTags: value.allowedTags ?? null, - tagFilter: value.tagFilter ?? null + tagFilter: value.tagFilter ?? null, + appliesTo: value.appliesTo ?? "all", }; } @@ -53712,8 +54298,8 @@ const buildOptions = function (options) { } // Always normalize processEntities for backward compatibility and validation - built.processEntities = normalizeProcessEntities(built.processEntities); - + built.processEntities = normalizeProcessEntities(built.processEntities, built.htmlEntities); + built.unpairedTagsSet = new Set(built.unpairedTags); // Convert old-style stopNodes for backward compatibility if (built.stopNodes && Array.isArray(built.stopNodes)) { built.stopNodes = built.stopNodes.map(node => { @@ -53774,11 +54360,15 @@ class XmlNode { class DocTypeReader { - constructor(options) { + constructor(options, xmlVersion) { this.suppressValidationErr = !options; this.options = options; + this.xmlVersion = xmlVersion || 1.0; } + setXmlVersion(xmlVersion = 1.0) { + this.xmlVersion = xmlVersion; + } readDocType(xmlData, i) { const entities = Object.create(null); let entityCount = 0; @@ -53801,18 +54391,15 @@ class DocTypeReader { [entityName, val, i] = this.readEntityExp(xmlData, i + 1, this.suppressValidationErr); if (val.indexOf("&") === -1) { //Parameter entities are not supported if (this.options.enabled !== false && - this.options.maxEntityCount && + this.options.maxEntityCount != null && entityCount >= this.options.maxEntityCount) { throw new Error( `Entity count (${entityCount + 1}) exceeds maximum allowed (${this.options.maxEntityCount})` ); } //const escaped = entityName.replace(/[.\-+*:]/g, '\\.'); - const escaped = entityName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - entities[entityName] = { - regx: RegExp(`&${escaped};`, "g"), - val: val - }; + //const escaped = entityName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + entities[entityName] = val; entityCount++; } } @@ -53873,12 +54460,13 @@ class DocTypeReader { i = skipWhitespace(xmlData, i); // Read entity name - let entityName = ""; + const startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i]) && xmlData[i] !== '"' && xmlData[i] !== "'") { - entityName += xmlData[i]; i++; } - validateEntityName(entityName); + let entityName = xmlData.substring(startIndex, i); + + validateEntityName(entityName, { xmlVersion: this.xmlVersion }); // Skip whitespace after entity name i = skipWhitespace(xmlData, i); @@ -53898,7 +54486,7 @@ class DocTypeReader { // Validate entity size if (this.options.enabled !== false && - this.options.maxEntitySize && + this.options.maxEntitySize != null && entityValue.length > this.options.maxEntitySize) { throw new Error( `Entity "${entityName}" size (${entityValue.length}) exceeds maximum allowed size (${this.options.maxEntitySize})` @@ -53914,12 +54502,14 @@ class DocTypeReader { i = skipWhitespace(xmlData, i); // Read notation name - let notationName = ""; + + const startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - notationName += xmlData[i]; i++; } - !this.suppressValidationErr && validateEntityName(notationName); + let notationName = xmlData.substring(startIndex, i); + + !this.suppressValidationErr && validateEntityName(notationName, { xmlVersion: this.xmlVersion }); // Skip whitespace after notation name i = skipWhitespace(xmlData, i); @@ -53968,10 +54558,11 @@ class DocTypeReader { } i++; + const startIndex = i; while (i < xmlData.length && xmlData[i] !== startChar) { - identifierVal += xmlData[i]; i++; } + identifierVal = xmlData.substring(startIndex, i); if (xmlData[i] !== startChar) { throw new Error(`Unterminated ${type} value`); @@ -53991,14 +54582,14 @@ class DocTypeReader { i = skipWhitespace(xmlData, i); // Read element name - let elementName = ""; + const startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - elementName += xmlData[i]; i++; } + let elementName = xmlData.substring(startIndex, i); // Validate element name - if (!this.suppressValidationErr && !isName(elementName)) { + if (!this.suppressValidationErr && !qName(elementName, { xmlVersion: this.xmlVersion })) { throw new Error(`Invalid element name: "${elementName}"`); } @@ -54012,10 +54603,12 @@ class DocTypeReader { i++; // Move past '(' // Read content model + const startIndex = i; while (i < xmlData.length && xmlData[i] !== ")") { - contentModel += xmlData[i]; i++; } + contentModel = xmlData.substring(startIndex, i); + if (xmlData[i] !== ")") { throw new Error("Unterminated content model"); } @@ -54036,27 +54629,27 @@ class DocTypeReader { i = skipWhitespace(xmlData, i); // Read element name - let elementName = ""; + let startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - elementName += xmlData[i]; i++; } + let elementName = xmlData.substring(startIndex, i); // Validate element name - validateEntityName(elementName) + validateEntityName(elementName, { xmlVersion: this.xmlVersion }) // Skip whitespace after element name i = skipWhitespace(xmlData, i); // Read attribute name - let attributeName = ""; + startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - attributeName += xmlData[i]; i++; } + let attributeName = xmlData.substring(startIndex, i); // Validate attribute name - if (!validateEntityName(attributeName)) { + if (!validateEntityName(attributeName, { xmlVersion: this.xmlVersion })) { throw new Error(`Invalid attribute name: "${attributeName}"`); } @@ -54081,15 +54674,17 @@ class DocTypeReader { // Read the list of allowed notations let allowedNotations = []; while (i < xmlData.length && xmlData[i] !== ")") { - let notation = ""; + + + const startIndex = i; while (i < xmlData.length && xmlData[i] !== "|" && xmlData[i] !== ")") { - notation += xmlData[i]; i++; } + let notation = xmlData.substring(startIndex, i); // Validate notation name notation = notation.trim(); - if (!validateEntityName(notation)) { + if (!validateEntityName(notation, { xmlVersion: this.xmlVersion })) { throw new Error(`Invalid notation name: "${notation}"`); } @@ -54111,10 +54706,11 @@ class DocTypeReader { attributeType += " (" + allowedNotations.join("|") + ")"; } else { // Handle simple types (e.g., CDATA, ID, IDREF, etc.) + const startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - attributeType += xmlData[i]; i++; } + attributeType += xmlData.substring(startIndex, i); // Validate simple attribute type const validTypes = ["CDATA", "ID", "IDREF", "IDREFS", "ENTITY", "ENTITIES", "NMTOKEN", "NMTOKENS"]; @@ -54166,115 +54762,124 @@ function hasSeq(data, seq, i) { return true; } -function validateEntityName(name) { - if (isName(name)) +function validateEntityName(name, xmlVersion) { + if (qName(name, { xmlVersion: xmlVersion })) return name; else throw new Error(`Invalid entity name ${name}`); } ;// CONCATENATED MODULE: ./node_modules/strnum/strnum.js const hexRegex = /^[-+]?0x[a-fA-F0-9]+$/; +const binRegex = /^0b[01]+$/; +const octRegex = /^0o[0-7]+$/; const numRegex = /^([\-\+])?(0*)([0-9]*(\.[0-9]*)?)$/; -// const octRegex = /^0x[a-z0-9]+/; -// const binRegex = /0x[a-z0-9]+/; - const consider = { - hex : true, - // oct: false, + hex: true, + binary: false, + octal: false, leadingZeros: true, decimalPoint: "\.", eNotation: true, - //skipLike: /regex/ + //skipLike: /regex/, + infinity: "original", // "null", "infinity" (Infinity type), "string" ("Infinity" (the string literal)) }; -function toNumber(str, options = {}){ - options = Object.assign({}, consider, options ); - if(!str || typeof str !== "string" ) return str; - - let trimmedStr = str.trim(); - - if(options.skipLike !== undefined && options.skipLike.test(trimmedStr)) return str; - else if(str==="0") return 0; +function toNumber(str, options = {}) { + options = Object.assign({}, consider, options); + if (!str || typeof str !== "string") return str; + + let trimmedStr = str.trim(); + + if (trimmedStr.length === 0) return str; + else if (options.skipLike !== undefined && options.skipLike.test(trimmedStr)) return str; + else if (trimmedStr === "0") return 0; else if (options.hex && hexRegex.test(trimmedStr)) { return parse_int(trimmedStr, 16); - // }else if (options.oct && octRegex.test(str)) { - // return Number.parseInt(val, 8); - }else if (trimmedStr.includes('e') || trimmedStr.includes('E')) { //eNotation - return resolveEnotation(str,trimmedStr,options); - // }else if (options.parseBin && binRegex.test(str)) { - // return Number.parseInt(val, 2); - }else{ + } else if (options.binary && binRegex.test(trimmedStr)) { + return parse_int(trimmedStr, 2); + } else if (options.octal && octRegex.test(trimmedStr)) { + return parse_int(trimmedStr, 8); + } else if (!isFinite(trimmedStr)) { //Infinity + return handleInfinity(str, Number(trimmedStr), options); + } else if (trimmedStr.includes('e') || trimmedStr.includes('E')) { //eNotation + return resolveEnotation(str, trimmedStr, options); + } else { //separate negative sign, leading zeros, and rest number const match = numRegex.exec(trimmedStr); // +00.123 => [ , '+', '00', '.123', .. - if(match){ + if (match) { const sign = match[1] || ""; const leadingZeros = match[2]; let numTrimmedByZeros = trimZeros(match[3]); //complete num without leading zeros const decimalAdjacentToLeadingZeros = sign ? // 0., -00., 000. - str[leadingZeros.length+1] === "." + str[leadingZeros.length + 1] === "." : str[leadingZeros.length] === "."; //trim ending zeros for floating number - if(!options.leadingZeros //leading zeros are not allowed - && (leadingZeros.length > 1 - || (leadingZeros.length === 1 && !decimalAdjacentToLeadingZeros))){ + if (!options.leadingZeros //leading zeros are not allowed + && (leadingZeros.length > 1 + || (leadingZeros.length === 1 && !decimalAdjacentToLeadingZeros))) { // 00, 00.3, +03.24, 03, 03.24 return str; } - else{//no leading zeros or leading zeros are allowed + else {//no leading zeros or leading zeros are allowed const num = Number(trimmedStr); const parsedStr = String(num); - if( num === 0) return num; - if(parsedStr.search(/[eE]/) !== -1){ //given number is long and parsed to eNotation - if(options.eNotation) return num; + if (num === 0) return num; + if (parsedStr.search(/[eE]/) !== -1) { //given number is long and parsed to eNotation + if (options.eNotation) return num; else return str; - }else if(trimmedStr.indexOf(".") !== -1){ //floating number - if(parsedStr === "0") return num; //0.0 - else if(parsedStr === numTrimmedByZeros) return num; //0.456. 0.79000 - else if( parsedStr === `${sign}${numTrimmedByZeros}`) return num; + } else if (trimmedStr.indexOf(".") !== -1) { //floating number + if (parsedStr === "0") return num; //0.0 + else if (parsedStr === numTrimmedByZeros) return num; //0.456. 0.79000 + else if (parsedStr === `${sign}${numTrimmedByZeros}`) return num; else return str; } - - let n = leadingZeros? numTrimmedByZeros : trimmedStr; - if(leadingZeros){ + + let n = leadingZeros ? numTrimmedByZeros : trimmedStr; + if (leadingZeros) { // -009 => -9 - return (n === parsedStr) || (sign+n === parsedStr) ? num : str - }else { + return (n === parsedStr) || (sign + n === parsedStr) ? num : str + } else { // +9 - return (n === parsedStr) || (n === sign+parsedStr) ? num : str + return (n === parsedStr) || (n === sign + parsedStr) ? num : str } } - }else{ //non-numeric string + } else { //non-numeric string return str; } } } const eNotationRegx = /^([-+])?(0*)(\d*(\.\d*)?[eE][-\+]?\d+)$/; -function resolveEnotation(str,trimmedStr,options){ - if(!options.eNotation) return str; - const notation = trimmedStr.match(eNotationRegx); - if(notation){ +function resolveEnotation(str, trimmedStr, options) { + if (!options.eNotation) return str; + const notation = trimmedStr.match(eNotationRegx); + if (notation) { let sign = notation[1] || ""; const eChar = notation[3].indexOf("e") === -1 ? "E" : "e"; const leadingZeros = notation[2]; const eAdjacentToLeadingZeros = sign ? // 0E. - str[leadingZeros.length+1] === eChar + str[leadingZeros.length + 1] === eChar : str[leadingZeros.length] === eChar; - if(leadingZeros.length > 1 && eAdjacentToLeadingZeros) return str; - else if(leadingZeros.length === 1 - && (notation[3].startsWith(`.${eChar}`) || notation[3][0] === eChar)){ - return Number(trimmedStr); - }else if(options.leadingZeros && !eAdjacentToLeadingZeros){ //accept with leading zeros - //remove leading 0s - trimmedStr = (notation[1] || "") + notation[3]; + if (leadingZeros.length > 1 && eAdjacentToLeadingZeros) return str; + else if (leadingZeros.length === 1 + && (notation[3].startsWith(`.${eChar}`) || notation[3][0] === eChar)) { return Number(trimmedStr); - }else return str; - }else{ + } else if (leadingZeros.length > 0) { + // Has leading zeros — only accept if leadingZeros option allows it + if (options.leadingZeros && !eAdjacentToLeadingZeros) { + trimmedStr = (notation[1] || "") + notation[3]; + return Number(trimmedStr); + } else return str; + } else { + // No leading zeros — always valid e-notation, parse it + return Number(trimmedStr); + } + } else { return str; } } @@ -54284,23 +54889,48 @@ function resolveEnotation(str,trimmedStr,options){ * @param {string} numStr without leading zeros * @returns */ -function trimZeros(numStr){ - if(numStr && numStr.indexOf(".") !== -1){//float +function trimZeros(numStr) { + if (numStr && numStr.indexOf(".") !== -1) {//float numStr = numStr.replace(/0+$/, ""); //remove ending zeros - if(numStr === ".") numStr = "0"; - else if(numStr[0] === ".") numStr = "0"+numStr; - else if(numStr[numStr.length-1] === ".") numStr = numStr.substring(0,numStr.length-1); + if (numStr === ".") numStr = "0"; + else if (numStr[0] === ".") numStr = "0" + numStr; + else if (numStr[numStr.length - 1] === ".") numStr = numStr.substring(0, numStr.length - 1); return numStr; } return numStr; } -function parse_int(numStr, base){ - //polyfill - if(parseInt) return parseInt(numStr, base); - else if(Number.parseInt) return Number.parseInt(numStr, base); - else if(window && window.parseInt) return window.parseInt(numStr, base); - else throw new Error("parseInt, Number.parseInt, window.parseInt are not supported") +function parse_int(numStr, base) { + const str = numStr.trim(); + if (base === 2 || base === 8) numStr = str.substring(2); + + if (parseInt) return parseInt(numStr, base); + else if (Number.parseInt) return Number.parseInt(numStr, base); + else if (window && window.parseInt) return window.parseInt(numStr, base); + else throw new Error("parseInt, Number.parseInt, window.parseInt are not supported"); +} + +/** + * Handle infinite values based on user option + * @param {string} str - original input string + * @param {number} num - parsed number (Infinity or -Infinity) + * @param {object} options - user options + * @returns {string|number|null} based on infinity option + */ +function handleInfinity(str, num, options) { + const isPositive = num === Infinity; + + switch (options.infinity.toLowerCase()) { + case "null": + return null; + case "infinity": + return num; // Return Infinity or -Infinity + case "string": + return isPositive ? "Infinity" : "-Infinity"; + case "original": + default: + return str; // Return original string like "1e1000" + } } ;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/ignoreAttributes.js function ignoreAttributes_getIgnoreAttributesFn(ignoreAttributes) { @@ -54321,6 +54951,1939 @@ function ignoreAttributes_getIgnoreAttributesFn(ignoreAttributes) { } return () => false } +;// CONCATENATED MODULE: ./node_modules/path-expression-matcher/src/ExpressionSet.js +/** + * ExpressionSet - An indexed collection of Expressions for efficient bulk matching + * + * Instead of iterating all expressions on every tag, ExpressionSet pre-indexes + * them at insertion time by depth and terminal tag name. At match time, only + * the relevant bucket is evaluated — typically reducing checks from O(E) to O(1) + * lookup plus O(small bucket) matches. + * + * Three buckets are maintained: + * - `_byDepthAndTag` — exact depth + exact tag name (tightest, used first) + * - `_wildcardByDepth` — exact depth + wildcard tag `*` (depth-matched only) + * - `_deepWildcards` — expressions containing `..` (cannot be depth-indexed) + * + * @example + * import { Expression, ExpressionSet } from 'fast-xml-tagger'; + * + * // Build once at config time + * const stopNodes = new ExpressionSet(); + * stopNodes.add(new Expression('root.users.user')); + * stopNodes.add(new Expression('root.config.setting')); + * stopNodes.add(new Expression('..script')); + * + * // Query on every tag — hot path + * if (stopNodes.matchesAny(matcher)) { ... } + */ +class ExpressionSet { + constructor() { + /** @type {Map} depth:tag → expressions */ + this._byDepthAndTag = new Map(); + + /** @type {Map} depth → wildcard-tag expressions */ + this._wildcardByDepth = new Map(); + + /** @type {import('./Expression.js').default[]} expressions containing deep wildcard (..) */ + this._deepWildcards = []; + + /** @type {Set} pattern strings already added — used for deduplication */ + this._patterns = new Set(); + + /** @type {boolean} whether the set is sealed against further additions */ + this._sealed = false; + } + + /** + * Add an Expression to the set. + * Duplicate patterns (same pattern string) are silently ignored. + * + * @param {import('./Expression.js').default} expression - A pre-constructed Expression instance + * @returns {this} for chaining + * @throws {TypeError} if called after seal() + * + * @example + * set.add(new Expression('root.users.user')); + * set.add(new Expression('..script')); + */ + add(expression) { + if (this._sealed) { + throw new TypeError( + 'ExpressionSet is sealed. Create a new ExpressionSet to add more expressions.' + ); + } + + // Deduplicate by pattern string + if (this._patterns.has(expression.pattern)) return this; + this._patterns.add(expression.pattern); + + if (expression.hasDeepWildcard()) { + this._deepWildcards.push(expression); + return this; + } + + const depth = expression.length; + const lastSeg = expression.segments[expression.segments.length - 1]; + const tag = lastSeg?.tag; + + if (!tag || tag === '*') { + // Can index by depth but not by tag + if (!this._wildcardByDepth.has(depth)) this._wildcardByDepth.set(depth, []); + this._wildcardByDepth.get(depth).push(expression); + } else { + // Tightest bucket: depth + tag + const key = `${depth}:${tag}`; + if (!this._byDepthAndTag.has(key)) this._byDepthAndTag.set(key, []); + this._byDepthAndTag.get(key).push(expression); + } + + return this; + } + + /** + * Add multiple expressions at once. + * + * @param {import('./Expression.js').default[]} expressions - Array of Expression instances + * @returns {this} for chaining + * + * @example + * set.addAll([ + * new Expression('root.users.user'), + * new Expression('root.config.setting'), + * ]); + */ + addAll(expressions) { + for (const expr of expressions) this.add(expr); + return this; + } + + /** + * Check whether a pattern string is already present in the set. + * + * @param {import('./Expression.js').default} expression + * @returns {boolean} + */ + has(expression) { + return this._patterns.has(expression.pattern); + } + + /** + * Number of expressions in the set. + * @type {number} + */ + get size() { + return this._patterns.size; + } + + /** + * Seal the set against further modifications. + * Useful to prevent accidental mutations after config is built. + * Calling add() or addAll() on a sealed set throws a TypeError. + * + * @returns {this} + */ + seal() { + this._sealed = true; + return this; + } + + /** + * Whether the set has been sealed. + * @type {boolean} + */ + get isSealed() { + return this._sealed; + } + + /** + * Test whether the matcher's current path matches any expression in the set. + * + * Evaluation order (cheapest → most expensive): + * 1. Exact depth + tag bucket — O(1) lookup, typically 0–2 expressions + * 2. Depth-only wildcard bucket — O(1) lookup, rare + * 3. Deep-wildcard list — always checked, but usually small + * + * @param {import('./Matcher.js').default} matcher - Matcher instance (or readOnly view) + * @returns {boolean} true if any expression matches the current path + * + * @example + * if (stopNodes.matchesAny(matcher)) { + * // handle stop node + * } + */ + matchesAny(matcher) { + return this.findMatch(matcher) !== null; + } + /** + * Find and return the first Expression that matches the matcher's current path. + * + * Uses the same evaluation order as matchesAny (cheapest → most expensive): + * 1. Exact depth + tag bucket + * 2. Depth-only wildcard bucket + * 3. Deep-wildcard list + * + * @param {import('./Matcher.js').default} matcher - Matcher instance (or readOnly view) + * @returns {import('./Expression.js').default | null} the first matching Expression, or null + * + * @example + * const expr = stopNodes.findMatch(matcher); + * if (expr) { + * // access expr.config, expr.pattern, etc. + * } + */ + findMatch(matcher) { + const depth = matcher.getDepth(); + const tag = matcher.getCurrentTag(); + + // 1. Tightest bucket — most expressions live here + const exactKey = `${depth}:${tag}`; + const exactBucket = this._byDepthAndTag.get(exactKey); + if (exactBucket) { + for (let i = 0; i < exactBucket.length; i++) { + if (matcher.matches(exactBucket[i])) return exactBucket[i]; + } + } + + // 2. Depth-matched wildcard-tag expressions + const wildcardBucket = this._wildcardByDepth.get(depth); + if (wildcardBucket) { + for (let i = 0; i < wildcardBucket.length; i++) { + if (matcher.matches(wildcardBucket[i])) return wildcardBucket[i]; + } + } + + // 3. Deep wildcards — cannot be pre-filtered by depth or tag + for (let i = 0; i < this._deepWildcards.length; i++) { + if (matcher.matches(this._deepWildcards[i])) return this._deepWildcards[i]; + } + + return null; + } +} + +;// CONCATENATED MODULE: ./node_modules/@nodable/entities/src/entities.js +// --------------------------------------------------------------------------- +// Complete HTML5 named entity reference +// Organized by logical categories for easy maintenance and selective importing +// --------------------------------------------------------------------------- + +/** + * Basic Latin & Special Characters + * @type {Record} + */ +const BASIC_LATIN = { + amp: '&', + AMP: '&', + lt: '<', + LT: '<', + gt: '>', + GT: '>', + quot: '"', + QUOT: '"', + apos: "'", + lsquo: '‘', + rsquo: '’', + ldquo: '“', + rdquo: '”', + lsquor: '‚', + rsquor: '’', + ldquor: '„', + bdquo: '„', + comma: ',', + period: '.', + colon: ':', + semi: ';', + excl: '!', + quest: '?', + num: '#', + dollar: '$', + percent: '%', + amp: '&', + ast: '*', + commat: '@', + lowbar: '_', + verbar: '|', + vert: '|', + sol: '/', + bsol: '\\', + lbrace: '{', + rbrace: '}', + lbrack: '[', + rbrack: ']', + lpar: '(', + rpar: ')', + nbsp: '\u00a0', + iexcl: '¡', + cent: '¢', + pound: '£', + curren: '¤', + yen: '¥', + brvbar: '¦', + sect: '§', + uml: '¨', + copy: '©', + COPY: '©', + ordf: 'ª', + laquo: '«', + not: '¬', + shy: '\u00ad', + reg: '®', + REG: '®', + macr: '¯', + deg: '°', + plusmn: '±', + sup2: '²', + sup3: '³', + acute: '´', + micro: 'µ', + para: '¶', + middot: '·', + cedil: '¸', + sup1: '¹', + ordm: 'º', + raquo: '»', + frac14: '¼', + frac12: '½', + half: '½', + frac34: '¾', + iquest: '¿', + times: '×', + div: '÷', + divide: '÷', +}; + +/** + * Latin Extended & Accented Letters (A-Z) + * @type {Record} + */ +const LATIN_ACCENTS = { + Agrave: 'À', + agrave: 'à', + Aacute: 'Á', + aacute: 'á', + Acirc: 'Â', + acirc: 'â', + Atilde: 'Ã', + atilde: 'ã', + Auml: 'Ä', + auml: 'ä', + Aring: 'Å', + aring: 'å', + AElig: 'Æ', + aelig: 'æ', + Ccedil: 'Ç', + ccedil: 'ç', + Egrave: 'È', + egrave: 'è', + Eacute: 'É', + eacute: 'é', + Ecirc: 'Ê', + ecirc: 'ê', + Euml: 'Ë', + euml: 'ë', + Igrave: 'Ì', + igrave: 'ì', + Iacute: 'Í', + iacute: 'í', + Icirc: 'Î', + icirc: 'î', + Iuml: 'Ï', + iuml: 'ï', + ETH: 'Ð', + eth: 'ð', + Ntilde: 'Ñ', + ntilde: 'ñ', + Ograve: 'Ò', + ograve: 'ò', + Oacute: 'Ó', + oacute: 'ó', + Ocirc: 'Ô', + ocirc: 'ô', + Otilde: 'Õ', + otilde: 'õ', + Ouml: 'Ö', + ouml: 'ö', + Oslash: 'Ø', + oslash: 'ø', + Ugrave: 'Ù', + ugrave: 'ù', + Uacute: 'Ú', + uacute: 'ú', + Ucirc: 'Û', + ucirc: 'û', + Uuml: 'Ü', + uuml: 'ü', + Yacute: 'Ý', + yacute: 'ý', + THORN: 'Þ', + thorn: 'þ', + szlig: 'ß', + yuml: 'ÿ', + Yuml: 'Ÿ', +}; + +/** + * Latin Extended (Letters with diacritics) + * @type {Record} + */ +const LATIN_EXTENDED = { + Amacr: 'Ā', + amacr: 'ā', + Abreve: 'Ă', + abreve: 'ă', + Aogon: 'Ą', + aogon: 'ą', + Cacute: 'Ć', + cacute: 'ć', + Ccirc: 'Ĉ', + ccirc: 'ĉ', + Cdot: 'Ċ', + cdot: 'ċ', + Ccaron: 'Č', + ccaron: 'č', + Dcaron: 'Ď', + dcaron: 'ď', + Dstrok: 'Đ', + dstrok: 'đ', + Emacr: 'Ē', + emacr: 'ē', + Ecaron: 'Ě', + ecaron: 'ě', + Edot: 'Ė', + edot: 'ė', + Eogon: 'Ę', + eogon: 'ę', + Gcirc: 'Ĝ', + gcirc: 'ĝ', + Gbreve: 'Ğ', + gbreve: 'ğ', + Gdot: 'Ġ', + gdot: 'ġ', + Gcedil: 'Ģ', + Hcirc: 'Ĥ', + hcirc: 'ĥ', + Hstrok: 'Ħ', + hstrok: 'ħ', + Itilde: 'Ĩ', + itilde: 'ĩ', + Imacr: 'Ī', + imacr: 'ī', + Iogon: 'Į', + iogon: 'į', + Idot: 'İ', + IJlig: 'IJ', + ijlig: 'ij', + Jcirc: 'Ĵ', + jcirc: 'ĵ', + Kcedil: 'Ķ', + kcedil: 'ķ', + kgreen: 'ĸ', + Lacute: 'Ĺ', + lacute: 'ĺ', + Lcedil: 'Ļ', + lcedil: 'ļ', + Lcaron: 'Ľ', + lcaron: 'ľ', + Lmidot: 'Ŀ', + lmidot: 'ŀ', + Lstrok: 'Ł', + lstrok: 'ł', + Nacute: 'Ń', + nacute: 'ń', + Ncaron: 'Ň', + ncaron: 'ň', + Ncedil: 'Ņ', + ncedil: 'ņ', + ENG: 'Ŋ', + eng: 'ŋ', + Omacr: 'Ō', + omacr: 'ō', + Odblac: 'Ő', + odblac: 'ő', + OElig: 'Œ', + oelig: 'œ', + Racute: 'Ŕ', + racute: 'ŕ', + Rcaron: 'Ř', + rcaron: 'ř', + Rcedil: 'Ŗ', + rcedil: 'ŗ', + Sacute: 'Ś', + sacute: 'ś', + Scirc: 'Ŝ', + scirc: 'ŝ', + Scedil: 'Ş', + scedil: 'ş', + Scaron: 'Š', + scaron: 'š', + Tcedil: 'Ţ', + tcedil: 'ţ', + Tcaron: 'Ť', + tcaron: 'ť', + Tstrok: 'Ŧ', + tstrok: 'ŧ', + Utilde: 'Ũ', + utilde: 'ũ', + Umacr: 'Ū', + umacr: 'ū', + Ubreve: 'Ŭ', + ubreve: 'ŭ', + Uring: 'Ů', + uring: 'ů', + Udblac: 'Ű', + udblac: 'ű', + Uogon: 'Ų', + uogon: 'ų', + Wcirc: 'Ŵ', + wcirc: 'ŵ', + Ycirc: 'Ŷ', + ycirc: 'ŷ', + Zacute: 'Ź', + zacute: 'ź', + Zdot: 'Ż', + zdot: 'ż', + Zcaron: 'Ž', + zcaron: 'ž', +}; + +/** + * Greek Letters + * @type {Record} + */ +const GREEK = { + Alpha: 'Α', + alpha: 'α', + Beta: 'Β', + beta: 'β', + Gamma: 'Γ', + gamma: 'γ', + Delta: 'Δ', + delta: 'δ', + Epsilon: 'Ε', + epsilon: 'ε', + epsiv: 'ϵ', + varepsilon: 'ϵ', + Zeta: 'Ζ', + zeta: 'ζ', + Eta: 'Η', + eta: 'η', + Theta: 'Θ', + theta: 'θ', + thetasym: 'ϑ', + vartheta: 'ϑ', + Iota: 'Ι', + iota: 'ι', + Kappa: 'Κ', + kappa: 'κ', + kappav: 'ϰ', + varkappa: 'ϰ', + Lambda: 'Λ', + lambda: 'λ', + Mu: 'Μ', + mu: 'μ', + Nu: 'Ν', + nu: 'ν', + Xi: 'Ξ', + xi: 'ξ', + Omicron: 'Ο', + omicron: 'ο', + Pi: 'Π', + pi: 'π', + piv: 'ϖ', + varpi: 'ϖ', + Rho: 'Ρ', + rho: 'ρ', + rhov: 'ϱ', + varrho: 'ϱ', + Sigma: 'Σ', + sigma: 'σ', + sigmaf: 'ς', + sigmav: 'ς', + varsigma: 'ς', + Tau: 'Τ', + tau: 'τ', + Upsilon: 'Υ', + upsilon: 'υ', + upsi: 'υ', + Upsi: 'ϒ', + upsih: 'ϒ', + Phi: 'Φ', + phi: 'φ', + phiv: 'ϕ', + varphi: 'ϕ', + Chi: 'Χ', + chi: 'χ', + Psi: 'Ψ', + psi: 'ψ', + Omega: 'Ω', + omega: 'ω', + ohm: 'Ω', + Gammad: 'Ϝ', + gammad: 'ϝ', + digamma: 'ϝ', +}; + +/** + * Cyrillic Letters + * @type {Record} + */ +const CYRILLIC = { + Afr: '𝔄', + afr: '𝔞', + Acy: 'А', + acy: 'а', + Bcy: 'Б', + bcy: 'б', + Vcy: 'В', + vcy: 'в', + Gcy: 'Г', + gcy: 'г', + Dcy: 'Д', + dcy: 'д', + IEcy: 'Е', + iecy: 'е', + IOcy: 'Ё', + iocy: 'ё', + ZHcy: 'Ж', + zhcy: 'ж', + Zcy: 'З', + zcy: 'з', + Icy: 'И', + icy: 'и', + Jcy: 'Й', + jcy: 'й', + Kcy: 'К', + kcy: 'к', + Lcy: 'Л', + lcy: 'л', + Mcy: 'М', + mcy: 'м', + Ncy: 'Н', + ncy: 'н', + Ocy: 'О', + ocy: 'о', + Pcy: 'П', + pcy: 'п', + Rcy: 'Р', + rcy: 'р', + Scy: 'С', + scy: 'с', + Tcy: 'Т', + tcy: 'т', + Ucy: 'У', + ucy: 'у', + Fcy: 'Ф', + fcy: 'ф', + KHcy: 'Х', + khcy: 'х', + TScy: 'Ц', + tscy: 'ц', + CHcy: 'Ч', + chcy: 'ч', + SHcy: 'Ш', + shcy: 'ш', + SHCHcy: 'Щ', + shchcy: 'щ', + HARDcy: 'Ъ', + hardcy: 'ъ', + Ycy: 'Ы', + ycy: 'ы', + SOFTcy: 'Ь', + softcy: 'ь', + Ecy: 'Э', + ecy: 'э', + YUcy: 'Ю', + yucy: 'ю', + YAcy: 'Я', + yacy: 'я', + DJcy: 'Ђ', + djcy: 'ђ', + GJcy: 'Ѓ', + gjcy: 'ѓ', + Jukcy: 'Є', + jukcy: 'є', + DScy: 'Ѕ', + dscy: 'ѕ', + Iukcy: 'І', + iukcy: 'і', + YIcy: 'Ї', + yicy: 'ї', + Jsercy: 'Ј', + jsercy: 'ј', + LJcy: 'Љ', + ljcy: 'љ', + NJcy: 'Њ', + njcy: 'њ', + TSHcy: 'Ћ', + tshcy: 'ћ', + KJcy: 'Ќ', + kjcy: 'ќ', + Ubrcy: 'Ў', + ubrcy: 'ў', + DZcy: 'Џ', + dzcy: 'џ', +}; + +/** + * Mathematical Operators & Relations + * @type {Record} + */ +const MATH = { + plus: '+', + minus: '−', + mnplus: '∓', + mp: '∓', + pm: '±', + times: '×', + div: '÷', + divide: '÷', + sdot: '⋅', + star: '☆', + starf: '★', + bigstar: '★', + lowast: '∗', + ast: '*', + midast: '*', + compfn: '∘', + smallcircle: '∘', + bullet: '•', + bull: '•', + nbsp: '\u00a0', + hellip: '…', + mldr: '…', + prime: '′', + Prime: '″', + tprime: '‴', + bprime: '‵', + backprime: '‵', + minus: '−', + minusd: '∸', + dotminus: '∸', + plusdo: '∔', + dotplus: '∔', + plusmn: '±', + minusplus: '∓', + mnplus: '∓', + mp: '∓', + setminus: '∖', + smallsetminus: '∖', + Backslash: '∖', + setmn: '∖', + ssetmn: '∖', + lowbar: '_', + verbar: '|', + vert: '|', + VerticalLine: '|', + colon: ':', + Colon: '∷', + Proportion: '∷', + ratio: '∶', + equals: '=', + ne: '≠', + nequiv: '≢', + equiv: '≡', + Congruent: '≡', + sim: '∼', + thicksim: '∼', + thksim: '∼', + sime: '≃', + simeq: '≃', + TildeEqual: '≃', + asymp: '≈', + approx: '≈', + thickapprox: '≈', + thkap: '≈', + TildeTilde: '≈', + ncong: '≇', + cong: '≅', + TildeFullEqual: '≅', + asympeq: '≍', + CupCap: '≍', + bump: '≎', + Bumpeq: '≎', + HumpDownHump: '≎', + bumpe: '≏', + bumpeq: '≏', + HumpEqual: '≏', + dotminus: '∸', + minusd: '∸', + plusdo: '∔', + dotplus: '∔', + le: '≤', + LessEqual: '≤', + ge: '≥', + GreaterEqual: '≥', + lesseqgtr: '⋚', + lesseqqgtr: '⪋', + greater: '>', + less: '<', +}; + +/** + * Mathematical Operators (Advanced) + * @type {Record} + */ +const MATH_ADVANCED = { + alefsym: 'ℵ', + aleph: 'ℵ', + beth: 'ℶ', + gimel: 'ℷ', + daleth: 'ℸ', + forall: '∀', + ForAll: '∀', + part: '∂', + PartialD: '∂', + exist: '∃', + Exists: '∃', + nexist: '∄', + nexists: '∄', + empty: '∅', + emptyset: '∅', + emptyv: '∅', + varnothing: '∅', + nabla: '∇', + Del: '∇', + isin: '∈', + isinv: '∈', + in: '∈', + Element: '∈', + notin: '∉', + notinva: '∉', + ni: '∋', + niv: '∋', + SuchThat: '∋', + ReverseElement: '∋', + notni: '∌', + notniva: '∌', + prod: '∏', + Product: '∏', + coprod: '∐', + Coproduct: '∐', + sum: '∑', + Sum: '∑', + minus: '−', + mp: '∓', + plusdo: '∔', + dotplus: '∔', + setminus: '∖', + lowast: '∗', + radic: '√', + Sqrt: '√', + prop: '∝', + propto: '∝', + Proportional: '∝', + varpropto: '∝', + infin: '∞', + infintie: '⧝', + ang: '∠', + angle: '∠', + angmsd: '∡', + measuredangle: '∡', + angsph: '∢', + mid: '∣', + VerticalBar: '∣', + nmid: '∤', + nsmid: '∤', + npar: '∦', + parallel: '∥', + spar: '∥', + nparallel: '∦', + nspar: '∦', + and: '∧', + wedge: '∧', + or: '∨', + vee: '∨', + cap: '∩', + cup: '∪', + int: '∫', + Integral: '∫', + conint: '∮', + ContourIntegral: '∮', + Conint: '∯', + DoubleContourIntegral: '∯', + Cconint: '∰', + there4: '∴', + therefore: '∴', + Therefore: '∴', + becaus: '∵', + because: '∵', + Because: '∵', + ratio: '∶', + Proportion: '∷', + minusd: '∸', + dotminus: '∸', + mDDot: '∺', + homtht: '∻', + sim: '∼', + bsimg: '∽', + backsim: '∽', + ac: '∾', + mstpos: '∾', + acd: '∿', + VerticalTilde: '≀', + wr: '≀', + wreath: '≀', + nsime: '≄', + nsimeq: '≄', + nsimeq: '≄', + ncong: '≇', + simne: '≆', + ncongdot: '⩭̸', + ngsim: '≵', + nsim: '≁', + napprox: '≉', + nap: '≉', + ngeq: '≱', + nge: '≱', + nleq: '≰', + nle: '≰', + ngtr: '≯', + ngt: '≯', + nless: '≮', + nlt: '≮', + nprec: '⊀', + npr: '⊀', + nsucc: '⊁', + nsc: '⊁', +}; + +/** + * Arrows + * @type {Record} + */ +const ARROWS = { + larr: '←', + leftarrow: '←', + LeftArrow: '←', + uarr: '↑', + uparrow: '↑', + UpArrow: '↑', + rarr: '→', + rightarrow: '→', + RightArrow: '→', + darr: '↓', + downarrow: '↓', + DownArrow: '↓', + harr: '↔', + leftrightarrow: '↔', + LeftRightArrow: '↔', + varr: '↕', + updownarrow: '↕', + UpDownArrow: '↕', + nwarr: '↖', + nwarrow: '↖', + UpperLeftArrow: '↖', + nearr: '↗', + nearrow: '↗', + UpperRightArrow: '↗', + searr: '↘', + searrow: '↘', + LowerRightArrow: '↘', + swarr: '↙', + swarrow: '↙', + LowerLeftArrow: '↙', + lArr: '⇐', + Leftarrow: '⇐', + uArr: '⇑', + Uparrow: '⇑', + rArr: '⇒', + Rightarrow: '⇒', + dArr: '⇓', + Downarrow: '⇓', + hArr: '⇔', + Leftrightarrow: '⇔', + iff: '⇔', + vArr: '⇕', + Updownarrow: '⇕', + lAarr: '⇚', + Lleftarrow: '⇚', + rAarr: '⇛', + Rrightarrow: '⇛', + lrarr: '⇆', + leftrightarrows: '⇆', + rlarr: '⇄', + rightleftarrows: '⇄', + lrhar: '⇋', + leftrightharpoons: '⇋', + ReverseEquilibrium: '⇋', + rlhar: '⇌', + rightleftharpoons: '⇌', + Equilibrium: '⇌', + udarr: '⇅', + UpArrowDownArrow: '⇅', + duarr: '⇵', + DownArrowUpArrow: '⇵', + llarr: '⇇', + leftleftarrows: '⇇', + rrarr: '⇉', + rightrightarrows: '⇉', + ddarr: '⇊', + downdownarrows: '⇊', + har: '↽', + lhard: '↽', + leftharpoondown: '↽', + lharu: '↼', + leftharpoonup: '↼', + rhard: '⇁', + rightharpoondown: '⇁', + rharu: '⇀', + rightharpoonup: '⇀', + lsh: '↰', + Lsh: '↰', + rsh: '↱', + Rsh: '↱', + ldsh: '↲', + rdsh: '↳', + hookleftarrow: '↩', + hookrightarrow: '↪', + mapstoleft: '↤', + mapstoup: '↥', + map: '↦', + mapsto: '↦', + mapstodown: '↧', + crarr: '↵', + nwarrow: '↖', + nearrow: '↗', + searrow: '↘', + swarrow: '↙', + nleftarrow: '↚', + nleftrightarrow: '↮', + nrightarrow: '↛', + nrarr: '↛', + larrtl: '↢', + rarrtl: '↣', + leftarrowtail: '↢', + rightarrowtail: '↣', + twoheadleftarrow: '↞', + twoheadrightarrow: '↠', + Larr: '↞', + Rarr: '↠', + larrhk: '↩', + rarrhk: '↪', + larrlp: '↫', + looparrowleft: '↫', + rarrlp: '↬', + looparrowright: '↬', + harrw: '↭', + leftrightsquigarrow: '↭', + nrarrw: '↝̸', + rarrw: '↝', + rightsquigarrow: '↝', + larrbfs: '⤟', + rarrbfs: '⤠', + nvHarr: '⤄', + nvlArr: '⤂', + nvrArr: '⤃', + larrfs: '⤝', + rarrfs: '⤞', + Map: '⤅', + larrsim: '⥳', + rarrsim: '⥴', + harrcir: '⥈', + Uarrocir: '⥉', + lurdshar: '⥊', + ldrdhar: '⥧', + ldrushar: '⥋', + rdldhar: '⥩', + lrhard: '⥭', + rlhar: '⇌', + uharr: '↾', + uharl: '↿', + dharr: '⇂', + dharl: '⇃', + Uarr: '↟', + Darr: '↡', + zigrarr: '⇝', + nwArr: '⇖', + neArr: '⇗', + seArr: '⇘', + swArr: '⇙', + nharr: '↮', + nhArr: '⇎', + nlarr: '↚', + nlArr: '⇍', + nrarr: '↛', + nrArr: '⇏', + larrb: '⇤', + LeftArrowBar: '⇤', + rarrb: '⇥', + RightArrowBar: '⇥', +}; + +/** + * Geometric Shapes + * @type {Record} + */ +const SHAPES = { + square: '□', + Square: '□', + squ: '□', + squf: '▪', + squarf: '▪', + blacksquar: '▪', + blacksquare: '▪', + FilledVerySmallSquare: '▪', + blk34: '▓', + blk12: '▒', + blk14: '░', + block: '█', + srect: '▭', + rect: '▭', + sdot: '⋅', + sdotb: '⊡', + dotsquare: '⊡', + triangle: '▵', + tri: '▵', + trine: '▵', + utri: '▵', + triangledown: '▿', + dtri: '▿', + tridown: '▿', + triangleleft: '◃', + ltri: '◃', + triangleright: '▹', + rtri: '▹', + blacktriangle: '▴', + utrif: '▴', + blacktriangledown: '▾', + dtrif: '▾', + blacktriangleleft: '◂', + ltrif: '◂', + blacktriangleright: '▸', + rtrif: '▸', + loz: '◊', + lozenge: '◊', + blacklozenge: '⧫', + lozf: '⧫', + bigcirc: '◯', + xcirc: '◯', + circ: 'ˆ', + Circle: '○', + cir: '○', + o: '○', + bullet: '•', + bull: '•', + hellip: '…', + mldr: '…', + nldr: '‥', + boxh: '─', + HorizontalLine: '─', + boxv: '│', + boxdr: '┌', + boxdl: '┐', + boxur: '└', + boxul: '┘', + boxvr: '├', + boxvl: '┤', + boxhd: '┬', + boxhu: '┴', + boxvh: '┼', + boxH: '═', + boxV: '║', + boxdR: '╒', + boxDr: '╓', + boxDR: '╔', + boxDl: '╕', + boxdL: '╖', + boxDL: '╗', + boxuR: '╘', + boxUr: '╙', + boxUR: '╚', + boxUl: '╜', + boxuL: '╛', + boxUL: '╝', + boxvR: '╞', + boxVr: '╟', + boxVR: '╠', + boxVl: '╢', + boxvL: '╡', + boxVL: '╣', + boxHd: '╤', + boxhD: '╥', + boxHD: '╦', + boxHu: '╧', + boxhU: '╨', + boxHU: '╩', + boxvH: '╪', + boxVh: '╫', + boxVH: '╬', +}; + +/** + * Punctuation & Diacritics + * @type {Record} + */ +const PUNCTUATION = { + excl: '!', + iexcl: '¡', + brvbar: '¦', + sect: '§', + uml: '¨', + copy: '©', + ordf: 'ª', + laquo: '«', + not: '¬', + shy: '\u00ad', + reg: '®', + macr: '¯', + deg: '°', + plusmn: '±', + sup2: '²', + sup3: '³', + acute: '´', + micro: 'µ', + para: '¶', + middot: '·', + cedil: '¸', + sup1: '¹', + ordm: 'º', + raquo: '»', + frac14: '¼', + frac12: '½', + frac34: '¾', + iquest: '¿', + nbsp: '\u00a0', + comma: ',', + period: '.', + colon: ':', + semi: ';', + vert: '|', + Verbar: '‖', + verbar: '|', + dblac: '˝', + circ: 'ˆ', + caron: 'ˇ', + breve: '˘', + dot: '˙', + ring: '˚', + ogon: '˛', + tilde: '˜', + DiacriticalGrave: '`', + DiacriticalAcute: '´', + DiacriticalTilde: '˜', + DiacriticalDot: '˙', + DiacriticalDoubleAcute: '˝', + grave: '`', + acute: '´', +}; + +/** + * Currency Symbols + * @type {Record} + */ +const CURRENCY = { + cent: '¢', + pound: '£', + curren: '¤', + yen: '¥', + euro: '€', + dollar: '$', + euro: '€', + fnof: 'ƒ', + inr: '₹', + af: '؋', + birr: 'ብር', + peso: '₱', + rub: '₽', + won: '₩', + yuan: '¥', + cedil: '¸', +}; + +/** + * Fractions + * @type {Record} + */ +const FRACTIONS = { + frac12: '½', + half: '½', + frac13: '⅓', + frac14: '¼', + frac15: '⅕', + frac16: '⅙', + frac18: '⅛', + frac23: '⅔', + frac25: '⅖', + frac34: '¾', + frac35: '⅗', + frac38: '⅜', + frac45: '⅘', + frac56: '⅚', + frac58: '⅝', + frac78: '⅞', + frasl: '⁄', +}; + +/** + * Miscellaneous Symbols + * @type {Record} + */ +const MISC_SYMBOLS = { + trade: '™', + TRADE: '™', + telrec: '⌕', + target: '⌖', + ulcorn: '⌜', + ulcorner: '⌜', + urcorn: '⌝', + urcorner: '⌝', + dlcorn: '⌞', + llcorner: '⌞', + drcorn: '⌟', + lrcorner: '⌟', + intercal: '⊺', + intcal: '⊺', + oplus: '⊕', + CirclePlus: '⊕', + ominus: '⊖', + CircleMinus: '⊖', + otimes: '⊗', + CircleTimes: '⊗', + osol: '⊘', + odot: '⊙', + CircleDot: '⊙', + oast: '⊛', + circledast: '⊛', + odash: '⊝', + circleddash: '⊝', + ocirc: '⊚', + circledcirc: '⊚', + boxplus: '⊞', + plusb: '⊞', + boxminus: '⊟', + minusb: '⊟', + boxtimes: '⊠', + timesb: '⊠', + boxdot: '⊡', + sdotb: '⊡', + veebar: '⊻', + vee: '∨', + barvee: '⊽', + and: '∧', + wedge: '∧', + Cap: '⋒', + Cup: '⋓', + Fork: '⋔', + pitchfork: '⋔', + epar: '⋕', + ltlarr: '⥶', + nvap: '≍⃒', + nvsim: '∼⃒', + nvge: '≥⃒', + nvle: '≤⃒', + nvlt: '<⃒', + nvgt: '>⃒', + nvltrie: '⊴⃒', + nvrtrie: '⊵⃒', + Vdash: '⊩', + dashv: '⊣', + vDash: '⊨', + Vdash: '⊩', + Vvdash: '⊪', + nvdash: '⊬', + nvDash: '⊭', + nVdash: '⊮', + nVDash: '⊯', +}; + +/** + * All entities combined (if you need everything) + * @type {Record} + */ +const ALL_ENTITIES = { + ...BASIC_LATIN, + ...LATIN_ACCENTS, + ...LATIN_EXTENDED, + ...GREEK, + ...CYRILLIC, + ...MATH, + ...MATH_ADVANCED, + ...ARROWS, + ...SHAPES, + ...PUNCTUATION, + ...CURRENCY, + ...FRACTIONS, + ...MISC_SYMBOLS, +}; + +const XML = { + amp: "&", + apos: "'", + gt: ">", + lt: "<", + quot: "\"" +} +const COMMON_HTML = { + nbsp: '\u00a0', + copy: '\u00a9', + reg: '\u00ae', + trade: '\u2122', + mdash: '\u2014', + ndash: '\u2013', + hellip: '\u2026', + laquo: '\u00ab', + raquo: '\u00bb', + lsquo: '\u2018', + rsquo: '\u2019', + ldquo: '\u201c', + rdquo: '\u201d', + bull: '\u2022', + para: '\u00b6', + sect: '\u00a7', + deg: '\u00b0', + frac12: '\u00bd', + frac14: '\u00bc', + frac34: '\u00be', +} +// --------------------------------------------------------------------------- +// Note: NUMERIC_ENTITIES (&#NNN; / &#xHH;) are handled by the scanner directly +// via String.fromCodePoint() without any map lookup. +// --------------------------------------------------------------------------- +;// CONCATENATED MODULE: ./node_modules/@nodable/entities/src/EntityDecoder.js +// --------------------------------------------------------------------------- +// Built-in named entity map (name → replacement string) +// No regex, no {regex,val} objects — just flat key/value pairs. +// --------------------------------------------------------------------------- + + + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +const SPECIAL_CHARS = new Set('!?\\\\/[]$%{}^&*()<>|+'); + +/** + * Validate that an entity name contains no dangerous characters. + * @param {string} name + * @returns {string} the name, unchanged + * @throws {Error} on invalid characters + */ +function EntityDecoder_validateEntityName(name) { + if (name[0] === '#') { + throw new Error(`[EntityReplacer] Invalid character '#' in entity name: "${name}"`); + } + for (const ch of name) { + if (SPECIAL_CHARS.has(ch)) { + throw new Error(`[EntityReplacer] Invalid character '${ch}' in entity name: "${name}"`); + } + } + return name; +} + +/** + * Merge one or more entity maps into a flat name→string map. + * Accepts either: + * - plain string values: { amp: '&' } + * - legacy {regex,val} / {regx,val}: { lt: { regex: /.../, val: '<' } } + * + * Values containing '&' are skipped (recursive expansion risk). + * + * @param {...object} maps + * @returns {Record} + */ +function mergeEntityMaps(...maps) { + const out = Object.create(null); + for (const map of maps) { + if (!map) continue; + for (const key of Object.keys(map)) { + const raw = map[key]; + if (typeof raw === 'string') { + out[key] = raw; + } else if (raw && typeof raw === 'object' && raw.val !== undefined) { + // Legacy {regex,val} or {regx,val} — extract the string val only + const val = raw.val; + if (typeof val === 'string') { + out[key] = val; + } + // function vals are not supported in the scanner — skip + } + } + } + return out; +} + +// --------------------------------------------------------------------------- +// applyLimitsTo helpers +// --------------------------------------------------------------------------- + +const LIMIT_TIER_EXTERNAL = 'external'; // input/runtime + persistent external maps +const LIMIT_TIER_BASE = 'base'; // DEFAULT_XML_ENTITIES + namedEntities (system) maps +const LIMIT_TIER_ALL = 'all'; // every entity regardless of tier + +/** + * Resolve `applyLimitsTo` option into a normalised Set of tier strings. + * Accepted values: 'external' | 'base' | 'all' | string[] + * Default: 'external' (only untrusted injected entities are counted). + * @param {string|string[]|undefined} raw + * @returns {Set} + */ +function parseLimitTiers(raw) { + if (!raw || raw === LIMIT_TIER_EXTERNAL) return new Set([LIMIT_TIER_EXTERNAL]); + if (raw === LIMIT_TIER_ALL) return new Set([LIMIT_TIER_ALL]); + if (raw === LIMIT_TIER_BASE) return new Set([LIMIT_TIER_BASE]); + if (Array.isArray(raw)) return new Set(raw); + return new Set([LIMIT_TIER_EXTERNAL]); // safe default for unrecognised values +} + +// --------------------------------------------------------------------------- +// NCR (Numeric Character Reference) classification +// --------------------------------------------------------------------------- + +// Severity order — higher number = stricter action. +// Used to enforce minimum action levels for specific codepoint ranges. +const NCR_LEVEL = Object.freeze({ allow: 0, leave: 1, remove: 2, throw: 3 }); + +// XML 1.0 §2.2: allowed chars are #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] +// Restricted C0: U+0001–U+001F excluding U+0009, U+000A, U+000D +const XML10_ALLOWED_C0 = new Set([0x09, 0x0A, 0x0D]); + +/** + * Parse the `ncr` constructor option into flat, hot-path-friendly fields. + * @param {object|undefined} ncr + * @returns {{ xmlVersion: number, onLevel: number, nullLevel: number }} + */ +function parseNCRConfig(ncr) { + if (!ncr) { + return { xmlVersion: 1.0, onLevel: NCR_LEVEL.allow, nullLevel: NCR_LEVEL.remove }; + } + const xmlVersion = ncr.xmlVersion === 1.1 ? 1.1 : 1.0; + const onLevel = NCR_LEVEL[ncr.onNCR] ?? NCR_LEVEL.allow; + const nullLevel = NCR_LEVEL[ncr.nullNCR] ?? NCR_LEVEL.remove; + // 'allow' is not meaningful for null — clamp to at least 'remove' + const clampedNull = Math.max(nullLevel, NCR_LEVEL.remove); + return { xmlVersion, onLevel, nullLevel: clampedNull }; +} + +// --------------------------------------------------------------------------- +// EntityReplacer +// --------------------------------------------------------------------------- + +/** + * Single-pass, zero-regex entity replacer for XML/HTML content. + * + * Algorithm: scan the string once for '&', read to ';', resolve via map + * or direct codepoint conversion, build output chunks, join once at the end. + * + * Entity lookup priority (highest → lowest): + * 1. input / runtime (DOCTYPE entities for current document) + * 2. persistent external (survive across documents) + * 3. base named map (DEFAULT_XML_ENTITIES + user-supplied namedEntities) + * + * Both input and external resolve as the 'external' tier for limit purposes. + * Base map entities resolve as the 'base' tier. + * + * Numeric / hex references (&#NNN; / &#xHH;) are resolved directly via + * String.fromCodePoint() — no map needed. They count as 'base' tier. + * + * @example + * const replacer = new EntityReplacer({ namedEntities: COMMON_HTML }); + * replacer.setExternalEntities({ brand: 'Acme' }); + * + * const instance = replacer.reset(); + * instance.addInputEntities({ version: '1.0' }); + * instance.encode('&brand; v&version; <'); // 'Acme v1.0 <' + */ +class EntityDecoder { + /** + * @param {object} [options] + * @param {object|null} [options.namedEntities] — extra named entities merged into base map + * @param {object} [options.limit] — security limits + * @param {number} [options.limit.maxTotalExpansions=0] — 0 = unlimited + * @param {number} [options.limit.maxExpandedLength=0] — 0 = unlimited + * @param {'external'|'base'|'all'|string[]} [options.limit.applyLimitsTo='external'] + * Which entity tiers count against the security limits: + * - 'external' (default) — only input/runtime + persistent external entities + * - 'base' — only DEFAULT_XML_ENTITIES + namedEntities + * - 'all' — every entity regardless of tier + * - string[] — explicit combination, e.g. ['external', 'base'] + * @param {((resolved: string, original: string) => string)|null} [options.postCheck=null] + * @param {string[]} [options.remove=[]] — entity names (e.g. ['nbsp', '#13']) to delete (replace with empty string) + * @param {string[]} [options.leave=[]] — entity names to keep as literal (unchanged in output) + * @param {object} [options.ncr] — Numeric Character Reference controls + * @param {1.0|1.1} [options.ncr.xmlVersion=1.0] + * XML version governing which codepoint ranges are restricted: + * - 1.0 — C0 controls U+0001–U+001F (except U+0009/000A/000D) are prohibited + * - 1.1 — C0 controls are allowed when written as NCRs; C1 (U+007F–U+009F) decoded as-is + * @param {'allow'|'leave'|'remove'|'throw'} [options.ncr.onNCR='allow'] + * Base action for numeric references. Severity order: allow < leave < remove < throw. + * For codepoint ranges that carry a minimum level (surrogates → remove, XML 1.0 C0 → remove), + * the effective action is max(onNCR, rangeMinimum). + * @param {'remove'|'throw'} [options.ncr.nullNCR='remove'] + * Action for U+0000 (null). 'allow' and 'leave' are clamped to 'remove' since null is never safe. + */ + constructor(options = {}) { + this._limit = options.limit || {}; + this._maxTotalExpansions = this._limit.maxTotalExpansions || 0; + this._maxExpandedLength = this._limit.maxExpandedLength || 0; + this._postCheck = typeof options.postCheck === 'function' ? options.postCheck : r => r; + this._limitTiers = parseLimitTiers(this._limit.applyLimitsTo ?? LIMIT_TIER_EXTERNAL); + this._numericAllowed = options.numericAllowed ?? true; + // Base map: DEFAULT_XML_ENTITIES + user-supplied extras. Immutable after construction. + this._baseMap = mergeEntityMaps(XML, options.namedEntities || null); + + // Persistent external entities — survive across documents. + // Stored as a separate map so reset() never touches them. + /** @type {Record} */ + this._externalMap = Object.create(null); + + // Input / runtime entities — current document only, wiped on reset(). + /** @type {Record} */ + this._inputMap = Object.create(null); + + // Per-document counters + this._totalExpansions = 0; + this._expandedLength = 0; + + // --- New: remove / leave sets --- + /** @type {Set} */ + this._removeSet = new Set(options.remove && Array.isArray(options.remove) ? options.remove : []); + /** @type {Set} */ + this._leaveSet = new Set(options.leave && Array.isArray(options.leave) ? options.leave : []); + + // --- NCR config (parsed into flat fields for hot-path speed) --- + const ncrCfg = parseNCRConfig(options.ncr); + this._ncrXmlVersion = ncrCfg.xmlVersion; + this._ncrOnLevel = ncrCfg.onLevel; + this._ncrNullLevel = ncrCfg.nullLevel; + } + + // ------------------------------------------------------------------------- + // Persistent external entity registration + // ------------------------------------------------------------------------- + + /** + * Replace the full set of persistent external entities. + * All keys are validated — throws on invalid characters. + * @param {Record} map + */ + setExternalEntities(map) { + if (map) { + for (const key of Object.keys(map)) { + EntityDecoder_validateEntityName(key); + } + } + this._externalMap = mergeEntityMaps(map); + } + + /** + * Add a single persistent external entity. + * @param {string} key + * @param {string} value + */ + addExternalEntity(key, value) { + EntityDecoder_validateEntityName(key); + if (typeof value === 'string' && value.indexOf('&') === -1) { + this._externalMap[key] = value; + } + } + + // ------------------------------------------------------------------------- + // Input / runtime entity registration (per document) + // ------------------------------------------------------------------------- + + /** + * Inject DOCTYPE entities for the current document. + * Also resets per-document expansion counters. + * @param {Record} map + */ + addInputEntities(map) { + this._totalExpansions = 0; + this._expandedLength = 0; + this._inputMap = mergeEntityMaps(map); + } + + // ------------------------------------------------------------------------- + // Per-document reset + // ------------------------------------------------------------------------- + + /** + * Wipe input/runtime entities and reset counters. + * Call this before processing each new document. + * @returns {this} + */ + reset() { + this._inputMap = Object.create(null); + this._totalExpansions = 0; + this._expandedLength = 0; + return this; + } + + // ------------------------------------------------------------------------- + // XML version (can be set after construction, e.g. once parser reads ) + // ------------------------------------------------------------------------- + + /** + * Update the XML version used for NCR classification. + * Call this as soon as the document's `` declaration is parsed. + * @param {1.0|1.1|number} version + */ + setXmlVersion(version) { + this._ncrXmlVersion = version === 1.1 ? 1.1 : 1.0; + } + + // ------------------------------------------------------------------------- + // Primary API + // ------------------------------------------------------------------------- + + /** + * Replace all entity references in `str` in a single pass. + * + * @param {string} str + * @returns {string} + */ + decode(str) { + if (typeof str !== 'string' || str.length === 0) return str; + //TODO: check if needed + //if (str.indexOf('&') === -1) return str; // fast path — no entities at all + + const original = str; + const chunks = []; + const len = str.length; + let last = 0; // start of next unprocessed literal chunk + let i = 0; + + const limitExpansions = this._maxTotalExpansions > 0; + const limitLength = this._maxExpandedLength > 0; + const checkLimits = limitExpansions || limitLength; + + while (i < len) { + // Scan forward to next '&' + if (str.charCodeAt(i) !== 38 /* '&' */) { i++; continue; } + + // --- Found '&' at position i --- + + // Scan forward to ';' + let j = i + 1; + while (j < len && str.charCodeAt(j) !== 59 /* ';' */ && (j - i) <= 32) j++; + + if (j >= len || str.charCodeAt(j) !== 59) { + // No closing ';' within window — treat '&' as literal + i++; + continue; + } + + // Raw token between '&' and ';' (exclusive) + const token = str.slice(i + 1, j); + if (token.length === 0) { i++; continue; } + + let replacement; + let tier; // which limit tier this entity belongs to + + if (this._removeSet.has(token)) { + // Remove entity: replace with empty string + replacement = ''; + // If entity was unknown (replacement undefined), we still need a tier for limits. + // Treat as external tier because it's user-directed removal of an unknown reference. + if (tier === undefined) { + tier = LIMIT_TIER_EXTERNAL; + } + } else if (this._leaveSet.has(token)) { + // Do not replace — keep original &token; as literal + i++; + continue; + } else if (token.charCodeAt(0) === 35 /* '#' */) { + // ---- Numeric / NCR reference ---- + // NCR classification always runs first — prohibited codepoints must be + // caught regardless of numericAllowed. + const ncrResult = this._resolveNCR(token); + if (ncrResult === undefined) { + // 'leave' action — keep original &token; as-is + i++; + continue; + } + replacement = ncrResult; // '' for remove, char string for allow + tier = LIMIT_TIER_BASE; + } else { + // ---- Named reference ---- + const resolved = this._resolveName(token); + replacement = resolved?.value; + tier = resolved?.tier; + } + + if (replacement === undefined) { + // Unknown entity — leave as-is, advance past '&' only + i++; + continue; + } + + // Flush literal chunk before this entity + if (i > last) chunks.push(str.slice(last, i)); + chunks.push(replacement); + last = j + 1; // skip past ';' + i = last; + + // Apply expansion limits only if this tier is being tracked + if (checkLimits && this._tierCounts(tier)) { + if (limitExpansions) { + this._totalExpansions++; + if (this._totalExpansions > this._maxTotalExpansions) { + throw new Error( + `[EntityReplacer] Entity expansion count limit exceeded: ` + + `${this._totalExpansions} > ${this._maxTotalExpansions}` + ); + } + } + if (limitLength) { + // delta: replacement.length minus the raw &token; length (token.length + 2 for '&' and ';') + const delta = replacement.length - (token.length + 2); + if (delta > 0) { + this._expandedLength += delta; + if (this._expandedLength > this._maxExpandedLength) { + throw new Error( + `[EntityReplacer] Expanded content length limit exceeded: ` + + `${this._expandedLength} > ${this._maxExpandedLength}` + ); + } + } + } + } + } + + // Flush trailing literal + if (last < len) chunks.push(str.slice(last)); + + // If nothing was replaced, chunks is empty — return original + const result = chunks.length === 0 ? str : chunks.join(''); + + return this._postCheck(result, original); + } + + // ------------------------------------------------------------------------- + // Private: limit tier check + // ------------------------------------------------------------------------- + + /** + * Returns true if a resolved entity of the given tier should count + * against the expansion/length limits. + * @param {string} tier — LIMIT_TIER_EXTERNAL | LIMIT_TIER_BASE + * @returns {boolean} + */ + _tierCounts(tier) { + if (this._limitTiers.has(LIMIT_TIER_ALL)) return true; + return this._limitTiers.has(tier); + } + + // ------------------------------------------------------------------------- + // Private: entity resolution + // ------------------------------------------------------------------------- + + /** + * Resolve a named entity token (without & and ;). + * Priority: inputMap > externalMap > baseMap + * Returns the resolved value tagged with its limit tier. + * + * @param {string} name + * @returns {{ value: string, tier: string }|undefined} + */ + _resolveName(name) { + // input and external both count as 'external' tier for limit purposes — + // they are injected at runtime and are the untrusted surface. + if (name in this._inputMap) return { value: this._inputMap[name], tier: LIMIT_TIER_EXTERNAL }; + if (name in this._externalMap) return { value: this._externalMap[name], tier: LIMIT_TIER_EXTERNAL }; + if (name in this._baseMap) return { value: this._baseMap[name], tier: LIMIT_TIER_BASE }; + return undefined; + } + + /** + * Classify a codepoint and return the minimum action level that must be applied. + * Returns -1 when no minimum is imposed (normal allow path). + * + * Ranges checked (in priority order): + * 1. U+0000 — null, governed by nullNCR (always ≥ remove) + * 2. U+D800–U+DFFF — surrogates, always prohibited (min: remove) + * 3. U+0001–U+001F \ {0x09,0x0A,0x0D} — XML 1.0 restricted C0 (min: remove) + * (skipped in XML 1.1 — C0 controls are allowed when written as NCRs) + * + * @param {number} cp — codepoint + * @returns {number} — minimum NCR_LEVEL value, or -1 for no restriction + */ + _classifyNCR(cp) { + // 1. Null + if (cp === 0) return this._ncrNullLevel; + + // 2. Surrogates — always prohibited, minimum 'remove' + if (cp >= 0xD800 && cp <= 0xDFFF) return NCR_LEVEL.remove; + + // 3. XML 1.0 restricted C0 controls + if (this._ncrXmlVersion === 1.0) { + if (cp >= 0x01 && cp <= 0x1F && !XML10_ALLOWED_C0.has(cp)) return NCR_LEVEL.remove; + } + + return -1; // no restriction + } + + /** + * Execute a resolved NCR action. + * + * @param {number} action — NCR_LEVEL value + * @param {string} token — raw token (e.g. '#38') for error messages + * @param {number} cp — codepoint, used only for error messages + * @returns {string|undefined} + * - decoded character string → 'allow' + * - '' → 'remove' + * - undefined → 'leave' (caller must skip past '&' only) + * - throws Error → 'throw' + */ + _applyNCRAction(action, token, cp) { + switch (action) { + case NCR_LEVEL.allow: return String.fromCodePoint(cp); + case NCR_LEVEL.remove: return ''; + case NCR_LEVEL.leave: return undefined; // signal: keep literal + case NCR_LEVEL.throw: + throw new Error( + `[EntityDecoder] Prohibited numeric character reference ` + + `&${token}; (U+${cp.toString(16).toUpperCase().padStart(4, '0')})` + ); + default: return String.fromCodePoint(cp); + } + } + + /** + * Full NCR resolution pipeline for a numeric token. + * + * Steps: + * 1. Parse the codepoint (decimal or hex). + * 2. Validate the raw codepoint range (NaN, <0, >0x10FFFF). + * 3. If numericAllowed is false and no minimum restriction applies → leave as-is. + * 4. Classify the codepoint to find the minimum required action level. + * 5. Resolve effective action = max(onNCR, minimum). + * 6. Apply and return. + * + * @param {string} token — e.g. '#38', '#x26', '#X26' + * @returns {string|undefined} + * - string (incl. '') — replacement ('' = remove) + * - undefined — leave original &token; as-is + */ + _resolveNCR(token) { + // Step 1: parse codepoint + const second = token.charCodeAt(1); + let cp; + if (second === 120 /* x */ || second === 88 /* X */) { + cp = parseInt(token.slice(2), 16); + } else { + cp = parseInt(token.slice(1), 10); + } + + // Step 2: out-of-range → leave as-is unconditionally + if (Number.isNaN(cp) || cp < 0 || cp > 0x10FFFF) return undefined; + + // Step 3: classify to get minimum action level + const minimum = this._classifyNCR(cp); + + // Step 4: if numericAllowed is false and no hard minimum → leave + if (!this._numericAllowed && minimum < NCR_LEVEL.remove) return undefined; + + // Step 5: effective action = max(configured onNCR, range minimum) + const effective = minimum === -1 + ? this._ncrOnLevel + : Math.max(this._ncrOnLevel, minimum); + + // Step 6: apply + return this._applyNCRAction(effective, token, cp); + } +} ;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/xmlparser/OrderedObjParser.js ///@ts-check @@ -54332,6 +56895,8 @@ function ignoreAttributes_getIgnoreAttributesFn(ignoreAttributes) { + + // const regx = // '<((!\\[CDATA\\[([\\s\\S]*?)(]]>))|((NAME:)?(NAME))([^>]*)>|((\\/)(NAME)\\s*>))([^<]*)' // .replace(/NAME/g, util.nameRegexp); @@ -54391,36 +56956,10 @@ function extractNamespace(rawTagName) { } class OrderedObjParser { - constructor(options) { + constructor(options, externalEntities) { this.options = options; this.currentNode = null; this.tagsNodeStack = []; - this.docTypeEntities = {}; - this.lastEntities = { - "apos": { regex: /&(apos|#39|#x27);/g, val: "'" }, - "gt": { regex: /&(gt|#62|#x3E);/g, val: ">" }, - "lt": { regex: /&(lt|#60|#x3C);/g, val: "<" }, - "quot": { regex: /&(quot|#34|#x22);/g, val: "\"" }, - }; - this.ampEntity = { regex: /&(amp|#38|#x26);/g, val: "&" }; - this.htmlEntities = { - "space": { regex: /&(nbsp|#160);/g, val: " " }, - // "lt" : { regex: /&(lt|#60);/g, val: "<" }, - // "gt" : { regex: /&(gt|#62);/g, val: ">" }, - // "amp" : { regex: /&(amp|#38);/g, val: "&" }, - // "quot" : { regex: /&(quot|#34);/g, val: "\"" }, - // "apos" : { regex: /&(apos|#39);/g, val: "'" }, - "cent": { regex: /&(cent|#162);/g, val: "¢" }, - "pound": { regex: /&(pound|#163);/g, val: "£" }, - "yen": { regex: /&(yen|#165);/g, val: "¥" }, - "euro": { regex: /&(euro|#8364);/g, val: "€" }, - "copyright": { regex: /&(copy|#169);/g, val: "©" }, - "reg": { regex: /&(reg|#174);/g, val: "®" }, - "inr": { regex: /&(inr|#8377);/g, val: "₹" }, - "num_dec": { regex: /&#([0-9]{1,7});/g, val: (_, str) => fromCodePoint(str, 10, "&#") }, - "num_hex": { regex: /&#x([0-9a-fA-F]{1,6});/g, val: (_, str) => fromCodePoint(str, 16, "&#x") }, - }; - this.addExternalEntities = addExternalEntities; this.parseXml = parseXml; this.parseTextData = parseTextData; this.resolveNameSpace = resolveNameSpace; @@ -54433,42 +56972,51 @@ class OrderedObjParser { this.ignoreAttributesFn = ignoreAttributes_getIgnoreAttributesFn(this.options.ignoreAttributes) this.entityExpansionCount = 0; this.currentExpandedLength = 0; + let namedEntities = { ...XML }; + if (this.options.entityDecoder) { + this.entityDecoder = this.options.entityDecoder + } else { + if (typeof this.options.htmlEntities === "object") namedEntities = this.options.htmlEntities; + else if (this.options.htmlEntities === true) namedEntities = { ...COMMON_HTML, ...CURRENCY }; + this.entityDecoder = new EntityDecoder({ + namedEntities: { ...namedEntities, ...externalEntities }, + numericAllowed: this.options.htmlEntities, + limit: { + maxTotalExpansions: this.options.processEntities.maxTotalExpansions, + maxExpandedLength: this.options.processEntities.maxExpandedLength, + applyLimitsTo: this.options.processEntities.appliesTo, + } + //postCheck: resolved => resolved + }); + } // Initialize path matcher for path-expression-matcher this.matcher = new Matcher(); + this.readonlyMatcher = this.matcher.readOnly(); // Flag to track if current node is a stop node (optimization) this.isCurrentNodeStopNode = false; // Pre-compile stopNodes expressions - if (this.options.stopNodes && this.options.stopNodes.length > 0) { - this.stopNodeExpressions = []; - for (let i = 0; i < this.options.stopNodes.length; i++) { - const stopNodeExp = this.options.stopNodes[i]; + this.stopNodeExpressionsSet = new ExpressionSet(); + const stopNodesOpts = this.options.stopNodes; + if (stopNodesOpts && stopNodesOpts.length > 0) { + for (let i = 0; i < stopNodesOpts.length; i++) { + const stopNodeExp = stopNodesOpts[i]; if (typeof stopNodeExp === 'string') { // Convert string to Expression object - this.stopNodeExpressions.push(new Expression(stopNodeExp)); + this.stopNodeExpressionsSet.add(new Expression(stopNodeExp)); } else if (stopNodeExp instanceof Expression) { // Already an Expression object - this.stopNodeExpressions.push(stopNodeExp); + this.stopNodeExpressionsSet.add(stopNodeExp); } } + this.stopNodeExpressionsSet.seal(); } } } -function addExternalEntities(externalEntities) { - const entKeys = Object.keys(externalEntities); - for (let i = 0; i < entKeys.length; i++) { - const ent = entKeys[i]; - const escaped = ent.replace(/[.\-+*:]/g, '\\.'); - this.lastEntities[ent] = { - regex: new RegExp("&" + escaped + ";", "g"), - val: externalEntities[ent] - } - } -} /** * @param {string} val @@ -54480,28 +57028,29 @@ function addExternalEntities(externalEntities) { * @param {boolean} escapeEntities */ function parseTextData(val, tagName, jPath, dontTrim, hasAttributes, isLeafNode, escapeEntities) { + const options = this.options; if (val !== undefined) { - if (this.options.trimValues && !dontTrim) { + if (options.trimValues && !dontTrim) { val = val.trim(); } if (val.length > 0) { if (!escapeEntities) val = this.replaceEntitiesValue(val, tagName, jPath); // Pass jPath string or matcher based on options.jPath setting - const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath; - const newval = this.options.tagValueProcessor(tagName, val, jPathOrMatcher, hasAttributes, isLeafNode); + const jPathOrMatcher = options.jPath ? jPath.toString() : jPath; + const newval = options.tagValueProcessor(tagName, val, jPathOrMatcher, hasAttributes, isLeafNode); if (newval === null || newval === undefined) { //don't parse return val; } else if (typeof newval !== typeof val || newval !== val) { //overwrite return newval; - } else if (this.options.trimValues) { - return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions); + } else if (options.trimValues) { + return parseValue(val, options.parseTagValue, options.numberParseOptions); } else { const trimmedVal = val.trim(); if (trimmedVal === val) { - return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions); + return parseValue(val, options.parseTagValue, options.numberParseOptions); } else { return val; } @@ -54528,8 +57077,9 @@ function resolveNameSpace(tagname) { //const attrsRegx = new RegExp("([\\w\\-\\.\\:]+)\\s*=\\s*(['\"])((.|\n)*?)\\2","gm"); const attrsRegx = new RegExp('([^\\s=]+)\\s*(=\\s*([\'"])([\\s\\S]*?)\\3)?', 'gm'); -function buildAttributesMap(attrStr, jPath, tagName) { - if (this.options.ignoreAttributes !== true && typeof attrStr === 'string') { +function buildAttributesMap(attrStr, jPath, tagName, force = false) { + const options = this.options; + if (force === true || (options.ignoreAttributes !== true && typeof attrStr === 'string')) { // attrStr = attrStr.replace(/\r?\n/g, ' '); //attrStr = attrStr || attrStr.trim(); @@ -54537,89 +57087,80 @@ function buildAttributesMap(attrStr, jPath, tagName) { const len = matches.length; //don't make it inline const attrs = {}; - // First pass: parse all attributes and update matcher with raw values - // This ensures the matcher has all attribute values when processors run + // Pre-process values once: trim + entity replacement + // Reused in both matcher update and second pass + const processedVals = new Array(len); + let hasRawAttrs = false; const rawAttrsForMatcher = {}; + for (let i = 0; i < len; i++) { const attrName = this.resolveNameSpace(matches[i][1]); const oldVal = matches[i][4]; if (attrName.length && oldVal !== undefined) { - let parsedVal = oldVal; - if (this.options.trimValues) { - parsedVal = parsedVal.trim(); - } - parsedVal = this.replaceEntitiesValue(parsedVal, tagName, jPath); - rawAttrsForMatcher[attrName] = parsedVal; + let val = oldVal; + if (options.trimValues) val = val.trim(); + val = this.replaceEntitiesValue(val, tagName, this.readonlyMatcher); + processedVals[i] = val; + + rawAttrsForMatcher[attrName] = val; + hasRawAttrs = true; } } - // Update matcher with raw attribute values BEFORE running processors - if (Object.keys(rawAttrsForMatcher).length > 0 && typeof jPath === 'object' && jPath.updateCurrent) { + // Update matcher ONCE before second pass, if applicable + if (hasRawAttrs && typeof jPath === 'object' && jPath.updateCurrent) { jPath.updateCurrent(rawAttrsForMatcher); } - // Second pass: now process attributes with matcher having full attribute context + // Hoist toString() once — path doesn't change during attribute processing + const jPathStr = options.jPath ? jPath.toString() : this.readonlyMatcher; + + // Second pass: apply processors, build final attrs + let hasAttrs = false; for (let i = 0; i < len; i++) { const attrName = this.resolveNameSpace(matches[i][1]); - // Convert jPath to string if needed for ignoreAttributesFn - const jPathStr = this.options.jPath ? jPath.toString() : jPath; - if (this.ignoreAttributesFn(attrName, jPathStr)) { - continue - } + if (this.ignoreAttributesFn(attrName, jPathStr)) continue; - let oldVal = matches[i][4]; - let aName = this.options.attributeNamePrefix + attrName; + let aName = options.attributeNamePrefix + attrName; if (attrName.length) { - if (this.options.transformAttributeName) { - aName = this.options.transformAttributeName(aName); + if (options.transformAttributeName) { + aName = options.transformAttributeName(aName); } - //if (aName === "__proto__") aName = "#__proto__"; - aName = sanitizeName(aName, this.options); + aName = sanitizeName(aName, options); - if (oldVal !== undefined) { - if (this.options.trimValues) { - oldVal = oldVal.trim(); - } - oldVal = this.replaceEntitiesValue(oldVal, tagName, jPath); + if (matches[i][4] !== undefined) { + // Reuse already-processed value — no double entity replacement + const oldVal = processedVals[i]; - // Pass jPath string or matcher based on options.jPath setting - const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath; - const newVal = this.options.attributeValueProcessor(attrName, oldVal, jPathOrMatcher); + const newVal = options.attributeValueProcessor(attrName, oldVal, jPathStr); if (newVal === null || newVal === undefined) { - //don't parse attrs[aName] = oldVal; } else if (typeof newVal !== typeof oldVal || newVal !== oldVal) { - //overwrite attrs[aName] = newVal; } else { - //parse - attrs[aName] = parseValue( - oldVal, - this.options.parseAttributeValue, - this.options.numberParseOptions - ); + attrs[aName] = parseValue(oldVal, options.parseAttributeValue, options.numberParseOptions); } - } else if (this.options.allowBooleanAttributes) { + hasAttrs = true; + } else if (options.allowBooleanAttributes) { attrs[aName] = true; + hasAttrs = true; } } } - if (!Object.keys(attrs).length) { - return; - } - if (this.options.attributesGroupName) { + if (!hasAttrs) return; + + if (options.attributesGroupName && !options.preserveOrder) { const attrCollection = {}; - attrCollection[this.options.attributesGroupName] = attrs; + attrCollection[options.attributesGroupName] = attrs; return attrCollection; } - return attrs + return attrs; } } - const parseXml = function (xmlData) { xmlData = xmlData.replace(/\r\n?/g, "\n"); //TODO: remove this line const xmlObj = new XmlNode('!xml'); @@ -54628,40 +57169,43 @@ const parseXml = function (xmlData) { // Reset matcher for new document this.matcher.reset(); + this.entityDecoder.reset(); // Reset entity expansion counters for this document this.entityExpansionCount = 0; this.currentExpandedLength = 0; - - const docTypeReader = new DocTypeReader(this.options.processEntities); - for (let i = 0; i < xmlData.length; i++) {//for each char in XML data + const options = this.options; + const docTypeReader = new DocTypeReader(options.processEntities); + const xmlLen = xmlData.length; + for (let i = 0; i < xmlLen; i++) {//for each char in XML data const ch = xmlData[i]; if (ch === '<') { // const nextIndex = i+1; // const _2ndChar = xmlData[nextIndex]; - if (xmlData[i + 1] === '/') {//Closing Tag + const c1 = xmlData.charCodeAt(i + 1); + if (c1 === 47) {//Closing Tag '/' const closeIndex = findClosingIndex(xmlData, ">", i, "Closing Tag is not closed.") let tagName = xmlData.substring(i + 2, closeIndex).trim(); - if (this.options.removeNSPrefix) { + if (options.removeNSPrefix) { const colonIndex = tagName.indexOf(":"); if (colonIndex !== -1) { tagName = tagName.substr(colonIndex + 1); } } - tagName = transformTagName(this.options.transformTagName, tagName, "", this.options).tagName; + tagName = transformTagName(options.transformTagName, tagName, "", options).tagName; if (currentNode) { - textData = this.saveTextToParentTag(textData, currentNode, this.matcher); + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher); } //check if last tag of nested tag was unpaired tag const lastTagName = this.matcher.getCurrentTag(); - if (tagName && this.options.unpairedTags.indexOf(tagName) !== -1) { + if (tagName && options.unpairedTagsSet.has(tagName)) { throw new Error(`Unpaired tag can not be used as closing tag: `); } - if (lastTagName && this.options.unpairedTags.indexOf(lastTagName) !== -1) { + if (lastTagName && options.unpairedTagsSet.has(lastTagName)) { // Pop the unpaired tag this.matcher.pop(); this.tagsNodeStack.pop(); @@ -54673,65 +57217,75 @@ const parseXml = function (xmlData) { currentNode = this.tagsNodeStack.pop();//avoid recursion, set the parent tag scope textData = ""; i = closeIndex; - } else if (xmlData[i + 1] === '?') { + } else if (c1 === 63) { //'?' let tagData = readTagExp(xmlData, i, false, "?>"); if (!tagData) throw new Error("Pi Tag is not closed."); - textData = this.saveTextToParentTag(textData, currentNode, this.matcher); - if ((this.options.ignoreDeclaration && tagData.tagName === "?xml") || this.options.ignorePiTags) { + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher); + const attsMap = this.buildAttributesMap(tagData.tagExp, this.matcher, tagData.tagName, true); + if (attsMap) { + const ver = attsMap[this.options.attributeNamePrefix + "version"]; + this.entityDecoder.setXmlVersion(Number(ver) || 1.0); + docTypeReader.setXmlVersion(Number(ver) || 1.0); + } + if ((options.ignoreDeclaration && tagData.tagName === "?xml") || options.ignorePiTags) { //do nothing } else { const childNode = new XmlNode(tagData.tagName); - childNode.add(this.options.textNodeName, ""); + childNode.add(options.textNodeName, ""); - if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent) { - childNode[":@"] = this.buildAttributesMap(tagData.tagExp, this.matcher, tagData.tagName); + if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent && options.ignoreAttributes !== true) { + childNode[":@"] = attsMap } - this.addChild(currentNode, childNode, this.matcher, i); + this.addChild(currentNode, childNode, this.readonlyMatcher, i); } i = tagData.closeIndex + 1; - } else if (xmlData.substr(i + 1, 3) === '!--') { + } else if (c1 === 33 + && xmlData.charCodeAt(i + 2) === 45 + && xmlData.charCodeAt(i + 3) === 45) { //'!--' const endIndex = findClosingIndex(xmlData, "-->", i + 4, "Comment is not closed.") - if (this.options.commentPropName) { + if (options.commentPropName) { const comment = xmlData.substring(i + 4, endIndex - 2); - textData = this.saveTextToParentTag(textData, currentNode, this.matcher); + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher); - currentNode.add(this.options.commentPropName, [{ [this.options.textNodeName]: comment }]); + currentNode.add(options.commentPropName, [{ [options.textNodeName]: comment }]); } i = endIndex; - } else if (xmlData.substr(i + 1, 2) === '!D') { + } else if (c1 === 33 + && xmlData.charCodeAt(i + 2) === 68) { //'!D' const result = docTypeReader.readDocType(xmlData, i); - this.docTypeEntities = result.entities; + this.entityDecoder.addInputEntities(result.entities); i = result.i; - } else if (xmlData.substr(i + 1, 2) === '![') { + } else if (c1 === 33 + && xmlData.charCodeAt(i + 2) === 91) { // '![' const closeIndex = findClosingIndex(xmlData, "]]>", i, "CDATA is not closed.") - 2; const tagExp = xmlData.substring(i + 9, closeIndex); - textData = this.saveTextToParentTag(textData, currentNode, this.matcher); + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher); - let val = this.parseTextData(tagExp, currentNode.tagname, this.matcher, true, false, true, true); + let val = this.parseTextData(tagExp, currentNode.tagname, this.readonlyMatcher, true, false, true, true); if (val == undefined) val = ""; //cdata should be set even if it is 0 length string - if (this.options.cdataPropName) { - currentNode.add(this.options.cdataPropName, [{ [this.options.textNodeName]: tagExp }]); + if (options.cdataPropName) { + currentNode.add(options.cdataPropName, [{ [options.textNodeName]: tagExp }]); } else { - currentNode.add(this.options.textNodeName, val); + currentNode.add(options.textNodeName, val); } i = closeIndex + 2; } else {//Opening tag - let result = readTagExp(xmlData, i, this.options.removeNSPrefix); + let result = readTagExp(xmlData, i, options.removeNSPrefix); // Safety check: readTagExp can return undefined if (!result) { // Log context for debugging - const context = xmlData.substring(Math.max(0, i - 50), Math.min(xmlData.length, i + 50)); + const context = xmlData.substring(Math.max(0, i - 50), Math.min(xmlLen, i + 50)); throw new Error(`readTagExp returned undefined at position ${i}. Context: "${context}"`); } @@ -54741,11 +57295,13 @@ const parseXml = function (xmlData) { let attrExpPresent = result.attrExpPresent; let closeIndex = result.closeIndex; - ({ tagName, tagExp } = transformTagName(this.options.transformTagName, tagName, tagExp, this.options)); + ({ tagName, tagExp } = transformTagName(options.transformTagName, tagName, tagExp, options)); - if (this.options.strictReservedNames && - (tagName === this.options.commentPropName - || tagName === this.options.cdataPropName + if (options.strictReservedNames && + (tagName === options.commentPropName + || tagName === options.cdataPropName + || tagName === options.textNodeName + || tagName === options.attributesGroupName )) { throw new Error(`Invalid tag name: ${tagName}`); } @@ -54754,13 +57310,13 @@ const parseXml = function (xmlData) { if (currentNode && textData) { if (currentNode.tagname !== '!xml') { //when nested tag is found - textData = this.saveTextToParentTag(textData, currentNode, this.matcher, false); + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher, false); } } //check if last tag was unpaired tag const lastTag = currentNode; - if (lastTag && this.options.unpairedTags.indexOf(lastTag.tagname) !== -1) { + if (lastTag && options.unpairedTagsSet.has(lastTag.tagname)) { currentNode = this.tagsNodeStack.pop(); this.matcher.pop(); } @@ -54802,13 +57358,14 @@ const parseXml = function (xmlData) { if (prefixedAttrs) { // Extract raw attributes (without prefix) for our use - rawAttrs = extractRawAttributes(prefixedAttrs, this.options); + //TODO: seems a performance overhead + rawAttrs = extractRawAttributes(prefixedAttrs, options); } } // Now check if this is a stop node (after attributes are set) if (tagName !== xmlObj.tagname) { - this.isCurrentNodeStopNode = this.isItStopNode(this.stopNodeExpressions, this.matcher); + this.isCurrentNodeStopNode = this.isItStopNode(); } const startIndex = i; @@ -54820,7 +57377,7 @@ const parseXml = function (xmlData) { i = result.closeIndex; } //unpaired tag - else if (this.options.unpairedTags.indexOf(tagName) !== -1) { + else if (options.unpairedTagsSet.has(tagName)) { i = result.closeIndex; } //normal tag @@ -54839,31 +57396,31 @@ const parseXml = function (xmlData) { } // For stop nodes, store raw content as-is without any processing - childNode.add(this.options.textNodeName, tagContent); + childNode.add(options.textNodeName, tagContent); this.matcher.pop(); // Pop the stop node tag this.isCurrentNodeStopNode = false; // Reset flag - this.addChild(currentNode, childNode, this.matcher, startIndex); + this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); } else { //selfClosing tag if (isSelfClosing) { - ({ tagName, tagExp } = transformTagName(this.options.transformTagName, tagName, tagExp, this.options)); + ({ tagName, tagExp } = transformTagName(options.transformTagName, tagName, tagExp, options)); const childNode = new XmlNode(tagName); if (prefixedAttrs) { childNode[":@"] = prefixedAttrs; } - this.addChild(currentNode, childNode, this.matcher, startIndex); + this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); this.matcher.pop(); // Pop self-closing tag this.isCurrentNodeStopNode = false; // Reset flag } - else if (this.options.unpairedTags.indexOf(tagName) !== -1) {//unpaired tag + else if (options.unpairedTagsSet.has(tagName)) {//unpaired tag const childNode = new XmlNode(tagName); if (prefixedAttrs) { childNode[":@"] = prefixedAttrs; } - this.addChild(currentNode, childNode, this.matcher, startIndex); + this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); this.matcher.pop(); // Pop unpaired tag this.isCurrentNodeStopNode = false; // Reset flag i = result.closeIndex; @@ -54873,7 +57430,7 @@ const parseXml = function (xmlData) { //opening tag else { const childNode = new XmlNode(tagName); - if (this.tagsNodeStack.length > this.options.maxNestedTags) { + if (this.tagsNodeStack.length > options.maxNestedTags) { throw new Error("Maximum nested tags exceeded"); } this.tagsNodeStack.push(currentNode); @@ -54881,7 +57438,7 @@ const parseXml = function (xmlData) { if (prefixedAttrs) { childNode[":@"] = prefixedAttrs; } - this.addChild(currentNode, childNode, this.matcher, startIndex); + this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); currentNode = childNode; } textData = ""; @@ -54944,79 +57501,7 @@ function OrderedObjParser_replaceEntitiesValue(val, tagName, jPath) { } } - // Replace DOCTYPE entities - for (const entityName of Object.keys(this.docTypeEntities)) { - const entity = this.docTypeEntities[entityName]; - const matches = val.match(entity.regx); - - if (matches) { - // Track expansions - this.entityExpansionCount += matches.length; - - // Check expansion limit - if (entityConfig.maxTotalExpansions && - this.entityExpansionCount > entityConfig.maxTotalExpansions) { - throw new Error( - `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}` - ); - } - - // Store length before replacement - const lengthBefore = val.length; - val = val.replace(entity.regx, entity.val); - - // Check expanded length immediately after replacement - if (entityConfig.maxExpandedLength) { - this.currentExpandedLength += (val.length - lengthBefore); - - if (this.currentExpandedLength > entityConfig.maxExpandedLength) { - throw new Error( - `Total expanded content size exceeded: ${this.currentExpandedLength} > ${entityConfig.maxExpandedLength}` - ); - } - } - } - } - // Replace standard entities - for (const entityName of Object.keys(this.lastEntities)) { - const entity = this.lastEntities[entityName]; - const matches = val.match(entity.regex); - if (matches) { - this.entityExpansionCount += matches.length; - if (entityConfig.maxTotalExpansions && - this.entityExpansionCount > entityConfig.maxTotalExpansions) { - throw new Error( - `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}` - ); - } - } - val = val.replace(entity.regex, entity.val); - } - if (val.indexOf('&') === -1) return val; - - // Replace HTML entities if enabled - if (this.options.htmlEntities) { - for (const entityName of Object.keys(this.htmlEntities)) { - const entity = this.htmlEntities[entityName]; - const matches = val.match(entity.regex); - if (matches) { - //console.log(matches); - this.entityExpansionCount += matches.length; - if (entityConfig.maxTotalExpansions && - this.entityExpansionCount > entityConfig.maxTotalExpansions) { - throw new Error( - `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}` - ); - } - } - val = val.replace(entity.regex, entity.val); - } - } - - // Replace ampersand entity last - val = val.replace(this.ampEntity.regex, this.ampEntity.val); - - return val; + return this.entityDecoder.decode(val); } @@ -55038,20 +57523,14 @@ function saveTextToParentTag(textData, parentNode, matcher, isLeafNode) { return textData; } -//TODO: use jPath to simplify the logic /** * @param {Array} stopNodeExpressions - Array of compiled Expression objects * @param {Matcher} matcher - Current path matcher */ -function isItStopNode(stopNodeExpressions, matcher) { - if (!stopNodeExpressions || stopNodeExpressions.length === 0) return false; +function isItStopNode() { + if (this.stopNodeExpressionsSet.size === 0) return false; - for (let i = 0; i < stopNodeExpressions.length; i++) { - if (matcher.matches(stopNodeExpressions[i])) { - return true; - } - } - return false; + return this.matcher.matchesAny(this.stopNodeExpressionsSet); } /** @@ -55061,32 +57540,38 @@ function isItStopNode(stopNodeExpressions, matcher) { * @returns */ function tagExpWithClosingIndex(xmlData, i, closingChar = ">") { - let attrBoundary; - let tagExp = ""; - for (let index = i; index < xmlData.length; index++) { - let ch = xmlData[index]; + //TODO: ignore boolean attributes in tag expression + //TODO: if ignore attributes, dont read full attribute expression but the end. But read for xml declaration + let attrBoundary = 0; + const len = xmlData.length; + const closeCode0 = closingChar.charCodeAt(0); + const closeCode1 = closingChar.length > 1 ? closingChar.charCodeAt(1) : -1; + + let result = ''; + let segmentStart = i; + + for (let index = i; index < len; index++) { + const code = xmlData.charCodeAt(index); + if (attrBoundary) { - if (ch === attrBoundary) attrBoundary = "";//reset - } else if (ch === '"' || ch === "'") { - attrBoundary = ch; - } else if (ch === closingChar[0]) { - if (closingChar[1]) { - if (xmlData[index + 1] === closingChar[1]) { - return { - data: tagExp, - index: index - } + if (code === attrBoundary) attrBoundary = 0; + } else if (code === 34 || code === 39) { // " or ' + attrBoundary = code; + } else if (code === closeCode0) { + if (closeCode1 !== -1) { + if (xmlData.charCodeAt(index + 1) === closeCode1) { + result += xmlData.substring(segmentStart, index); + return { data: result, index }; } } else { - return { - data: tagExp, - index: index - } + result += xmlData.substring(segmentStart, index); + return { data: result, index }; } - } else if (ch === '\t') { - ch = " " + } else if (code === 9 && !attrBoundary) { // \t - only replace with space outside attribute values + // Flush accumulated segment, add space, start new segment + result += xmlData.substring(segmentStart, index) + ' '; + segmentStart = index + 1; } - tagExp += ch; } } @@ -55099,6 +57584,12 @@ function findClosingIndex(xmlData, str, i, errMsg) { } } +function findClosingChar(xmlData, char, i, errMsg) { + const closingIndex = xmlData.indexOf(char, i); + if (closingIndex === -1) throw new Error(errMsg); + return closingIndex; // no offset needed +} + function readTagExp(xmlData, i, removeNSPrefix, closingChar = ">") { const result = tagExpWithClosingIndex(xmlData, i + 1, closingChar); if (!result) return; @@ -55140,10 +57631,12 @@ function readStopNodeData(xmlData, tagName, i) { // Starting at 1 since we already have an open tag let openTagCount = 1; - for (; i < xmlData.length; i++) { + const xmllen = xmlData.length; + for (; i < xmllen; i++) { if (xmlData[i] === "<") { - if (xmlData[i + 1] === "/") {//close tag - const closeIndex = findClosingIndex(xmlData, ">", i, `${tagName} is not closed`); + const c1 = xmlData.charCodeAt(i + 1); + if (c1 === 47) {//close tag '/' + const closeIndex = findClosingChar(xmlData, ">", i, `${tagName} is not closed`); let closeTagName = xmlData.substring(i + 2, closeIndex).trim(); if (closeTagName === tagName) { openTagCount--; @@ -55155,17 +57648,20 @@ function readStopNodeData(xmlData, tagName, i) { } } i = closeIndex; - } else if (xmlData[i + 1] === '?') { + } else if (c1 === 63) { //? const closeIndex = findClosingIndex(xmlData, "?>", i + 1, "StopNode is not closed.") i = closeIndex; - } else if (xmlData.substr(i + 1, 3) === '!--') { + } else if (c1 === 33 + && xmlData.charCodeAt(i + 2) === 45 + && xmlData.charCodeAt(i + 3) === 45) { // '!--' const closeIndex = findClosingIndex(xmlData, "-->", i + 3, "StopNode is not closed.") i = closeIndex; - } else if (xmlData.substr(i + 1, 2) === '![') { + } else if (c1 === 33 + && xmlData.charCodeAt(i + 2) === 91) { // '![' const closeIndex = findClosingIndex(xmlData, "]]>", i, "StopNode is not closed.") - 2; i = closeIndex; } else { - const tagData = readTagExp(xmlData, i, '>') + const tagData = readTagExp(xmlData, i, false) if (tagData) { const openTagName = tagData && tagData.tagName; @@ -55265,18 +57761,17 @@ function stripAttributePrefix(attrs, prefix) { * @param {Matcher} matcher - Path matcher instance * @returns */ -function prettify(node, options, matcher) { - return compress(node, options, matcher); +function prettify(node, options, matcher, readonlyMatcher) { + return compress(node, options, matcher, readonlyMatcher); } /** - * * @param {array} arr * @param {object} options * @param {Matcher} matcher - Path matcher instance * @returns object */ -function compress(arr, options, matcher) { +function compress(arr, options, matcher, readonlyMatcher) { let text; const compressedObj = {}; //This is intended to be a plain object for (let i = 0; i < arr.length; i++) { @@ -55299,11 +57794,15 @@ function compress(arr, options, matcher) { continue; } else if (tagObj[property]) { - let val = compress(tagObj[property], options, matcher); + let val = compress(tagObj[property], options, matcher, readonlyMatcher); const isLeaf = isLeafTag(val, options); + if (Object.keys(val).length === 0 && options.alwaysCreateTextNode) { + val[options.textNodeName] = ""; + } + if (tagObj[":@"]) { - assignAttributes(val, tagObj[":@"], matcher, options); + assignAttributes(val, tagObj[":@"], readonlyMatcher, options); } else if (Object.keys(val).length === 1 && val[options.textNodeName] !== undefined && !options.alwaysCreateTextNode) { val = val[options.textNodeName]; } else if (Object.keys(val).length === 0) { @@ -55325,8 +57824,8 @@ function compress(arr, options, matcher) { //TODO: if a node is not an array, then check if it should be an array //also determine if it is a leaf node - // Pass jPath string or matcher based on options.jPath setting - const jPathOrMatcher = options.jPath ? matcher.toString() : matcher; + // Pass jPath string or readonlyMatcher based on options.jPath setting + const jPathOrMatcher = options.jPath ? readonlyMatcher.toString() : readonlyMatcher; if (options.isArray(property, jPathOrMatcher, isLeaf)) { compressedObj[property] = [val]; } else { @@ -55358,7 +57857,7 @@ function node2json_propName(obj) { } } -function assignAttributes(obj, attrMap, matcher, options) { +function assignAttributes(obj, attrMap, readonlyMatcher, options) { if (attrMap) { const keys = Object.keys(attrMap); const len = keys.length; //don't make it inline @@ -55373,8 +57872,8 @@ function assignAttributes(obj, attrMap, matcher, options) { // For attributes, we need to create a temporary path // Pass jPath string or matcher based on options.jPath setting const jPathOrMatcher = options.jPath - ? matcher.toString() + "." + rawAttrName - : matcher; + ? readonlyMatcher.toString() + "." + rawAttrName + : readonlyMatcher; if (options.isArray(atrrName, jPathOrMatcher, true, true)) { obj[atrrName] = [attrMap[atrrName]]; @@ -55431,16 +57930,16 @@ 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}`) } } - const orderedObjParser = new OrderedObjParser(this.options); - orderedObjParser.addExternalEntities(this.externalEntities); + const orderedObjParser = new OrderedObjParser(this.options, this.externalEntities); + // orderedObjParser.entityDecoder.setExternalEntities(this.externalEntities); const orderedResult = orderedObjParser.parseXml(xmlData); if (this.options.preserveOrder || orderedResult === undefined) return orderedResult; - else return prettify(orderedResult, this.options, orderedObjParser.matcher); + else return prettify(orderedResult, this.options, orderedObjParser.matcher, orderedObjParser.readonlyMatcher); } /** @@ -55474,7 +57973,6 @@ class XMLParser { return XmlNode.getMetaDataSymbol(); } } - ;// CONCATENATED MODULE: ./node_modules/@azure/core-xml/dist/esm/xml.common.js // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. @@ -55493,20 +57991,34 @@ const xml_common_XML_CHARKEY = "_"; function getCommonOptions(options) { - var _a; return { attributesGroupName: xml_common_XML_ATTRKEY, - textNodeName: (_a = options.xmlCharKey) !== null && _a !== void 0 ? _a : xml_common_XML_CHARKEY, + textNodeName: options.xmlCharKey ?? xml_common_XML_CHARKEY, ignoreAttributes: false, suppressBooleanAttributes: false, }; } function getSerializerOptions(options = {}) { - var _a, _b; - return Object.assign(Object.assign({}, getCommonOptions(options)), { attributeNamePrefix: "@_", format: true, suppressEmptyNode: true, indentBy: "", rootNodeName: (_a = options.rootName) !== null && _a !== void 0 ? _a : "root", cdataPropName: (_b = options.cdataPropName) !== null && _b !== void 0 ? _b : "__cdata" }); + return { + ...getCommonOptions(options), + attributeNamePrefix: "@_", + format: true, + suppressEmptyNode: true, + indentBy: "", + rootNodeName: options.rootName ?? "root", + cdataPropName: options.cdataPropName ?? "__cdata", + }; } function getParserOptions(options = {}) { - return Object.assign(Object.assign({}, getCommonOptions(options)), { parseAttributeValue: false, parseTagValue: false, attributeNamePrefix: "", stopNodes: options.stopNodes, processEntities: true, trimValues: false }); + return { + ...getCommonOptions(options), + parseAttributeValue: false, + parseTagValue: false, + attributeNamePrefix: "", + stopNodes: options.stopNodes, + processEntities: true, + trimValues: false, + }; } /** * Converts given JSON object to XML string @@ -55545,7 +58057,7 @@ async function parseXML(str, opts = {}) { if (!opts.includeRoot) { for (const key of Object.keys(parsedXml)) { const value = parsedXml[key]; - return typeof value === "object" ? Object.assign({}, value) : value; + return typeof value === "object" ? { ...value } : value; } } return parsedXml; @@ -57130,39 +59642,6 @@ class StorageSharedKeyCredential extends Credential { } } //# sourceMappingURL=StorageSharedKeyCredential.js.map -;// CONCATENATED MODULE: ./node_modules/@azure/storage-common/node_modules/@azure/abort-controller/dist/esm/AbortError.js -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -/** - * This error is thrown when an asynchronous operation has been aborted. - * Check for this error by testing the `name` that the name property of the - * error matches `"AbortError"`. - * - * @example - * ```ts - * const controller = new AbortController(); - * controller.abort(); - * try { - * doAsyncWork(controller.signal) - * } catch (e) { - * if (e.name === 'AbortError') { - * // handle abort error here. - * } - * } - * ``` - */ -class esm_AbortError_AbortError extends Error { - constructor(message) { - super(message); - this.name = "AbortError"; - } -} -//# sourceMappingURL=AbortError.js.map -;// CONCATENATED MODULE: ./node_modules/@azure/storage-common/node_modules/@azure/abort-controller/dist/esm/index.js -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -//# sourceMappingURL=index.js.map ;// CONCATENATED MODULE: ./node_modules/@azure/storage-common/dist/esm/log.js // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. @@ -57220,7 +59699,7 @@ const DEFAULT_RETRY_OPTIONS = { secondaryHost: "", tryTimeoutInMs: undefined, // Use server side default timeout strategy }; -const RETRY_ABORT_ERROR = new esm_AbortError_AbortError("The operation was aborted."); +const RETRY_ABORT_ERROR = new AbortError_AbortError("The operation was aborted."); /** * Retry policy with exponential retry and linear retry implemented. */ @@ -57533,7 +60012,7 @@ const retriableErrors = [ "EPIPE", "REQUEST_SEND_ERROR", ]; -const StorageRetryPolicyV2_RETRY_ABORT_ERROR = new esm_AbortError_AbortError("The operation was aborted."); +const StorageRetryPolicyV2_RETRY_ABORT_ERROR = new AbortError_AbortError("The operation was aborted."); /** * Retry policy with exponential retry and linear retry implemented. */ @@ -57896,15 +60375,12 @@ class UserDelegationKeyCredential { - - - //# sourceMappingURL=index.js.map ;// CONCATENATED MODULE: ./node_modules/@azure/storage-blob/dist/esm/utils/constants.js // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -const esm_utils_constants_SDK_VERSION = "12.30.0"; +const esm_utils_constants_SDK_VERSION = "12.31.0"; const SERVICE_VERSION = "2026-02-06"; const BLOCK_BLOB_MAX_UPLOAD_BLOB_BYTES = 256 * 1024 * 1024; // 256MB const BLOCK_BLOB_MAX_STAGE_BLOCK_BYTES = 4000 * 1024 * 1024; // 4000MB @@ -74402,39 +76878,6 @@ class BlobLeaseClient { } } //# sourceMappingURL=BlobLeaseClient.js.map -;// CONCATENATED MODULE: ./node_modules/@azure/storage-blob/node_modules/@azure/abort-controller/dist/esm/AbortError.js -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -/** - * This error is thrown when an asynchronous operation has been aborted. - * Check for this error by testing the `name` that the name property of the - * error matches `"AbortError"`. - * - * @example - * ```ts - * const controller = new AbortController(); - * controller.abort(); - * try { - * doAsyncWork(controller.signal) - * } catch (e) { - * if (e.name === 'AbortError') { - * // handle abort error here. - * } - * } - * ``` - */ -class dist_esm_AbortError_AbortError extends Error { - constructor(message) { - super(message); - this.name = "AbortError"; - } -} -//# sourceMappingURL=AbortError.js.map -;// CONCATENATED MODULE: ./node_modules/@azure/storage-blob/node_modules/@azure/abort-controller/dist/esm/index.js -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -//# sourceMappingURL=index.js.map ;// CONCATENATED MODULE: ./node_modules/@azure/storage-blob/dist/esm/utils/RetriableReadableStream.js // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. @@ -74514,7 +76957,7 @@ class RetriableReadableStream extends external_node_stream_.Readable { } }; sourceAbortedHandler = () => { - const abortError = new dist_esm_AbortError_AbortError("The operation was aborted."); + const abortError = new AbortError_AbortError("The operation was aborted."); this.destroy(abortError); }; sourceErrorOrEndHandler = (err) => { @@ -75509,7 +77952,7 @@ const external_buffer_namespaceObject = __WEBPACK_EXTERNAL_createRequire(import. -const ABORT_ERROR = new dist_esm_AbortError_AbortError("Reading from the avro stream was aborted."); +const ABORT_ERROR = new AbortError_AbortError("Reading from the avro stream was aborted."); class AvroReadableFromStream extends AvroReadable { _position; _readable; @@ -78129,7 +80572,7 @@ class BlobClient extends StorageClient_StorageClient { options.conditions = options.conditions || {}; ensureCpkIfSpecified(options.customerProvidedKey, this.isHttps); return tracingClient.withSpan("BlobClient-download", options, async (updatedOptions) => { - const res = utils_common_assertResponse(await this.blobContext.download({ + const res = utils_common_assertResponse((await this.blobContext.download({ abortSignal: options.abortSignal, leaseAccessConditions: options.conditions, modifiedAccessConditions: { @@ -78145,7 +80588,7 @@ class BlobClient extends StorageClient_StorageClient { snapshot: options.snapshot, cpkInfo: options.customerProvidedKey, tracingOptions: updatedOptions.tracingOptions, - })); + }))); const wrappedRes = { ...res, _response: res._response, // _response is made non-enumerable @@ -79467,7 +81910,7 @@ class Clients_BlockBlobClient extends BlobClient { throw new Error("This operation currently is only supported in Node.js."); } return tracingClient.withSpan("BlockBlobClient-query", options, async (updatedOptions) => { - const response = utils_common_assertResponse(await this._blobContext.query({ + const response = utils_common_assertResponse((await this._blobContext.query({ abortSignal: options.abortSignal, queryRequest: { queryType: "SQL", @@ -79482,7 +81925,7 @@ class Clients_BlockBlobClient extends BlobClient { }, cpkInfo: options.customerProvidedKey, tracingOptions: updatedOptions.tracingOptions, - })); + }))); return new BlobQueryResponse(response, { abortSignal: options.abortSignal, onProgress: options.onProgress, @@ -81378,9 +83821,9 @@ class BlobBatchClient { return tracingClient.withSpan("BlobBatchClient-submitBatch", options, async (updatedOptions) => { const batchRequestBody = batchRequest.getHttpRequestBody(); // ServiceSubmitBatchResponseModel and ContainerSubmitBatchResponse are compatible for now. - const rawBatchResponse = utils_common_assertResponse(await this.serviceOrContainerContext.submitBatch(utf8ByteLength(batchRequestBody), batchRequest.getMultiPartContentType(), batchRequestBody, { + const rawBatchResponse = utils_common_assertResponse((await this.serviceOrContainerContext.submitBatch(utf8ByteLength(batchRequestBody), batchRequest.getMultiPartContentType(), batchRequestBody, { ...updatedOptions, - })); + }))); // Parse the sub responses result, if logic reaches here(i.e. the batch request succeeded with status code 202). const batchResponseParser = new BatchResponseParser(rawBatchResponse, batchRequest.getSubRequests()); const responseSummary = await batchResponseParser.parseBatchResponse(); @@ -83960,7 +86403,7 @@ NetworkError.isNetworkErrorCode = (code) => { }; class UsageError extends Error { constructor() { - const message = `Cache storage quota has been hit. Unable to upload any new cache entries. Usage is recalculated every 6-12 hours.\nMore info on storage limits: https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#calculating-minute-and-storage-spending`; + const message = `Cache storage quota has been hit. Unable to upload any new cache entries.\nMore info on storage limits: https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#calculating-minute-and-storage-spending`; super(message); this.name = 'UsageError'; }