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
47 changes: 46 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,57 @@ const xslt = new Xslt(options);
- `cData` (`boolean`, default `true`): resolves CDATA elements in the output. Content under CDATA is resolved as text. This overrides `escape` for CDATA content.
- `escape` (`boolean`, default `true`): replaces symbols like `<`, `>`, `&` and `"` by the corresponding [HTML/XML entities](https://www.tutorialspoint.com/xml/xml_character_entities.htm). Can be overridden by `disable-output-escaping`, that also does the opposite, unescaping `&gt;` and `&lt;` by `<` and `>`, respectively.
- `selfClosingTags` (`boolean`, default `true`): Self-closes tags that don't have inner elements, if `true`. For instance, `<test></test>` becomes `<test />`.
- `outputMethod` (`string`, default `xml`): Specifies the default output method. if `<xsl:output>` is declared in your XSLT file, this will be overridden. Valid values: `xml`, `html`, `text`, `name`, `xhtml`.
- `outputMethod` (`string`, default `xml`): Specifies the default output method. if `<xsl:output>` is declared in your XSLT file, this will be overridden. Valid values: `xml`, `html`, `text`, `name`, `xhtml`, `json`.
- `parameters` (`array`, default `[]`): external parameters that you want to use.
- `name`: the parameter name;
- `namespaceUri` (optional): the namespace;
- `value`: the value.

#### JSON Output Format

When using `outputMethod: 'json'`, the XSLT processor will convert the resulting XML document to JSON format. This is useful for APIs and modern JavaScript applications.

**Example:**

```js
const xslt = new Xslt({ outputMethod: 'json' });
const xmlParser = new XmlParser();

const xmlString = `<root>
<users>
<user>Alice</user>
<user>Bob</user>
</users>
</root>`;

const xsltString = `<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<xsl:copy-of select="root"/>
</xsl:template>
</xsl:stylesheet>`;

const result = await xslt.xsltProcess(
xmlParser.xmlParse(xmlString),
xmlParser.xmlParse(xsltString)
);

// result will be a JSON string:
// {"root":{"users":{"user":["Alice","Bob"]}}}

const parsed = JSON.parse(result);
console.log(parsed.root.users.user); // ["Alice", "Bob"]
```

**JSON Structure Rules:**

- Each element becomes a property in a JSON object
- Text-only elements become string values
- Elements with multiple children of the same name become arrays
- Empty elements are omitted from the output
- Attributes are prefixed with `@` (when present in the output)
- Mixed text and element content uses the `#text` property for text nodes

### Direct use in browsers

You can simply add a tag like this:
Expand Down
175 changes: 175 additions & 0 deletions src/dom/xml-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,3 +449,178 @@

return xmlOwnerDocument(node.ownerDocument);
}

