@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
|
@@ -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 (€).
|
|
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
|
+
}
|