@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,266 @@
1
+ /**
2
+ * In-scope namespaces of stylesheet elements.
3
+ *
4
+ * Prefixes in XPath expressions, patterns and QName-valued attributes resolve
5
+ * against the namespace declarations in scope on the element that carries
6
+ * them (XSLT 1.0 section 2.4), not against one stylesheet-wide table: a
7
+ * declaration on `xsl:template` or on a literal result element applies inside
8
+ * it only, and an included stylesheet may bind a prefix differently from the
9
+ * including one. The same bindings are the namespace nodes that literal
10
+ * result elements copy to the result tree (section 7.1.1), minus the XSLT
11
+ * namespace and the excluded and extension prefixes.
12
+ *
13
+ * Every map is computed once per element and cached; an element without
14
+ * declarations of its own shares its parent's (frozen) map, so identical
15
+ * scopes can be recognised by identity.
16
+ *
17
+ * @module xslt/stylesheetNamespaces
18
+ */
19
+
20
+ "use strict";
21
+
22
+ import { XSLT_NAMESPACE } from "./elements.js";
23
+
24
+ /** Namespace of `xmlns` attributes. */
25
+ export const XMLNS_NAMESPACE = "http://www.w3.org/2000/xmlns/";
26
+
27
+ /** Namespace bound to the `xml` prefix. */
28
+ export const XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace";
29
+
30
+ /** The scope above the stylesheet document element. */
31
+ const EMPTY_SCOPE = Object.freeze({});
32
+
33
+ /** Namespaces excluded above the stylesheet document element. */
34
+ const BASE_EXCLUSIONS = new Set([XSLT_NAMESPACE]);
35
+
36
+ /** No extension namespaces, above the stylesheet document element. */
37
+ const NO_EXTENSIONS = new Set();
38
+
39
+ const scopes = new WeakMap();
40
+ const exclusions = new WeakMap();
41
+ const extensions = new WeakMap();
42
+ const namespaceNodes = new WeakMap();
43
+
44
+ /**
45
+ * The prefix a namespace declaration attribute declares, or null when the
46
+ * attribute is not a declaration.
47
+ *
48
+ * @param {Attr} attribute - Any attribute
49
+ * @returns {string|null} The prefix, "" for a default namespace declaration
50
+ */
51
+ export function declaredPrefix(attribute) {
52
+ const name = attribute.name;
53
+ if (name === "xmlns") return "";
54
+ if (name.startsWith("xmlns:")) return name.slice(6);
55
+ return null;
56
+ }
57
+
58
+ /**
59
+ * The namespace bindings in scope on a stylesheet element.
60
+ *
61
+ * @param {Node|null} node - A stylesheet element (other nodes have no scope)
62
+ * @returns {Readonly<Object<string, string>>} URIs by prefix; "" holds the
63
+ * default namespace, an empty URI means "undeclared"
64
+ *
65
+ * @example
66
+ * inScopeNamespaces(templateElement).f; // "urn:f"
67
+ */
68
+ export function inScopeNamespaces(node) {
69
+ if (node?.nodeType !== 1) return EMPTY_SCOPE;
70
+
71
+ let scope = scopes.get(node);
72
+ if (scope) return scope;
73
+
74
+ const parentScope = inScopeNamespaces(node.parentNode);
75
+ let own = null;
76
+ for (const attribute of node.attributes) {
77
+ const prefix = declaredPrefix(attribute);
78
+ if (prefix === null) continue;
79
+ own ??= {};
80
+ own[prefix] = attribute.value;
81
+ }
82
+
83
+ scope = own ? Object.freeze({ ...parentScope, ...own }) : parentScope;
84
+ scopes.set(node, scope);
85
+ return scope;
86
+ }
87
+
88
+ /**
89
+ * Split an XSLT attribute holding whitespace separated prefixes.
90
+ *
91
+ * @param {string|null} value - e.g. `"a b #default"`
92
+ * @returns {string[]} The prefixes, "" standing for `#default`
93
+ */
94
+ function prefixList(value) {
95
+ if (!value) return [];
96
+ return value
97
+ .split(/[ \t\r\n]+/)
98
+ .filter(Boolean)
99
+ .map((prefix) => (prefix === "#default" ? "" : prefix));
100
+ }
101
+
102
+ /** Attributes whose prefixes are excluded from the result tree. */
103
+ const EXCLUSION_ATTRIBUTES = [
104
+ "exclude-result-prefixes",
105
+ "extension-element-prefixes",
106
+ ];
107
+
108
+ /** Attributes declaring extension namespaces (XSLT 1.0 section 14.1). */
109
+ const EXTENSION_ATTRIBUTES = ["extension-element-prefixes"];
110
+
111
+ /**
112
+ * Read prefix list attributes of one stylesheet element: unqualified on
113
+ * `xsl:stylesheet`, `xsl:`-qualified on literal result elements.
114
+ *
115
+ * @param {Element} element - A stylesheet element
116
+ * @param {string[]} attributes - Local names of the attributes to read
117
+ * @returns {Array<{prefix: string, attribute: string}>} Prefixes declared on
118
+ * this element, with the attribute declaring each
119
+ */
120
+ function ownPrefixes(element, attributes) {
121
+ const isXslt = element.namespaceURI === XSLT_NAMESPACE;
122
+ const read = (localName) =>
123
+ isXslt
124
+ ? element.getAttribute(localName)
125
+ : element.getAttributeNS(XSLT_NAMESPACE, localName);
126
+ return attributes.flatMap((attribute) =>
127
+ prefixList(read(attribute)).map((prefix) => ({ prefix, attribute })),
128
+ );
129
+ }
130
+
131
+ /**
132
+ * Namespace URIs named by prefix list attributes on a stylesheet element or
133
+ * its ancestors, cached per element.
134
+ *
135
+ * @param {Node|null} node - A stylesheet element
136
+ * @param {object} kind - What to collect
137
+ * @param {string[]} kind.attributes - The prefix list attributes
138
+ * @param {WeakMap<Element, Set<string>>} kind.cache - Results by element
139
+ * @param {Set<string>} kind.base - The set above the document element
140
+ * @param {boolean} kind.report - Whether undeclared prefixes are reported
141
+ * @returns {Set<string>} The namespace URIs
142
+ */
143
+ function declaredNamespaces(node, kind) {
144
+ if (node?.nodeType !== 1) return kind.base;
145
+
146
+ let found = kind.cache.get(node);
147
+ if (found) return found;
148
+
149
+ found = declaredNamespaces(node.parentNode, kind);
150
+ const prefixes = ownPrefixes(node, kind.attributes);
151
+ if (prefixes.length > 0) {
152
+ const scope = inScopeNamespaces(node);
153
+ found = new Set(found);
154
+ for (const { prefix, attribute } of prefixes) {
155
+ const uri = resolvePrefix(scope, prefix);
156
+ if (uri) {
157
+ found.add(uri);
158
+ } else if (kind.report) {
159
+ // An error in XSLT 1.0 (section 7.1.1) that libxslt reports and
160
+ // recovers from; reported once, as the result is cached per element
161
+ console.warn(
162
+ `XSLT: ${attribute}: undefined namespace prefix "${prefix || "#default"}" is ignored`,
163
+ );
164
+ }
165
+ }
166
+ }
167
+ kind.cache.set(node, found);
168
+ return found;
169
+ }
170
+
171
+ /** Excluded namespaces: excluded and extension prefixes, plus XSLT. */
172
+ const EXCLUDED = {
173
+ attributes: EXCLUSION_ATTRIBUTES,
174
+ cache: exclusions,
175
+ base: BASE_EXCLUSIONS,
176
+ report: true,
177
+ };
178
+
179
+ /** Extension namespaces (reported through EXCLUDED already). */
180
+ const EXTENSIONS = {
181
+ attributes: EXTENSION_ATTRIBUTES,
182
+ cache: extensions,
183
+ base: NO_EXTENSIONS,
184
+ report: false,
185
+ };
186
+
187
+ /**
188
+ * Namespace URIs excluded from the result tree around a stylesheet element:
189
+ * the XSLT namespace, plus the namespaces of every excluded or extension
190
+ * prefix declared on the element or its ancestors. A prefix that is not
191
+ * declared (including `#all`, which XSLT 1.0 does not know) is reported with
192
+ * console.warn and ignored.
193
+ *
194
+ * @param {Node|null} node - A stylesheet element
195
+ * @returns {Set<string>} Excluded namespace URIs
196
+ */
197
+ function excludedNamespaces(node) {
198
+ return declaredNamespaces(node, EXCLUDED);
199
+ }
200
+
201
+ /**
202
+ * Whether a stylesheet element is an extension element: an element in a
203
+ * namespace that an `extension-element-prefixes` attribute on it or on an
204
+ * ancestor declares as an extension namespace (XSLT 1.0 section 14.1).
205
+ *
206
+ * @param {Element} element - A stylesheet element that is not in the XSLT namespace
207
+ * @returns {boolean} True for extension elements
208
+ *
209
+ * @example
210
+ * // <xsl:stylesheet xmlns:e="urn:e" extension-element-prefixes="e">
211
+ * isExtensionElement(eElement); // true
212
+ */
213
+ export function isExtensionElement(element) {
214
+ const uri = element.namespaceURI;
215
+ if (!uri) return false;
216
+ // Resolving the exclusions first reports undeclared prefixes once
217
+ excludedNamespaces(element);
218
+ return declaredNamespaces(element, EXTENSIONS).has(uri);
219
+ }
220
+
221
+ /**
222
+ * The namespace nodes a literal result element copies to the result tree
223
+ * (XSLT 1.0 section 7.1.1).
224
+ *
225
+ * @param {Element} node - The literal result element
226
+ * The list is shared by every element with the same scope and exclusions,
227
+ * so equal lists can be compared by identity.
228
+ *
229
+ * @param {Element} node - The literal result element
230
+ * @returns {Array<[string, string]>} `[prefix, uri]` pairs, "" for the default namespace
231
+ *
232
+ * @example
233
+ * resultNamespaceNodes(literalElement); // [["q", "urn:q"]]
234
+ */
235
+ export function resultNamespaceNodes(node) {
236
+ const scope = inScopeNamespaces(node);
237
+ const excluded = excludedNamespaces(node);
238
+
239
+ let byExclusions = namespaceNodes.get(scope);
240
+ if (!byExclusions) {
241
+ byExclusions = new WeakMap();
242
+ namespaceNodes.set(scope, byExclusions);
243
+ }
244
+ let nodes = byExclusions.get(excluded);
245
+ if (!nodes) {
246
+ nodes = Object.entries(scope).filter(
247
+ ([, uri]) => uri !== "" && !excluded.has(uri),
248
+ );
249
+ byExclusions.set(excluded, nodes);
250
+ }
251
+ return nodes;
252
+ }
253
+
254
+ /**
255
+ * Resolve a QName prefix against a scope.
256
+ *
257
+ * @param {Object<string, string>} scope - Bindings from {@link inScopeNamespaces}
258
+ * @param {string} prefix - The prefix, "" for the default namespace
259
+ * @returns {string|null} The namespace URI, or null when unbound
260
+ */
261
+ export function resolvePrefix(scope, prefix) {
262
+ if (prefix === "xml") return XML_NAMESPACE;
263
+ return Object.hasOwn(scope, prefix) && scope[prefix] !== ""
264
+ ? scope[prefix]
265
+ : null;
266
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Default template priorities (XSLT 1.0 section 5.5).
3
+ *
4
+ * A template rule without an explicit `priority` attribute gets a default
5
+ * priority derived from the shape of its match pattern. A union pattern is
6
+ * treated as a set of template rules, one per alternative, so each
7
+ * alternative must be assigned its own priority by the caller.
8
+ *
9
+ * @module xslt/templatePriority
10
+ */
11
+
12
+ const NAME = String.raw`[A-Za-z_][\w.-]*`;
13
+ const QNAME = `(?:${NAME}:)?${NAME}`;
14
+
15
+ /** Patterns of the form `name`, `prefix:name`, `@name`, `@prefix:name`. */
16
+ const QNAME_PATTERN = new RegExp(`^(?:child::|attribute::|@)?${QNAME}$`);
17
+
18
+ /** Patterns of the form `prefix:*` or `@prefix:*`. */
19
+ const PREFIX_WILDCARD_PATTERN = new RegExp(
20
+ String.raw`^(?:child::|attribute::|@)?${NAME}:\*$`,
21
+ );
22
+
23
+ /** Patterns of the form `*`, `@*`, `node()`, `text()`, `comment()`, `processing-instruction()`. */
24
+ const NODE_TEST_PATTERN =
25
+ /^(?:child::|attribute::|@)?(?:\*|node\(\)|text\(\)|comment\(\)|processing-instruction\(\))$/;
26
+
27
+ /** `processing-instruction('literal')` patterns. */
28
+ const PI_LITERAL_PATTERN =
29
+ /^(?:child::)?processing-instruction\(\s*(?:"[^"]*"|'[^']*')\s*\)$/;
30
+
31
+ /**
32
+ * Compute the default priority of a single (non-union) match pattern.
33
+ *
34
+ * @param {string|null|undefined} pattern - The match pattern, already trimmed
35
+ * @returns {number} -0.5, -0.25, 0 or 0.5 as defined by the specification
36
+ */
37
+ export function calculatePriority(pattern) {
38
+ if (!pattern) return 0.5;
39
+
40
+ if (NODE_TEST_PATTERN.test(pattern)) return -0.5;
41
+ if (PREFIX_WILDCARD_PATTERN.test(pattern)) return -0.25;
42
+ if (QNAME_PATTERN.test(pattern) || PI_LITERAL_PATTERN.test(pattern)) return 0;
43
+
44
+ return 0.5;
45
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * URI helpers for XSLT stylesheet and document resolution.
3
+ *
4
+ * Kept deliberately small and dependency free: the engine only needs enough
5
+ * URI arithmetic to turn a relative `href` into something a host supplied
6
+ * loader can resolve, plus fragment removal for the `document()` function.
7
+ */
8
+
9
+ "use strict";
10
+
11
+ /** Matches an absolute URI such as `http://`, `https://` or `file://`. */
12
+ const ABSOLUTE_URI_PATTERN = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
13
+
14
+ /**
15
+ * Resolve a possibly relative URI against a base URI.
16
+ *
17
+ * Absolute URIs (with a scheme) and root relative URIs (starting with `/`)
18
+ * are returned untouched, as is any URI when no base is available.
19
+ *
20
+ * @param {string} href - The URI to resolve
21
+ * @param {string} [baseUri] - The base URI, typically the stylesheet location
22
+ * @returns {string} The resolved URI
23
+ *
24
+ * @example
25
+ * resolveUri('common.xsl', '/styles/main.xsl'); // '/styles/common.xsl'
26
+ */
27
+ export function resolveUri(href, baseUri) {
28
+ if (!href) return href;
29
+ if (!baseUri || isAbsoluteUri(href) || href.startsWith("/")) {
30
+ return href;
31
+ }
32
+
33
+ const lastSlash = baseUri.lastIndexOf("/");
34
+ const baseDir = lastSlash >= 0 ? baseUri.substring(0, lastSlash + 1) : "";
35
+
36
+ return baseDir + href;
37
+ }
38
+
39
+ /**
40
+ * Check whether a URI is absolute (has a scheme).
41
+ *
42
+ * @param {string} uri - The URI to inspect
43
+ * @returns {boolean} True when the URI carries a scheme
44
+ *
45
+ * @example
46
+ * isAbsoluteUri('https://example.com/a.xml'); // true
47
+ */
48
+ export function isAbsoluteUri(uri) {
49
+ return ABSOLUTE_URI_PATTERN.test(uri);
50
+ }
51
+
52
+ /**
53
+ * Remove a fragment identifier from a URI.
54
+ *
55
+ * XSLT 1.0 leaves the meaning of fragment identifiers passed to `document()`
56
+ * implementation defined; this processor ignores them.
57
+ *
58
+ * @param {string} uri - The URI, possibly carrying a `#fragment`
59
+ * @returns {string} The URI without its fragment
60
+ *
61
+ * @example
62
+ * stripFragment('data.xml#section'); // 'data.xml'
63
+ */
64
+ export function stripFragment(uri) {
65
+ if (typeof uri !== "string") return "";
66
+ const hash = uri.indexOf("#");
67
+ return hash === -1 ? uri : uri.substring(0, hash);
68
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Variable and parameter bindings (XSLT 1.0 section 11).
3
+ *
4
+ * Local bindings live in the `variables` and `parameters` objects of an
5
+ * XSLT context; the engine gives every template invocation empty ones and a
6
+ * new copy to every sequence constructor that declares a variable, which
7
+ * makes local variables lexically scoped. Global variables and parameters are
8
+ * kept in a {@link GlobalBindings} table shared by all contexts of one
9
+ * transformation and are evaluated lazily, so they may refer to each other in
10
+ * any order (section 11.4).
11
+ *
12
+ * @module xslt/variables
13
+ */
14
+
15
+ "use strict";
16
+
17
+ /** Evaluation states of a global binding. */
18
+ const PENDING = 0;
19
+ const EVALUATING = 1;
20
+ const DONE = 2;
21
+
22
+ /**
23
+ * The global variables and parameters of one transformation.
24
+ */
25
+ export class GlobalBindings {
26
+ /**
27
+ * @param {(definition: object) => *} evaluate - Computes the value of a
28
+ * definition; called at most once per name
29
+ */
30
+ constructor(evaluate) {
31
+ this.evaluate = evaluate;
32
+ this.entries = new Map();
33
+ }
34
+
35
+ /**
36
+ * Declare a global binding. A later declaration of the same name replaces
37
+ * an earlier one.
38
+ *
39
+ * @param {string} name - The variable or parameter name
40
+ * @param {object} definition - Passed to the evaluate callback
41
+ * @returns {void}
42
+ *
43
+ * @example
44
+ * globals.define('title', { select: "'Report'" });
45
+ */
46
+ define(name, definition) {
47
+ this.entries.set(name, { definition, state: PENDING, value: undefined });
48
+ }
49
+
50
+ /**
51
+ * Whether a global binding with that name exists.
52
+ *
53
+ * @param {string} name - The variable name
54
+ * @returns {boolean} True when declared
55
+ */
56
+ has(name) {
57
+ return this.entries.has(name);
58
+ }
59
+
60
+ /**
61
+ * The value of a global binding, evaluating it on first use.
62
+ *
63
+ * @param {string} name - The variable name
64
+ * @returns {*} The value, undefined when the name is not declared
65
+ * @throws {Error} When the definition (indirectly) refers to itself
66
+ *
67
+ * @example
68
+ * globals.get('title'); // "Report"
69
+ */
70
+ get(name) {
71
+ const entry = this.entries.get(name);
72
+ if (!entry) return undefined;
73
+ if (entry.state === DONE) return entry.value;
74
+ if (entry.state === EVALUATING) {
75
+ throw new Error(
76
+ `Circular definition of global variable $${name} (XSLT 1.0 section 11.4)`,
77
+ );
78
+ }
79
+
80
+ entry.state = EVALUATING;
81
+ try {
82
+ entry.value = this.evaluate(entry.definition);
83
+ } catch (error) {
84
+ entry.state = PENDING;
85
+ throw error;
86
+ }
87
+ entry.state = DONE;
88
+ return entry.value;
89
+ }
90
+
91
+ /**
92
+ * Evaluate every binding, in declaration order, so that errors and
93
+ * `xsl:message` side effects of unused globals still surface.
94
+ *
95
+ * @returns {void}
96
+ */
97
+ evaluateAll() {
98
+ for (const name of this.entries.keys()) this.get(name);
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Look a variable up in an XSLT context: local variables, then local
104
+ * parameters, then the globals.
105
+ *
106
+ * @param {{variables: object, parameters: object, globals?: GlobalBindings|null}} context - The XSLT context
107
+ * @param {string} name - The variable name
108
+ * @returns {{found: boolean, value: *}} Whether it is bound, and its value
109
+ */
110
+ export function lookupVariable(context, name) {
111
+ if (Object.hasOwn(context.variables, name)) {
112
+ return { found: true, value: context.variables[name] };
113
+ }
114
+ if (Object.hasOwn(context.parameters, name)) {
115
+ return { found: true, value: context.parameters[name] };
116
+ }
117
+ const globals = context.globals;
118
+ if (globals?.has(name)) return { found: true, value: globals.get(name) };
119
+ return { found: false, value: undefined };
120
+ }
121
+
122
+ /**
123
+ * Read-only object view of the variables in scope of an XSLT context, in the
124
+ * shape the XPath evaluator expects (`hasOwnProperty` + property access).
125
+ * Nothing is copied: lookups read the context live, and globals are only
126
+ * evaluated when an expression actually refers to them.
127
+ *
128
+ * @param {object} context - The XSLT context
129
+ * @returns {object} A proxy mapping variable names to values
130
+ *
131
+ * @example
132
+ * new XPathContext(node, 1, 1, createVariableView(xsltContext), namespaces);
133
+ */
134
+ export function createVariableView(context) {
135
+ const binding = (name) =>
136
+ typeof name === "string" ? lookupVariable(context, name) : null;
137
+
138
+ return new Proxy(Object.create(null), {
139
+ has: (_target, name) => binding(name)?.found === true,
140
+ get: (_target, name) => binding(name)?.value,
141
+ getOwnPropertyDescriptor: (_target, name) => {
142
+ const found = binding(name);
143
+ if (!found?.found) return undefined;
144
+ return {
145
+ value: found.value,
146
+ writable: false,
147
+ enumerable: true,
148
+ configurable: true,
149
+ };
150
+ },
151
+ });
152
+ }