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
3 changes: 2 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ XSLT-processor TODO

* XSLT validation, besides the version number;
* XSL:number
* Implement `<xsl:import>` with correct template precedence.
* Implement `<xsl:import>` with correct template precedence.
* **BUG: concat() function with XPath expressions** - When using `concat()` with XPath expressions as arguments (e.g., `concat(root/first, ' ', root/second)`), the function returns malformed output like "1, first, null 1, second, null" instead of properly concatenating the string values. This appears to be an issue with how XPath NodeSetValue results are being converted to strings within the concat function.

Help is much appreciated. It seems to currently work for most of our purposes, but fixes and additions are always welcome!
102 changes: 94 additions & 8 deletions src/dom/xml-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,21 +230,31 @@
buffer.push(finalText);
}
} else if (nodeType === DOM_CDATA_SECTION_NODE) {
if (options.cData) {
if (options.outputMethod === 'text') {
// For text output, extract the raw content without CDATA markers
buffer.push(nodeValue);

Check warning on line 235 in src/dom/xml-functions.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 (options.cData) {
buffer.push(xmlEscapeText(nodeValue));
} else {
buffer.push(`<![CDATA[${nodeValue}]]>`);
}

Check warning on line 240 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🌿 Branch is not covered

Warning! Not covered branch
} else if (nodeType == DOM_COMMENT_NODE) {
buffer.push(`<!-- ${nodeValue} -->`);
if (options.outputMethod !== 'text') {
buffer.push(`<!-- ${nodeValue} -->`);

Check warning on line 243 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🧾 Statement is not covered

Warning! Not covered statement
}

Check warning on line 244 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 244 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🌿 Branch is not covered

Warning! Not covered branch
} else if (nodeType == DOM_ELEMENT_NODE) {
// If node didn't have a transformed name, but its children
// had transformations, children should be present at output.
// This is called here "muted logic".
if (node.nodeName !== null && node.nodeName !== undefined) {
xmlElementLogicTrivial(node, buffer, options);
if (options.outputMethod === 'text') {
// For text output, only extract text content from elements
xmlElementLogicTextOnly(node, buffer, options);

Check warning on line 248 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🧾 Statement is not covered

Warning! Not covered statement
} else {
xmlElementLogicMuted(node, buffer, options);
// If node didn't have a transformed name, but its children
// had transformations, children should be present at output.
// This is called here "muted logic".
if (node.nodeName !== null && node.nodeName !== undefined) {
xmlElementLogicTrivial(node, buffer, options);
} else {
xmlElementLogicMuted(node, buffer, options);

Check warning on line 256 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🧾 Statement is not covered

Warning! Not covered statement
}

Check warning on line 257 in src/dom/xml-functions.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 258 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🌿 Branch is not covered

Warning! Not covered branch
} else if (nodeType === DOM_DOCUMENT_NODE || nodeType === DOM_DOCUMENT_FRAGMENT_NODE) {
let childNodes = node.firstChild ? [] : node.childNodes;
Expand Down Expand Up @@ -356,6 +366,29 @@
}
}

/**
* XML element output for text mode - extracts only text content without tags.
* @param node The XML node.
* @param buffer The output buffer.
* @param options XML output options.
*/
function xmlElementLogicTextOnly(node: XNode, buffer: string[], options: XmlOutputOptions) {

Check warning on line 375 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🕹️ Function is not covered

Warning! Not covered function
let childNodes: XNode[] = [];

Check warning on line 376 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🧾 Statement is not covered

Warning! Not covered statement
if (node.firstChild) {
let child = node.firstChild;

Check warning on line 378 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🧾 Statement is not covered

Warning! Not covered statement
while (child) {
childNodes.push(child);

Check warning on line 380 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🧾 Statement is not covered

Warning! Not covered statement
child = child.nextSibling;

Check warning on line 381 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🧾 Statement is not covered

Warning! Not covered statement
}

Check warning on line 382 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🧾 Statement is not covered

Warning! Not covered statement
} else {
childNodes = node.childNodes;

Check warning on line 384 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🧾 Statement is not covered

Warning! Not covered statement
}

Check warning on line 385 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 385 in src/dom/xml-functions.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 385 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🌿 Branch is not covered

Warning! Not covered branch
childNodes = childNodes.sort((a, b) => a.siblingPosition - b.siblingPosition);

Check warning on line 386 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 386 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 386 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🕹️ Function is not covered

Warning! Not covered function
for (let i = 0; i < childNodes.length; ++i) {

Check warning on line 387 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🧾 Statement is not covered

Warning! Not covered statement
xmlTransformedTextRecursive(childNodes[i], buffer, options);

Check warning on line 388 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🧾 Statement is not covered

Warning! Not covered statement
}

Check warning on line 389 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🧾 Statement is not covered

Warning! Not covered statement
}

/**
* Gets the full node name.
* When namespace is set, the node name is `namespace:node`.
Expand Down Expand Up @@ -570,6 +603,59 @@
return null;
}

/**
* Detects the most appropriate output format for a node based on its structure.
* This implements XSLT 3.1 adaptive output behavior.
* @param node The node to analyze.
* @returns The detected output method: 'text' or 'xml'.
*/
export function detectAdaptiveOutputFormat(node: XNode): 'text' | 'xml' {
if (!node) {
return 'xml';

Check warning on line 614 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🧾 Statement is not covered

Warning! Not covered statement
}

Check warning on line 615 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🌿 Branch is not covered

Warning! Not covered branch

const nodeType = node.nodeType;

// If it's a document or fragment, check its children
if (nodeType === DOM_DOCUMENT_NODE || nodeType === DOM_DOCUMENT_FRAGMENT_NODE) {

Check warning on line 620 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🌿 Branch is not covered

Warning! Not covered branch
const children = node.childNodes || [];

Check warning on line 621 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🌿 Branch is not covered

Warning! Not covered branch
let elementCount = 0;
let textCount = 0;
let hasSignificantText = false;

for (let i = 0; i < children.length; i++) {
const child = children[i];
if (child.nodeType === DOM_ELEMENT_NODE) {
elementCount++;
} else if (child.nodeType === DOM_TEXT_NODE) {
const text = child.nodeValue ? child.nodeValue.trim() : '';

Check warning on line 631 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🌿 Branch is not covered

Warning! Not covered branch
if (text.length > 0) {
textCount++;
hasSignificantText = true;
}
}
}

// If there's only text content and no elements, use text output
if (elementCount === 0 && hasSignificantText) {
return 'text';
}
// Otherwise, use XML output
return 'xml';
}

// If it's a single text node with content, use text output
if (nodeType === DOM_TEXT_NODE || nodeType === DOM_CDATA_SECTION_NODE) {

Check warning on line 648 in src/dom/xml-functions.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 648 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🌿 Branch is not covered

Warning! Not covered branch
const text = node.nodeValue ? node.nodeValue.trim() : '';

Check warning on line 649 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 649 in src/dom/xml-functions.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 649 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🌿 Branch is not covered

Warning! Not covered branch
if (text.length > 0) {
return 'text';

Check warning on line 651 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🧾 Statement is not covered

Warning! Not covered statement
}

Check warning on line 652 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 652 in src/dom/xml-functions.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 653 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 653 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🌿 Branch is not covered

Warning! Not covered branch

// For elements and other node types, use XML output
return 'xml';

Check warning on line 656 in src/dom/xml-functions.ts

View workflow job for this annotation

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

🧾 Statement is not covered

Warning! Not covered statement
}

/**
* Converts an XML document to a JSON string.
* The root element becomes the top-level object.
Expand Down
2 changes: 1 addition & 1 deletion src/xslt/xslt-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ export type XsltOptions = {
cData: boolean,
escape: boolean,
selfClosingTags: boolean,
outputMethod?: 'xml' | 'html' | 'text' | 'xhtml' | 'json',
outputMethod?: 'xml' | 'html' | 'text' | 'xhtml' | 'json' | 'adaptive',
parameters?: XsltParameter[]
}
13 changes: 10 additions & 3 deletions src/xslt/xslt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
xmlGetAttribute,
xmlTransformedText,
xmlToJson,
detectAdaptiveOutputFormat,
xmlValue,
xmlValueLegacyBehavior
} from '../dom';
Expand Down Expand Up @@ -77,7 +78,7 @@ export class Xslt {
decimalFormatSettings: XsltDecimalFormatSettings;

outputDocument: XDocument;
outputMethod: 'xml' | 'html' | 'text' | 'name' | 'xhtml' | 'json';
outputMethod: 'xml' | 'html' | 'text' | 'name' | 'xhtml' | 'json' | 'adaptive';
outputOmitXmlDeclaration: string;
version: string;
firstTemplateRan: boolean;
Expand Down Expand Up @@ -143,7 +144,7 @@ export class Xslt {
* The exported entry point of the XSL-T processor.
* @param xmlDoc The input document root, as DOM node.
* @param stylesheet The stylesheet document root, as DOM node.
* @returns the processed document, as XML text in a string, or JSON string if outputMethod is 'json'.
* @returns the processed document, as XML text in a string, JSON string if outputMethod is 'json', or text if outputMethod is 'text' or 'adaptive' (with text content).
*/
async xsltProcess(xmlDoc: XDocument, stylesheet: XDocument) {
const outputDocument = new XDocument();
Expand All @@ -163,11 +164,17 @@ export class Xslt {
return xmlToJson(outputDocument);
}

// Handle adaptive output format
let outputMethod = this.outputMethod;
if (this.outputMethod === 'adaptive') {
outputMethod = detectAdaptiveOutputFormat(outputDocument);
}

const transformedOutputXml: string = xmlTransformedText(outputDocument, {
cData: this.options.cData,
escape: this.options.escape,
selfClosingTags: this.options.selfClosingTags,
outputMethod: this.outputMethod
outputMethod: outputMethod as 'xml' | 'html' | 'text' | 'xhtml'
});

return transformedOutputXml;
Expand Down
Loading