xslt-processor 4.2.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -89,12 +89,57 @@ const xslt = new Xslt(options);
89
89
  - `cData` (`boolean`, default `true`): resolves CDATA elements in the output. Content under CDATA is resolved as text. This overrides `escape` for CDATA content.
90
90
  - `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.
91
91
  - `selfClosingTags` (`boolean`, default `true`): Self-closes tags that don't have inner elements, if `true`. For instance, `<test></test>` becomes `<test />`.
92
- - `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`.
92
+ - `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`.
93
93
  - `parameters` (`array`, default `[]`): external parameters that you want to use.
94
94
  - `name`: the parameter name;
95
95
  - `namespaceUri` (optional): the namespace;
96
96
  - `value`: the value.
97
97
 
98
+ #### JSON Output Format
99
+
100
+ 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.
101
+
102
+ **Example:**
103
+
104
+ ```js
105
+ const xslt = new Xslt({ outputMethod: 'json' });
106
+ const xmlParser = new XmlParser();
107
+
108
+ const xmlString = `<root>
109
+ <users>
110
+ <user>Alice</user>
111
+ <user>Bob</user>
112
+ </users>
113
+ </root>`;
114
+
115
+ const xsltString = `<?xml version="1.0"?>
116
+ <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
117
+ <xsl:template match="/">
118
+ <xsl:copy-of select="root"/>
119
+ </xsl:template>
120
+ </xsl:stylesheet>`;
121
+
122
+ const result = await xslt.xsltProcess(
123
+ xmlParser.xmlParse(xmlString),
124
+ xmlParser.xmlParse(xsltString)
125
+ );
126
+
127
+ // result will be a JSON string:
128
+ // {"root":{"users":{"user":["Alice","Bob"]}}}
129
+
130
+ const parsed = JSON.parse(result);
131
+ console.log(parsed.root.users.user); // ["Alice", "Bob"]
132
+ ```
133
+
134
+ **JSON Structure Rules:**
135
+
136
+ - Each element becomes a property in a JSON object
137
+ - Text-only elements become string values
138
+ - Elements with multiple children of the same name become arrays
139
+ - Empty elements are omitted from the output
140
+ - Attributes are prefixed with `@` (when present in the output)
141
+ - Mixed text and element content uses the `#text` property for text nodes
142
+
98
143
  ### Direct use in browsers
99
144
 
100
145
  You can simply add a tag like this:
package/index.d.mts CHANGED
@@ -532,6 +532,7 @@ type XsltOptions = {
532
532
  cData: boolean;
533
533
  escape: boolean;
534
534
  selfClosingTags: boolean;
535
+ outputMethod?: 'xml' | 'html' | 'text' | 'xhtml' | 'json';
535
536
  parameters?: XsltParameter[];
536
537
  };
537
538
 
