@tradik/xslt-processor 1.0.3 → 1.1.1

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.
Files changed (47) hide show
  1. package/README.md +290 -45
  2. package/bin/lib/options.js +114 -0
  3. package/bin/lib/paths.js +186 -0
  4. package/bin/lib/transform.js +115 -0
  5. package/bin/xslt.js +68 -162
  6. package/dist/xslt-processor.browser.js +2074 -159
  7. package/dist/xslt-processor.browser.js.map +4 -4
  8. package/dist/xslt-processor.browser.min.js +6 -2
  9. package/dist/xslt-processor.browser.min.js.map +4 -4
  10. package/dist/xslt-processor.cjs +2078 -158
  11. package/dist/xslt-processor.cjs.map +4 -4
  12. package/dist/xslt-processor.d.cts +299 -0
  13. package/dist/xslt-processor.d.ts +92 -4
  14. package/dist/xslt-processor.js +2073 -157
  15. package/dist/xslt-processor.js.map +4 -4
  16. package/package.json +26 -15
  17. package/src/XSLTProcessor.js +177 -8
  18. package/src/index.js +11 -5
  19. package/src/xpath/evaluator.js +48 -7
  20. package/src/xslt/elements.js +57 -0
  21. package/src/xslt/engine.js +471 -179
  22. package/src/xslt/formatNumber.js +220 -0
  23. package/src/xslt/functions.js +191 -0
  24. package/src/xslt/index.js +31 -0
  25. package/src/xslt/keys.js +141 -0
  26. package/src/xslt/literalResult.js +167 -0
  27. package/src/xslt/number.js +178 -0
  28. package/src/xslt/numberFormat.js +155 -0
  29. package/src/xslt/resultTree.js +74 -0
  30. package/src/xslt/serializer/baseWriter.js +283 -0
  31. package/src/xslt/serializer/constants.js +78 -0
  32. package/src/xslt/serializer/escape.js +98 -0
  33. package/src/xslt/serializer/htmlSerializer.js +141 -0
  34. package/src/xslt/serializer/indent.js +51 -0
  35. package/src/xslt/serializer/namespaces.js +68 -0
  36. package/src/xslt/serializer/rawText.js +41 -0
  37. package/src/xslt/serializer/settings.js +103 -0
  38. package/src/xslt/serializer/textSerializer.js +29 -0
  39. package/src/xslt/serializer/xmlSerializer.js +127 -0
  40. package/src/xslt/serializer.js +57 -0
  41. package/src/xslt/templatePriority.js +45 -0
  42. package/src/xslt/uri.js +68 -0
  43. package/src/xslt/whitespace.js +184 -0
  44. package/src/XSLTProcessor.test.js +0 -930
  45. package/src/xpath/evaluator.test.js +0 -1852
  46. package/src/xpath/tokenizer.test.js +0 -224
  47. package/src/xslt/engine.test.js +0 -3130
