@tradik/xslt-processor 1.0.3 → 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 (131) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +110 -520
  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 +131 -0
  7. package/bin/lib/output.js +114 -0
  8. package/bin/lib/paths.js +186 -0
  9. package/bin/lib/transform.js +206 -0
  10. package/bin/xslt.js +73 -168
  11. package/dist/xslt-processor.browser.js +9564 -1585
  12. package/dist/xslt-processor.browser.js.map +4 -4
  13. package/dist/xslt-processor.browser.min.js +13 -2
  14. package/dist/xslt-processor.browser.min.js.map +4 -4
  15. package/dist/xslt-processor.cjs +9572 -1586
  16. package/dist/xslt-processor.cjs.map +4 -4
  17. package/dist/xslt-processor.d.cts +658 -0
  18. package/dist/xslt-processor.d.ts +459 -12
  19. package/dist/xslt-processor.js +9546 -1582
  20. package/dist/xslt-processor.js.map +4 -4
  21. package/package.json +71 -20
  22. package/src/XSLTProcessor.js +494 -48
  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 +26 -8
  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 +518 -357
  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 +57 -0
  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 +184 -1736
  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 +233 -0
  87. package/src/xslt/forwardsCompatible.js +75 -0
  88. package/src/xslt/functions.js +270 -0
  89. package/src/xslt/index.js +38 -1
  90. package/src/xslt/keys.js +164 -0
  91. package/src/xslt/literalResult.js +223 -0
  92. package/src/xslt/matchScope.js +116 -0
  93. package/src/xslt/number.js +271 -0
  94. package/src/xslt/numberFormat.js +253 -0
  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 +211 -0
  102. package/src/xslt/serializer/baseWriter.js +390 -0
  103. package/src/xslt/serializer/chunks.js +120 -0
  104. package/src/xslt/serializer/constants.js +92 -0
  105. package/src/xslt/serializer/encoding.js +327 -0
  106. package/src/xslt/serializer/escape.js +135 -0
  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 +239 -0
  111. package/src/xslt/serializer/indent.js +51 -0
  112. package/src/xslt/serializer/namespaces.js +68 -0
  113. package/src/xslt/serializer/rawText.js +41 -0
  114. package/src/xslt/serializer/settings.js +179 -0
  115. package/src/xslt/serializer/textSerializer.js +77 -0
  116. package/src/xslt/serializer/xhtmlDocument.js +103 -0
  117. package/src/xslt/serializer/xmlSerializer.js +227 -0
  118. package/src/xslt/serializer.js +90 -0
  119. package/src/xslt/sort.js +151 -0
  120. package/src/xslt/spaceNameTests.js +115 -0
  121. package/src/xslt/stylesheetChecks.js +206 -0
  122. package/src/xslt/stylesheetNamespaces.js +266 -0
  123. package/src/xslt/templatePriority.js +45 -0
  124. package/src/xslt/uri.js +68 -0
  125. package/src/xslt/variables.js +152 -0
  126. package/src/xslt/whitespace.js +200 -0
  127. package/LICENSE +0 -29
  128. package/src/XSLTProcessor.test.js +0 -930
  129. package/src/xpath/evaluator.test.js +0 -1852
  130. package/src/xpath/tokenizer.test.js +0 -224
  131. package/src/xslt/engine.test.js +0 -3130
