@tradik/xslt-processor 1.1.1 → 1.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.
Files changed (122) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +102 -757
  3. package/bin/lib/decode.js +15 -0
  4. package/bin/lib/dom.js +177 -0
  5. package/bin/lib/loaders.js +127 -0
  6. package/bin/lib/options.js +17 -0
  7. package/bin/lib/output.js +114 -0
  8. package/bin/lib/paths.js +3 -3
  9. package/bin/lib/transform.js +124 -33
  10. package/bin/xslt.js +26 -27
  11. package/dist/xslt-processor.browser.js +8784 -2720
  12. package/dist/xslt-processor.browser.js.map +4 -4
  13. package/dist/xslt-processor.browser.min.js +13 -6
  14. package/dist/xslt-processor.browser.min.js.map +4 -4
  15. package/dist/xslt-processor.cjs +8789 -2723
  16. package/dist/xslt-processor.cjs.map +4 -4
  17. package/dist/xslt-processor.d.cts +380 -21
  18. package/dist/xslt-processor.d.ts +380 -21
  19. package/dist/xslt-processor.js +8770 -2722
  20. package/dist/xslt-processor.js.map +4 -4
  21. package/package.json +51 -11
  22. package/src/XSLTProcessor.js +343 -66
  23. package/src/async/abort.js +63 -0
  24. package/src/async/documentUris.js +128 -0
  25. package/src/async/loaders.js +134 -0
  26. package/src/async/preload.js +159 -0
  27. package/src/async/processor.js +206 -0
  28. package/src/async/stream.js +125 -0
  29. package/src/bridge/engine.js +221 -0
  30. package/src/bridge/loader.js +78 -0
  31. package/src/bridge/results.js +75 -0
  32. package/src/bridge/version.js +63 -0
  33. package/src/index.js +16 -4
  34. package/src/io/decode.js +140 -0
  35. package/src/io/readSource.js +167 -0
  36. package/src/xpath/axes.js +562 -0
  37. package/src/xpath/documentOrder.js +270 -0
  38. package/src/xpath/evaluator.js +475 -355
  39. package/src/xpath/index.js +8 -2
  40. package/src/xpath/namespaceNodes.js +172 -0
  41. package/src/xpath/nodeSetFunctions.js +169 -0
  42. package/src/xpath/parser.js +30 -5
  43. package/src/xpath/strings.js +183 -0
  44. package/src/xpath/tokenizer.js +37 -23
  45. package/src/xslt/attributeSets.js +95 -0
  46. package/src/xslt/avt.js +103 -0
  47. package/src/xslt/computedNames.js +91 -0
  48. package/src/xslt/copying.js +212 -0
  49. package/src/xslt/declarationNames.js +80 -0
  50. package/src/xslt/domParsing.js +95 -0
  51. package/src/xslt/elements.js +1 -1
  52. package/src/xslt/engine/bindings.js +195 -0
  53. package/src/xslt/engine/context.js +105 -0
  54. package/src/xslt/engine/controlFlow.js +145 -0
  55. package/src/xslt/engine/copyInstructions.js +133 -0
  56. package/src/xslt/engine/declarations.js +233 -0
  57. package/src/xslt/engine/functionSupport.js +103 -0
  58. package/src/xslt/engine/methods.js +33 -0
  59. package/src/xslt/engine/nodeConstruction.js +187 -0
  60. package/src/xslt/engine/numbering.js +104 -0
  61. package/src/xslt/engine/outputDeclaration.js +77 -0
  62. package/src/xslt/engine/sequenceConstructor.js +228 -0
  63. package/src/xslt/engine/stylesheetLoading.js +208 -0
  64. package/src/xslt/engine/templateInvocation.js +253 -0
  65. package/src/xslt/engine/templateRules.js +243 -0
  66. package/src/xslt/engine/textInstructions.js +171 -0
  67. package/src/xslt/engine/topLevel.js +130 -0
  68. package/src/xslt/engine/transformation.js +263 -0
  69. package/src/xslt/engine/workStack.js +245 -0
  70. package/src/xslt/engine.js +176 -2020
  71. package/src/xslt/exslt/arguments.js +99 -0
  72. package/src/xslt/exslt/calendar.js +120 -0
  73. package/src/xslt/exslt/common.js +44 -0
  74. package/src/xslt/exslt/dateCalc.js +261 -0
  75. package/src/xslt/exslt/dateFormat.js +150 -0
  76. package/src/xslt/exslt/dateParse.js +265 -0
  77. package/src/xslt/exslt/dates.js +259 -0
  78. package/src/xslt/exslt/duration.js +207 -0
  79. package/src/xslt/exslt/dynamic.js +59 -0
  80. package/src/xslt/exslt/index.js +59 -0
  81. package/src/xslt/exslt/math.js +177 -0
  82. package/src/xslt/exslt/sets.js +96 -0
  83. package/src/xslt/exslt/stringOps.js +163 -0
  84. package/src/xslt/exslt/strings.js +147 -0
  85. package/src/xslt/exslt/uri.js +92 -0
  86. package/src/xslt/formatNumber.js +22 -9
  87. package/src/xslt/forwardsCompatible.js +75 -0
  88. package/src/xslt/functions.js +94 -15
  89. package/src/xslt/index.js +7 -1
  90. package/src/xslt/keys.js +51 -28
  91. package/src/xslt/literalResult.js +63 -7
  92. package/src/xslt/matchScope.js +116 -0
  93. package/src/xslt/number.js +171 -78
  94. package/src/xslt/numberFormat.js +124 -26
  95. package/src/xslt/outputNames.js +58 -0
  96. package/src/xslt/patternCompiler.js +175 -0
  97. package/src/xslt/patterns.js +324 -0
  98. package/src/xslt/qname.js +90 -0
  99. package/src/xslt/resultDocument.js +98 -0
  100. package/src/xslt/resultNamespaces.js +219 -0
  101. package/src/xslt/resultTree.js +143 -6
  102. package/src/xslt/serializer/baseWriter.js +173 -66
  103. package/src/xslt/serializer/chunks.js +120 -0
  104. package/src/xslt/serializer/constants.js +14 -0
  105. package/src/xslt/serializer/encoding.js +327 -0
  106. package/src/xslt/serializer/escape.js +49 -12
  107. package/src/xslt/serializer/frames.js +168 -0
  108. package/src/xslt/serializer/htmlDoctype.js +102 -0
  109. package/src/xslt/serializer/htmlEntities.js +77 -0
  110. package/src/xslt/serializer/htmlSerializer.js +123 -25
  111. package/src/xslt/serializer/settings.js +89 -13
  112. package/src/xslt/serializer/textSerializer.js +58 -10
  113. package/src/xslt/serializer/xhtmlDocument.js +103 -0
  114. package/src/xslt/serializer/xmlSerializer.js +113 -13
  115. package/src/xslt/serializer.js +50 -17
  116. package/src/xslt/sort.js +151 -0
  117. package/src/xslt/spaceNameTests.js +115 -0
  118. package/src/xslt/stylesheetChecks.js +206 -0
  119. package/src/xslt/stylesheetNamespaces.js +266 -0
  120. package/src/xslt/variables.js +152 -0
  121. package/src/xslt/whitespace.js +43 -27
  122. package/LICENSE +0 -29