package/package.json CHANGED
@@ -1,27 +1,37 @@
1
1
  {
2
2
  "name": "@tradik/xslt-processor",
3
- "version": "1.0.3",
3
+ "version": "1.1.1",
4
4
  "description": "JavaScript implementation of XSLTProcessor for browser environments and CLI",
5
5
  "type": "module",
6
6
  "main": "dist/xslt-processor.cjs",
7
7
  "module": "dist/xslt-processor.js",
8
- "browser": "dist/xslt-processor.browser.js",
8
+ "browser": "dist/xslt-processor.js",
9
9
  "types": "dist/xslt-processor.d.ts",
10
10
  "bin": {
11
- "xslt": "./bin/xslt.js"
11
+ "xslt": "bin/xslt.js"
12
12
  },
13
13
  "exports": {
14
14
  ".": {
15
- "types": "./dist/xslt-processor.d.ts",
16
- "browser": "./dist/xslt-processor.browser.js",
17
- "import": "./dist/xslt-processor.js",
18
- "require": "./dist/xslt-processor.cjs"
15
+ "browser": {
16
+ "types": "./dist/xslt-processor.d.ts",
17
+ "default": "./dist/xslt-processor.js"
18
+ },
19
+ "import": {
20
+ "types": "./dist/xslt-processor.d.ts",
21
+ "default": "./dist/xslt-processor.js"
22
+ },
23
+ "require": {
24
+ "types": "./dist/xslt-processor.d.cts",
25
+ "default": "./dist/xslt-processor.cjs"
26
+ }
19
27
  }
20
28
  },
21
29
  "files": [
22
30
  "dist",
23
31
  "src",
24
- "bin"
32
+ "bin",
33
+ "!src/**/*.test.js",
34
+ "!bin/**/*.test.js"
25
35
  ],
26
36
  "publishConfig": {
27
37
  "access": "public"
@@ -32,8 +42,8 @@
32
42
  "test:watch": "node --test --watch src/**/*.test.js",
33
43
  "test:browser": "node tests/browser-test-runner.js",
34
44
  "lint": "eslint src/",
35
- "format": "prettier --write \"src/**/*.js\"",
36
- "format:check": "prettier --check \"src/**/*.js\"",
45
+ "format": "prettier --write \"src/**/*.js\" \"bin/**/*.js\"",
46
+ "format:check": "prettier --check \"src/**/*.js\" \"bin/**/*.js\"",
37
47
  "prepublishOnly": "npm run build && npm run test"
38
48
  },
39
49
  "keywords": [
@@ -57,12 +67,13 @@
57
67
  },
58
68
  "homepage": "https://github.com/spagu/XSLT-Processor#readme",
59
69
  "engines": {
60
- "node": ">=18.0.0"
70
+ "node": ">=20.19.0"
61
71
  },
62
72
  "devDependencies": {
63
- "eslint": "^9.0.0",
64
- "prettier": "^3.4.0",
65
- "esbuild": "^0.24.0",
66
- "jsdom": "^25.0.0"
73
+ "@eslint/js": "^10.0.1",
74
+ "esbuild": "^0.28.2",
75
+ "eslint": "^10.10.0",
76
+ "jsdom": "^29.1.1",
77
+ "prettier": "^3.9.6"
67
78
  }
68
79
  }