@@ -566,7 +567,7 @@ declare class Xslt {
566
567
  options: XsltOptions;
567
568
  decimalFormatSettings: XsltDecimalFormatSettings;
568
569
  outputDocument: XDocument;
569
- outputMethod: 'xml' | 'html' | 'text' | 'name' | 'xhtml';
570
+ outputMethod: 'xml' | 'html' | 'text' | 'name' | 'xhtml' | 'json';
570
571
  outputOmitXmlDeclaration: string;
571
572
  version: string;
572
573
  firstTemplateRan: boolean;
@@ -591,7 +592,7 @@ declare class Xslt {
591
592
  * The exported entry point of the XSL-T processor.
592
593
  * @param xmlDoc The input document root, as DOM node.
593
594
  * @param stylesheet The stylesheet document root, as DOM node.
594
- * @returns the processed document, as XML text in a string.
595
+ * @returns the processed document, as XML text in a string, or JSON string if outputMethod is 'json'.
595
596
  */
596
597
  xsltProcess(xmlDoc: XDocument, stylesheet: XDocument): Promise<string>;
597
598
  /**
package/index.d.ts CHANGED
@@ -532,6 +532,7 @@ type XsltOptions = {
532
532
  cData: boolean;
533
533
  escape: boolean;
534
534
  selfClosingTags: boolean;
535
+ outputMethod?: 'xml' | 'html' | 'text' | 'xhtml' | 'json';
535
536
  parameters?: XsltParameter[];
536
537
  };
537
538
 
@@ -566,7 +567,7 @@ declare class Xslt {
566
567
  options: XsltOptions;
567
568
  decimalFormatSettings: XsltDecimalFormatSettings;
568
569
  outputDocument: XDocument;
569
- outputMethod: 'xml' | 'html' | 'text' | 'name' | 'xhtml';
570
+ outputMethod: 'xml' | 'html' | 'text' | 'name' | 'xhtml' | 'json';
570
571
  outputOmitXmlDeclaration: string;
571
572
  version: string;
572
573
  firstTemplateRan: boolean;
@@ -591,7 +592,7 @@ declare class Xslt {
591
592
  * The exported entry point of the XSL-T processor.
592
593
  * @param xmlDoc The input document root, as DOM node.
593
594
  * @param stylesheet The stylesheet document root, as DOM node.
594
- * @returns the processed document, as XML text in a string.
595
+ * @returns the processed document, as XML text in a string, or JSON string if outputMethod is 'json'.
595
596
  */
596
597
  xsltProcess(xmlDoc: XDocument, stylesheet: XDocument): Promise<string>;
597
598
  /**
package/index.js CHANGED
@@ -2213,6 +2213,122 @@ function xmlGetAttribute(node, name) {
2213
2213
  }
2214
2214
  return value;
2215
2215
  }
2216
+ function nodeToJsonObject(node) {
2217
+ if (!node) {
2218
+ return null;
2219
+ }
2220
+ const nodeType = node.nodeType;
2221
+ if (nodeType === DOM_TEXT_NODE || nodeType === DOM_CDATA_SECTION_NODE) {
2222
+ const text = node.nodeValue ? node.nodeValue.trim() : "";
2223
+ return text.length > 0 ? text : null;
2224
+ }
2225
+ if (nodeType === DOM_COMMENT_NODE) {
2226
+ return null;
2227
+ }
2228
+ if (nodeType === DOM_DOCUMENT_NODE || nodeType === DOM_DOCUMENT_FRAGMENT_NODE) {
2229
+ const children = node.childNodes || [];
2230
+ const childObjects = [];
2231
+ for (let i = 0; i < children.length; i++) {
2232
+ const child = children[i];
2233
+ const childObj = nodeToJsonObject(child);
2234
+ if (childObj !== null) {
2235
+ childObjects.push(childObj);
2236
+ }
2237
+ }
2238
+ if (childObjects.length === 0) {
2239
+ return null;
2240
+ } else if (childObjects.length === 1) {
2241
+ return childObjects[0];
2242
+ } else {
2243
+ return childObjects;
2244
+ }
2245
+ }
2246
+ if (nodeType === DOM_ELEMENT_NODE) {
2247
+ const obj = {};
2248
+ const element = node;
2249
+ const hasAttributes = element.attributes && element.attributes.length > 0;
2250
+ if (hasAttributes) {
2251
+ for (let i = 0; i < element.attributes.length; i++) {
2252
+ const attr = element.attributes[i];
2253
+ obj["@" + attr.nodeName] = attr.nodeValue;
2254
+ }
2255
+ }
2256
+ const children = element.childNodes || [];
2257
+ let textContent = "";
2258
+ let hasElementChildren = false;
2259
+ const childElements = {};
2260
+ for (let i = 0; i < children.length; i++) {
2261
+ const child = children[i];
2262
+ const childType = child.nodeType;
2263
+ if (childType === DOM_TEXT_NODE || childType === DOM_CDATA_SECTION_NODE) {
2264
+ const text = child.nodeValue ? child.nodeValue.trim() : "";
2265
+ if (text.length > 0) {
2266
+ textContent += text;
2267
+ }
2268
+ } else if (childType === DOM_ELEMENT_NODE) {
2269
+ hasElementChildren = true;
2270
+ const childElement = child;
2271
+ const childName = childElement.localName || childElement.nodeName;
2272
+ const childObj = nodeToJsonObject(child);
2273
+ if (childObj !== null) {
2274
+ if (childElements[childName]) {
2275
+ if (!Array.isArray(childElements[childName])) {
2276
+ childElements[childName] = [childElements[childName]];
2277
+ }
2278
+ childElements[childName].push(childObj);
2279
+ } else {
2280
+ childElements[childName] = childObj;
2281
+ }
2282
+ }
2283
+ }
2284
+ }
2285
+ Object.assign(obj, childElements);
2286
+ if (!hasElementChildren && textContent.length > 0) {
2287
+ if (!hasAttributes && Object.keys(childElements).length === 0) {
2288
+ return textContent;
2289
+ } else {
2290
+ obj["#text"] = textContent;
2291
+ }
2292
+ }
2293
+ if (Object.keys(obj).length === 0) {
2294
+ return null;
2295
+ }
2296
+ return obj;
2297
+ }
2298
+ return null;
2299
+ }
2300
+ function xmlToJson(node) {
2301
+ if (!node) {
2302
+ return "{}";
2303
+ }
2304
+ let rootElement = node;
2305
+ if (node.nodeType === DOM_DOCUMENT_NODE || node.nodeType === DOM_DOCUMENT_FRAGMENT_NODE) {
2306
+ const children = node.childNodes || [];
2307
+ for (let i = 0; i < children.length; i++) {
2308
+ if (children[i].nodeType === DOM_ELEMENT_NODE) {
2309
+ rootElement = children[i];
2310
+ break;
2311
+ }
2312
+ }
2313
+ }
2314
+ const element = rootElement;
2315
+ const rootName = element.localName || element.nodeName;
2316
+ const jsonObj = {};
2317
+ const elementContent = nodeToJsonObject(rootElement);
2318
+ if (elementContent === null) {
2319
+ jsonObj[rootName] = {};
2320
+ } else if (typeof elementContent === "object" && !Array.isArray(elementContent)) {
2321
+ jsonObj[rootName] = elementContent;
2322
+ } else {
2323
+ jsonObj[rootName] = elementContent;
2324
+ }
2325
+ try {
2326
+ const cleaned = JSON.parse(JSON.stringify(jsonObj));
2327
+ return JSON.stringify(cleaned);
2328
+ } catch (error) {
2329
+ return JSON.stringify(jsonObj);
2330
+ }
2331
+ }
2216
2332
 
2217
2333
  // src/dom/xml-parser.ts
2218
2334
  var import_he2 = __toESM(require("he"));
@@ -3446,24 +3562,31 @@ function nodeMatchesSinglePattern(node, pattern, context, matchResolver, xPath)
3446
3562
  const patternLocalName = attrPattern.includes(":") ? attrPattern.substring(attrPattern.indexOf(":") + 1) : attrPattern;
3447
3563
  return attrName === patternLocalName || node.nodeName === attrPattern;
3448
3564
  }
3449
- const nodeContext = context.clone([node], 0);
3450
- try {
3451
- const expr = xPath.xPathParse(pattern, "self-and-siblings");
3452
- const nodes = matchResolver.expressionMatch(expr, nodeContext);
3453
- if (nodes.some((n) => n.id === node.id)) {
3454
- return true;
3455
- }
3456
- } catch (e) {
3457
- }
3458
3565
  if (pattern === "*" && node.nodeType === DOM_ELEMENT_NODE) {
3459
3566
  return true;
3460
3567
  }
3461
- if (pattern.includes("[") || pattern.includes("/")) {
3568
+ if (!pattern.includes("/") && !pattern.includes("[") && !pattern.startsWith("@")) {
3569
+ if (pattern === node.nodeName || pattern === node.localName) {
3570
+ return true;
3571
+ }
3572
+ }
3573
+ if (pattern.includes("/") || pattern.includes("[")) {
3462
3574
  try {
3575
+ const evaluationPattern = pattern.startsWith("/") ? pattern : "//" + pattern;
3463
3576
  const rootContext = context.clone([context.root], 0);
3464
- const descendantPattern = pattern.startsWith("/") ? pattern : "//" + pattern;
3465
- const expr = xPath.xPathParse(descendantPattern);
3466
- const nodes = matchResolver.expressionMatch(expr, rootContext);
3577
+ const evalResult = xPath.xPathEval(evaluationPattern, rootContext);
3578
+ const nodes = evalResult.nodeSetValue();
3579
+ if (nodes.some((n) => n.id === node.id)) {
3580
+ return true;
3581
+ }
3582
+ } catch (e) {
3583
+ }
3584
+ }
3585
+ if (!pattern.includes("/") && !pattern.includes("[") && !pattern.startsWith("@")) {
3586
+ try {
3587
+ const nodeContext = context.clone([node], 0);
3588
+ const expr = xPath.xPathParse(pattern, "self-and-siblings");
3589
+ const nodes = matchResolver.expressionMatch(expr, nodeContext);
3467
3590
  if (nodes.some((n) => n.id === node.id)) {
3468
3591
  return true;
3469
3592
  }
@@ -3544,9 +3667,10 @@ var Xslt = class {
3544
3667
  cData: options.cData === true,
3545
3668
  escape: options.escape === true,
3546
3669
  selfClosingTags: options.selfClosingTags === true,
3670
+ outputMethod: options.outputMethod,
3547
3671
  parameters: options.parameters || []
3548
3672
  };
3549
- this.outputMethod = "xml";
3673
+ this.outputMethod = options.outputMethod || "xml";
3550
3674
  this.outputOmitXmlDeclaration = "no";
3551
3675
  this.stripSpacePatterns = [];
3552
3676
  this.preserveSpacePatterns = [];
@@ -3569,7 +3693,7 @@ var Xslt = class {
3569
3693
  * The exported entry point of the XSL-T processor.
3570
3694
  * @param xmlDoc The input document root, as DOM node.
3571
3695
  * @param stylesheet The stylesheet document root, as DOM node.
3572
- * @returns the processed document, as XML text in a string.
3696
+ * @returns the processed document, as XML text in a string, or JSON string if outputMethod is 'json'.
3573
3697
  */
3574
3698
  xsltProcess(xmlDoc, stylesheet) {
3575
3699
  return __async(this, null, function* () {
@@ -3582,6 +3706,9 @@ var Xslt = class {
3582
3706
  }
3583
3707
  }
3584
3708
  yield this.xsltProcessContext(expressionContext, stylesheet, this.outputDocument);
3709
+ if (this.outputMethod === "json") {
3710
+ return xmlToJson(outputDocument);
3711
+ }
3585
3712
  const transformedOutputXml = xmlTransformedText(outputDocument, {
3586
3713
  cData: this.options.cData,
3587
3714
  escape: this.options.escape,
@@ -3741,7 +3868,9 @@ var Xslt = class {
3741
3868
  const mode = xmlGetAttribute(template, "mode");
3742
3869
  const top = template.ownerDocument.documentElement;
3743
3870
  const expandedTemplates = collectAndExpandTemplates(top, mode, this.xPath);
3744
- const modifiedContext = context.clone(nodes);
3871
+ const paramContext = context.clone();
3872
+ yield this.xsltWithParam(paramContext, template);
3873
+ const modifiedContext = paramContext.clone(nodes);
3745
3874
  for (let j = 0; j < modifiedContext.contextSize(); ++j) {
3746
3875
  const currentNode = modifiedContext.nodeList[j];
3747
3876
  if (currentNode.nodeType === DOM_TEXT_NODE) {
@@ -4612,13 +4741,76 @@ var Xslt = class {
4612
4741
  contextClone.baseTemplateMatched = true;
4613
4742
  const templateContext = contextClone.clone(winner.matchedNodes, 0);
4614
4743
  yield this.xsltChildNodes(templateContext, winner.priority.template, output);
4744
+ } else {
4745
+ const rootNode = context.nodeList[context.position];
4746
+ if (rootNode && rootNode.childNodes && rootNode.childNodes.length > 0) {
4747
+ const childNodes = rootNode.childNodes.filter((n) => n.nodeName !== "#dtd-section");
4748
+ if (childNodes.length > 0) {
4749
+ const childContext = context.clone(childNodes);
4750
+ for (let j = 0; j < childContext.contextSize(); ++j) {
4751
+ const currentNode = childContext.nodeList[j];
4752
+ if (currentNode.nodeType === DOM_TEXT_NODE) {
4753
+ const textNodeContext = context.clone([currentNode], 0);
4754
+ this.commonLogicTextNode(textNodeContext, currentNode, output);
4755
+ } else {
4756
+ const clonedContext = childContext.clone([currentNode], 0);
4757
+ const selection = selectBestTemplate(
4758
+ expandedTemplates,
4759
+ clonedContext,
4760
+ this.matchResolver,
4761
+ this.xPath
4762
+ );
4763
+ if (selection.selectedTemplate) {
4764
+ const templateContext = clonedContext.clone([currentNode], 0);
4765
+ templateContext.inApplyTemplates = true;
4766
+ yield this.xsltChildNodes(templateContext, selection.selectedTemplate, output);
4767
+ } else {
4768
+ if (currentNode.childNodes && currentNode.childNodes.length > 0) {
4769
+ const grandchildNodes = currentNode.childNodes.filter((n) => n.nodeName !== "#dtd-section");
4770
+ if (grandchildNodes.length > 0) {
4771
+ const grandchildContext = context.clone(grandchildNodes);
4772
+ for (let k = 0; k < grandchildContext.contextSize(); ++k) {
4773
+ const grandchildNode = grandchildContext.nodeList[k];
4774
+ if (grandchildNode.nodeType === DOM_TEXT_NODE) {
4775
+ const textNodeContext = context.clone([grandchildNode], 0);
4776
+ this.commonLogicTextNode(textNodeContext, grandchildNode, output);
4777
+ } else {
4778
+ const grandchildClonedContext = grandchildContext.clone([grandchildNode], 0);
4779
+ const grandchildSelection = selectBestTemplate(
4780
+ expandedTemplates,
4781
+ grandchildClonedContext,
4782
+ this.matchResolver,
4783
+ this.xPath
4784
+ );
4785
+ if (grandchildSelection.selectedTemplate) {
4786
+ const grandchildTemplateContext = grandchildClonedContext.clone([grandchildNode], 0);
4787
+ grandchildTemplateContext.inApplyTemplates = true;
4788
+ yield this.xsltChildNodes(grandchildTemplateContext, grandchildSelection.selectedTemplate, output);
4789
+ }
4790
+ }
4791
+ }
4792
+ }
4793
+ }
4794
+ }
4795
+ }
4796
+ }
4797
+ }
4798
+ }
4615
4799
  }
4616
4800
  }
4617
4801
  });
4618
4802
  }
4619
4803
  xsltValueOf(context, template, output) {
4620
4804
  const select = xmlGetAttribute(template, "select");
4621
- const attribute = this.xPath.xPathEval(select, context);
4805
+ const current = context.nodeList[context.position];
4806
+ let attribute = this.xPath.xPathEval(select, context);
4807
+ if (current && current.nodeName === "#document" && (attribute.stringValue() === "" || attribute instanceof NodeSetValue && attribute.nodeSetValue().length === 0)) {
4808
+ const docChild = current.childNodes.find((c) => c.nodeName !== "#dtd-section");
4809
+ if (docChild) {
4810
+ const fallbackContext = context.clone([docChild], 0);
4811
+ attribute = this.xPath.xPathEval(select, fallbackContext);
4812
+ }
4813
+ }
4622
4814
  const value = attribute.stringValue();
4623
4815
  const node = domCreateTextNode(this.outputDocument, value);
4624
4816
  const targetOutput = output || this.outputDocument;