@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,15 @@
1
+ /**
2
+ * XSLT Processor CLI - Character Encoding Detection
3
+ *
4
+ * Thin wrapper: the decoding core (XML 1.0 Appendix F: byte order mark, then
5
+ * the XML declaration, then UTF-8) lives in src/io/decode.js, shared with the
6
+ * asynchronous API.
7
+ */
8
+
9
+ "use strict";
10
+
11
+ export {
12
+ EncodingError,
13
+ decodeXml,
14
+ detectEncoding,
15
+ } from "../../src/io/decode.js";
package/bin/lib/dom.js ADDED
@@ -0,0 +1,177 @@
1
+ /**
2
+ * XSLT Processor CLI - DOM implementations
3
+ *
4
+ * The library has no runtime dependencies: in Node.js the host brings a DOM.
5
+ * The command line tool (and the test suites) can run on two of them, both
6
+ * optional peer dependencies:
7
+ *
8
+ * - `jsdom`: the complete one, with HTML documents (the default);
9
+ * - `@xmldom/xmldom`: a small XML-only DOM, faster to load; it is used when
10
+ * jsdom is not installed, or when chosen with `XSLT_DOM=xmldom`.
11
+ *
12
+ * Both are exposed through the same shape as a JSDOM instance
13
+ * (`{ window: { document, DOMParser, XMLSerializer } }`), so callers never
14
+ * need to know which one they got.
15
+ *
16
+ * @module bin/lib/dom
17
+ */
18
+
19
+ "use strict";
20
+
21
+ /** Names of the supported DOM implementations, in order of preference. */
22
+ export const DOM_IMPLEMENTATIONS = Object.freeze(["jsdom", "xmldom"]);
23
+
24
+ /**
25
+ * Loader of each supported DOM implementation. The specifiers are literal so
26
+ * that bundlers (the standalone binaries, see scripts/binaries/bundle.mjs)
27
+ * can follow them.
28
+ */
29
+ const IMPORTERS = Object.freeze({
30
+ jsdom: () => import("jsdom"),
31
+ xmldom: () => import("@xmldom/xmldom"),
32
+ });
33
+
34
+ /** Message shown when no DOM implementation is installed. */
35
+ export const DOM_MISSING_MESSAGE =
36
+ "The xslt command needs a DOM implementation, an optional peer dependency " +
37
+ "that is not installed. Install jsdom or @xmldom/xmldom (lighter, XML only) " +
38
+ "next to this package: npm install -g jsdom (global install) " +
39
+ "or npm install jsdom (project install).";
40
+
41
+ /**
42
+ * @typedef {Object} DomEnvironment
43
+ * @property {string} name - "jsdom" or "xmldom"
44
+ * @property {{document: Document, DOMParser: Function, XMLSerializer: Function}} window -
45
+ * The DOM classes, and the document results are created with
46
+ */
47
+
48
+ /**
49
+ * Whether an import failed because the package is not installed.
50
+ *
51
+ * @param {*} error - The rejection of a dynamic import
52
+ * @returns {boolean} True for a missing module
53
+ */
54
+ function isMissingModule(error) {
55
+ return (
56
+ error?.code === "ERR_MODULE_NOT_FOUND" || error?.code === "MODULE_NOT_FOUND"
57
+ );
58
+ }
59
+
60
+ /** The xmldom warning about well-formed markup. */
61
+ const BENIGN_WARNING = /^Unicode replacement character/;
62
+
63
+ /**
64
+ * A DOMParser class for xmldom that reports malformed XML as browsers do:
65
+ * a document holding a `parsererror` element instead of a thrown ParseError
66
+ * or a message on the console. xmldom only warns about some malformed
67
+ * markup (unquoted attribute values, attributes without a space between
68
+ * them), which browsers reject too, so warnings count as errors, except
69
+ * the one about U+FFFD characters (well formed). Lenient `text/html`
70
+ * parsing never fails.
71
+ *
72
+ * @param {object} xmldom - The @xmldom/xmldom module
73
+ * @returns {Function} The DOMParser class
74
+ */
75
+ function createXmldomParser(xmldom) {
76
+ return class DOMParser {
77
+ /**
78
+ * @param {string} text - Markup
79
+ * @param {string} type - MIME type ("application/xml", "text/html", ...)
80
+ * @returns {Document} The parsed document, or a parsererror document
81
+ */
82
+ parseFromString(text, type) {
83
+ let failure = null;
84
+ const onError = (level, message) => {
85
+ if (type === "text/html" || BENIGN_WARNING.test(message)) return;
86
+ failure ??= message;
87
+ throw new Error(message);
88
+ };
89
+ try {
90
+ return new xmldom.DOMParser({ onError }).parseFromString(text, type);
91
+ } catch (error) {
92
+ const doc = new xmldom.DOMImplementation().createDocument(
93
+ null,
94
+ "parsererror",
95
+ null,
96
+ );
97
+ doc.documentElement.appendChild(
98
+ doc.createTextNode(failure ?? error.message),
99
+ );
100
+ return doc;
101
+ }
102
+ }
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Build the environment of one DOM implementation from its module.
108
+ *
109
+ * @param {string} name - "jsdom" or "xmldom"
110
+ * @param {object} module - The imported package
111
+ * @returns {DomEnvironment} The environment
112
+ */
113
+ function environmentOf(name, module) {
114
+ if (name === "jsdom") {
115
+ const dom = new module.JSDOM("<!DOCTYPE html><html><body></body></html>", {
116
+ contentType: "text/html",
117
+ });
118
+ dom.name = "jsdom";
119
+ return dom;
120
+ }
121
+ return {
122
+ name,
123
+ window: {
124
+ document: new module.DOMImplementation().createDocument(null, null),
125
+ DOMParser: createXmldomParser(module),
126
+ XMLSerializer: module.XMLSerializer,
127
+ },
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Load a DOM implementation: the one named, else the first installed of
133
+ * {@link DOM_IMPLEMENTATIONS}.
134
+ *
135
+ * @param {string} [name] - "jsdom" or "xmldom"; empty for the first installed
136
+ * @param {Record<string, () => Promise<object>>} [importers] - Module loaders
137
+ * by implementation name (for tests)
138
+ * @returns {Promise<DomEnvironment>} The environment (not installed globally)
139
+ * @throws {Error} When the name is unknown or no implementation is installed
140
+ *
141
+ * @example
142
+ * const dom = await loadDomEnvironment("xmldom");
143
+ * new dom.window.DOMParser().parseFromString("<a/>", "application/xml");
144
+ */
145
+ export async function loadDomEnvironment(name, importers = IMPORTERS) {
146
+ if (name && !DOM_IMPLEMENTATIONS.includes(name)) {
147
+ throw new Error(
148
+ `Unknown DOM implementation "${name}": expected ${DOM_IMPLEMENTATIONS.join(" or ")}`,
149
+ );
150
+ }
151
+ const candidates = name ? [name] : DOM_IMPLEMENTATIONS;
152
+ let missing = null;
153
+ for (const candidate of candidates) {
154
+ try {
155
+ return environmentOf(candidate, await importers[candidate]());
156
+ } catch (error) {
157
+ if (!isMissingModule(error)) throw error;
158
+ missing ??= error;
159
+ }
160
+ }
161
+ throw new Error(DOM_MISSING_MESSAGE, { cause: missing });
162
+ }
163
+
164
+ /**
165
+ * Expose a DOM environment globally: the XSLT engine builds its results in
166
+ * the global `document` and parses imported stylesheets given as strings
167
+ * with the global `DOMParser`.
168
+ *
169
+ * @param {DomEnvironment} dom - The environment
170
+ * @returns {DomEnvironment} The same environment
171
+ */
172
+ export function installDomGlobals(dom) {
173
+ globalThis.document = dom.window.document;
174
+ globalThis.DOMParser = dom.window.DOMParser;
175
+ globalThis.XMLSerializer = dom.window.XMLSerializer;
176
+ return dom;
177
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * XSLT Processor CLI - Stylesheet and Document Loaders
3
+ *
4
+ * Resolves the URIs of `xsl:include`, `xsl:import` and `document()` against
5
+ * the stylesheet location and reads the referenced local files. Every URI is
6
+ * turned into a file system path and passed through resolveInputPath, which
7
+ * canonicalizes it with realpathSync and confines it to the base directory
8
+ * BEFORE the file is read. Only relative references, absolute paths and
9
+ * `file:` URLs are supported; network schemes are refused.
10
+ */
11
+
12
+ "use strict";
13
+
14
+ import { readFileSync } from "node:fs";
15
+ import { URL, fileURLToPath, pathToFileURL } from "node:url";
16
+ import { decodeXml } from "./decode.js";
17
+ import { resolveInputPath } from "./paths.js";
18
+
19
+ /**
20
+ * Error raised for a URI the CLI refuses to load.
21
+ */
22
+ export class CliUriError extends Error {
23
+ /**
24
+ * @param {string} message - Human readable explanation
25
+ */
26
+ constructor(message) {
27
+ super(message);
28
+ this.name = "CliUriError";
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Convert an absolute file path into the base URI handed to the engine.
34
+ *
35
+ * @param {string} path - Absolute file path
36
+ * @returns {string} The `file:` URL of the path
37
+ *
38
+ * @example
39
+ * toBaseUri('/work/main.xsl'); // 'file:///work/main.xsl'
40
+ */
41
+ export function toBaseUri(path) {
42
+ return pathToFileURL(path).href;
43
+ }
44
+
45
+ /**
46
+ * Turn a (possibly relative) URI into a local file path.
47
+ *
48
+ * @param {string} uri - URI as resolved by the engine
49
+ * @param {string} [baseUri] - Base URI of the referencing stylesheet
50
+ * @param {string} baseDir - Canonical base directory, used when no base URI is known
51
+ * @returns {string} Absolute file system path, not yet validated
52
+ * @throws {CliUriError} When the URI is malformed or uses a non-file scheme
53
+ */
54
+ export function uriToPath(uri, baseUri, baseDir) {
55
+ let url;
56
+
57
+ try {
58
+ url = new URL(uri, baseUri || toBaseUri(`${baseDir}/`));
59
+ } catch {
60
+ throw new CliUriError(`Invalid URI: ${uri}`);
61
+ }
62
+
63
+ if (url.protocol !== "file:") {
64
+ throw new CliUriError(
65
+ `Only local files can be loaded, refusing ${url.protocol} URI: ${uri}`,
66
+ );
67
+ }
68
+
69
+ return fileURLToPath(url);
70
+ }
71
+
72
+ /**
73
+ * Validate and read a referenced XML file.
74
+ *
75
+ * @param {string} uri - URI as resolved by the engine
76
+ * @param {string} [baseUri] - Base URI of the referencing stylesheet
77
+ * @param {string} baseDir - Canonical base directory
78
+ * @param {string} label - Human readable role of the file, used in errors
79
+ * @returns {string} The decoded file content
80
+ * @throws {Error} When the URI is refused or the file is missing, outside baseDir or undecodable
81
+ */
82
+ function readReferencedFile(uri, baseUri, baseDir, label) {
83
+ const path = resolveInputPath(
84
+ uriToPath(uri, baseUri, baseDir),
85
+ label,
86
+ baseDir,
87
+ );
88
+ return decodeXml(readFileSync(path), path);
89
+ }
90
+
91
+ /**
92
+ * Create the loader used for `xsl:include` and `xsl:import`.
93
+ *
94
+ * @param {string} baseDir - Canonical base directory
95
+ * @returns {(href: string, baseUri?: string) => string} Stylesheet loader
96
+ *
97
+ * @example
98
+ * processor.setStylesheetLoader(createStylesheetLoader(baseDir));
99
+ */
100
+ export function createStylesheetLoader(baseDir) {
101
+ return (href, baseUri) =>
102
+ readReferencedFile(href, baseUri, baseDir, "Stylesheet");
103
+ }
104
+
105
+ /**
106
+ * Create the loader used for the XSLT `document()` function.
107
+ *
108
+ * A document that cannot be loaded yields an empty node-set (XSLT 1.0
109
+ * section 12.1 allows recovering this way) and a one line warning.
110
+ *
111
+ * @param {string} baseDir - Canonical base directory
112
+ * @param {(message: string) => void} [warn] - Warning sink, stderr by default
113
+ * @returns {(uri: string, baseUri?: string) => (string|null)} Document loader
114
+ *
115
+ * @example
116
+ * processor.setDocumentLoader(createDocumentLoader(baseDir));
117
+ */
118
+ export function createDocumentLoader(baseDir, warn = console.error) {
119
+ return (uri, baseUri) => {
120
+ try {
121
+ return readReferencedFile(uri, baseUri, baseDir, "Document");
122
+ } catch (error) {
123
+ warn(`Warning: document('${uri}') is empty: ${error.message}`);
124
+ return null;
125
+ }
126
+ };
127
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * XSLT Processor CLI - Option Handling
3
+ *
4
+ * Command line option definitions, help/version banners and parameter parsing.
5
+ */
6
+
7
+ "use strict";
8
+
9
+ import { createRequire } from "node:module";
10
+
11
+ const require = createRequire(import.meta.url);
12
+
13
+ /**
14
+ * Version of the CLI, read from the package manifest.
15
+ * @type {string}
16
+ */
17
+ export const VERSION = require("../../package.json").version;
18
+
19
+ /**
20
+ * parseArgs option definitions.
21
+ * @type {object}
22
+ */
23
+ export const CLI_OPTIONS = {
24
+ output: { type: "string", short: "o" },
25
+ param: { type: "string", short: "p", multiple: true },
26
+ format: { type: "boolean", short: "f", default: false },
27
+ indent: { type: "boolean", default: false },
28
+ method: { type: "string" },
29
+ "no-declaration": { type: "boolean", default: false },
30
+ "xslt-version": { type: "string", default: "1.0" },
31
+ help: { type: "boolean", short: "h", default: false },
32
+ version: { type: "boolean", short: "v", default: false },
33
+ };
34
+
35
+ /**
36
+ * Print the usage banner.
37
+ * @returns {void}
38
+ */
39
+ export function printHelp() {
40
+ console.log(`
41
+ xslt-processor - Transform XML documents using XSLT stylesheets
42
+
43
+ USAGE:
44
+ xslt <xml-file> <xslt-file> [options]
45
+
46
+ ARGUMENTS:
47
+ <xml-file> Path to XML source document
48
+ <xslt-file> Path to XSLT stylesheet
49
+
50
+ OPTIONS:
51
+ -o, --output <file> Write output to file instead of stdout
52
+ -p, --param <n>=<v> Set XSLT parameter (can be used multiple times)
53
+ -f, --format Format output with indentation (same as --indent)
54
+ --indent Override xsl:output to indent="yes"
55
+ --method <m> Override xsl:output method (xml|html|xhtml|text)
56
+ --no-declaration Override xsl:output to omit the XML declaration
57
+ --xslt-version <v> 1.0 (default): XSLT 1.0 engine, a version="2.0"
58
+ stylesheet runs in forwards-compatible mode;
59
+ auto: XSLT 2.0/3.0 stylesheets run with
60
+ @tradik/xslt3 (npm install @tradik/xslt3)
61
+ -h, --help Show this help message
62
+ -v, --version Show version number
63
+
64
+ The output is serialized according to the xsl:output element of the
65
+ stylesheet; the options above override individual xsl:output settings.
66
+
67
+ All file arguments must live inside the current working directory, or inside
68
+ the directory named by the XSLT_BASE_DIR environment variable when it is set.
69
+ xsl:include, xsl:import and document() resolve relative to the stylesheet and
70
+ are confined to the same directory; only local files are loaded. Input files
71
+ are decoded by their byte order mark or XML encoding declaration (UTF-8 by
72
+ default), e.g. UTF-16, ISO-8859-1 or windows-1252. The result is written in
73
+ the encoding named by xsl:output (UTF-8 by default); characters that encoding
74
+ cannot represent are written as character references (&#8364;).
75
+
76
+ The DOM implementation is jsdom, or @xmldom/xmldom when jsdom is not
77
+ installed; set XSLT_DOM=xmldom or XSLT_DOM=jsdom to choose one.
78
+
79
+ EXAMPLES:
80
+ # Basic transformation
81
+ xslt data.xml transform.xsl
82
+
83
+ # Save output to file
84
+ xslt data.xml transform.xsl -o result.html
85
+
86
+ # With parameters
87
+ xslt data.xml transform.xsl -p title="My Page" -p count=10
88
+
89
+ # Multiple parameters with formatted output
90
+ xslt data.xml transform.xsl -p lang=en -p debug=true -f -o output.html
91
+
92
+ # An XSLT 2.0/3.0 stylesheet (needs @tradik/xslt3)
93
+ xslt data.xml grouping.xsl --xslt-version auto
94
+ `);
95
+ }
96
+
97
+ /**
98
+ * Print the version banner.
99
+ * @returns {void}
100
+ */
101
+ export function printVersion() {
102
+ console.log(`xslt-processor v${VERSION}`);
103
+ }
104
+
105
+ /**
106
+ * Parse `name=value` parameter arguments.
107
+ *
108
+ * @param {string[]|undefined} params - Raw `--param` values
109
+ * @returns {Record<string, string>} Parsed parameters
110
+ */
111
+ export function parseParameters(params) {
112
+ const result = {};
113
+
114
+ if (!params || !Array.isArray(params)) {
115
+ return result;
116
+ }
117
+
118
+ for (const param of params) {
119
+ const equalIndex = param.indexOf("=");
120
+ if (equalIndex === -1) {
121
+ console.error(
122
+ `Warning: Invalid parameter format "${param}". Expected name=value`,
123
+ );
124
+ continue;
125
+ }
126
+
127
+ result[param.substring(0, equalIndex)] = param.substring(equalIndex + 1);
128
+ }
129
+
130
+ return result;
131
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * XSLT Processor CLI - Result Output
3
+ *
4
+ * Writes the serialized result byte for byte, chunk by chunk as it is
5
+ * serialized (waiting for stdout to drain when its buffer is full): `-o file`
6
+ * and redirected stdout receive exactly what the stylesheet produced, encoded
7
+ * in the `xsl:output` encoding (UTF-8 by default; UTF-16 with a byte order mark; ISO-8859-1,
8
+ * US-ASCII, windows-125x and the other single-byte encodings byte per
9
+ * character). Encodings without an encoder (Shift_JIS, EUC-KR, ...) are
10
+ * written as UTF-8 with a warning. Only an interactive terminal gets a
11
+ * trailing newline appended, so the shell prompt starts on its own line.
12
+ */
13
+
14
+ "use strict";
15
+
16
+ import { open } from "node:fs/promises";
17
+ import {
18
+ createOutputEncoder,
19
+ getOutputEncoding,
20
+ } from "../../src/xslt/serializer/encoding.js";
21
+
22
+ /**
23
+ * Compute the text written to stdout.
24
+ *
25
+ * @param {string} output - Serialized transformation result
26
+ * @param {boolean} isTty - Whether stdout is an interactive terminal
27
+ * @returns {string} The output, with a newline appended only for a terminal
28
+ * when it does not already end with one
29
+ *
30
+ * @example
31
+ * forStdout('<a/>', false); // '<a/>'
32
+ * forStdout('<a/>', true); // '<a/>\n'
33
+ */
34
+ export function forStdout(output, isTty) {
35
+ return isTty && !output.endsWith("\n") ? `${output}\n` : output;
36
+ }
37
+
38
+ /**
39
+ * Write bytes to a stream, waiting for `drain` when its buffer is full
40
+ * (backpressure), so a large result is never queued whole in memory.
41
+ *
42
+ * @param {NodeJS.WritableStream} stream - Destination
43
+ * @param {Uint8Array} bytes - Bytes to write
44
+ * @returns {Promise<void>|undefined} Pending until the stream drains, if it must
45
+ */
46
+ function writeBytes(stream, bytes) {
47
+ if (stream.write(bytes) === false) {
48
+ return new Promise((resolve) => stream.once("drain", resolve));
49
+ }
50
+ return undefined;
51
+ }
52
+
53
+ /**
54
+ * Encode chunks of serialized output and hand the bytes to a sink. At least
55
+ * one (possibly empty) chunk is encoded, so an empty UTF-16 result still
56
+ * gets its byte order mark.
57
+ *
58
+ * @param {Iterable<string>} chunks - Serialized output
59
+ * @param {(text: string) => Uint8Array} encode - Output encoder
60
+ * @param {(bytes: Uint8Array) => (Promise<unknown>|unknown)} sink - Writer
61
+ * @returns {Promise<string>} The last chunk ("" when none)
62
+ */
63
+ async function pump(chunks, encode, sink) {
64
+ let last = null;
65
+ for (const chunk of chunks) {
66
+ await sink(encode(chunk));
67
+ last = chunk;
68
+ }
69
+ if (last === null) {
70
+ last = "";
71
+ await sink(encode(last));
72
+ }
73
+ return last;
74
+ }
75
+
76
+ /**
77
+ * Write the transformation result to a file or to stdout, chunk by chunk.
78
+ *
79
+ * @param {string|Iterable<string>} output - Serialized transformation
80
+ * result, whole or in chunks (see streamTransformation)
81
+ * @param {string|undefined} target - Validated absolute output path, if any
82
+ * @param {object} [options] - Encoding and output streams
83
+ * @param {string} [options.encoding] - The `xsl:output` encoding, UTF-8 by default
84
+ * @param {NodeJS.WriteStream} [options.stdout] - Result stream
85
+ * @param {NodeJS.WriteStream} [options.stderr] - Status message stream
86
+ * @returns {Promise<void>} Resolves once the result has been written
87
+ */
88
+ export async function writeResult(
89
+ output,
90
+ target,
91
+ { encoding = "UTF-8", stdout = process.stdout, stderr = process.stderr } = {},
92
+ ) {
93
+ if (!getOutputEncoding(encoding).isExact) {
94
+ stderr.write(
95
+ `Warning: cannot write the ${encoding} encoding, writing UTF-8 instead\n`,
96
+ );
97
+ }
98
+ const chunks = typeof output === "string" ? [output] : output;
99
+ const encode = createOutputEncoder(encoding);
100
+ if (target) {
101
+ const file = await open(target, "w");
102
+ try {
103
+ await pump(chunks, encode, (bytes) => file.write(bytes));
104
+ } finally {
105
+ await file.close();
106
+ }
107
+ stderr.write(`Output written to ${target}\n`);
108
+ return;
109
+ }
110
+ const last = await pump(chunks, encode, (bytes) => writeBytes(stdout, bytes));
111
+ if (forStdout(last, Boolean(stdout.isTTY)) !== last) {
112
+ await writeBytes(stdout, encode("\n"));
113
+ }
114
+ }