@@ -2,39 +2,44 @@
2
2
  * XSLT Processor CLI - Transformation Helpers
3
3
  *
4
4
  * DOM environment setup, document parsing and the transformation itself.
5
- * Output is serialized through XSLTProcessor#transformToString so that the
6
- * xsl:output settings of the stylesheet are honored.
5
+ * Output is serialized in chunks (src/async/stream.js, the same serializer
6
+ * as XSLTProcessor#transformToString) so that the xsl:output settings of the
7
+ * stylesheet are honored and large results are written as they are produced.
7
8
  */
8
9
 
9
10
  "use strict";
10
11
 
11
- import { JSDOM } from "jsdom";
12
12
  import { XSLTProcessor } from "../../src/XSLTProcessor.js";
13
+ import { transformToChunks } from "../../src/async/stream.js";
14
+ import { findParseError } from "../../src/xslt/domParsing.js";
15
+ import { XSLT_VERSION_MODES, usesXslt3 } from "../../src/bridge/version.js";
16
+ import {
17
+ createDocumentLoader,
18
+ createStylesheetLoader,
19
+ toBaseUri,
20
+ } from "./loaders.js";
21
+ import { installDomGlobals, loadDomEnvironment } from "./dom.js";
13
22
 
14
23
  /**
15
- * Create a JSDOM based DOM environment and expose it globally.
24
+ * Create the DOM environment of the command line tool and expose it
25
+ * globally (see dom.js): jsdom, else @xmldom/xmldom; `XSLT_DOM=xmldom` or
26
+ * `XSLT_DOM=jsdom` picks one.
16
27
  *
17
28
  * The XSLT engine builds its result documents through the global `document`,
18
29
  * so the globals have to be installed before transforming.
19
30
  *
20
- * @returns {JSDOM} The created JSDOM instance
31
+ * @param {string} [name] - DOM implementation, default `$XSLT_DOM`
32
+ * @returns {Promise<import("./dom.js").DomEnvironment>} The environment
33
+ * @throws {Error} When no DOM implementation is installed
21
34
  */
22
- export function createDomEnvironment() {
23
- const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>", {
24
- contentType: "text/html",
25
- });
26
-
27
- globalThis.document = dom.window.document;
28
- globalThis.DOMParser = dom.window.DOMParser;
29
- globalThis.XMLSerializer = dom.window.XMLSerializer;
30
-
31
- return dom;
35
+ export async function createDomEnvironment(name = process.env.XSLT_DOM) {
36
+ return installDomGlobals(await loadDomEnvironment(name));
32
37
  }