@@ -30,6 +30,113 @@ export class XSLTProcessor {
30
30
  this._engine = null;
31
31
  this._stylesheet = null;
32
32
  this._parameters = new Map();
33
+ this._stylesheetLoader = null;
34
+ this._documentLoader = null;
35
+ }
36
+
37
+ /**
38
+ * The underlying XSLT engine (advanced usage).
39
+ *
40
+ * The engine is created lazily by {@link XSLTProcessor#importStylesheet},
41
+ * so this getter returns `null` until a stylesheet has been imported.
42
+ * Prefer the public {@link XSLTProcessor#setStylesheetLoader} over reaching
43
+ * into the engine directly.
44
+ *
45
+ * @returns {import('./xslt/engine.js').XsltEngine|null} The engine, or null before import
46
+ *
47
+ * @example
48
+ * processor.importStylesheet(xslDoc, '/styles/main.xsl');
49
+ * console.log(processor.engine.outputSettings.method);
50
+ */
51
+ get engine() {
52
+ return this._engine;
53
+ }
54
+
55
+ /**
56
+ * Sets the loader used to resolve `xsl:import` and `xsl:include` references.
57
+ *
58
+ * The loader is synchronous: it MUST return the external stylesheet as a
59
+ * `Document` or as an XML string (which is parsed automatically). Promises
60
+ * are not awaited by the engine, so pre-load remote stylesheets before
61
+ * calling `importStylesheet`.
62
+ *
63
+ * The loader may be set before or after `importStylesheet`. When set before,
64
+ * it is passed to the engine on creation, which is required for the loader to
65
+ * be used while the stylesheet is being compiled. When set after, the live
66
+ * engine is updated as well.
67
+ *
68
+ * @param {((href: string, baseUri?: string) => (Document|string))|null} loader
69
+ * The loader function, or null to remove a previously configured loader
70
+ * @returns {XSLTProcessor} This processor, to allow chaining
71
+ * @throws {TypeError} If the loader is neither a function nor null
72
+ *
73
+ * @example
74
+ * processor.setStylesheetLoader((href) => readFileSync(href, 'utf8'));
75
+ * processor.importStylesheet(mainStylesheet, '/styles/main.xsl');
76
+ */
77
+ setStylesheetLoader(loader) {
78
+ if (
79
+ loader !== null &&
80
+ loader !== undefined &&
81
+ typeof loader !== "function"
82
+ ) {
83
+ throw new TypeError(
84
+ "Failed to execute 'setStylesheetLoader' on 'XSLTProcessor': The loader argument must be a function or null.",
85
+ );
86
+ }
87
+
88
+ this._stylesheetLoader = loader ?? null;
89
+
90
+ // Keep an already created engine in sync
91
+ if (this._engine) {
92
+ this._engine.setStylesheetLoader(this._stylesheetLoader);
93
+ }
94
+
95
+ return this;
96
+ }
97
+
98
+ /**
99
+ * Sets the loader used to resolve the XSLT `document()` function.
100
+ *
101
+ * The loader is synchronous: it MUST return the referenced document as a
102
+ * `Document`, as an XML string (which is parsed automatically) or as `null`
103
+ * when the document cannot be provided. Returning `null`, like configuring no
104
+ * loader at all, makes `document()` evaluate to an empty node-set rather than
105
+ * failing the transformation.
106
+ *
107
+ * The loader may be set before or after `importStylesheet`; a live engine is
108
+ * kept in sync.
109
+ *
110
+ * @param {((uri: string, baseUri?: string) => (Document|string|null))|null} loader
111
+ * The loader function, or null to remove a previously configured loader
112
+ * @returns {XSLTProcessor} This processor, to allow chaining
113
+ * @throws {TypeError} If the loader is neither a function nor null
114
+ *
115
+ * @example
116
+ * // Node.js: resolve document() against the file system
117
+ * import { readFileSync } from 'node:fs';
118
+ * processor.setDocumentLoader((uri) => readFileSync(uri, 'utf8'));
119
+ * processor.importStylesheet(xslDoc, '/styles/main.xsl');
120
+ */
121
+ setDocumentLoader(loader) {
122
+ if (
123
+ loader !== null &&
124
+ loader !== undefined &&
125
+ typeof loader !== "function"
126
+ ) {
127
+ throw new TypeError(
128
+ "Failed to execute 'setDocumentLoader' on 'XSLTProcessor': The loader argument must be a function or null.",
129
+ );
130
+ }
131
+
132
+ this._documentLoader = loader ?? null;
133
+
134
+ // Keep an already created engine in sync
135
+ if (this._engine) {
136
+ this._engine.setDocumentLoader(this._documentLoader);
137
+ }
138
+
139
+ return this;
33
140
  }
34
141
 
35
142
  /**
@@ -40,14 +147,17 @@ export class XSLTProcessor {
40
147
  * <xsl:stylesheet> or <xsl:transform> element.
41
148
  *
42
149
  * @param {Node} style - The XSLT stylesheet to import (Document or Element)
150
+ * @param {string} [stylesheetUri] - Optional URI of the stylesheet, used as the
151
+ * base URI when resolving relative `xsl:import`/`xsl:include` hrefs. When
152
+ * omitted, hrefs are passed to the loader unresolved.
43
153
  * @returns {void}
44
154
  *
45
155
  * @example
46
156
  * const parser = new DOMParser();
47
157
  * const xslDoc = parser.parseFromString(xslText, 'application/xml');
48
- * processor.importStylesheet(xslDoc);
158
+ * processor.importStylesheet(xslDoc, '/styles/main.xsl');
49
159
  */
