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
136 changes: 69 additions & 67 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,16 @@ const xslt = new Xslt();
const xPath = xslt.xPath;
```

Or ou can import it like this:
Or you can import it like this:

```js
import { XPath } from 'xslt-processor'

const xPath = new XPath();
```

`XPath` class is an external dependency, [living in its own repository](https://github.com/DesignLiquido/xpath).

If you write pre-2015 JS code, make adjustments as needed.

### `Xslt` class options
Expand Down Expand Up @@ -145,76 +147,11 @@ console.log(parsed.root.users.user); // ["Alice", "Bob"]
You can simply add a tag like this:

```html
<script type="application/javascript" src="https://www.unpkg.com/xslt-processor@3.0.0/umd/xslt-processor.js"></script>
<script type="application/javascript" src="https://www.unpkg.com/xslt-processor@latest/umd/xslt-processor.js"></script>
```

All the exports will live under `globalThis.XsltProcessor` and `window.XsltProcessor`. [See a usage example here](https://github.com/DesignLiquido/xslt-processor/blob/main/interactive-tests/xslt.html).

### Breaking Changes

#### Version 2

Until version 2.3.1, use like the example below:

```js
import { Xslt, XmlParser } from 'xslt-processor'

// xmlString: string of xml file contents
// xsltString: string of xslt file contents
// outXmlString: output xml string.
const xslt = new Xslt();
const xmlParser = new XmlParser();
const outXmlString = xslt.xsltProcess( // Not async.
xmlParser.xmlParse(xmlString),
xmlParser.xmlParse(xsltString)
);
```

Version 3 received `<xsl:include>` which relies on Fetch API, which is asynchronous. Version 2 doesn't support `<xsl:include>`.

If using Node.js older than version v17.5.0, please use version 3.2.3, that uses `node-fetch` package. Versions 3.3.0 onward require at least Node.js version v17.5.0, since they use native `fetch()` function.

#### Version 1

Until version 1.2.8, use like the example below:

```js
import { Xslt, xmlParse } from 'xslt-processor'

// xmlString: string of xml file contents
// xsltString: string of xslt file contents
// outXmlString: output xml string.
const xslt = new Xslt();
const outXmlString = xslt.xsltProcess(
xmlParse(xmlString),
xmlParse(xsltString)
);
```

#### Version 0

Until version 0.11.7, use like the example below:

```js
import { xsltProcess, xmlParse } from 'xslt-processor'

// xmlString: string of xml file contents
// xsltString: string of xslt file contents
// outXmlString: output xml string.
const outXmlString = xsltProcess(
xmlParse(xmlString),
xmlParse(xsltString)
);
```

and to access the XPath parser:

```js
import { xpathParse } from 'xslt-processor'
```

These functions are part of `Xslt` and `XPath` classes, respectively, at version 1.x onward.

## Introduction

XSLT-processor contains an implementation of XSLT in JavaScript. Because XSLT uses XPath, it also contains an implementation of XPath that can be used
Expand Down Expand Up @@ -312,6 +249,71 @@ Use `<xsl:preserve-space>` to preserve whitespace in specific elements, overridi
3. `xsl:strip-space` applies to remaining matches
4. By default (no declarations), whitespace is preserved

### Breaking Changes

#### Version 2

Until version 2.3.1, use like the example below:

```js
import { Xslt, XmlParser } from 'xslt-processor'

// xmlString: string of xml file contents
// xsltString: string of xslt file contents
// outXmlString: output xml string.
const xslt = new Xslt();
const xmlParser = new XmlParser();
const outXmlString = xslt.xsltProcess( // Not async.
xmlParser.xmlParse(xmlString),
xmlParser.xmlParse(xsltString)
);
```

Version 3 received `<xsl:include>` which relies on Fetch API, which is asynchronous. Version 2 doesn't support `<xsl:include>`.

If using Node.js older than version v17.5.0, please use version 3.2.3, that uses `node-fetch` package. Versions 3.3.0 onward require at least Node.js version v17.5.0, since they use native `fetch()` function.

#### Version 1

Until version 1.2.8, use like the example below:

```js
import { Xslt, xmlParse } from 'xslt-processor'