33
38
 
34
39
  /**
35
40
  * Parse an XML string, reporting parser errors as exceptions.
36
41
  *
37
- * @param {JSDOM} dom - DOM environment
42
+ * @param {import("./dom.js").DomEnvironment} dom - DOM environment
38
43
  * @param {string} content - XML source text
39
44
  * @param {string} label - Human readable document label used in errors
40
45
  * @returns {Document} Parsed document
@@ -45,7 +50,7 @@ export function parseDocument(dom, content, label) {
45
50
  "application/xml",
46
51
  );
47
52
 
48
- const error = doc.querySelector("parsererror");
53
+ const error = findParseError(doc);
49
54
  if (error) {
50
55
  throw new Error(`Error parsing ${label}: ${error.textContent}`);
51
56
  }
@@ -53,20 +58,29 @@ export function parseDocument(dom, content, label) {
53
58
  return doc;
54
59
  }
55
60
 
61
+ /** Output methods accepted by `--method`. */
62
+ export const OUTPUT_METHODS = Object.freeze(["xml", "html", "xhtml", "text"]);
63
+
56
64
  /**
57
65
  * Override the stylesheet xsl:output settings from the command line flags.
58
66
  *
59
67
  * @param {XSLTProcessor} processor - Processor with an imported stylesheet
60
68
  * @param {object} values - Parsed command line option values
61
69
  * @returns {object} The effective output settings
70
+ * @throws {Error} When `--method` is not one of {@link OUTPUT_METHODS}
62
71
  */