@@ -0,0 +1,186 @@
1
+ /**
2
+ * XSLT Processor CLI - Path Validation
3
+ *
4
+ * Every file the CLI reads or writes goes through this module first: the raw
5
+ * command line argument is resolved, canonicalized with `realpathSync`, checked
6
+ * to lie inside the trusted base directory and validated against the file
7
+ * system before any read or write is attempted. The base directory is the
8
+ * current working directory, or `XSLT_BASE_DIR` when that variable is set.
9
+ */
10
+
11
+ "use strict";
12
+
13
+ import { realpathSync, statSync } from "node:fs";
14
+ import { basename, dirname, join, resolve, sep } from "node:path";
15
+
16
+ /**
17
+ * Error raised for a command line path that cannot be used.
18
+ */
19
+ export class CliPathError extends Error {
20
+ /**
21
+ * @param {string} message - Human readable explanation
22
+ */
23
+ constructor(message) {
24
+ super(message);
25
+ this.name = "CliPathError";
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Resolve a raw path argument to an absolute path.
31
+ *
32
+ * @param {string} rawPath - Raw command line argument
33
+ * @param {string} label - Human readable role of the path, used in errors
34
+ * @returns {string} The absolute path
35
+ * @throws {CliPathError} When the argument is empty or contains a NUL byte
36
+ */
37
+ function toAbsolutePath(rawPath, label) {
38
+ if (typeof rawPath !== "string" || rawPath.length === 0) {
39
+ throw new CliPathError(`${label} path is missing`);
40
+ }
41
+
42
+ if (rawPath.includes("\0")) {
43
+ throw new CliPathError(`${label} path contains a NUL byte`);
44
+ }
45
+
46
+ return resolve(rawPath);
47
+ }
48
+
49
+ /**
50
+ * Canonicalize an existing path, turning a missing entry into a CLI error.
51
+ *
52
+ * @param {string} absolute - Absolute path that should exist
53
+ * @param {string} message - Error message when nothing exists there
54
+ * @returns {string} The canonical path with symbolic links resolved
55
+ * @throws {CliPathError} When the path does not exist
56
+ */
57
+ function canonicalize(absolute, message) {
58
+ try {
59
+ return realpathSync(absolute);
60
+ } catch {
61
+ throw new CliPathError(message);
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Ensure a canonical path lies inside the trusted base directory.
67
+ *
68
+ * This is the security boundary of the CLI: whatever the caller typed, the
69
+ * canonical path must be the base directory itself or a descendant of it.
70
+ *
71
+ * @param {string} canonical - Canonical absolute path
72
+ * @param {string} baseDir - Canonical absolute base directory
73
+ * @param {string} label - Human readable role of the path, used in errors
74
+ * @returns {string} The same path, now known to be inside baseDir
75
+ * @throws {CliPathError} When the path escapes the base directory
76
+ */
77
+ function assertInsideBase(canonical, baseDir, label) {
78
+ // A file system root ("/", "C:\\") already ends with the separator
79
+ const prefix = baseDir.endsWith(sep) ? baseDir : baseDir + sep;
80
+ const inside = canonical === baseDir || canonical.startsWith(prefix);
81
+
82
+ if (!inside) {
83
+ throw new CliPathError(
84
+ `${label} path is outside the allowed base directory (${baseDir}): ${canonical}. ` +
85
+ "Run the command from that directory or set XSLT_BASE_DIR.",
86
+ );
87
+ }
88
+
89
+ return canonical;
90
+ }
91
+
92
+ /**
93
+ * Resolve the trusted base directory all file arguments are confined to.
94
+ *
95
+ * Uses `XSLT_BASE_DIR` when set, otherwise the current working directory.
96
+ *
97
+ * @returns {string} The canonical absolute path of an existing directory
98
+ * @throws {CliPathError} When the configured directory does not exist or is not a directory
99
+ *
100
+ * @example
101
+ * const baseDir = resolveBaseDir(); // process.cwd() unless XSLT_BASE_DIR is set
102
+ */
103
+ export function resolveBaseDir() {
104
+ const configured = process.env.XSLT_BASE_DIR || process.cwd();
105
+ const absolute = toAbsolutePath(configured, "Base directory");
106
+ const canonical = canonicalize(
107
+ absolute,
108
+ `Base directory does not exist: ${absolute}`,
109
+ );
110
+
111
+ if (!statSync(canonical).isDirectory()) {
112
+ throw new CliPathError(`Base directory is not a directory: ${canonical}`);
113
+ }
114
+
115
+ return canonical;
116
+ }
117
+
118
+ /**
119
+ * Validate a path the CLI is going to read.
120
+ *
121
+ * @param {string} rawPath - Raw command line argument
122
+ * @param {string} label - Human readable role of the path, used in errors
123
+ * @param {string} baseDir - Canonical base directory from resolveBaseDir()
124
+ * @returns {string} The canonical path of an existing regular file inside baseDir
125
+ * @throws {CliPathError} When the path is malformed, missing, outside baseDir or not a file
126
+ *
127
+ * @example
128
+ * const xmlFile = resolveInputPath("data.xml", "XML", resolveBaseDir());
129
+ */
130
+ export function resolveInputPath(rawPath, label, baseDir) {
131
+ const absolute = toAbsolutePath(rawPath, label);
132
+ const canonical = assertInsideBase(
133
+ canonicalize(absolute, `File not found: ${absolute}`),
134
+ baseDir,
135
+ label,
136
+ );
137
+
138
+ if (!statSync(canonical).isFile()) {
139
+ throw new CliPathError(`${label} path is not a file: ${canonical}`);
140
+ }
141
+
142
+ return canonical;
143
+ }
144
+
145
+ /**
146
+ * Validate a path the CLI is going to write.
147
+ *
148
+ * The file itself may be missing, but its parent directory has to exist, lie
149
+ * inside the base directory, and an existing target has to be a regular file.
150
+ *
151
+ * @param {string} rawPath - Raw command line argument
152
+ * @param {string} baseDir - Canonical base directory from resolveBaseDir()
153
+ * @returns {string} The canonical path to write to
154
+ * @throws {CliPathError} When the path is malformed, outside baseDir or not writable as a file
155
+ *
156
+ * @example
157
+ * const target = resolveOutputPath("build/result.html", resolveBaseDir());
158
+ */
159
+ export function resolveOutputPath(rawPath, baseDir) {
160
+ const absolute = toAbsolutePath(rawPath, "Output");
161
+ const parent = assertInsideBase(
162
+ canonicalize(
163
+ dirname(absolute),
164
+ `Output directory does not exist: ${dirname(absolute)}`,
165
+ ),
166
+ baseDir,
167
+ "Output directory",
168
+ );
169
+
170
+ if (!statSync(parent).isDirectory()) {
171
+ throw new CliPathError(`Output directory is not a directory: ${parent}`);
172
+ }
173
+
174
+ const canonical = assertInsideBase(
175
+ join(parent, basename(absolute)),
176
+ baseDir,
177
+ "Output",
178
+ );
179
+ const targetStats = statSync(canonical, { throwIfNoEntry: false });
180
+
181
+ if (targetStats && !targetStats.isFile()) {
182
+ throw new CliPathError(`Output path is not a file: ${canonical}`);
183
+ }
184
+
185
+ return canonical;
186
+ }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * XSLT Processor CLI - Transformation Helpers
3
+ *
4
+ * DOM environment setup, document parsing and the transformation itself.
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.
8
+ */
9
+
10
+ "use strict";
11
+
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";
22
+
23
+ /**
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.
27
+ *
28
+ * The XSLT engine builds its result documents through the global `document`,
29
+ * so the globals have to be installed before transforming.
30
+ *
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
34
+ */
35
+ export async function createDomEnvironment(name = process.env.XSLT_DOM) {
36
+ return installDomGlobals(await loadDomEnvironment(name));
37
+ }
38
+
39
+ /**
40
+ * Parse an XML string, reporting parser errors as exceptions.
41
+ *
42
+ * @param {import("./dom.js").DomEnvironment} dom - DOM environment
43
+ * @param {string} content - XML source text
44
+ * @param {string} label - Human readable document label used in errors
45
+ * @returns {Document} Parsed document
46
+ */
47
+ export function parseDocument(dom, content, label) {
48
+ const doc = new dom.window.DOMParser().parseFromString(
49
+ content,
50
+ "application/xml",
51
+ );
52
+
53
+ const error = findParseError(doc);
54
+ if (error) {
55
+ throw new Error(`Error parsing ${label}: ${error.textContent}`);
56
+ }
57
+
58
+ return doc;
59
+ }
60
+
61
+ /** Output methods accepted by `--method`. */
62
+ export const OUTPUT_METHODS = Object.freeze(["xml", "html", "xhtml", "text"]);
63
+
64
+ /**
65
+ * Override the stylesheet xsl:output settings from the command line flags.
66
+ *
67
+ * @param {XSLTProcessor} processor - Processor with an imported stylesheet
68
+ * @param {object} values - Parsed command line option values
69
+ * @returns {object} The effective output settings
70
+ * @throws {Error} When `--method` is not one of {@link OUTPUT_METHODS}
71
+ */
72
+ export function applyOutputOverrides(processor, values) {
73
+ const settings = processor.engine.outputSettings;
74
+
75
+ if (values.format || values.indent) {
76
+ settings.indent = "yes";
77
+ }
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
+ }
84
+ settings.method = values.method;
85
+ }
86
+ if (values["no-declaration"]) {
87
+ settings.omitXmlDeclaration = "yes";
88
+ }
89
+
90
+ return settings;
91
+ }
92
+
93
+ /**
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.
153
+ *
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)
158
+ */
159
+ export function streamTransformation({
160
+ dom,
161
+ xmlContent,
162
+ xsltContent,
163
+ params,
164
+ values,
165
+ xsltFile,
166
+ baseDir,
167
+ warn,
168
+ }) {
169
+ const xmlDoc = parseDocument(dom, xmlContent, "XML");
170
+ const xsltDoc = parseDocument(dom, xsltContent, "XSLT");
171
+
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));
178
+
179
+ for (const [name, value] of Object.entries(params)) {
180
+ processor.setParameter(null, name, value);
181
+ }
182
+
183
+ applyOutputOverrides(processor, values);
184
+
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 });
191
+ }
192
+ return { chunks, encoding: processor.engine.outputSettings.encoding };
193
+ }
194
+
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 };
206
+ }
package/bin/xslt.js CHANGED
@@ -4,117 +4,54 @@
4
4
  * XSLT Processor CLI
