@tradik/xslt-processor 1.0.3 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +110 -520
  3. package/bin/lib/decode.js +15 -0
  4. package/bin/lib/dom.js +177 -0
  5. package/bin/lib/loaders.js +127 -0
  6. package/bin/lib/options.js +131 -0
  7. package/bin/lib/output.js +114 -0
  8. package/bin/lib/paths.js +186 -0
  9. package/bin/lib/transform.js +206 -0
  10. package/bin/xslt.js +73 -168
  11. package/dist/xslt-processor.browser.js +9564 -1585
  12. package/dist/xslt-processor.browser.js.map +4 -4
  13. package/dist/xslt-processor.browser.min.js +13 -2
  14. package/dist/xslt-processor.browser.min.js.map +4 -4
  15. package/dist/xslt-processor.cjs +9572 -1586
  16. package/dist/xslt-processor.cjs.map +4 -4
  17. package/dist/xslt-processor.d.cts +658 -0
  18. package/dist/xslt-processor.d.ts +459 -12
  19. package/dist/xslt-processor.js +9546 -1582
  20. package/dist/xslt-processor.js.map +4 -4
  21. package/package.json +71 -20
  22. package/src/XSLTProcessor.js +494 -48
  23. package/src/async/abort.js +63 -0
  24. package/src/async/documentUris.js +128 -0
  25. package/src/async/loaders.js +134 -0
  26. package/src/async/preload.js +159 -0
  27. package/src/async/processor.js +206 -0
  28. package/src/async/stream.js +125 -0
  29. package/src/bridge/engine.js +221 -0
  30. package/src/bridge/loader.js +78 -0
  31. package/src/bridge/results.js +75 -0
  32. package/src/bridge/version.js +63 -0
  33. package/src/index.js +26 -8
  34. package/src/io/decode.js +140 -0
  35. package/src/io/readSource.js +167 -0
  36. package/src/xpath/axes.js +562 -0
  37. package/src/xpath/documentOrder.js +270 -0
  38. package/src/xpath/evaluator.js +518 -357
  39. package/src/xpath/index.js +8 -2
  40. package/src/xpath/namespaceNodes.js +172 -0
  41. package/src/xpath/nodeSetFunctions.js +169 -0
  42. package/src/xpath/parser.js +30 -5
  43. package/src/xpath/strings.js +183 -0
  44. package/src/xpath/tokenizer.js +37 -23
  45. package/src/xslt/attributeSets.js +95 -0
  46. package/src/xslt/avt.js +103 -0
  47. package/src/xslt/computedNames.js +91 -0
  48. package/src/xslt/copying.js +212 -0
  49. package/src/xslt/declarationNames.js +80 -0
  50. package/src/xslt/domParsing.js +95 -0
  51. package/src/xslt/elements.js +57 -0
  52. package/src/xslt/engine/bindings.js +195 -0
  53. package/src/xslt/engine/context.js +105 -0
  54. package/src/xslt/engine/controlFlow.js +145 -0
  55. package/src/xslt/engine/copyInstructions.js +133 -0
  56. package/src/xslt/engine/declarations.js +233 -0
  57. package/src/xslt/engine/functionSupport.js +103 -0
  58. package/src/xslt/engine/methods.js +33 -0
  59. package/src/xslt/engine/nodeConstruction.js +187 -0
  60. package/src/xslt/engine/numbering.js +104 -0
  61. package/src/xslt/engine/outputDeclaration.js +77 -0
  62. package/src/xslt/engine/sequenceConstructor.js +228 -0
  63. package/src/xslt/engine/stylesheetLoading.js +208 -0
  64. package/src/xslt/engine/templateInvocation.js +253 -0
  65. package/src/xslt/engine/templateRules.js +243 -0
  66. package/src/xslt/engine/textInstructions.js +171 -0
  67. package/src/xslt/engine/topLevel.js +130 -0
  68. package/src/xslt/engine/transformation.js +263 -0
  69. package/src/xslt/engine/workStack.js +245 -0
  70. package/src/xslt/engine.js +184 -1736
  71. package/src/xslt/exslt/arguments.js +99 -0
  72. package/src/xslt/exslt/calendar.js +120 -0
  73. package/src/xslt/exslt/common.js +44 -0
  74. package/src/xslt/exslt/dateCalc.js +261 -0
  75. package/src/xslt/exslt/dateFormat.js +150 -0
  76. package/src/xslt/exslt/dateParse.js +265 -0
  77. package/src/xslt/exslt/dates.js +259 -0
  78. package/src/xslt/exslt/duration.js +207 -0
  79. package/src/xslt/exslt/dynamic.js +59 -0
  80. package/src/xslt/exslt/index.js +59 -0
  81. package/src/xslt/exslt/math.js +177 -0
  82. package/src/xslt/exslt/sets.js +96 -0
  83. package/src/xslt/exslt/stringOps.js +163 -0
  84. package/src/xslt/exslt/strings.js +147 -0
  85. package/src/xslt/exslt/uri.js +92 -0
  86. package/src/xslt/formatNumber.js +233 -0
  87. package/src/xslt/forwardsCompatible.js +75 -0
  88. package/src/xslt/functions.js +270 -0
  89. package/src/xslt/index.js +38 -1
  90. package/src/xslt/keys.js +164 -0
  91. package/src/xslt/literalResult.js +223 -0
  92. package/src/xslt/matchScope.js +116 -0
  93. package/src/xslt/number.js +271 -0
  94. package/src/xslt/numberFormat.js +253 -0
  95. package/src/xslt/outputNames.js +58 -0
  96. package/src/xslt/patternCompiler.js +175 -0
  97. package/src/xslt/patterns.js +324 -0
  98. package/src/xslt/qname.js +90 -0
  99. package/src/xslt/resultDocument.js +98 -0
  100. package/src/xslt/resultNamespaces.js +219 -0
  101. package/src/xslt/resultTree.js +211 -0
  102. package/src/xslt/serializer/baseWriter.js +390 -0
  103. package/src/xslt/serializer/chunks.js +120 -0
  104. package/src/xslt/serializer/constants.js +92 -0
  105. package/src/xslt/serializer/encoding.js +327 -0
  106. package/src/xslt/serializer/escape.js +135 -0
  107. package/src/xslt/serializer/frames.js +168 -0
  108. package/src/xslt/serializer/htmlDoctype.js +102 -0
  109. package/src/xslt/serializer/htmlEntities.js +77 -0
  110. package/src/xslt/serializer/htmlSerializer.js +239 -0
  111. package/src/xslt/serializer/indent.js +51 -0
  112. package/src/xslt/serializer/namespaces.js +68 -0
  113. package/src/xslt/serializer/rawText.js +41 -0
  114. package/src/xslt/serializer/settings.js +179 -0
  115. package/src/xslt/serializer/textSerializer.js +77 -0
  116. package/src/xslt/serializer/xhtmlDocument.js +103 -0
  117. package/src/xslt/serializer/xmlSerializer.js +227 -0
  118. package/src/xslt/serializer.js +90 -0
  119. package/src/xslt/sort.js +151 -0
  120. package/src/xslt/spaceNameTests.js +115 -0
  121. package/src/xslt/stylesheetChecks.js +206 -0
  122. package/src/xslt/stylesheetNamespaces.js +266 -0
  123. package/src/xslt/templatePriority.js +45 -0
  124. package/src/xslt/uri.js +68 -0
  125. package/src/xslt/variables.js +152 -0
  126. package/src/xslt/whitespace.js +200 -0
  127. package/LICENSE +0 -29
  128. package/src/XSLTProcessor.test.js +0 -930
  129. package/src/xpath/evaluator.test.js +0 -1852
  130. package/src/xpath/tokenizer.test.js +0 -224
  131. package/src/xslt/engine.test.js +0 -3130
@@ -0,0 +1,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
+ }