63
72
  export function applyOutputOverrides(processor, values) {
64
- const settings = processor._engine.outputSettings;
73
+ const settings = processor.engine.outputSettings;
65
74
 
66
75
  if (values.format || values.indent) {
67
76
  settings.indent = "yes";
68
77
  }
69
78
  if (values.method) {
79
+ if (!OUTPUT_METHODS.includes(values.method)) {
80
+ throw new Error(
81
+ `Invalid --method "${values.method}": expected xml, html, xhtml or text`,
82
+ );
83
+ }
70
84
  settings.method = values.method;
71
85
  }
72
86
  if (values["no-declaration"]) {
@@ -77,28 +91,90 @@ export function applyOutputOverrides(processor, values) {
77
91
  }
78
92
 
79
93
  /**
80
- * Run a transformation and serialize its result.
94
+ * The `xsltVersion` option selected by `--xslt-version`.
95
+ *
96
+ * @param {object} values - Parsed command line option values
97
+ * @returns {"1.0"|"auto"} The mode, "1.0" by default
98
+ * @throws {Error} When the flag is neither 1.0 nor auto
99
+ */
100
+ export function xsltVersionOf(values) {
101
+ const mode = values["xslt-version"] ?? "1.0";
102
+ if (!XSLT_VERSION_MODES.includes(mode)) {
103
+ throw new Error(`Invalid --xslt-version "${mode}": expected 1.0 or auto`);
104
+ }
105
+ return mode;
106
+ }
107
+
108
+ /**
109
+ * Load @tradik/xslt3 (dynamic import) when `--xslt-version auto` is given
110
+ * and the stylesheet declares version 2.0 or more, so that the synchronous
111
+ * transformation can use it.
112
+ *
113
+ * @param {import("./dom.js").DomEnvironment} dom - DOM environment
114
+ * @param {string} xsltContent - XSLT stylesheet text
115
+ * @param {object} values - Parsed command line option values
116
+ * @returns {Promise<void>} Resolves once the engine is available
117
+ * @throws {Error} For an invalid flag, or when @tradik/xslt3 is missing
118
+ */
119
+ export async function prepareXsltVersion(dom, xsltContent, values) {
120
+ const mode = xsltVersionOf(values);
121
+ if (mode === "auto") {
122
+ const xsltDoc = parseDocument(dom, xsltContent, "XSLT");
123
+ if (usesXslt3(xsltDoc, mode)) await XSLTProcessor.preload();
124
+ }
125
+ }
126
+
127
+ /**
128
+ * @typedef {Object} TransformationInputs
129
+ * @property {import("./dom.js").DomEnvironment} dom - DOM environment
130
+ * @property {string} xmlContent - XML source text
131
+ * @property {string} xsltContent - XSLT stylesheet text
132
+ * @property {Record<string, string>} params - Stylesheet parameters
133
+ * @property {object} values - Parsed command line option values
134
+ * @property {string} [xsltFile] - Canonical stylesheet path; enables
135
+ * xsl:include, xsl:import and document() relative to it
136
+ * @property {string} [baseDir] - Canonical base directory every loaded file
137
+ * is confined to (required together with xsltFile)
138
+ * @property {(message: string) => void} [warn] - Warning sink for document()
139
+ */
140
+
141
+ /**
142
+ * @typedef {Object} StreamedTransformation
143
+ * @property {Iterator<string>} chunks - The serialized result in chunks of
144
+ * bounded size (see src/xslt/serializer/chunks.js), with character
145
+ * references for the characters the output encoding cannot represent
146
+ * @property {string} encoding - The effective `xsl:output` encoding, the
147
+ * encoding the result has to be written in
148
+ */
149
+
150
+ /**
151
+ * Run a transformation; its result is serialized as the chunks are read,
152
+ * so the output is written without ever being held as one string.
81
153
  *
82
- * @param {object} options - Transformation inputs
83
- * @param {JSDOM} options.dom - DOM environment
84
- * @param {string} options.xmlContent - XML source text
85
- * @param {string} options.xsltContent - XSLT stylesheet text
86
- * @param {Record<string, string>} options.params - Stylesheet parameters
87
- * @param {object} options.values - Parsed command line option values
88
- * @returns {string} The serialized transformation result
154
+ * @param {TransformationInputs} inputs - Transformation inputs
155
+ * @returns {StreamedTransformation} The result chunks and their encoding
156
+ * @throws {Error} "Transformation failed" when the transformation fails
157
+ * (the cause is reported on stderr, as XSLTProcessor does)
89
158
  */
90
- export function runTransformation({
159
+ export function streamTransformation({
91
160
  dom,
92
161
  xmlContent,
93
162
  xsltContent,
94
163
  params,
95
164
  values,
165
+ xsltFile,
166
+ baseDir,
167
+ warn,
96
168
  }) {
97
169
  const xmlDoc = parseDocument(dom, xmlContent, "XML");
98
170
  const xsltDoc = parseDocument(dom, xsltContent, "XSLT");
99
171
 
100
- const processor = new XSLTProcessor();
101
- processor.importStylesheet(xsltDoc);
172
+ const processor = new XSLTProcessor({ xsltVersion: xsltVersionOf(values) });
173
+ if (xsltFile) {
174
+ processor.setStylesheetLoader(createStylesheetLoader(baseDir));
175
+ processor.setDocumentLoader(createDocumentLoader(baseDir, warn));
176
+ }
177
+ processor.importStylesheet(xsltDoc, xsltFile && toBaseUri(xsltFile));
102
178
 
103
179
  for (const [name, value] of Object.entries(params)) {
104
180
  processor.setParameter(null, name, value);
@@ -106,10 +182,25 @@ export function runTransformation({
106
182
 
107
183
  applyOutputOverrides(processor, values);
108
184
 
109
- const output = processor.transformToString(xmlDoc);
110
- if (output === null) {
111
- throw new Error("Transformation failed");
185
+ let chunks;
186
+ try {
187
+ chunks = transformToChunks(processor.engine, xmlDoc);
188
+ } catch (error) {
189
+ console.error("XSLT transformation error:", error);
190
+ throw new Error("Transformation failed", { cause: error });
112
191
  }
192
+ return { chunks, encoding: processor.engine.outputSettings.encoding };
193
+ }
113
194
 
114
- return output;
195
+ /**
196
+ * Run a transformation and serialize its result to one string.
197
+ *
198
+ * @param {TransformationInputs} inputs - Transformation inputs
199
+ * @returns {{output: string, encoding: string}} The serialized result and
200
+ * its encoding
201
+ * @throws {Error} "Transformation failed" when the transformation fails
202
+ */
203
+ export function runTransformation(inputs) {
204
+ const { chunks, encoding } = streamTransformation(inputs);
205
+ return { output: [...chunks].join(""), encoding };
115
206
  }
package/bin/xslt.js CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  "use strict";
12
12
 
13
- import { readFile, writeFile } from "node:fs/promises";
13
+ import { readFile } from "node:fs/promises";
14
14
  import { parseArgs } from "node:util";
15
15
  import {
16
16
  CLI_OPTIONS,
@@ -18,7 +18,13 @@ import {
18
18
  printHelp,
19
19
  printVersion,
20
20
  } from "./lib/options.js";
21
- import { createDomEnvironment, runTransformation } from "./lib/transform.js";
21
+ import {
22
+ createDomEnvironment,
23
+ prepareXsltVersion,
24
+ streamTransformation,
25
+ } from "./lib/transform.js";
26
+ import { decodeXml } from "./lib/decode.js";
27
+ import { writeResult } from "./lib/output.js";
22
28
  import {
23
29
  resolveBaseDir,
24
30
  resolveInputPath,
@@ -39,22 +45,6 @@ function readArguments() {
39
45
  }
40
46
  }
41
47
 
42
- /**
43
- * Write the transformation result to a file or to stdout.
44
- *
45
- * @param {string} output - Serialized transformation result
46
- * @param {string|undefined} target - Validated absolute output path, if any
47
- * @returns {Promise<void>} Resolves once the result has been written
48
- */
49
- async function writeOutput(output, target) {
50
- if (target) {
51
- await writeFile(target, output, "utf-8");
52
- console.error(`Output written to ${target}`);
53
- return;
54
- }
55
- console.log(output);
56
- }
57
-
58
48
  /**
59
49
  * CLI entry point.
60
50
  *
@@ -81,9 +71,8 @@ async function main() {
81
71
  process.exit(1);
82
72
  }
83
73
 
84
- const dom = createDomEnvironment();
85
-
86
74
  try {
75
+ const dom = await createDomEnvironment();
87
76
  const baseDir = resolveBaseDir();
88
77
  const xmlFile = resolveInputPath(xmlPath, "XML", baseDir);
89
78
  const xsltFile = resolveInputPath(xsltPath, "XSLT", baseDir);
@@ -91,24 +80,34 @@ async function main() {
91
80
  ? resolveOutputPath(args.values.output, baseDir)
92
81
  : undefined;
93
82
 
94
- const [xmlContent, xsltContent] = await Promise.all([
95
- readFile(xmlFile, "utf-8"),
96
- readFile(xsltFile, "utf-8"),
83
+ const [xmlBytes, xsltBytes] = await Promise.all([
84
+ readFile(xmlFile),
85
+ readFile(xsltFile),
97
86
  ]);
98
87
 
99
- const output = runTransformation({
88
+ const xsltContent = decodeXml(xsltBytes, xsltFile);
89
+ // --xslt-version auto: load @tradik/xslt3 for a 2.0/3.0 stylesheet
90
+ await prepareXsltVersion(dom, xsltContent, args.values);
91
+
92
+ const { chunks, encoding } = streamTransformation({
100
93
  dom,
101
- xmlContent,
94
+ xmlContent: decodeXml(xmlBytes, xmlFile),
102
95
  xsltContent,
103
96
  params: parseParameters(args.values.param),
104
97
  values: args.values,
98
+ xsltFile,
99
+ baseDir,
105
100
  });
106
101
 
107
- await writeOutput(output, outputFile);
102
+ await writeResult(chunks, outputFile, { encoding });
108
103
  } catch (err) {
109
104
  console.error(`Error: ${err.message}`);
110
105
  process.exit(1);
111
106
  }
112
107
  }
113
108
 
114
- main();
109
+ main().catch((err) => {
110
+ // main() reports expected failures itself; this catches anything unexpected.
111
+ console.error(`Error: ${err?.message ?? err}`);
112
+ process.exit(1);
113
+ });