@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.
- package/LICENSE.md +1 -1
- package/README.md +110 -520
- package/bin/lib/decode.js +15 -0
- package/bin/lib/dom.js +177 -0
- package/bin/lib/loaders.js +127 -0
- package/bin/lib/options.js +131 -0
- package/bin/lib/output.js +114 -0
- package/bin/lib/paths.js +186 -0
- package/bin/lib/transform.js +206 -0
- package/bin/xslt.js +73 -168
- package/dist/xslt-processor.browser.js +9564 -1585
- package/dist/xslt-processor.browser.js.map +4 -4
- package/dist/xslt-processor.browser.min.js +13 -2
- package/dist/xslt-processor.browser.min.js.map +4 -4
- package/dist/xslt-processor.cjs +9572 -1586
- package/dist/xslt-processor.cjs.map +4 -4
- package/dist/xslt-processor.d.cts +658 -0
- package/dist/xslt-processor.d.ts +459 -12
- package/dist/xslt-processor.js +9546 -1582
- package/dist/xslt-processor.js.map +4 -4
- package/package.json +71 -20
- package/src/XSLTProcessor.js +494 -48
- package/src/async/abort.js +63 -0
- package/src/async/documentUris.js +128 -0
- package/src/async/loaders.js +134 -0
- package/src/async/preload.js +159 -0
- package/src/async/processor.js +206 -0
- package/src/async/stream.js +125 -0
- package/src/bridge/engine.js +221 -0
- package/src/bridge/loader.js +78 -0
- package/src/bridge/results.js +75 -0
- package/src/bridge/version.js +63 -0
- package/src/index.js +26 -8
- package/src/io/decode.js +140 -0
- package/src/io/readSource.js +167 -0
- package/src/xpath/axes.js +562 -0
- package/src/xpath/documentOrder.js +270 -0
- package/src/xpath/evaluator.js +518 -357
- package/src/xpath/index.js +8 -2
- package/src/xpath/namespaceNodes.js +172 -0
- package/src/xpath/nodeSetFunctions.js +169 -0
- package/src/xpath/parser.js +30 -5
- package/src/xpath/strings.js +183 -0
- package/src/xpath/tokenizer.js +37 -23
- package/src/xslt/attributeSets.js +95 -0
- package/src/xslt/avt.js +103 -0
- package/src/xslt/computedNames.js +91 -0
- package/src/xslt/copying.js +212 -0
- package/src/xslt/declarationNames.js +80 -0
- package/src/xslt/domParsing.js +95 -0
- package/src/xslt/elements.js +57 -0
- package/src/xslt/engine/bindings.js +195 -0
- package/src/xslt/engine/context.js +105 -0
- package/src/xslt/engine/controlFlow.js +145 -0
- package/src/xslt/engine/copyInstructions.js +133 -0
- package/src/xslt/engine/declarations.js +233 -0
- package/src/xslt/engine/functionSupport.js +103 -0
- package/src/xslt/engine/methods.js +33 -0
- package/src/xslt/engine/nodeConstruction.js +187 -0
- package/src/xslt/engine/numbering.js +104 -0
- package/src/xslt/engine/outputDeclaration.js +77 -0
- package/src/xslt/engine/sequenceConstructor.js +228 -0
- package/src/xslt/engine/stylesheetLoading.js +208 -0
- package/src/xslt/engine/templateInvocation.js +253 -0
- package/src/xslt/engine/templateRules.js +243 -0
- package/src/xslt/engine/textInstructions.js +171 -0
- package/src/xslt/engine/topLevel.js +130 -0
- package/src/xslt/engine/transformation.js +263 -0
- package/src/xslt/engine/workStack.js +245 -0
- package/src/xslt/engine.js +184 -1736
- package/src/xslt/exslt/arguments.js +99 -0
- package/src/xslt/exslt/calendar.js +120 -0
- package/src/xslt/exslt/common.js +44 -0
- package/src/xslt/exslt/dateCalc.js +261 -0
- package/src/xslt/exslt/dateFormat.js +150 -0
- package/src/xslt/exslt/dateParse.js +265 -0
- package/src/xslt/exslt/dates.js +259 -0
- package/src/xslt/exslt/duration.js +207 -0
- package/src/xslt/exslt/dynamic.js +59 -0
- package/src/xslt/exslt/index.js +59 -0
- package/src/xslt/exslt/math.js +177 -0
- package/src/xslt/exslt/sets.js +96 -0
- package/src/xslt/exslt/stringOps.js +163 -0
- package/src/xslt/exslt/strings.js +147 -0
- package/src/xslt/exslt/uri.js +92 -0
- package/src/xslt/formatNumber.js +233 -0
- package/src/xslt/forwardsCompatible.js +75 -0
- package/src/xslt/functions.js +270 -0
- package/src/xslt/index.js +38 -1
- package/src/xslt/keys.js +164 -0
- package/src/xslt/literalResult.js +223 -0
- package/src/xslt/matchScope.js +116 -0
- package/src/xslt/number.js +271 -0
- package/src/xslt/numberFormat.js +253 -0
- package/src/xslt/outputNames.js +58 -0
- package/src/xslt/patternCompiler.js +175 -0
- package/src/xslt/patterns.js +324 -0
- package/src/xslt/qname.js +90 -0
- package/src/xslt/resultDocument.js +98 -0
- package/src/xslt/resultNamespaces.js +219 -0
- package/src/xslt/resultTree.js +211 -0
- package/src/xslt/serializer/baseWriter.js +390 -0
- package/src/xslt/serializer/chunks.js +120 -0
- package/src/xslt/serializer/constants.js +92 -0
- package/src/xslt/serializer/encoding.js +327 -0
- package/src/xslt/serializer/escape.js +135 -0
- package/src/xslt/serializer/frames.js +168 -0
- package/src/xslt/serializer/htmlDoctype.js +102 -0
- package/src/xslt/serializer/htmlEntities.js +77 -0
- package/src/xslt/serializer/htmlSerializer.js +239 -0
- package/src/xslt/serializer/indent.js +51 -0
- package/src/xslt/serializer/namespaces.js +68 -0
- package/src/xslt/serializer/rawText.js +41 -0
- package/src/xslt/serializer/settings.js +179 -0
- package/src/xslt/serializer/textSerializer.js +77 -0
- package/src/xslt/serializer/xhtmlDocument.js +103 -0
- package/src/xslt/serializer/xmlSerializer.js +227 -0
- package/src/xslt/serializer.js +90 -0
- package/src/xslt/sort.js +151 -0
- package/src/xslt/spaceNameTests.js +115 -0
- package/src/xslt/stylesheetChecks.js +206 -0
- package/src/xslt/stylesheetNamespaces.js +266 -0
- package/src/xslt/templatePriority.js +45 -0
- package/src/xslt/uri.js +68 -0
- package/src/xslt/variables.js +152 -0
- package/src/xslt/whitespace.js +200 -0
- package/LICENSE +0 -29
- package/src/XSLTProcessor.test.js +0 -930
- package/src/xpath/evaluator.test.js +0 -1852
- package/src/xpath/tokenizer.test.js +0 -224
- package/src/xslt/engine.test.js +0 -3130
package/bin/lib/paths.js
ADDED
|
@@ -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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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
|
+
});
|