Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 33 additions & 24 deletions src/xslt/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,40 +358,49 @@ function nodeMatchesSinglePattern(
return attrName === patternLocalName || node.nodeName === attrPattern;
}

// For element patterns, we need to check if the node would be selected by the pattern
// Create a context with just this node
const nodeContext = context.clone([node], 0);

// Try with 'self-and-siblings' axis - this works for patterns that don't start with *
// because xPathParse will set the axis correctly
try {
const expr = xPath.xPathParse(pattern, 'self-and-siblings');
const nodes = matchResolver.expressionMatch(expr, nodeContext);

// Check if the current node is in the matched nodes
if (nodes.some(n => n.id === node.id)) {
return true;
}
} catch (e) {
// Pattern parsing failed, try alternative approach
}

// For patterns starting with '*' (where axis override doesn't work),
// check if the node passes the pattern test directly
if (pattern === '*' && node.nodeType === DOM_ELEMENT_NODE) {
return true;
}

// For patterns with predicates like "item[@id='1']" or multi-step patterns,
// For simple element name patterns first (like "section" or "div")
// Try matching by element name directly
if (!pattern.includes('/') && !pattern.includes('[') && !pattern.startsWith('@')) {
if (pattern === node.nodeName || pattern === node.localName) {
return true;
}
}

// For patterns with '/' (absolute or descendant paths) or '[' (predicates),
// we need to evaluate from document root and check if node is in result
if (pattern.includes('[') || pattern.includes('/')) {
if (pattern.includes('/') || pattern.includes('[')) {
try {
// Evaluate pattern from document root with descendant-or-self axis
// Evaluate pattern from document root
// If pattern doesn't start with '/', add '//' to match anywhere in document
const evaluationPattern = pattern.startsWith('/') ? pattern : '//' + pattern;
const rootContext = context.clone([context.root], 0);
const descendantPattern = pattern.startsWith('/') ? pattern : '//' + pattern;
const expr = xPath.xPathParse(descendantPattern);
const nodes = matchResolver.expressionMatch(expr, rootContext);

// Use xPathEval for pattern evaluation (handles // patterns correctly)
const evalResult = xPath.xPathEval(evaluationPattern, rootContext);
const nodes = evalResult.nodeSetValue();

if (nodes.some(n => n.id === node.id)) {
return true;
}
} catch (e) {
// Pattern parsing failed, continue to next approach
}
}

// For simple element name patterns - try with 'self-and-siblings' axis override as fallback
if (!pattern.includes('/') && !pattern.includes('[') && !pattern.startsWith('@')) {
try {
const nodeContext = context.clone([node], 0);
const expr = xPath.xPathParse(pattern, 'self-and-siblings');
const nodes = matchResolver.expressionMatch(expr, nodeContext);

// Check if the current node is in the matched nodes
if (nodes.some(n => n.id === node.id)) {
return true;
}
Expand Down
63 changes: 63 additions & 0 deletions src/xslt/xslt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1371,6 +1371,69 @@
contextClone.baseTemplateMatched = true;
const templateContext = contextClone.clone(winner.matchedNodes, 0);
await this.xsltChildNodes(templateContext, winner.priority.template, output);
} else {
// No template matched the root element.
// Apply the default XSLT behavior: process child nodes
const rootNode = context.nodeList[context.position];
if (rootNode && rootNode.childNodes && rootNode.childNodes.length > 0) {
// Filter out DTD sections and apply templates to remaining children
const childNodes = rootNode.childNodes.filter((n: XNode) => n.nodeName !== '#dtd-section');
if (childNodes.length > 0) {
const childContext = context.clone(childNodes);
// Process each child node using xsltApplyTemplates logic
for (let j = 0; j < childContext.contextSize(); ++j) {
const currentNode = childContext.nodeList[j];

if (currentNode.nodeType === DOM_TEXT_NODE) {
const textNodeContext = context.clone([currentNode], 0);

Check warning on line 1388 in src/xslt/xslt.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
this.commonLogicTextNode(textNodeContext, currentNode, output);

Check warning on line 1389 in src/xslt/xslt.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
} else {
const clonedContext = childContext.clone([currentNode], 0);
const selection = selectBestTemplate(
expandedTemplates,
clonedContext,
this.matchResolver,
this.xPath
);

if (selection.selectedTemplate) {
const templateContext = clonedContext.clone([currentNode], 0);

Check warning on line 1400 in src/xslt/xslt.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
templateContext.inApplyTemplates = true;

Check warning on line 1401 in src/xslt/xslt.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
await this.xsltChildNodes(templateContext, selection.selectedTemplate, output);

Check warning on line 1402 in src/xslt/xslt.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
} else {
// If no template matches this child, recursively process its children
if (currentNode.childNodes && currentNode.childNodes.length > 0) {
const grandchildNodes = currentNode.childNodes.filter((n: XNode) => n.nodeName !== '#dtd-section');
if (grandchildNodes.length > 0) {
const grandchildContext = context.clone(grandchildNodes);
// Recursively process grandchildren
for (let k = 0; k < grandchildContext.contextSize(); ++k) {
const grandchildNode = grandchildContext.nodeList[k];
if (grandchildNode.nodeType === DOM_TEXT_NODE) {
const textNodeContext = context.clone([grandchildNode], 0);

Check warning on line 1413 in src/xslt/xslt.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
this.commonLogicTextNode(textNodeContext, grandchildNode, output);

Check warning on line 1414 in src/xslt/xslt.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
} else {
const grandchildClonedContext = grandchildContext.clone([grandchildNode], 0);
const grandchildSelection = selectBestTemplate(
expandedTemplates,
grandchildClonedContext,
this.matchResolver,
this.xPath
);
if (grandchildSelection.selectedTemplate) {
const grandchildTemplateContext = grandchildClonedContext.clone([grandchildNode], 0);
grandchildTemplateContext.inApplyTemplates = true;
await this.xsltChildNodes(grandchildTemplateContext, grandchildSelection.selectedTemplate, output);
}
}

Check warning on line 1428 in src/xslt/xslt.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
}
}
}
}

Check warning on line 1432 in src/xslt/xslt.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
}

Check warning on line 1433 in src/xslt/xslt.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
}
}
}
}
}
}
Expand Down
72 changes: 72 additions & 0 deletions tests/xslt/xpath-match.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Xslt, XmlParser } from '../../src/index';