50
- importStylesheet(style) {
160
+ importStylesheet(style, stylesheetUri) {
51
161
  if (!style) {
52
162
  throw new TypeError(
53
163
  "Failed to execute 'importStylesheet' on 'XSLTProcessor': 1 argument required, but only 0 present.",
@@ -70,14 +180,17 @@ export class XSLTProcessor {
70
180
  }
71
181
 
72
182
  this._stylesheet = style;
73
- this._engine = new XsltEngine();
183
+ this._engine = new XsltEngine({
184
+ stylesheetLoader: this._stylesheetLoader,
185
+ documentLoader: this._documentLoader,
186
+ });
74
187
 
75
188
  // Apply any previously set parameters
76
189
  for (const [key, value] of this._parameters) {
77
- this._engine.globalParameters[key] = { value };
190
+ this._engine.setParameterValue(key, value);
78
191
  }
79
192
 
80
- this._engine.importStylesheet(style);
193
+ this._engine.importStylesheet(style, stylesheetUri);
81
194
  }
82
195
 
83
196
  /**
@@ -182,6 +295,55 @@ export class XSLTProcessor {
182
295
  }
183
296
  }
184
297
 
298
+ /**
299
+ * Transforms the node source by applying the XSLT stylesheet and serializes
300
+ * the result to a string honoring the stylesheet `xsl:output` settings.
301
+ *
302
+ * Non-W3C convenience method: the native XSLTProcessor has no equivalent.
303
+ * Output method, indentation, XML declaration, document type declaration,
304
+ * CDATA sections and `disable-output-escaping` are all honored
305
+ * (XSLT 1.0 section 16).
306
+ *
307
+ * @param {Node} source - The XML document to transform
308
+ * @returns {string|null} The serialized result, or null on a transformation error
309
+ *
310
+ * @example
311
+ * const xml = processor.transformToString(xmlDoc);
312
+ * // '<?xml version="1.0" encoding="UTF-8"?>\n<BAR>\n <QUX/>\n</BAR>'
313
+ */
314
+ transformToString(source) {
315
+ if (!source) {
316
+ throw new TypeError(
317
+ "Failed to execute 'transformToString' on 'XSLTProcessor': 1 argument required, but only 0 present.",
318
+ );
319
+ }
320
+
321
+ if (!this._engine || !this._stylesheet) {
322
+ throw new Error(
323
+ "Failed to execute 'transformToString' on 'XSLTProcessor': No stylesheet has been imported.",
324
+ );
325
+ }
326
+
327
+ // Validate source node
328
+ if (
329
+ source.nodeType !== 1 &&
330
+ source.nodeType !== 9 &&
331
+ source.nodeType !== 11
332
+ ) {
333
+ throw new TypeError(
334
+ "Failed to execute 'transformToString' on 'XSLTProcessor': The source is not a valid node type.",
335
+ );
336
+ }
337
+
338
+ try {
339
+ return this._engine.transformToString(source);
340
+ } catch (error) {
341
+ // Match transformToDocument behavior - return null on error
342
+ console.error("XSLT transformation error:", error);
343
+ return null;
344
+ }
345
+ }
346
+
185
347
  /**
186
348
  * Sets a parameter in the XSLT stylesheet.
187
349
  *
@@ -212,7 +374,7 @@ export class XSLTProcessor {
212
374
 
213
375
  // If engine is already initialized, update it
214
376
  if (this._engine) {
215
- this._engine.globalParameters[key] = { value };
377
+ this._engine.setParameterValue(key, value);
216
378
  }
217
379
  }
218
380
 
@@ -279,7 +441,7 @@ export class XSLTProcessor {
279
441
  this._parameters.delete(key);
280
442
 
281
443
  if (this._engine) {
282
- delete this._engine.globalParameters[key];
444
+ this._engine.clearParameterValue(key);
283
445
  }
284
446
  }
285
447
 
@@ -297,13 +459,20 @@ export class XSLTProcessor {
297
459
  this._parameters.clear();
298
460
 
299
461
  if (this._engine) {
300
- this._engine.globalParameters = {};
462
+ this._engine.clearParameterValues();
301
463
  }
302
464
  }
303
465
 
304
466
  /**
305
467
  * Removes all parameters and stylesheets from the XSLTProcessor.
306
468
  *
469
+ * Per the W3C `XSLTProcessor` semantics, `reset()` clears stylesheet state and
470
+ * parameters only. The stylesheet and document loaders are processor
471
+ * configuration rather than stylesheet state, so they are deliberately
472
+ * preserved and stay effective for the next `importStylesheet()` call. Pass
473
+ * `null` to {@link XSLTProcessor#setStylesheetLoader} or
474
+ * {@link XSLTProcessor#setDocumentLoader} to remove them explicitly.
475
+ *
307
476
  * @returns {void}
308
477
  *
309
478
  * @example
package/src/index.js CHANGED
@@ -29,7 +29,7 @@ export {
29
29
  XSLTProcessor,
30
30
  isNativeXSLTSupported,
31
31
  installGlobal,
32
- default as default,
32
+ default,
33
33
  } from "./XSLTProcessor.js";
34
34
 
35
35
  // XPath module (for advanced users)
@@ -46,10 +46,18 @@ export {
46
46
  // XSLT engine (for advanced users)
47
47
  export { XsltEngine, XsltContext } from "./xslt/index.js";
48
48
 
49
+ // Output serializer (xsl:output, XSLT 1.0 section 16)
50
+ export {
51
+ serializeResult,
52
+ markRawText,
53
+ isRawText,
54
+ resolveOutputSettings,
55
+ } from "./xslt/serializer.js";
56
+
49
57
  /**
50
58
  * Version information
51
59
  */
52
- export const VERSION = "1.0.0";
60
+ export const VERSION = "1.1.1";
53
61
 
54
62
  /**
55
63
  * Check if we're running in a browser environment
@@ -61,6 +69,4 @@ export const isBrowser =
61
69
  * Check if we're running in Node.js
62
70
  */
63
71
  export const isNode =
64
- typeof process !== "undefined" &&
65
- process.versions != null &&
66
- process.versions.node != null;
72
+ typeof process !== "undefined" && process.versions?.node != null;
@@ -51,12 +51,29 @@ const FORBIDDEN_VARIABLE_NAMES = Object.freeze([
51
51
  * XPath evaluation context
52
52
  */
53
53
  export class XPathContext {
54
- constructor(node, position = 1, size = 1, variables = {}, namespaces = {}) {
54
+ /**
55
+ * @param {Node} node - The context node
56
+ * @param {number} [position] - The context position (1-based)
57
+ * @param {number} [size] - The context size
58
+ * @param {Object} [variables] - Variable bindings by name
59
+ * @param {Object} [namespaces] - Namespace bindings by prefix
60
+ * @param {*} [hostContext] - Opaque context of the host language (XSLT),
61
+ * carried through unchanged so host defined functions can reach it
62
+ */
63
+ constructor(
64
+ node,
65
+ position = 1,
66
+ size = 1,
67
+ variables = {},
68
+ namespaces = {},
69
+ hostContext = null,
70
+ ) {
55
71
  this.node = node;
56
72
  this.position = position;
57
73
  this.size = size;
58
74
  this.variables = variables;
59
75
  this.namespaces = namespaces;
76
+ this.hostContext = hostContext;
60
77
  }
61
78
 
62
79
  clone(overrides = {}) {
@@ -66,6 +83,7 @@ export class XPathContext {
66
83
  overrides.size ?? this.size,
67
84
  overrides.variables ?? this.variables,
68
85
  overrides.namespaces ?? this.namespaces,
86
+ overrides.hostContext ?? this.hostContext,
69
87
  );
70
88
  }
71
89
  }
@@ -75,7 +93,10 @@ export class XPathContext {
75
93
  */
76
94
  export class XPathEvaluator {
77
95
  constructor(options = {}) {
78
- this.functions = this.initCoreFunctions();
96
+ this.functions = Object.assign(
97
+ Object.create(null),
98
+ this.initCoreFunctions(),
99
+ );
79
100
  this.maxRecursionDepth =
80
101
  options.maxRecursionDepth ?? XPathLimits.MAX_RECURSION_DEPTH;
81
102
  this.maxResultSize = options.maxResultSize ?? XPathLimits.MAX_RESULT_SIZE;
@@ -535,7 +556,8 @@ export class XPathEvaluator {
535
556
  case "node":
536
557
  return true;
537
558
  case "text":
538
- return node.nodeType === 3;
559
+ // The XPath data model has no CDATA sections: they are text nodes
560
+ return node.nodeType === 3 || node.nodeType === 4;
539
561
  case "comment":
540
562
  return node.nodeType === 8;
541
563
  case "processing-instruction":
@@ -590,15 +612,34 @@ export class XPathEvaluator {
590
612
  return context.variables[name];
591
613
  }
592
614
 
615
+ /**
616
+ * Register additional functions, for example the XSLT function library.
617
+ *
618
+ * Existing names are overwritten, so a host language can also specialise a
619
+ * core function. Each function is called as `fn(args, context)` with the
620
+ * evaluator as `this`.
621
+ *
622
+ * @param {Object<string, Function>} functions - Functions by name
623
+ * @returns {XPathEvaluator} This evaluator, to allow chaining
624
+ *
625
+ * @example
626
+ * evaluator.registerFunctions({ 'my:double': (args, ctx) => 2 });
627
+ */
628
+ registerFunctions(functions) {
629
+ for (const [name, fn] of Object.entries(functions)) {
630
+ this.functions[name] = fn;
631
+ }
632
+ return this;
633
+ }
634
+
593
635
  evalFunctionCall(ast, context) {
594
636
  const name = ast.prefix ? `${ast.prefix}:${ast.name}` : ast.name;
595
- const fn = this.functions[name];
596
637
 
597
- if (!fn) {
638
+ if (!Object.hasOwn(this.functions, name)) {
598
639
  throw new Error(`Unknown function: ${name}`);
599
640
  }
600
641
 
601
- return fn.call(this, ast.args, context);
642
+ return this.functions[name].call(this, ast.args, context);
602
643
  }
603
644
 
604
645
  // Type conversion functions
@@ -659,7 +700,7 @@ export class XPathEvaluator {
659
700
  // Document Fragment
660
701
  let text = "";
661
702
  const walker = (n) => {
662
- if (n.nodeType === 3) {
703
+ if (n.nodeType === 3 || n.nodeType === 4) {
663
704
  text += n.nodeValue || "";
664
705
  } else if (n.childNodes) {
665
706
  for (const child of n.childNodes) {
@@ -0,0 +1,57 @@
1
+ /**
2
+ * XSLT element vocabulary.
3
+ *
4
+ * Single source of truth for the XSLT namespace URI and for the element names
5
+ * the engine can instantiate. `element-available()` reports against this table,
6
+ * so it stays in step with what {@link XsltEngine#processXsltElement} handles.
7
+ */
8
+
9
+ "use strict";
10
+
11
+ /** The XSLT 1.0 namespace URI. */
12
+ export const XSLT_NAMESPACE = "http://www.w3.org/1999/XSL/Transform";
13
+
14
+ /**
15
+ * Element names the engine dispatches, i.e. every element that
16
+ * `element-available()` must report as supported.
17
+ */
18
+ export const XSLT_ELEMENTS = Object.freeze([
19
+ "apply-imports",
20
+ "apply-templates",
21
+ "attribute",
22
+ "call-template",
23
+ "choose",
24
+ "comment",
25
+ "copy",
26
+ "copy-of",
27
+ "element",
28
+ "fallback",
29
+ "for-each",
30
+ "if",
31
+ "message",
32
+ "number",
33
+ "otherwise",
34
+ "param",
35
+ "processing-instruction",
36
+ "sort",
37
+ "text",
38
+ "value-of",
39
+ "variable",
40
+ "when",
41
+ "with-param",
42
+ ]);
43
+
44
+ const ELEMENT_SET = new Set(XSLT_ELEMENTS);
45
+
46
+ /**
47
+ * Check whether an XSLT element local name is supported by the engine.
48
+ *
49
+ * @param {string} localName - Element local name, e.g. `for-each`
50
+ * @returns {boolean} True when the engine instantiates the element
51
+ *
52
+ * @example
53
+ * isXsltElementAvailable('for-each'); // true
54
+ */
55
+ export function isXsltElementAvailable(localName) {
56
+ return ELEMENT_SET.has(localName);
57
+ }