/**
* Converts an XNode to a JSON-serializable object.
* Uses JSON.parse(JSON.stringify()) approach to filter out unwanted properties.
* @param node The node to convert.
* @returns A JSON-serializable object representation of the node.
*/
function nodeToJsonObject(node: XNode): any {
if (!node) {
return null;

Check warning on line 461 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 462 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;

// Handle text nodes
if (nodeType === DOM_TEXT_NODE || nodeType === DOM_CDATA_SECTION_NODE) {
const text = node.nodeValue ? node.nodeValue.trim() : '';

Check warning on line 468 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 468 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 468 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
return text.length > 0 ? text : null;

Check warning on line 469 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 469 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 469 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 470 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

// Handle comment nodes
if (nodeType === DOM_COMMENT_NODE) {
return null; // Skip comments in JSON output

Check warning on line 474 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 475 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

// Handle document and document fragments
if (nodeType === DOM_DOCUMENT_NODE || nodeType === DOM_DOCUMENT_FRAGMENT_NODE) {
const children = node.childNodes || [];

Check warning on line 479 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 479 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 479 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 childObjects = [];

Check warning on line 480 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

for (let i = 0; i < children.length; i++) {

Check warning on line 482 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
const child = children[i];

Check warning on line 483 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
const childObj = nodeToJsonObject(child);

Check warning on line 484 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 (childObj !== null) {
childObjects.push(childObj);

Check warning on line 486 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 487 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 487 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 488 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 (childObjects.length === 0) {
return null;

Check warning on line 491 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 (childObjects.length === 1) {
return childObjects[0];

Check warning on line 493 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 {
return childObjects;

Check warning on line 495 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 496 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 496 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 496 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 496 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 496 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 496 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 497 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

// Handle element nodes
if (nodeType === DOM_ELEMENT_NODE) {
const obj: any = {};
const element = node as any;
const hasAttributes = element.attributes && element.attributes.length > 0;

Check warning on line 503 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

// Add attributes with @ prefix
if (hasAttributes) {
for (let i = 0; i < element.attributes.length; i++) {

Check warning on line 507 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
const attr = element.attributes[i];

Check warning on line 508 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
obj['@' + attr.nodeName] = attr.nodeValue;

Check warning on line 509 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 510 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 511 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

// Process child nodes
const children = element.childNodes || [];

Check warning on line 514 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 textContent = '';
let hasElementChildren = false;
const childElements: { [key: string]: any } = {};

for (let i = 0; i < children.length; i++) {
const child = children[i];
const childType = child.nodeType;

if (childType === DOM_TEXT_NODE || childType === DOM_CDATA_SECTION_NODE) {
const text = child.nodeValue ? child.nodeValue.trim() : '';

Check warning on line 524 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) {
textContent += text;
}
} else if (childType === DOM_ELEMENT_NODE) {
hasElementChildren = true;
const childElement = child as any;
const childName = childElement.localName || childElement.nodeName;

Check warning on line 531 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 childObj = nodeToJsonObject(child);

if (childObj !== null) {
if (childElements[childName]) {
// Multiple elements with same name - convert to array
if (!Array.isArray(childElements[childName])) {
childElements[childName] = [childElements[childName]];
}
childElements[childName].push(childObj);
} else {
childElements[childName] = childObj;
}
}
}
}

// Add child elements to object
Object.assign(obj, childElements);

// Add text content if no element children and has text
if (!hasElementChildren && textContent.length > 0) {
if (!hasAttributes && Object.keys(childElements).length === 0) {
// Only text, no attributes or element children
return textContent;
} else {
// Has attributes and/or element children plus text
obj['#text'] = textContent;

Check warning on line 558 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 559 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 completely empty (no attributes, no children, no text), return null
if (Object.keys(obj).length === 0) {
return null;
}

return obj;
}

return null;

Check warning on line 570 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.
* Element attributes are prefixed with '@'.
* Text nodes become the '#text' property or the value itself.
* @param node The root node to convert.
* @returns A JSON string representation of the document.
*/
export function xmlToJson(node: XNode): string {
if (!node) {
return '{}';

Check warning on line 583 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 584 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 document nodes, find the root element and wrap it
let rootElement: XNode = node;
if (node.nodeType === DOM_DOCUMENT_NODE || node.nodeType === DOM_DOCUMENT_FRAGMENT_NODE) {

Check warning on line 588 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 || [];
for (let i = 0; i < children.length; i++) {
if (children[i].nodeType === DOM_ELEMENT_NODE) {
rootElement = children[i];
break;
}
}
}

// Convert the root element to JSON
const element = rootElement as any;
const rootName = element.localName || element.nodeName;
const jsonObj: any = {};

// Build the root element object
const elementContent = nodeToJsonObject(rootElement);

if (elementContent === null) {
// Empty root element
jsonObj[rootName] = {};
} else if (typeof elementContent === 'object' && !Array.isArray(elementContent)) {
// Object with properties/attributes
jsonObj[rootName] = elementContent;
} else {
// Simple text content
jsonObj[rootName] = elementContent;

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
}

// Use JSON.stringify to clean up the object and then JSON.parse and stringify again
// This ensures we only have plain properties without circular references
try {
const cleaned = JSON.parse(JSON.stringify(jsonObj));
return JSON.stringify(cleaned);
} catch (error) {
// Fallback if stringification fails
return JSON.stringify(jsonObj);

Check warning on line 624 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
}
}
1 change: 1 addition & 0 deletions src/xslt/xslt-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export type XsltOptions = {
cData: boolean,
escape: boolean,
selfClosingTags: boolean,
outputMethod?: 'xml' | 'html' | 'text' | 'xhtml' | 'json',
parameters?: XsltParameter[]
}
14 changes: 11 additions & 3 deletions src/xslt/xslt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
domSetAttribute,
xmlGetAttribute,
xmlTransformedText,
xmlToJson,
xmlValue,
xmlValueLegacyBehavior
} from '../dom';
Expand Down Expand Up @@ -76,7 +77,7 @@ export class Xslt {
decimalFormatSettings: XsltDecimalFormatSettings;

outputDocument: XDocument;
outputMethod: 'xml' | 'html' | 'text' | 'name' | 'xhtml';
outputMethod: 'xml' | 'html' | 'text' | 'name' | 'xhtml' | 'json';
outputOmitXmlDeclaration: string;
version: string;
firstTemplateRan: boolean;
Expand Down Expand Up @@ -115,9 +116,10 @@ export class Xslt {
cData: options.cData === true,
escape: options.escape === true,
selfClosingTags: options.selfClosingTags === true,
outputMethod: options.outputMethod,
parameters: options.parameters || []
};
this.outputMethod = 'xml';
this.outputMethod = options.outputMethod || 'xml';
this.outputOmitXmlDeclaration = 'no';
this.stripSpacePatterns = [];
this.preserveSpacePatterns = [];
Expand All @@ -141,7 +143,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.
* @returns the processed document, as XML text in a string, or JSON string if outputMethod is 'json'.
*/
async xsltProcess(xmlDoc: XDocument, stylesheet: XDocument) {
const outputDocument = new XDocument();
Expand All @@ -155,6 +157,12 @@ export class Xslt {
}

await this.xsltProcessContext(expressionContext, stylesheet, this.outputDocument);

// Handle JSON output format
if (this.outputMethod === 'json') {
return xmlToJson(outputDocument);
}

const transformedOutputXml: string = xmlTransformedText(outputDocument, {
cData: this.options.cData,
escape: this.options.escape,
Expand Down
Loading