describe('XPath template matching, special cases', () => {
let xslt: Xslt;
let xmlParser: XmlParser;

beforeEach(() => {
xslt = new Xslt();
xmlParser = new XmlParser();
});

// Issue #117: xsltProcess fails matching `//section` XPath expression
// In some cases templates that use `//name` do not get applied while `//*` works.
// This test reproduces that scenario and ensures `//section` and `section`
// both match the `section` elements as expected.
it('should match section elements using //section in template match attribute', async () => {
const xml = xmlParser.xmlParse(
'<root id="n12"><section id="n1">1</section><section id="n2">2</section></root>'
);

const stylesheet = xmlParser.xmlParse(`<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="UTF-8" />
<xsl:template match="//section">
<xsl:value-of select="." />
</xsl:template>
</xsl:stylesheet>`);

const result = await xslt.xsltProcess(xml, stylesheet);

// Expected output: "12" (concatenation of both section values)
expect(result).toBe('12');
});

it('should match elements using //* as an alternative to //section', async () => {
const xml = xmlParser.xmlParse(
'<root id="n12"><section id="n1">1</section><section id="n2">2</section></root>'
);

const stylesheet = xmlParser.xmlParse(`<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="UTF-8" />
<xsl:template match="//*">
<xsl:value-of select="." />
</xsl:template>
</xsl:stylesheet>`);

const result = await xslt.xsltProcess(xml, stylesheet);

// This should work (mentioned in the issue as "behaves as expected")
expect(result).toBeTruthy();
});

it('should match section elements using section element name', async () => {
const xml = xmlParser.xmlParse(
'<root id="n12"><section id="n1">1</section><section id="n2">2</section></root>'
);

const stylesheet = xmlParser.xmlParse(`<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="UTF-8" />
<xsl:template match="section">
<xsl:value-of select="." />
</xsl:template>
</xsl:stylesheet>`);

const result = await xslt.xsltProcess(xml, stylesheet);

// Expected output: "12" (concatenation of both section values)
expect(result).toBe('12');
});
});