5
5
  *
6
6
  * Command-line interface for transforming XML using XSLT stylesheets.
7
+ * The result is serialized according to the xsl:output element of the
8
+ * stylesheet (XSLT 1.0 section 16).
7
9
  */
8
10
 
9
- 'use strict';
11
+ "use strict";
12
+
13
+ import { readFile } from "node:fs/promises";
14
+ import { parseArgs } from "node:util";
15
+ import {
16
+ CLI_OPTIONS,
17
+ parseParameters,
18
+ printHelp,
19
+ printVersion,
20
+ } from "./lib/options.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";
28
+ import {
29
+ resolveBaseDir,
30
+ resolveInputPath,
31
+ resolveOutputPath,
32
+ } from "./lib/paths.js";
10
33
 
11
- import { readFile, writeFile } from 'node:fs/promises';
12
- import { parseArgs } from 'node:util';
13
- import { JSDOM } from 'jsdom';
14
- import { XSLTProcessor } from '../src/XSLTProcessor.js';
15
-
16
- const VERSION = '1.0.5';
17
-
18
- function printHelp() {
19
- console.log(`
20
- xslt-processor - Transform XML documents using XSLT stylesheets
21
-
22
- USAGE:
23
- xslt <xml-file> <xslt-file> [options]
24
-
25
- ARGUMENTS:
26
- <xml-file> Path to XML source document
27
- <xslt-file> Path to XSLT stylesheet
28
-
29
- OPTIONS:
30
- -o, --output <file> Write output to file instead of stdout
31
- -p, --param <n>=<v> Set XSLT parameter (can be used multiple times)
32
- -f, --format Format output with indentation
33
- -h, --help Show this help message
34
- -v, --version Show version number
35
-
36
- EXAMPLES:
37
- # Basic transformation
38
- xslt data.xml transform.xsl
39
-
40
- # Save output to file
41
- xslt data.xml transform.xsl -o result.html
42
-
43
- # With parameters
44
- xslt data.xml transform.xsl -p title="My Page" -p count=10
45
-
46
- # Multiple parameters with formatted output
47
- xslt data.xml transform.xsl -p lang=en -p debug=true -f -o output.html
48
- `);
49
- }
50
-
51
- function printVersion() {
52
- console.log(`xslt-processor v${VERSION}`);
53
- }
54
-
55
- function parseParameters(params) {
56
- const result = {};
57
-
58
- if (!params || !Array.isArray(params)) {
59
- return result;
60
- }
61
-
62
- for (const param of params) {
63
- const equalIndex = param.indexOf('=');
64
- if (equalIndex === -1) {
65
- console.error(`Warning: Invalid parameter format "${param}". Expected name=value`);
66
- continue;
67
- }
68
-
69
- const name = param.substring(0, equalIndex);
70
- const value = param.substring(equalIndex + 1);
71
- result[name] = value;
72
- }
73
-
74
- return result;
75
- }
76
-
77
- function formatXml(xml) {
78
- let formatted = '';
79
- let indent = 0;
80
- const lines = xml.replace(/>\s*</g, '>\n<').split('\n');
81
-
82
- for (const line of lines) {
83
- const trimmed = line.trim();
84
- if (!trimmed) continue;
85
-
86
- if (trimmed.startsWith('</')) {
87
- indent = Math.max(0, indent - 1);
88
- }
89
-
90
- formatted += ' '.repeat(indent) + trimmed + '\n';
91
-
92
- if (trimmed.startsWith('<') && !trimmed.startsWith('</') &&
93
- !trimmed.startsWith('<?') && !trimmed.startsWith('<!') &&
94
- !trimmed.endsWith('/>') && !trimmed.includes('</')) {
95
- indent++;
96
- }
97
- }
98
-
99
- return formatted;
100
- }
101
-
102
- async function main() {
103
- const options = {
104
- output: { type: 'string', short: 'o' },
105
- param: { type: 'string', short: 'p', multiple: true },
106
- format: { type: 'boolean', short: 'f', default: false },
107
- help: { type: 'boolean', short: 'h', default: false },
108
- version: { type: 'boolean', short: 'v', default: false }
109
- };
110
-
111
- let args;
34
+ /**
35
+ * Parse the command line, exiting on malformed input.
36
+ *
37
+ * @returns {object} The parseArgs result
38
+ */
39
+ function readArguments() {
112
40
  try {
113
- args = parseArgs({ options, allowPositionals: true });
41
+ return parseArgs({ options: CLI_OPTIONS, allowPositionals: true });
114
42
  } catch (err) {
115
43
  console.error(`Error: ${err.message}`);
116
44
  process.exit(1);
117
45
  }
46
+ }
47
+
48
+ /**
49
+ * CLI entry point.
50
+ *
51
+ * @returns {Promise<void>} Resolves once the CLI has finished
52
+ */
53
+ async function main() {
54
+ const args = readArguments();
118
55
 
119
56
  if (args.values.help) {
120
57
  printHelp();
@@ -129,80 +66,48 @@ async function main() {
129
66
  const [xmlPath, xsltPath] = args.positionals;
130
67
 
131
68
  if (!xmlPath || !xsltPath) {
132
- console.error('Error: Both XML and XSLT file paths are required');
69
+ console.error("Error: Both XML and XSLT file paths are required");
133
70
  console.error('Run "xslt --help" for usage information');
134
71
  process.exit(1);
135
72
  }
136
73
 
137
- // Setup JSDOM for DOM parsing
138
- const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
139
- contentType: 'text/html'
140
- });
141
- const { DOMParser, XMLSerializer } = dom.window;
142
-
143
74
  try {
144
- // Read input files
145
- const [xmlContent, xsltContent] = await Promise.all([
146
- readFile(xmlPath, 'utf-8'),
147
- readFile(xsltPath, 'utf-8')
75
+ const dom = await createDomEnvironment();
76
+ const baseDir = resolveBaseDir();
77
+ const xmlFile = resolveInputPath(xmlPath, "XML", baseDir);
78
+ const xsltFile = resolveInputPath(xsltPath, "XSLT", baseDir);
79
+ const outputFile = args.values.output
80
+ ? resolveOutputPath(args.values.output, baseDir)
81
+ : undefined;
82
+
83
+ const [xmlBytes, xsltBytes] = await Promise.all([
84
+ readFile(xmlFile),
85
+ readFile(xsltFile),
148
86
  ]);
149
87
 
150
- // Parse documents
151
- const parser = new DOMParser();
152
- const xmlDoc = parser.parseFromString(xmlContent, 'application/xml');
153
- const xsltDoc = parser.parseFromString(xsltContent, 'application/xml');
154
-
155
- // Check for parsing errors
156
- const xmlError = xmlDoc.querySelector('parsererror');
157
- if (xmlError) {
158
- console.error(`Error parsing XML: ${xmlError.textContent}`);
159
- process.exit(1);
160
- }
161
-
162
- const xsltError = xsltDoc.querySelector('parsererror');
163
- if (xsltError) {
164
- console.error(`Error parsing XSLT: ${xsltError.textContent}`);
165
- process.exit(1);
166
- }
167
-
168
- // Create processor
169
- const processor = new XSLTProcessor();
170
- processor.importStylesheet(xsltDoc);
171
-
172
- // Set parameters
173
- const params = parseParameters(args.values.param);
174
- for (const [name, value] of Object.entries(params)) {
175
- processor.setParameter(null, name, value);
176
- }
177
-
178
- // Transform
179
- const fragment = processor.transformToFragment(xmlDoc, dom.window.document);
180
-
181
- // Serialize result
182
- const serializer = new XMLSerializer();
183
- let output = serializer.serializeToString(fragment);
184
-
185
- // Format if requested
186
- if (args.values.format) {
187
- output = formatXml(output);
188
- }
189
-
190
- // Output result
191
- if (args.values.output) {
192
- await writeFile(args.values.output, output, 'utf-8');
193
- console.error(`Output written to ${args.values.output}`);
194
- } else {
195
- console.log(output);
196
- }
197
-
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({
93
+ dom,
94
+ xmlContent: decodeXml(xmlBytes, xmlFile),
95
+ xsltContent,
96
+ params: parseParameters(args.values.param),
97
+ values: args.values,
98
+ xsltFile,
99
+ baseDir,
100
+ });
101
+
102
+ await writeResult(chunks, outputFile, { encoding });
198
103
  } catch (err) {
199
- if (err.code === 'ENOENT') {
200
- console.error(`Error: File not found: ${err.path}`);
201
- } else {
202
- console.error(`Error: ${err.message}`);
203
- }
104
+ console.error(`Error: ${err.message}`);
204
105
  process.exit(1);
205
106
  }
206
107
  }
207
108
 
208
- 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
+ });