@tradik/xslt-processor 1.1.1 → 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 (122) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +102 -757
  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 +17 -0
  7. package/bin/lib/output.js +114 -0
  8. package/bin/lib/paths.js +3 -3
  9. package/bin/lib/transform.js +124 -33
  10. package/bin/xslt.js +26 -27
  11. package/dist/xslt-processor.browser.js +8784 -2720
  12. package/dist/xslt-processor.browser.js.map +4 -4
  13. package/dist/xslt-processor.browser.min.js +13 -6
  14. package/dist/xslt-processor.browser.min.js.map +4 -4
  15. package/dist/xslt-processor.cjs +8789 -2723
  16. package/dist/xslt-processor.cjs.map +4 -4
  17. package/dist/xslt-processor.d.cts +380 -21
  18. package/dist/xslt-processor.d.ts +380 -21
  19. package/dist/xslt-processor.js +8770 -2722
  20. package/dist/xslt-processor.js.map +4 -4
  21. package/package.json +51 -11
  22. package/src/XSLTProcessor.js +343 -66
  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 +16 -4
  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 +475 -355
  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 +1 -1
  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 +176 -2020
  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 +22 -9
  87. package/src/xslt/forwardsCompatible.js +75 -0
  88. package/src/xslt/functions.js +94 -15
  89. package/src/xslt/index.js +7 -1
  90. package/src/xslt/keys.js +51 -28
  91. package/src/xslt/literalResult.js +63 -7
  92. package/src/xslt/matchScope.js +116 -0
  93. package/src/xslt/number.js +171 -78
  94. package/src/xslt/numberFormat.js +124 -26
  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 +143 -6
  102. package/src/xslt/serializer/baseWriter.js +173 -66
  103. package/src/xslt/serializer/chunks.js +120 -0
  104. package/src/xslt/serializer/constants.js +14 -0
  105. package/src/xslt/serializer/encoding.js +327 -0
  106. package/src/xslt/serializer/escape.js +49 -12
  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 +123 -25
  111. package/src/xslt/serializer/settings.js +89 -13
  112. package/src/xslt/serializer/textSerializer.js +58 -10
  113. package/src/xslt/serializer/xhtmlDocument.js +103 -0
  114. package/src/xslt/serializer/xmlSerializer.js +113 -13
  115. package/src/xslt/serializer.js +50 -17
  116. package/src/xslt/sort.js +151 -0
  117. package/src/xslt/spaceNameTests.js +115 -0
  118. package/src/xslt/stylesheetChecks.js +206 -0
  119. package/src/xslt/stylesheetNamespaces.js +266 -0
  120. package/src/xslt/variables.js +152 -0
  121. package/src/xslt/whitespace.js +43 -27
  122. package/LICENSE +0 -29
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Static checks run while a stylesheet is compiled.
3
+ *
4
+ * - Patterns (XSLT 1.0 section 5.2) of `xsl:template match`, `xsl:key match`
5
+ * and `xsl:number count`/`from` are compiled at import time, so an invalid
6
+ * pattern makes `importStylesheet` fail with an error naming it, as libxslt
7
+ * (and so the browsers' XSLTProcessor) rejects such a stylesheet.
8
+ * - Two global xsl:variable elements of the same name and import precedence
9
+ * (section 11.4) and text at the top level of a stylesheet (section 2.2)
10
+ * make `importStylesheet` fail, as libxslt rejects such a stylesheet.
11
+ * - Two local variables or parameters of the same name where one is in the
12
+ * scope of the other (section 11.5), and a global xsl:param clashing with
13
+ * another global binding of the same import precedence (section 11.4),
14
+ * are errors libxslt only reports; they are warned about here, and the
15
+ * binding used so far keeps being used.
16
+ *
17
+ * @module xslt/stylesheetChecks
18
+ */
19
+
20
+ "use strict";
21
+
22
+ import { XSLT_NAMESPACE } from "./elements.js";
23
+
24
+ /**
25
+ * The local name of an XSLT element, or null for any other node.
26
+ *
27
+ * @param {Node} node - A stylesheet node
28
+ * @returns {string|null} e.g. "number" for xsl:number
29
+ */
30
+ export function xsltLocalName(node) {
31
+ if (node.nodeType !== 1) return null;
32
+ if (node.namespaceURI === XSLT_NAMESPACE) return node.localName;
33
+ return node.nodeName.startsWith("xsl:") ? node.nodeName.slice(4) : null;
34
+ }
35
+
36
+ /**
37
+ * Visit every element below a node, in document order.
38
+ *
39
+ * @param {Node} root - The subtree root (not visited itself)
40
+ * @param {(element: Element) => void} visit - Called for each element
41
+ * @returns {void}
42
+ */
43
+ function forEachDescendant(root, visit) {
44
+ for (let child = root.firstChild; child; child = child.nextSibling) {
45
+ if (child.nodeType !== 1) continue;
46
+ visit(child);
47
+ forEachDescendant(child, visit);
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Compile a pattern of the stylesheet, reporting where it comes from when
53
+ * it is invalid.
54
+ *
55
+ * @param {{compile: (pattern: string) => object[]}} matcher - The pattern matcher
56
+ * @param {string} pattern - The pattern
57
+ * @param {string} where - The attribute holding it, e.g. "xsl:template match"
58
+ * @returns {void}
59
+ * @throws {Error} When the pattern is invalid
60
+ *
61
+ * @example
62
+ * checkPattern(matcher, "a/..", "xsl:template match");
63
+ * // Error: xsl:template match: Invalid pattern "a/..": Axis not allowed ...
64
+ */
65
+ export function checkPattern(matcher, pattern, where) {
66
+ try {
67
+ matcher.compile(pattern);
68
+ } catch (error) {
69
+ throw new Error(`${where}: ${error.message}`, { cause: error });
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Compile the `count` and `from` patterns of every `xsl:number` below a
75
+ * stylesheet element (they are not attribute value templates, so they are
76
+ * known at compile time).
77
+ *
78
+ * @param {{compile: (pattern: string) => object[]}} matcher - The pattern matcher
79
+ * @param {Element} root - The stylesheet (or simplified stylesheet) element
80
+ * @returns {void}
81
+ * @throws {Error} When one of the patterns is invalid
82
+ */
83
+ export function checkNumberPatterns(matcher, root) {
84
+ forEachDescendant(root, (element) => {
85
+ if (xsltLocalName(element) !== "number") return;
86
+ for (const attribute of ["count", "from"]) {
87
+ const pattern = element.getAttribute(attribute);
88
+ if (pattern) checkPattern(matcher, pattern, `xsl:number ${attribute}`);
89
+ }
90
+ });
91
+ }
92
+
93
+ /**
94
+ * A short description of the declaration holding local bindings.
95
+ *
96
+ * @param {Element} declaration - xsl:template, xsl:variable, ...
97
+ * @returns {string} e.g. `template match="/"`
98
+ */
99
+ function describeDeclaration(declaration) {
100
+ const kind = xsltLocalName(declaration) ?? declaration.nodeName;
101
+ for (const attribute of ["match", "name"]) {
102
+ const value = declaration.getAttribute(attribute);
103
+ if (value !== null) return `${kind} ${attribute}="${value}"`;
104
+ }
105
+ return kind;
106
+ }
107
+
108
+ /**
109
+ * Walk a sequence constructor, reporting bindings that shadow another local
110
+ * binding in scope. A binding is in scope for its following siblings and
111
+ * their descendants, not for its own content.
112
+ *
113
+ * @param {Element} parent - The element whose children are walked
114
+ * @param {Set<string>} inScope - Local names bound around `parent`
115
+ * @param {(name: string) => void} report - Called for each duplicate
116
+ * @returns {void}
117
+ */
118
+ function walkBindings(parent, inScope, report) {
119
+ let scope = inScope;
120
+ for (let child = parent.firstChild; child; child = child.nextSibling) {
121
+ if (child.nodeType !== 1) continue;
122
+ const kind = xsltLocalName(child);
123
+ walkBindings(child, scope, report);
124
+ if (kind !== "variable" && kind !== "param") continue;
125
+
126
+ const name = child.getAttribute("name");
127
+ if (scope.has(name)) report(name);
128
+ scope = new Set(scope).add(name);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Warn about local variables and parameters that shadow another local
134
+ * binding of the same declaration (XSLT 1.0 section 11.5). A local binding
135
+ * shadowing a global one is allowed and not reported.
136
+ *
137
+ * @param {Element} declaration - A top-level element (xsl:template,
138
+ * xsl:variable, xsl:param, xsl:attribute-set)
139
+ * @param {(message: string) => void} warn - Reports one duplicate
140
+ * @returns {void}
141
+ *
142
+ * @example
143
+ * checkLocalBindings(templateElement, (m) => console.warn(m));
144
+ */
145
+ export function checkLocalBindings(declaration, warn) {
146
+ walkBindings(declaration, new Set(), (name) =>
147
+ warn(
148
+ `duplicate binding of variable $${name} in ${describeDeclaration(declaration)}; the later one is used (XSLT 1.0 section 11.5)`,
149
+ ),
150
+ );
151
+ }
152
+
153
+ /**
154
+ * Check a global variable or parameter repeating the name of an earlier one
155
+ * with the same import precedence (XSLT 1.0 section 11.4). Two
156
+ * xsl:variable elements are an error, as in libxslt; a clash involving an
157
+ * xsl:param is warned about, naming the binding the engine uses: the later
158
+ * declaration, except that a global xsl:variable wins over an xsl:param.
159
+ *
160
+ * @param {{name: string, kind: string, precedence: number}} declaration - The
161
+ * new declaration: its name, "variable" or "param", and import precedence
162
+ * @param {{variable?: object, param?: object}} earlier - The definitions
163
+ * registered so far under that name (with `node` and `importPrecedence`)
164
+ * @param {(message: string) => void} warn - Reports the duplicate
165
+ * @returns {void}
166
+ * @throws {Error} When an xsl:variable repeats an xsl:variable
167
+ *
168
+ * @example
169
+ * checkGlobalDuplicate({ name: "v", kind: "param", precedence: 1 },
170
+ * { variable: { node, importPrecedence: 1 } }, console.warn);
171
+ */
172
+ export function checkGlobalDuplicate(declaration, earlier, warn) {
173
+ const { name, kind, precedence } = declaration;
174
+ const clashes = (definition) =>
175
+ Boolean(definition?.node) && definition.importPrecedence === precedence;
176
+ if (kind === "variable" && clashes(earlier.variable)) {
177
+ throw new Error(
178
+ `redefinition of global variable $${name} at the same import precedence (XSLT 1.0 section 11.4)`,
179
+ );
180
+ }
181
+ if (!clashes(earlier.variable) && !clashes(earlier.param)) return;
182
+ const used =
183
+ kind === "param" && earlier.variable ? "the xsl:variable" : "the later one";
184
+ warn(
185
+ `duplicate global binding of variable $${name} at the same import precedence; ${used} is used (XSLT 1.0 section 11.4)`,
186
+ );
187
+ }
188
+
189
+ /**
190
+ * Reject text other than whitespace among the top-level elements of a
191
+ * stylesheet (XSLT 1.0 section 2.2), as libxslt does.
192
+ *
193
+ * @param {Element} root - The xsl:stylesheet or xsl:transform element
194
+ * @returns {void}
195
+ * @throws {Error} When a text node holds non-whitespace characters
196
+ */
197
+ export function checkTopLevelText(root) {
198
+ for (let child = root.firstChild; child; child = child.nextSibling) {
199
+ const isText = child.nodeType === 3 || child.nodeType === 4;
200
+ if (isText && /[^ \t\r\n]/.test(child.nodeValue)) {
201
+ throw new Error(
202
+ `misplaced text at the top level of the stylesheet: "${child.nodeValue.trim()}" (XSLT 1.0 section 2.2)`,
203
+ );
204
+ }
205
+ }
206
+ }
@@ -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,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
+ }