@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,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AbortSignal Helpers
|
|
3
|
+
*
|
|
4
|
+
* Every asynchronous entry point takes an optional `signal`. It is checked
|
|
5
|
+
* between steps (and between output chunks), passed to loaders, and raced
|
|
6
|
+
* against loader promises so that a loader ignoring the signal cannot keep an
|
|
7
|
+
* aborted call pending. The synchronous parts (compiling the stylesheet,
|
|
8
|
+
* building the result tree) cannot be interrupted once started.
|
|
9
|
+
*
|
|
10
|
+
* @module async/abort
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The reason of an aborted signal, with a DOMException fallback for
|
|
15
|
+
* implementations without `reason`.
|
|
16
|
+
*
|
|
17
|
+
* @param {AbortSignal} signal - An aborted signal
|
|
18
|
+
* @returns {*} The abort reason
|
|
19
|
+
*/
|
|
20
|
+
function abortReason(signal) {
|
|
21
|
+
return (
|
|
22
|
+
signal.reason ??
|
|
23
|
+
new globalThis.DOMException("This operation was aborted", "AbortError")
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Throw the abort reason when a signal is aborted.
|
|
29
|
+
*
|
|
30
|
+
* @param {AbortSignal} [signal] - Optional signal
|
|
31
|
+
* @returns {void}
|
|
32
|
+
* @throws {*} The abort reason
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* throwIfAborted(options.signal);
|
|
36
|
+
*/
|
|
37
|
+
export function throwIfAborted(signal) {
|
|
38
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Settle with a promise, or reject with the abort reason as soon as the
|
|
43
|
+
* signal aborts, whichever comes first.
|
|
44
|
+
*
|
|
45
|
+
* @template T
|
|
46
|
+
* @param {Promise<T>|T} work - The pending work
|
|
47
|
+
* @param {AbortSignal} [signal] - Optional signal
|
|
48
|
+
* @returns {Promise<T>} The work's outcome, or the abort rejection
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* const text = await abortable(loader(uri), signal);
|
|
52
|
+
*/
|
|
53
|
+
export function abortable(work, signal) {
|
|
54
|
+
if (!signal) return Promise.resolve(work);
|
|
55
|
+
if (signal.aborted) return Promise.reject(abortReason(signal));
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const onAbort = () => reject(abortReason(signal));
|
|
58
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
59
|
+
Promise.resolve(work)
|
|
60
|
+
.then(resolve, reject)
|
|
61
|
+
.finally(() => signal.removeEventListener("abort", onAbort));
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Statically Known document() URIs
|
|
3
|
+
*
|
|
4
|
+
* Finds the `document('literal')` calls of a stylesheet module, so that the
|
|
5
|
+
* asynchronous API can load those documents before the synchronous
|
|
6
|
+
* transformation runs. Expressions are parsed with the XPath parser and the
|
|
7
|
+
* syntax tree is searched for `document()` calls whose first argument is a
|
|
8
|
+
* string literal; URIs computed at run time (`document(@href)`) and calls
|
|
9
|
+
* with an explicit base (second argument) are left to the synchronous
|
|
10
|
+
* document loader.
|
|
11
|
+
*
|
|
12
|
+
* Expressions are read from the expression attributes of XSLT instructions
|
|
13
|
+
* (`select`, `test`, `use`, `value`) and from the `{...}` parts of every other
|
|
14
|
+
* attribute (attribute value templates).
|
|
15
|
+
*
|
|
16
|
+
* @module async/documentUris
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { NodeType, parse } from "../xpath/parser.js";
|
|
20
|
+
import { parseAvt } from "../xslt/avt.js";
|
|
21
|
+
import { XSLT_NAMESPACE } from "../xslt/elements.js";
|
|
22
|
+
import { resolveUri, stripFragment } from "../xslt/uri.js";
|
|
23
|
+
|
|
24
|
+
/** Attributes of XSLT instructions holding a whole expression. */
|
|
25
|
+
const EXPRESSION_ATTRIBUTES = new Set(["select", "test", "use", "value"]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The literal arguments of the `document()` calls of an expression.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} expression - XPath expression
|
|
31
|
+
* @returns {string[]} The literal URIs, empty when the expression is invalid
|
|
32
|
+
* (the engine reports that when compiling)
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* literalDocumentArguments("document('a.xml')/x | document(@b)"); // ["a.xml"]
|
|
36
|
+
*/
|
|
37
|
+
export function literalDocumentArguments(expression) {
|
|
38
|
+
let tree;
|
|
39
|
+
try {
|
|
40
|
+
tree = parse(expression);
|
|
41
|
+
} catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
const found = [];
|
|
45
|
+
const stack = [tree];
|
|
46
|
+
while (stack.length > 0) {
|
|
47
|
+
const node = stack.pop();
|
|
48
|
+
const [first] = node.args ?? [];
|
|
49
|
+
const isStatic =
|
|
50
|
+
node.type === NodeType.FUNCTION_CALL &&
|
|
51
|
+
node.name === "document" &&
|
|
52
|
+
!node.prefix &&
|
|
53
|
+
node.args.length === 1 &&
|
|
54
|
+
first.type === NodeType.LITERAL;
|
|
55
|
+
if (isStatic) found.push(first.value);
|
|
56
|
+
for (const value of Object.values(node)) {
|
|
57
|
+
for (const child of Array.isArray(value) ? value : [value]) {
|
|
58
|
+
if (typeof child?.type === "string") stack.push(child);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return found;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The expressions held by an attribute.
|
|
67
|
+
*
|
|
68
|
+
* @param {Attr} attribute - An attribute of a stylesheet element
|
|
69
|
+
* @returns {string[]} The expressions
|
|
70
|
+
*/
|
|
71
|
+
function attributeExpressions(attribute) {
|
|
72
|
+
const element = attribute.ownerElement;
|
|
73
|
+
const isExpression =
|
|
74
|
+
element.namespaceURI === XSLT_NAMESPACE &&
|
|
75
|
+
!attribute.namespaceURI &&
|
|
76
|
+
EXPRESSION_ATTRIBUTES.has(attribute.localName);
|
|
77
|
+
if (isExpression) return [attribute.value];
|
|
78
|
+
try {
|
|
79
|
+
return parseAvt(attribute.value)
|
|
80
|
+
.filter((part) => typeof part !== "string")
|
|
81
|
+
.map((part) => part.expr);
|
|
82
|
+
} catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The resolved URIs of the literal `document()` calls of a stylesheet module.
|
|
89
|
+
* They are resolved against the main stylesheet URI, as the engine resolves
|
|
90
|
+
* `document()` calls without a base argument; fragment identifiers are
|
|
91
|
+
* dropped and `document('')` (the stylesheet itself) is skipped.
|
|
92
|
+
*
|
|
93
|
+
* @param {Node} module - Stylesheet document or element
|
|
94
|
+
* @param {string|undefined} stylesheetUri - URI of the main stylesheet
|
|
95
|
+
* @returns {string[]} Resolved URIs, possibly repeated
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* staticDocumentUris(xslDoc, "/xsl/main.xsl"); // ["/xsl/data.xml"]
|
|
99
|
+
*/
|
|
100
|
+
export function staticDocumentUris(module, stylesheetUri) {
|
|
101
|
+
const root = module.documentElement ?? module;
|
|
102
|
+
const uris = [];
|
|
103
|
+
for (const element of [root, ...root.getElementsByTagName("*")]) {
|
|
104
|
+
if (!hasDocumentCall(element)) continue;
|
|
105
|
+
for (const attribute of element.attributes) {
|
|
106
|
+
for (const expression of attributeExpressions(attribute)) {
|
|
107
|
+
for (const literal of literalDocumentArguments(expression)) {
|
|
108
|
+
const target = stripFragment(literal);
|
|
109
|
+
if (target) uris.push(resolveUri(target, stylesheetUri));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return uris;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Cheap pre-filter: whether any attribute of an element mentions `document`.
|
|
119
|
+
*
|
|
120
|
+
* @param {Element} element - Stylesheet element
|
|
121
|
+
* @returns {boolean} True when an attribute may hold a document() call
|
|
122
|
+
*/
|
|
123
|
+
function hasDocumentCall(element) {
|
|
124
|
+
for (const attribute of element.attributes) {
|
|
125
|
+
if (attribute.value.includes("document")) return true;
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asynchronous Loaders
|
|
3
|
+
*
|
|
4
|
+
* An asynchronous loader fetches a stylesheet module or a `document()`
|
|
5
|
+
* document before the synchronous transformation runs:
|
|
6
|
+
*
|
|
7
|
+
* (uri, baseUri, { signal }) => Promise<Document | string | Uint8Array |
|
|
8
|
+
* ArrayBuffer | Response | null>
|
|
9
|
+
*
|
|
10
|
+
* `uri` is already resolved against `baseUri` (as for the synchronous
|
|
11
|
+
* loaders). Strings are parsed; bytes and `Response` bodies are decoded like
|
|
12
|
+
* files (byte order mark, XML declaration encoding, else UTF-8). Without a
|
|
13
|
+
* loader, the global `fetch` is used when there is one (browsers, Node.js
|
|
14
|
+
* for http(s) URIs).
|
|
15
|
+
*
|
|
16
|
+
* @module async/loaders
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { decodeXml } from "../io/decode.js";
|
|
20
|
+
import { parseXml, resolveDomParser } from "../xslt/domParsing.js";
|
|
21
|
+
import { abortable } from "./abort.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {(uri: string, baseUri: string|undefined, init: {signal?: AbortSignal}) =>
|
|
25
|
+
* Promise<Document|string|Uint8Array|ArrayBuffer|Response|null>|Document|string|null} AsyncLoader
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Loader reading a URI with the global `fetch`.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} uri - The resolved URI
|
|
32
|
+
* @param {string} [_baseUri] - Unused
|
|
33
|
+
* @param {{signal?: AbortSignal}} [init] - Fetch options
|
|
34
|
+
* @returns {Promise<Response>} The response
|
|
35
|
+
*/
|
|
36
|
+
function fetchLoader(uri, _baseUri, init = {}) {
|
|
37
|
+
return globalThis.fetch(uri, { signal: init.signal });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The loader to use: the given one, else `fetch` when available.
|
|
42
|
+
*
|
|
43
|
+
* @param {AsyncLoader|null|undefined} loader - Configured loader
|
|
44
|
+
* @returns {AsyncLoader} A loader
|
|
45
|
+
* @throws {TypeError} When `loader` is not a function, or none is given and
|
|
46
|
+
* there is no global fetch
|
|
47
|
+
*/
|
|
48
|
+
export function resolveAsyncLoader(loader) {
|
|
49
|
+
if (loader !== undefined && loader !== null) {
|
|
50
|
+
if (typeof loader !== "function") {
|
|
51
|
+
throw new TypeError("The loader must be a function");
|
|
52
|
+
}
|
|
53
|
+
return loader;
|
|
54
|
+
}
|
|
55
|
+
if (typeof globalThis.fetch !== "function") {
|
|
56
|
+
throw new TypeError(
|
|
57
|
+
"No loader given and no global fetch available to load external resources",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return fetchLoader;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Whether a value is a fetch `Response` (or looks like one).
|
|
65
|
+
*
|
|
66
|
+
* @param {unknown} value - Candidate
|
|
67
|
+
* @returns {boolean} True for response-like objects
|
|
68
|
+
*/
|
|
69
|
+
function isResponse(value) {
|
|
70
|
+
return typeof value?.arrayBuffer === "function" && "ok" in value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Turn what a loader returned into a document.
|
|
75
|
+
*
|
|
76
|
+
* @param {unknown} loaded - Loader result
|
|
77
|
+
* @param {string} uri - The URI, for error messages
|
|
78
|
+
* @param {object|null} parser - DOMParser to parse text with
|
|
79
|
+
* @returns {Promise<Document|null>} The document, or null for a null result
|
|
80
|
+
* @throws {Error} For failed responses, malformed markup or unsupported values
|
|
81
|
+
*/
|
|
82
|
+
async function toDocument(loaded, uri, parser) {
|
|
83
|
+
if (loaded === null || loaded === undefined) return null;
|
|
84
|
+
if (typeof loaded.nodeType === "number") return loaded;
|
|
85
|
+
let value = loaded;
|
|
86
|
+
if (isResponse(value)) {
|
|
87
|
+
if (!value.ok) {
|
|
88
|
+
throw new Error(`Failed to load "${uri}": HTTP ${value.status}`);
|
|
89
|
+
}
|
|
90
|
+
value = await value.arrayBuffer();
|
|
91
|
+
}
|
|
92
|
+
if (value instanceof ArrayBuffer) value = new Uint8Array(value);
|
|
93
|
+
if (value instanceof Uint8Array) value = decodeXml(value, uri);
|
|
94
|
+
if (typeof value !== "string") {
|
|
95
|
+
throw new TypeError(
|
|
96
|
+
`The loader returned an unsupported value for "${uri}"`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return parseXml(value, parser);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Create a function loading documents with an asynchronous loader, each URI
|
|
104
|
+
* once (concurrent requests for the same URI share one load).
|
|
105
|
+
*
|
|
106
|
+
* @param {AsyncLoader} loader - The loader
|
|
107
|
+
* @param {object} options - Loading options
|
|
108
|
+
* @param {AbortSignal} [options.signal] - Passed to the loader; aborting
|
|
109
|
+
* rejects pending loads
|
|
110
|
+
* @param {object|null} [options.domParser] - Parser for loaded text
|
|
111
|
+
* @param {Document|null} [options.referenceDoc] - Document whose window may
|
|
112
|
+
* provide a DOMParser
|
|
113
|
+
* @returns {(uri: string, baseUri?: string) => Promise<Document|null>} The
|
|
114
|
+
* de-duplicating load function
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* const load = createDocumentFetcher(resolveAsyncLoader(), { signal });
|
|
118
|
+
* const doc = await load("https://example.com/common.xsl");
|
|
119
|
+
*/
|
|
120
|
+
export function createDocumentFetcher(loader, options = {}) {
|
|
121
|
+
const { signal, domParser = null, referenceDoc = null } = options;
|
|
122
|
+
const parser = resolveDomParser(domParser, referenceDoc);
|
|
123
|
+
const pending = new Map();
|
|
124
|
+
return (uri, baseUri) => {
|
|
125
|
+
if (!pending.has(uri)) {
|
|
126
|
+
const work = abortable(
|
|
127
|
+
Promise.resolve().then(() => loader(uri, baseUri, { signal })),
|
|
128
|
+
signal,
|
|
129
|
+
).then((loaded) => toDocument(loaded, uri, parser));
|
|
130
|
+
pending.set(uri, work);
|
|
131
|
+
}
|
|
132
|
+
return pending.get(uri);
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stylesheet and Document Preloading
|
|
3
|
+
*
|
|
4
|
+
* The XSLT engine compiles and transforms synchronously. To use asynchronous
|
|
5
|
+
* loaders (`fetch`), everything the engine will ask for is loaded first:
|
|
6
|
+
*
|
|
7
|
+
* 1. the `xsl:import`/`xsl:include` tree, breadth first, every module in
|
|
8
|
+
* parallel and each URI once (diamond imports share one load). A cycle
|
|
9
|
+
* cannot make preloading loop, since a URI is never loaded twice; it is
|
|
10
|
+
* reported by the engine while compiling ("Circular stylesheet reference
|
|
11
|
+
* detected"), exactly as with a synchronous loader;
|
|
12
|
+
* 2. the documents named by literal `document('...')` calls of every module
|
|
13
|
+
* (see documentUris.js).
|
|
14
|
+
*
|
|
15
|
+
* The preloaded documents are then served by synchronous loaders, falling
|
|
16
|
+
* back to the synchronous loaders configured on the processor.
|
|
17
|
+
*
|
|
18
|
+
* @module async/preload
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { resolveUri } from "../xslt/uri.js";
|
|
22
|
+
import { XSLT_NAMESPACE } from "../xslt/elements.js";
|
|
23
|
+
import { staticDocumentUris } from "./documentUris.js";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The resolved hrefs of the `xsl:import` and `xsl:include` elements of a
|
|
27
|
+
* stylesheet module (top-level children of its root element).
|
|
28
|
+
*
|
|
29
|
+
* @param {Node} module - Stylesheet document or root element
|
|
30
|
+
* @param {string|undefined} moduleUri - URI of the module
|
|
31
|
+
* @returns {string[]} Resolved URIs, in document order
|
|
32
|
+
*/
|
|
33
|
+
export function moduleReferences(module, moduleUri) {
|
|
34
|
+
const root = module.documentElement ?? module;
|
|
35
|
+
const uris = [];
|
|
36
|
+
for (const child of root.childNodes) {
|
|
37
|
+
const isReference =
|
|
38
|
+
child.nodeType === 1 &&
|
|
39
|
+
child.namespaceURI === XSLT_NAMESPACE &&
|
|
40
|
+
(child.localName === "import" || child.localName === "include");
|
|
41
|
+
const href = isReference ? child.getAttribute("href") : null;
|
|
42
|
+
if (href) uris.push(resolveUri(href, moduleUri));
|
|
43
|
+
}
|
|
44
|
+
return uris;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Load the whole import/include tree of a stylesheet.
|
|
49
|
+
*
|
|
50
|
+
* @param {Node} style - The main stylesheet (document or root element)
|
|
51
|
+
* @param {string|undefined} stylesheetUri - Its URI (base of relative hrefs)
|
|
52
|
+
* @param {(uri: string, baseUri?: string) => Promise<Document|null>} fetchDocument -
|
|
53
|
+
* De-duplicating loader (see createDocumentFetcher)
|
|
54
|
+
* @returns {Promise<Map<string, Document>>} Every module by resolved URI
|
|
55
|
+
* @throws {Error} When a module cannot be loaded
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* const modules = await preloadModules(xslDoc, "/xsl/main.xsl", load);
|
|
59
|
+
*/
|
|
60
|
+
export async function preloadModules(style, stylesheetUri, fetchDocument) {
|
|
61
|
+
const modules = new Map();
|
|
62
|
+
const seen = new Set(stylesheetUri ? [stylesheetUri] : []);
|
|
63
|
+
let level = [{ module: style, uri: stylesheetUri }];
|
|
64
|
+
|
|
65
|
+
while (level.length > 0) {
|
|
66
|
+
const requests = [];
|
|
67
|
+
for (const { module, uri } of level) {
|
|
68
|
+
for (const target of moduleReferences(module, uri)) {
|
|
69
|
+
if (seen.has(target)) continue;
|
|
70
|
+
seen.add(target);
|
|
71
|
+
requests.push(loadModule(target, uri, fetchDocument));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
level = await Promise.all(requests);
|
|
75
|
+
for (const { module, uri } of level) modules.set(uri, module);
|
|
76
|
+
}
|
|
77
|
+
return modules;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Load one stylesheet module.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} uri - Resolved URI
|
|
84
|
+
* @param {string|undefined} baseUri - URI of the referencing module
|
|
85
|
+
* @param {(uri: string, baseUri?: string) => Promise<Document|null>} fetchDocument - Loader
|
|
86
|
+
* @returns {Promise<{module: Document, uri: string}>} The module
|
|
87
|
+
* @throws {Error} When the loader fails or returns nothing
|
|
88
|
+
*/
|
|
89
|
+
async function loadModule(uri, baseUri, fetchDocument) {
|
|
90
|
+
const module = await fetchDocument(uri, baseUri);
|
|
91
|
+
if (!module) throw new Error(`Cannot load stylesheet "${uri}"`);
|
|
92
|
+
return { module, uri };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Load the documents named by literal `document()` calls. A failed load is
|
|
97
|
+
* not reported here: it is kept and reported when the transformation actually
|
|
98
|
+
* evaluates that call (it may sit in a branch that never runs). Aborting
|
|
99
|
+
* rejects at once.
|
|
100
|
+
*
|
|
101
|
+
* @param {Array<Node>} modules - Stylesheet modules to scan
|
|
102
|
+
* @param {string|undefined} stylesheetUri - Base URI of `document()` calls
|
|
103
|
+
* @param {(uri: string, baseUri?: string) => Promise<Document|null>} fetchDocument - Loader
|
|
104
|
+
* @param {AbortSignal} [signal] - Optional signal
|
|
105
|
+
* @returns {Promise<Map<string, {document?: Document|null, error?: Error}>>}
|
|
106
|
+
* Outcome by resolved URI
|
|
107
|
+
*/
|
|
108
|
+
export async function preloadDocuments(
|
|
109
|
+
modules,
|
|
110
|
+
stylesheetUri,
|
|
111
|
+
fetchDocument,
|
|
112
|
+
signal,
|
|
113
|
+
) {
|
|
114
|
+
const uris = new Set(
|
|
115
|
+
modules.flatMap((module) => staticDocumentUris(module, stylesheetUri)),
|
|
116
|
+
);
|
|
117
|
+
const outcomes = await Promise.all(
|
|
118
|
+
[...uris].map((uri) =>
|
|
119
|
+
fetchDocument(uri, stylesheetUri).then(
|
|
120
|
+
(document) => ({ document }),
|
|
121
|
+
(error) => {
|
|
122
|
+
if (signal?.aborted) throw error;
|
|
123
|
+
return { error };
|
|
124
|
+
},
|
|
125
|
+
),
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
return new Map([...uris].map((uri, index) => [uri, outcomes[index]]));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Synchronous stylesheet loader serving the preloaded modules. Every href of
|
|
133
|
+
* the import tree is literal, so every module the engine asks for while
|
|
134
|
+
* compiling has been preloaded.
|
|
135
|
+
*
|
|
136
|
+
* @param {Map<string, Document>} modules - Preloaded modules by resolved URI
|
|
137
|
+
* @returns {(href: string) => Document} The loader
|
|
138
|
+
*/
|
|
139
|
+
export function preloadedStylesheetLoader(modules) {
|
|
140
|
+
return (href) => modules.get(href);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Synchronous `document()` loader serving preloaded documents first; a
|
|
145
|
+
* preload failure is thrown when the document is actually needed.
|
|
146
|
+
*
|
|
147
|
+
* @param {Map<string, {document?: Document|null, error?: Error}>} documents -
|
|
148
|
+
* Preloaded outcomes
|
|
149
|
+
* @param {Function|null} fallback - Loader configured on the processor
|
|
150
|
+
* @returns {(uri: string, baseUri?: string) => (Document|string|null)} The loader
|
|
151
|
+
*/
|
|
152
|
+
export function preloadedDocumentLoader(documents, fallback) {
|
|
153
|
+
return (uri, baseUri) => {
|
|
154
|
+
const outcome = documents.get(uri);
|
|
155
|
+
if (outcome?.error) throw outcome.error;
|
|
156
|
+
if (outcome) return outcome.document;
|
|
157
|
+
return fallback ? fallback(uri, baseUri) : null;
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asynchronous XSLTProcessor Methods
|
|
3
|
+
*
|
|
4
|
+
* Implementation of `importStylesheetAsync`, `transformAsync` and
|
|
5
|
+
* `transformToStream` of {@link XSLTProcessor}. The transformation itself
|
|
6
|
+
* stays synchronous (XPath evaluation cannot await): the asynchronous work is
|
|
7
|
+
* reading the inputs and preloading what the stylesheet references, before
|
|
8
|
+
* the synchronous compile and transform.
|
|
9
|
+
*
|
|
10
|
+
* @module async/processor
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readSource } from "../io/readSource.js";
|
|
14
|
+
import { toChunkSize } from "../xslt/serializer/chunks.js";
|
|
15
|
+
import { throwIfAborted } from "./abort.js";
|
|
16
|
+
import { createDocumentFetcher, resolveAsyncLoader } from "./loaders.js";
|
|
17
|
+
import {
|
|
18
|
+
preloadDocuments,
|
|
19
|
+
preloadModules,
|
|
20
|
+
preloadedStylesheetLoader,
|
|
21
|
+
} from "./preload.js";
|
|
22
|
+
import { chunkStream, transformToChunks } from "./stream.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Reject a configured loader that is not a function.
|
|
26
|
+
*
|
|
27
|
+
* @param {Function|null|undefined} loader - Configured loader
|
|
28
|
+
* @returns {void}
|
|
29
|
+
* @throws {TypeError} When `loader` is set but not a function
|
|
30
|
+
*/
|
|
31
|
+
function checkLoader(loader) {
|
|
32
|
+
if (loader !== undefined && loader !== null) resolveAsyncLoader(loader);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A loader that is resolved when first used, so that a stylesheet without
|
|
37
|
+
* external references needs neither a loader nor `fetch`.
|
|
38
|
+
*
|
|
39
|
+
* @param {Function|null|undefined} loader - Configured loader
|
|
40
|
+
* @returns {Function} The lazy loader
|
|
41
|
+
*/
|
|
42
|
+
function lazyLoader(loader) {
|
|
43
|
+
return (...args) => resolveAsyncLoader(loader)(...args);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The document owning a node (for its window's DOMParser).
|
|
48
|
+
*
|
|
49
|
+
* @param {Node} node - A node
|
|
50
|
+
* @returns {Document} The owner document
|
|
51
|
+
*/
|
|
52
|
+
function ownerOf(node) {
|
|
53
|
+
return node.ownerDocument ?? node;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Load the documents of the literal `document()` calls of stylesheet modules.
|
|
58
|
+
*
|
|
59
|
+
* @param {Node[]} modules - The stylesheet modules
|
|
60
|
+
* @param {string|undefined} stylesheetUri - URI of the main stylesheet
|
|
61
|
+
* @param {Function|null|undefined} loader - Asynchronous document loader
|
|
62
|
+
* @param {AbortSignal} [signal] - Optional signal
|
|
63
|
+
* @returns {Promise<Map<string, object>>} Preloaded outcomes by URI
|
|
64
|
+
*/
|
|
65
|
+
async function preloadStaticDocuments(modules, stylesheetUri, loader, signal) {
|
|
66
|
+
const fetchDocument = createDocumentFetcher(lazyLoader(loader), {
|
|
67
|
+
signal,
|
|
68
|
+
referenceDoc: ownerOf(modules[0]),
|
|
69
|
+
});
|
|
70
|
+
const documents = await preloadDocuments(
|
|
71
|
+
modules,
|
|
72
|
+
stylesheetUri,
|
|
73
|
+
fetchDocument,
|
|
74
|
+
signal,
|
|
75
|
+
);
|
|
76
|
+
throwIfAborted(signal);
|
|
77
|
+
return documents;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Import a stylesheet whose `xsl:import`/`xsl:include` modules and literal
|
|
82
|
+
* `document()` documents are loaded asynchronously first.
|
|
83
|
+
*
|
|
84
|
+
* @param {import('../XSLTProcessor.js').XSLTProcessor} processor - Processor
|
|
85
|
+
* @param {Node|string|Uint8Array|ArrayBuffer|ReadableStream|AsyncIterable} style -
|
|
86
|
+
* The stylesheet, as a node or as markup (read and parsed first)
|
|
87
|
+
* @param {string} [stylesheetUri] - Base URI of relative hrefs and document() URIs
|
|
88
|
+
* @param {object} [options] - Loading options
|
|
89
|
+
* @param {Function} [options.loader] - Asynchronous stylesheet loader, `fetch` by default
|
|
90
|
+
* @param {Function} [options.documentLoader] - Asynchronous document() loader,
|
|
91
|
+
* `options.loader` by default
|
|
92
|
+
* @param {AbortSignal} [options.signal] - Cancels loading
|
|
93
|
+
* @returns {Promise<void>} Resolves once the stylesheet is compiled
|
|
94
|
+
*/
|
|
95
|
+
export async function importStylesheetAsync(
|
|
96
|
+
processor,
|
|
97
|
+
style,
|
|
98
|
+
stylesheetUri,
|
|
99
|
+
options = {},
|
|
100
|
+
) {
|
|
101
|
+
const { signal, loader } = options;
|
|
102
|
+
const documentLoader = options.documentLoader ?? loader;
|
|
103
|
+
checkLoader(loader);
|
|
104
|
+
checkLoader(documentLoader);
|
|
105
|
+
|
|
106
|
+
const node = await readSource(style, { signal });
|
|
107
|
+
// XSLT 2.0/3.0 with xsltVersion "auto": load @tradik/xslt3 first
|
|
108
|
+
await processor._prepareEngine(node);
|
|
109
|
+
const modules = await preloadModules(
|
|
110
|
+
node,
|
|
111
|
+
stylesheetUri,
|
|
112
|
+
createDocumentFetcher(lazyLoader(loader), {
|
|
113
|
+
signal,
|
|
114
|
+
referenceDoc: ownerOf(node),
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
const allModules = [node, ...modules.values()];
|
|
118
|
+
const documents = await preloadStaticDocuments(
|
|
119
|
+
allModules,
|
|
120
|
+
stylesheetUri,
|
|
121
|
+
documentLoader,
|
|
122
|
+
signal,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
processor._compile(node, stylesheetUri, {
|
|
126
|
+
stylesheetLoader: preloadedStylesheetLoader(modules),
|
|
127
|
+
modules: allModules,
|
|
128
|
+
documents,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Transform asynchronously: read the source (node, markup or stream),
|
|
134
|
+
* optionally import a stylesheet or preload `document()` documents first,
|
|
135
|
+
* then transform and serialize.
|
|
136
|
+
*
|
|
137
|
+
* @param {import('../XSLTProcessor.js').XSLTProcessor} processor - Processor
|
|
138
|
+
* @param {Node|string|Uint8Array|ArrayBuffer|ReadableStream|AsyncIterable} source - Input
|
|
139
|
+
* @param {object} [options] - Options
|
|
140
|
+
* @param {AbortSignal} [options.signal] - Cancels the call
|
|
141
|
+
* @param {Node|string|ReadableStream|AsyncIterable} [options.stylesheet] -
|
|
142
|
+
* Stylesheet to import first (see importStylesheetAsync)
|
|
143
|
+
* @param {string} [options.stylesheetUri] - Its URI
|
|
144
|
+
* @param {Function} [options.fetchStylesheet] - Asynchronous stylesheet loader
|
|
145
|
+
* @param {Function} [options.fetchDocument] - Asynchronous document() loader
|
|
146
|
+
* @returns {Promise<string>} The serialized result
|
|
147
|
+
* @throws {Error} When loading, parsing or the transformation fails
|
|
148
|
+
*/
|
|
149
|
+
export async function transformAsync(processor, source, options = {}) {
|
|
150
|
+
const { signal, stylesheet, fetchStylesheet, fetchDocument } = options;
|
|
151
|
+
if (stylesheet !== undefined && stylesheet !== null) {
|
|
152
|
+
await importStylesheetAsync(processor, stylesheet, options.stylesheetUri, {
|
|
153
|
+
loader: fetchStylesheet,
|
|
154
|
+
documentLoader: fetchDocument,
|
|
155
|
+
signal,
|
|
156
|
+
});
|
|
157
|
+
} else {
|
|
158
|
+
processor._requireStylesheet("transformAsync");
|
|
159
|
+
if (fetchDocument) {
|
|
160
|
+
checkLoader(fetchDocument);
|
|
161
|
+
processor._usePreloadedDocuments(
|
|
162
|
+
await preloadStaticDocuments(
|
|
163
|
+
processor._modules,
|
|
164
|
+
processor._stylesheetUri,
|
|
165
|
+
fetchDocument,
|
|
166
|
+
signal,
|
|
167
|
+
),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const node = await readSource(source, {
|
|
172
|
+
signal,
|
|
173
|
+
referenceDoc: ownerOf(processor._stylesheet),
|
|
174
|
+
});
|
|
175
|
+
processor._checkSource("transformAsync", node);
|
|
176
|
+
throwIfAborted(signal);
|
|
177
|
+
return processor.engine.transformToString(node);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Transform and stream the serialized result (see async/stream.js).
|
|
182
|
+
*
|
|
183
|
+
* @param {import('../XSLTProcessor.js').XSLTProcessor} processor - Processor
|
|
184
|
+
* @param {Node|string|Uint8Array|ArrayBuffer|ReadableStream|AsyncIterable} source - Input
|
|
185
|
+
* @param {{signal?: AbortSignal, chunkSize?: number}} [options] - Options
|
|
186
|
+
* @returns {ReadableStream<string>} The serialized result
|
|
187
|
+
* @throws {TypeError|RangeError} For a missing stylesheet, an invalid source
|
|
188
|
+
* node or chunk size (at once; later failures error the stream)
|
|
189
|
+
*/
|
|
190
|
+
export function transformToStream(processor, source, options = {}) {
|
|
191
|
+
processor._requireStylesheet("transformToStream");
|
|
192
|
+
if (typeof source?.nodeType === "number") {
|
|
193
|
+
processor._checkSource("transformToStream", source);
|
|
194
|
+
}
|
|
195
|
+
const chunkSize = toChunkSize(options.chunkSize);
|
|
196
|
+
const engine = processor.engine;
|
|
197
|
+
const { signal } = options;
|
|
198
|
+
return chunkStream(async () => {
|
|
199
|
+
const node = await readSource(source, {
|
|
200
|
+
signal,
|
|
201
|
+
referenceDoc: ownerOf(processor._stylesheet),
|
|
202
|
+
});
|
|
203
|
+
processor._checkSource("transformToStream", node);
|
|
204
|
+
return transformToChunks(engine, node, { chunkSize });
|
|
205
|
+
}, options);
|
|
206
|
+
}
|