// xmlString: string of xml file contents
// xsltString: string of xslt file contents
// outXmlString: output xml string.
const xslt = new Xslt();
const outXmlString = xslt.xsltProcess(
xmlParse(xmlString),
xmlParse(xsltString)
);
```

#### Version 0

Until version 0.11.7, use like the example below:

```js
import { xsltProcess, xmlParse } from 'xslt-processor'

// xmlString: string of xml file contents
// xsltString: string of xslt file contents
// outXmlString: output xml string.
const outXmlString = xsltProcess(
xmlParse(xmlString),
xmlParse(xsltString)
);
```

and to access the XPath parser:

```js
import { xpathParse } from 'xslt-processor'
```

These functions are part of `Xslt` and `XPath` classes, respectively, at version 1.x onward.

## References

- XPath Specification: http://www.w3.org/TR/1999/REC-xpath-19991116
Expand Down
2 changes: 1 addition & 1 deletion src/dom/functions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2023-2024 Design Liquido
// Copyright 2023-2026 Design Liquido
// Copyright 2018 Johannes Wilm
// Copyright 2005 Google Inc.
// All Rights Reserved
Expand Down
2 changes: 1 addition & 1 deletion src/dom/util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2023-2024 Design Liquido
// Copyright 2023-2026 Design Liquido
// Copyright 2018 Johannes Wilm
// Copyright 2005 Google
//
Expand Down
2 changes: 1 addition & 1 deletion src/dom/xmltoken.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2023-2024 Design Liquido
// Copyright 2023-2026 Design Liquido
// Copyright 2018 Johannes Wilm
// Copyright 2006 Google Inc.
// All Rights Reserved
Expand Down
2 changes: 1 addition & 1 deletion src/xpath/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2023-2024 Design Liquido
// Copyright 2023-2026 Design Liquido
// XPath implementation exports

// Main XPath class
Expand Down
2 changes: 1 addition & 1 deletion src/xpath/match-resolver.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2023-2024 Design Liquido
// Copyright 2023-2026 Design Liquido
// Match resolver that works with the new XPath implementation.

import { XNode } from '../dom';
Expand Down
2 changes: 1 addition & 1 deletion src/xpath/tokens.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2023-2024 Design Liquido
// Copyright 2023-2026 Design Liquido
// XPath tokens and axis constants

// The axes of XPath expressions.
Expand Down
112 changes: 109 additions & 3 deletions src/xpath/xpath.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2023-2024 Design Liquido
// Copyright 2023-2026 Design Liquido
// XPath adapter that uses the new lexer/parser implementation
// while maintaining backward compatibility with the existing XSLT API.

Expand All @@ -8,6 +8,7 @@ import { XPathParser } from './lib/src/parser';
import { XPathExpression, XPathLocationPath, XPathUnionExpression } from './lib/src/expressions';
import { XPathContext, XPathResult, createContext } from './lib/src/context';
import { XPathNode } from './lib/src/node';
import { JsonToXmlConverter } from './lib/src/expressions/json-to-xml-converter';
import { ExprContext } from './expr-context';
import { NodeValue, StringValue, NumberValue, BooleanValue, NodeSetValue } from './values';

Expand Down Expand Up @@ -134,6 +135,7 @@ class NodeConverter {
variables: this.convertVariables(exprContext),
functions: this.createCustomFunctions(exprContext),
namespaces: exprContext.knownNamespaces,
xsltVersion: exprContext.xsltVersion,
});
}

Expand Down Expand Up @@ -202,8 +204,14 @@ class NodeConverter {
*/
xPathNodeToXNode(xpathNode: XPathNode): XNode | null {
if (!xpathNode) return null;
// The nodes are already XNodes, just cast back
return xpathNode as unknown as XNode;

// Check if this is already an XNode (from native parsing)
if (xpathNode instanceof XNode) {
return xpathNode as unknown as XNode;
}

// Otherwise, convert XPathNode interface (from json-to-xml or xpath/lib) to XNode
return this.convertXPathNodeToXNode(xpathNode);
}

/**
Expand Down Expand Up @@ -304,9 +312,107 @@ class NodeConverter {
return this.xmlToJson(node);
};

// json-to-xml() function - XSLT 3.0 specific
functions['json-to-xml'] = (jsonText: any) => {
// Check XSLT version - only supported in 3.0
if (exprContext.xsltVersion === '1.0') {
throw new Error('json-to-xml() is not supported in XSLT 1.0. Use version="3.0" in your stylesheet.');
}
Comment on lines +315 to +320
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

json-to-xml() is documented/implemented as XSLT 3.0-specific, but the guard only rejects XSLT 1.0. As written, XSLT 2.0 (or other versions) would incorrectly allow the function. Update the version check to allow only XSLT 3.0 (or explicitly reject anything other than 3.0) and adjust the error message accordingly.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback


// Handle node set or single value
const jsonStr = Array.isArray(jsonText) ? jsonText[0] : jsonText;
if (!jsonStr) {
return null;
}
Comment on lines +324 to +326
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For XPath/XSLT functions that conceptually return a node-set/sequence, returning null can be ambiguous and can force downstream callers/operators to special-case it. Consider returning an empty node-set representation (e.g., []) for empty/invalid inputs to behave more like an empty sequence in XPath.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback


// Convert JSON string to XML document node using xpath lib converter
const converter = new JsonToXmlConverter();
const xpathNode = converter.convert(String(jsonStr));

if (!xpathNode) {
return null;
}

// Get owner document from context
const ownerDoc = exprContext.nodeList && exprContext.nodeList.length > 0
? exprContext.nodeList[0].ownerDocument
: null;

// Convert XPathNode interface tree to actual XNode objects
const convertedNode = this.convertXPathNodeToXNode(xpathNode, ownerDoc);

// Return as array for consistency with xpath processor
return convertedNode ? [convertedNode] : null;
};

return functions;
}

/**
* Convert an XPathNode interface tree to actual XNode objects.
* This is needed to convert json-to-xml() output to XSLT-compatible nodes.
*/
private convertXPathNodeToXNode(xpathNode: XPathNode, ownerDoc?: any): XNode | null {
if (!xpathNode) {
return null;
}

const { XNode: XNodeClass } = require('../dom');
const { DOM_DOCUMENT_NODE, DOM_TEXT_NODE, DOM_ELEMENT_NODE } = require('../constants');

let node: XNode;

if (xpathNode.nodeType === DOM_DOCUMENT_NODE) {
// For document nodes, extract and return the root element
if (xpathNode.childNodes && xpathNode.childNodes.length > 0) {
const rootChild = xpathNode.childNodes[0] as any;
node = this.convertXPathNodeToXNode(rootChild, ownerDoc);
return node;
}
return null;
} else if (xpathNode.nodeType === DOM_TEXT_NODE) {
// Create a text node
const textContent = xpathNode.textContent || '';
node = new XNodeClass(
DOM_TEXT_NODE,
'#text',
textContent,
ownerDoc
);
} else {
// Element node (DOM_ELEMENT_NODE)
node = new XNodeClass(
DOM_ELEMENT_NODE,
xpathNode.nodeName || 'element',
'',
ownerDoc
);

// Copy namespace URI if present
if (xpathNode.namespaceUri) {
node.namespaceUri = xpathNode.namespaceUri;
}

// Recursively convert child nodes
if (xpathNode.childNodes && xpathNode.childNodes.length > 0) {
for (let i = 0; i < xpathNode.childNodes.length; i++) {
const childXPathNode = xpathNode.childNodes[i] as any;
const childXNode = this.convertXPathNodeToXNode(childXPathNode, ownerDoc);
if (childXNode) {
childXNode.parentNode = node;
node.childNodes.push(childXNode);
}
}
if (node.childNodes.length > 0) {
node.firstChild = node.childNodes[0];
node.lastChild = node.childNodes[node.childNodes.length - 1];
}
}
}

return node;
}

/**
* Convert an XML node to a JSON string representation.
* This is a simplified implementation of XSLT 3.0's xml-to-json().
Expand Down
2 changes: 1 addition & 1 deletion src/xslt/xslt.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2023-2024 Design Liquido
// Copyright 2023-2026 Design Liquido
// Copyright 2018 Johannes Wilm
// Copyright 2005 Google Inc.
// All Rights Reserved
Expand Down
2 changes: 1 addition & 1 deletion tests/dom.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2023-2024 Design Liquido
// Copyright 2023-2026 Design Liquido
// Copyright 2018 Johannes Wilm
// Copyright 2005 Google Inc.
// All Rights Reserved
Expand Down
Loading