diff --git a/src/dom/xml-functions.ts b/src/dom/xml-functions.ts index 8c03246..5964ab7 100644 --- a/src/dom/xml-functions.ts +++ b/src/dom/xml-functions.ts @@ -224,7 +224,10 @@ function xmlTransformedTextRecursive(node: XNode, buffer: string[], options: Xml const nodeType = node.nodeType const nodeValue = node.nodeValue; if (nodeType === DOM_TEXT_NODE) { - if (node.nodeValue && node.nodeValue.trim() !== '') { + // For text nodes created by xsl:text, don't trim whitespace + // For other text nodes, skip whitespace-only ones + const isFromXslText = node.fromXslText === true; + if (node.nodeValue && (isFromXslText || node.nodeValue.trim() !== '')) { const finalText = node.escape && options.escape ? xmlEscapeText(node.nodeValue): xmlUnescapeText(node.nodeValue); buffer.push(finalText); diff --git a/src/dom/xml-parser.ts b/src/dom/xml-parser.ts index 9436af4..3ae7a3b 100644 --- a/src/dom/xml-parser.ts +++ b/src/dom/xml-parser.ts @@ -285,7 +285,7 @@ export class XmlParser { } else if (!tag && char === '<') { let text = xml.slice(start, i); if (text && parent !== root) { - domAppendChild(parent, domCreateTextNode(xmlDocument, text)); + domAppendChild(parent, domCreateTextNode(xmlDocument, he.decode(text))); } if (xml.slice(i + 1, i + 4) === '!--') { let endTagIndex = xml.slice(i + 4).indexOf('-->'); diff --git a/src/dom/xnode.ts b/src/dom/xnode.ts index 8c1f199..2a8a7d4 100644 --- a/src/dom/xnode.ts +++ b/src/dom/xnode.ts @@ -28,6 +28,7 @@ export class XNode { visited: boolean; escape: boolean; + fromXslText: boolean; static _unusedXNodes: any[] = []; @@ -36,6 +37,7 @@ export class XNode { this.childNodes = []; this.visited = false; this.escape = true; + this.fromXslText = false; this.siblingPosition = -1; this.init(type, name, opt_value, opt_owner, opt_namespace); diff --git a/src/xpath/lib b/src/xpath/lib index 3186237..eee689c 160000 --- a/src/xpath/lib +++ b/src/xpath/lib @@ -1 +1 @@ -Subproject commit 31862374f940816acfecb0757c693820c4228c29 +Subproject commit eee689c487ff2a67d50fc166541959dd6611711f diff --git a/src/xpath/selector.ts b/src/xpath/selector.ts index 6e001e7..653ab7d 100644 --- a/src/xpath/selector.ts +++ b/src/xpath/selector.ts @@ -43,7 +43,7 @@ export class XPathSelector { nodeType: this.getNodeType(node), nodeName: node.nodeName || '#document', localName: node.localName || node.nodeName, - namespaceURI: node.namespaceUri || null, + namespaceUri: node.namespaceUri || null, textContent: node.nodeValue, parentNode: null, // Não converter para evitar ciclos childNodes: [], // Será preenchido depois @@ -99,7 +99,7 @@ export class XPathSelector { text: xpathNode.textContent, value: xpathNode.textContent, localName: xpathNode.localName, - namespaceURI: xpathNode.namespaceURI, + namespaceUri: xpathNode.namespaceUri, parentNode: xpathNode.parentNode ? this.convertFromXPathNode(xpathNode.parentNode) : undefined, children: xpathNode.childNodes ? Array.from(xpathNode.childNodes).map(child => this.convertFromXPathNode(child)) : undefined, attributes: xpathNode.attributes ? Array.from(xpathNode.attributes).map(attr => this.convertFromXPathNode(attr)) : undefined, diff --git a/src/xpath/xpath.ts b/src/xpath/xpath.ts index 0916053..9f61599 100644 --- a/src/xpath/xpath.ts +++ b/src/xpath/xpath.ts @@ -149,13 +149,7 @@ class NodeConverter { const adapted = node as any; // Add XPathNode-compatible properties if not present - if (!('namespaceURI' in adapted)) { - Object.defineProperty(adapted, 'namespaceURI', { - get() { return this.namespaceUri; }, - enumerable: true, - configurable: true - }); - } + // namespaceUri is now the standard property in XPathNode interface if (!('textContent' in adapted)) { Object.defineProperty(adapted, 'textContent', { diff --git a/src/xslt/xslt.ts b/src/xslt/xslt.ts index 3d9bd2d..6c6692e 100644 --- a/src/xslt/xslt.ts +++ b/src/xslt/xslt.ts @@ -357,10 +357,14 @@ export class Xslt { // This is the XSLT 3.0 compliant behavior - only ONE template executes per node. for (let j = 0; j < modifiedContext.contextSize(); ++j) { const currentNode = modifiedContext.nodeList[j]; - // If the current node is text, there's no need to test all the templates // against it. Just appending it to its parent is fine. if (currentNode.nodeType === DOM_TEXT_NODE) { + // Check if this whitespace-only text node should be stripped + if (!this.xsltPassText(currentNode)) { + // Skip whitespace-only text nodes in apply-templates + continue; + } const textNodeContext = context.clone( [currentNode], 0 @@ -1265,6 +1269,8 @@ export class Xslt { protected xsltText(context: ExprContext, template: XNode, output?: XNode) { const text = xmlValue(template); const node = domCreateTextNode(this.outputDocument, text); + // Mark this node as coming from xsl:text so it won't be trimmed during serialization + node.fromXslText = true; const disableOutputEscaping = template.childNodes.filter( (a) => a.nodeType === DOM_ATTRIBUTE_NODE && a.nodeName === 'disable-output-escaping' ); @@ -1272,6 +1278,8 @@ export class Xslt { node.escape = false; } const destinationTextNode = output || this.outputDocument; + // Set siblingPosition to preserve insertion order during serialization + node.siblingPosition = destinationTextNode.childNodes.length; destinationTextNode.appendChild(node); } @@ -1429,8 +1437,6 @@ export class Xslt { for (const t of expandedTemplates) { try { - // For initial template selection, evaluate patterns from document root - // without axis override to ensure consistent matching for all patterns // For initial template selection, evaluate patterns from document root // without axis override to ensure consistent matching for all patterns const matchedNodes = this.xsltMatch(t.matchPattern, contextClone); @@ -1444,19 +1450,28 @@ export class Xslt { } if (matchCandidates.length > 0) { - // Sort by: importPrecedence DESC, effectivePriority DESC, documentOrder DESC - matchCandidates.sort((a, b) => { - if (a.priority.importPrecedence !== b.priority.importPrecedence) { - return b.priority.importPrecedence - a.priority.importPrecedence; - } - if (a.priority.effectivePriority !== b.priority.effectivePriority) { - return b.priority.effectivePriority - a.priority.effectivePriority; - } - return b.priority.documentOrder - a.priority.documentOrder; - }); + // First, check if "/" pattern matches - it's the document entry point and should be preferred + const rootPatternMatch = matchCandidates.find(c => c.priority.matchPattern === '/'); + let winner: { priority: TemplatePriority; matchedNodes: XNode[] }; + + if (rootPatternMatch) { + // Use the root template as entry point + winner = rootPatternMatch; + } else { + // Sort by: importPrecedence DESC, effectivePriority DESC, documentOrder DESC + matchCandidates.sort((a, b) => { + if (a.priority.importPrecedence !== b.priority.importPrecedence) { + return b.priority.importPrecedence - a.priority.importPrecedence; + } + if (a.priority.effectivePriority !== b.priority.effectivePriority) { + return b.priority.effectivePriority - a.priority.effectivePriority; + } + return b.priority.documentOrder - a.priority.documentOrder; + }); + winner = matchCandidates[0]; + } // Detect conflicts - const winner = matchCandidates[0]; const conflicts = matchCandidates.filter(t => t.priority.importPrecedence === winner.priority.importPrecedence && t.priority.effectivePriority === winner.priority.effectivePriority @@ -1625,7 +1640,13 @@ export class Xslt { // siblings of the children. const contextClone = context.clone(); for (let i = 0; i < template.childNodes.length; ++i) { - await this.xsltProcessContext(contextClone, template.childNodes[i], output); + const child = template.childNodes[i]; + // Skip attribute nodes - they are stored in childNodes but should not be + // processed as template content. Attributes belong to the element itself. + if (child.nodeType === DOM_ATTRIBUTE_NODE) { + continue; + } + await this.xsltProcessContext(contextClone, child, output); } } diff --git a/tests/namespaced-attributes.test.tsx b/tests/namespaced-attributes.test.tsx new file mode 100644 index 0000000..62c9dec --- /dev/null +++ b/tests/namespaced-attributes.test.tsx @@ -0,0 +1,233 @@ +/* eslint-env mocha */ +import assert from 'assert'; + +import { Xslt } from '../src/xslt'; +import { XmlParser } from '../src/dom'; + +describe('namespaced attributes', () => { + it('extract namespaced attribute with direct namespace prefix (@ns:attribute)', async () => { + const xml = ` + + +`; + + const xslt = ` + + + + + + + + +`; + + const xmlDoc = new XmlParser().xmlParse(xml); + const xsltDoc = new XmlParser().xmlParse(xslt); + + const processor = new Xslt(); + const result = await processor.xsltProcess(xmlDoc, xsltDoc); + + assert.equal(result, 'TestValue', 'Should extract namespaced attribute using direct prefix'); + }); + + it('extract namespaced attribute using local-name()', async () => { + const xml = ` + + +`; + + const xslt = ` + + + + + + + + +`; + + const xmlDoc = new XmlParser().xmlParse(xml); + const xsltDoc = new XmlParser().xmlParse(xslt); + + const processor = new Xslt(); + const result = await processor.xsltProcess(xmlDoc, xsltDoc); + + assert.equal(result, 'TestValue', 'Should extract namespaced attribute using local-name()'); + }); + + it('extract namespaced attribute using namespace-uri() and local-name()', async () => { + const xml = ` + + +`; + + const xslt = ` + + + + + + + + +`; + + const xmlDoc = new XmlParser().xmlParse(xml); + const xsltDoc = new XmlParser().xmlParse(xslt); + + const processor = new Xslt(); + const result = await processor.xsltProcess(xmlDoc, xsltDoc); + + assert.equal(result, 'TestValue', 'Should extract namespaced attribute using namespace-uri() and local-name()'); + }); + + it('all three methods for extracting namespaced attributes', async () => { + const xml = ` + + +`; + + const xslt = ` + + + + + + Method 1 (@ns:attribute): + + + + Method 2 (local-name): + + + + Method 3 (namespace-uri): + + + + +`; + + const xmlDoc = new XmlParser().xmlParse(xml); + const xsltDoc = new XmlParser().xmlParse(xslt); + + const processor = new Xslt(); + const result = await processor.xsltProcess(xmlDoc, xsltDoc); + + const expectedOutput = `Method 1 (@ns:attribute): TestValue +Method 2 (local-name): TestValue +Method 3 (namespace-uri): TestValue +`; + + assert.equal(result, expectedOutput, 'All three methods should return the same value'); + }); + + it('namespaced attribute with call-template', async () => { + const xml = ``; + + const xslt = ` + + + + A| + + |C + + + + B| + + |D + + + + + +`; + + const parser = new XmlParser(); + const xmlDoc = await parser.xmlParse(xml); + const xsltDoc = await parser.xmlParse(xslt); + const xsltProc = new Xslt(); + const result = await xsltProc.xsltProcess(xmlDoc, xsltDoc); + + const expectedOutput = 'A|B|VALUE|D|C'; + assert.equal(result, expectedOutput, `Expected "${expectedOutput}" but got "${result}". Output order bug: VALUE should appear inline, not deferred to the end`); + }); + + it('multiple namespaced attributes', async () => { + const xml = ` + + +`; + + const xslt = ` + + + + + + + | + + | + + + +`; + + const xmlDoc = new XmlParser().xmlParse(xml); + const xsltDoc = new XmlParser().xmlParse(xslt); + + const processor = new Xslt(); + const result = await processor.xsltProcess(xmlDoc, xsltDoc); + + const expectedOutput = 'Value1|Value2|Value3'; + assert.equal(result, expectedOutput, 'Should extract multiple namespaced attributes'); + }); + + it('mixed namespaced and non-namespaced attributes', async () => { + const xml = ` + + +`; + + const xslt = ` + + + + + + + | + + + +`; + + const xmlDoc = new XmlParser().xmlParse(xml); + const xsltDoc = new XmlParser().xmlParse(xslt); + + const processor = new Xslt(); + const result = await processor.xsltProcess(xmlDoc, xsltDoc); + + const expectedOutput = 'Normal|Namespaced'; + assert.equal(result, expectedOutput, 'Should extract both namespaced and non-namespaced attributes'); + }); +});