From fa95588ce8f85ad93d4da81e3939482190a6ea60 Mon Sep 17 00:00:00 2001 From: vhinayindia Date: Tue, 6 Oct 2026 19:56:48 +0530 Subject: [PATCH] fix: preserve stop-node path for updateTag callbacks --- spec/stopNodes_spec.js | 55 +++++++++++++++++++++++++++++++ src/xmlparser/OrderedObjParser.js | 5 +-- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/spec/stopNodes_spec.js b/spec/stopNodes_spec.js index 939ac90d..221dcbff 100644 --- a/spec/stopNodes_spec.js +++ b/spec/stopNodes_spec.js @@ -1,6 +1,7 @@ "use strict"; import { XMLParser, XMLValidator } from "../src/fxp.js"; +import { Expression } from "path-expression-matcher"; describe("XMLParser StopNodes", function () { it("should support single stopNode with namespace and removeNSPrefix set", function () { @@ -473,4 +474,58 @@ describe("XMLParser StopNodes", function () { expect(result).toEqual(expected); }); + for (const preserveOrder of [false, true]) { + it(`should use stop-node paths in updateTag with preserveOrder=${preserveOrder}`, function () { + const paths = []; + const parser = new XMLParser({ + preserveOrder, + stopNodes: ["root.skip", "root.keep"], + updateTag(tagName, jPath) { + paths.push([tagName, jPath]); + if (jPath === "root.skip") return false; + if (jPath === "root.keep") return "renamed"; + return tagName; + } + }); + + const result = parser.parse(""); + const expected = preserveOrder + ? [{ root: [{ renamed: [{ "#text": "" }] }, { after: [] }] }] + : { root: { renamed: "", after: "" } }; + + expect(result).toEqual(expected); + expect(paths).toEqual([ + ["root", "root"], + ["skip", "root.skip"], + ["keep", "root.keep"], + ["after", "root.after"] + ]); + }); + } + + for (const item of ['', '']) { + it(`should pass the current matcher for an empty stop node: ${item}`, function () { + const expression = new Expression("root.item[id=2]"); + const paths = []; + const parser = new XMLParser({ + ignoreAttributes: false, + jPath: false, + unpairedTags: ["item"], + stopNodes: ["root.item"], + updateTag(tagName, matcher, attrs) { + paths.push(matcher.toString()); + if (matcher.matches(expression)) { + expect(attrs).toEqual({ "@_id": "2" }); + return "matched"; + } + return tagName; + } + }); + + expect(parser.parse(`${item}`)) + .toEqual({ root: { matched: { "@_id": "2" }, after: "" } }); + expect(paths).toEqual(["root", "root.item", "root.after"]); + }); + } + }); diff --git a/src/xmlparser/OrderedObjParser.js b/src/xmlparser/OrderedObjParser.js index 58c6a5f2..bb2a6308 100644 --- a/src/xmlparser/OrderedObjParser.js +++ b/src/xmlparser/OrderedObjParser.js @@ -532,11 +532,12 @@ const parseXml = function (xmlData) { // For stop nodes, store raw content as-is without any processing childNode.add(options.textNodeName, tagContent); + // Keep the stop node in the matcher until updateTag has been called. + this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); + this.matcher.pop(); // Pop the stop node tag this.isCurrentNodeStopNode = false; // Reset flag - this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); - if (options.captureMetaData) { currentNode.addEndIndex(i + 1); }