@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
@@ -1,9 +1,12 @@
1
1
  /**
2
2
  * XSLT-defined XPath functions.
3
3
  *
4
- * XSLT 1.0 section 12 adds functions to the XPath function library. They live
5
- * here rather than in `src/xpath` so that module stays a pure XPath 1.0
6
- * implementation; the engine registers this map on its evaluator through
4
+ * XSLT 1.0 section 12 adds functions to the XPath function library, and the
5
+ * EXSLT `node-set()` extension (also under the msxsl namespace) is registered
6
+ * by expanded name next to them, together with the other EXSLT functions of
7
+ * `./exslt/index.js`. They live here rather than in `src/xpath` so
8
+ * that module stays a pure XPath 1.0 implementation; the engine registers this
9
+ * map on its evaluator through
7
10
  * {@link XPathEvaluator#registerFunctions}.
8
11
  */
9
12
 
@@ -11,6 +14,18 @@
11
14
 
12
15
  import { formatNumber, DEFAULT_DECIMAL_FORMAT } from "./formatNumber.js";
13
16
  import { isXsltElementAvailable, XSLT_NAMESPACE } from "./elements.js";
17
+ import {
18
+ expandedFunctionName,
19
+ resolveNamespacePrefix,
20
+ } from "../xpath/evaluator.js";
21
+ import { rootNodeOf } from "../xpath/axes.js";
22
+ import { EXSLT_COMMON, createExsltFunctions } from "./exslt/index.js";
23
+
24
+ /** Namespace of the EXSLT common module (`exsl:node-set()`). */
25
+ export const EXSLT_COMMON_NAMESPACE = EXSLT_COMMON;
26
+
27
+ /** Namespace of the MSXML extension functions (`msxsl:node-set()`). */
28
+ export const MSXSL_NAMESPACE = "urn:schemas-microsoft-com:xslt";
14
29
 
15
30
  /** Vendor identification reported by `system-property()`. */
16
31
  export const VENDOR = "@tradik/xslt-processor";
@@ -72,6 +87,29 @@ function splitQName(qname) {
72
87
  };
73
88
  }
74
89
 
90
+ /**
91
+ * Expanded name key of the decimal format named by the third argument of
92
+ * `format-number()`, with the prefix resolved in the expression's scope
93
+ * (XSLT 1.0 section 12.3); the table is keyed the same way (see
94
+ * declarationNames.js).
95
+ *
96
+ * @param {string} qname - The decimal format QName
97
+ * @param {{namespaces: Object<string, string>}} ctx - The XPath context
98
+ * @returns {string} `{uri}local`, or the local name in no namespace
99
+ * @throws {Error} When the prefix is not declared
100
+ */
101
+ function decimalFormatKey(qname, ctx) {
102
+ const { prefix, localName } = splitQName(qname);
103
+ if (prefix === null) return localName;
104
+ const namespaceUri = resolveNamespacePrefix(prefix, ctx.namespaces);
105
+ if (!namespaceUri) {
106
+ throw new Error(
107
+ `format-number(): undeclared namespace prefix "${prefix}" in "${qname}"`,
108
+ );
109
+ }
110
+ return `{${namespaceUri}}${localName}`;
111
+ }
112
+
75
113
  /**
76
114
  * Build the XSLT function map for an engine.
77
115
  *
@@ -96,7 +134,30 @@ export function createXsltFunctions(engine) {
96
134
  const evaluate = (arg, ctx) => evaluator.evaluate(arg, ctx);
97
135
  const asString = (arg, ctx) => stringify(evaluate(arg, ctx));
98
136
 
137
+ /**
138
+ * `exsl:node-set(object)` - EXSLT common, also known as `msxsl:node-set()`.
139
+ *
140
+ * A result tree fragment (a DocumentFragment in this engine) becomes a
141
+ * node-set holding its root node, so `exsl:node-set($rtf)/item` selects the
142
+ * fragment's top-level `item` elements. A node-set is returned unchanged; any
143
+ * other value becomes a node-set holding one text node with its string value.
144
+ *
145
+ * @param {Array} args - Argument expressions
146
+ * @param {import('../xpath/evaluator.js').XPathContext} ctx - Evaluation context
147
+ * @returns {Node[]} The node-set
148
+ */
149
+ const nodeSet = (args, ctx) => {
150
+ const value = evaluate(args[0], ctx);
151
+ if (Array.isArray(value)) return value;
152
+ if (value?.nodeType) return [value];
153
+ return [ownerDocumentOf(ctx.node).createTextNode(stringify(value))];
154
+ };
155
+
99
156
  return {
157
+ ...createExsltFunctions(engine),
158
+ [expandedFunctionName(EXSLT_COMMON_NAMESPACE, "node-set")]: nodeSet,
159
+ [expandedFunctionName(MSXSL_NAMESPACE, "node-set")]: nodeSet,
160
+
100
161
  /**
101
162
  * `document(object, base?)` - load external XML documents.
102
163
  *
@@ -117,18 +178,23 @@ export function createXsltFunctions(engine) {
117
178
  return result;
118
179
  },
119
180
 
120
- /** `key(name, value)` - look up nodes through an `xsl:key` index. */
181
+ /**
182
+ * `key(name, value)` - look up nodes through an `xsl:key` index of the
183
+ * tree containing the context node: its document, or the fragment of a
184
+ * result tree fragment converted with `exsl:node-set()`.
185
+ */
121
186
  key: (args, ctx) => {
122
187
  const name = asString(args[0], ctx);
123
188
  const values = toStringList(evaluator, stringify, evaluate(args[1], ctx));
124
- return engine.keyRegistry.lookup(name, values, ownerDocumentOf(ctx.node));
189
+ return engine.keyRegistry.lookup(name, values, rootNodeOf(ctx.node));
125
190
  },
126
191
 
127
192
  /** `format-number(number, pattern, decimalFormat?)`. */
128
193
  "format-number": (args, ctx) => {
129
194
  const value = evaluator.toNumber(evaluate(args[0], ctx));
130
195
  const pattern = asString(args[1], ctx);
131
- const formatName = args.length > 2 ? asString(args[2], ctx) : "";
196
+ const formatName =
197
+ args.length > 2 ? decimalFormatKey(asString(args[2], ctx), ctx) : "";
132
198
  const format =
133
199
  engine.decimalFormats[formatName] || DEFAULT_DECIMAL_FORMAT;
134
200
  return formatNumber(value, pattern, format);
@@ -160,22 +226,35 @@ export function createXsltFunctions(engine) {
160
226
  : "";
161
227
  },
162
228
 
163
- /** `function-available(name)` - reflects the evaluator function table. */
229
+ /**
230
+ * `function-available(name)` - reflects the evaluator function table. A
231
+ * prefixed name is resolved through the stylesheet's namespace bindings,
232
+ * just as a call of that function would be. A function can report itself
233
+ * unavailable through an `isAvailable()` property (`dyn:evaluate()` does
234
+ * until the engine enables it).
235
+ */
164
236
  "function-available": (args, ctx) => {
165
- const name = asString(args[0], ctx);
166
- return Object.hasOwn(evaluator.functions, name);
237
+ const { prefix, localName } = splitQName(asString(args[0], ctx));
238
+ const fn = evaluator.resolveFunction(localName, prefix, ctx.namespaces);
239
+ return fn !== null && fn.isAvailable?.() !== false;
167
240
  },
168
241
 
169
242
  /** `element-available(name)` - reflects the XSLT elements the engine runs. */
170
243
  "element-available": (args, ctx) => {
171
244
  const { prefix, localName } = splitQName(asString(args[0], ctx));
172
- if (!prefix) return false;
245
+ // An unprefixed name is in the default namespace (XSLT 1.0 section
246
+ // 15), which may be the XSLT namespace itself (libxslt bug-200)
247
+ const namespaceUri = prefix
248
+ ? (ctx.namespaces[prefix] ?? (prefix === "xsl" ? XSLT_NAMESPACE : null))
249
+ : ctx.namespaces[""] || null;
250
+ if (!namespaceUri) return false;
173
251
 
174
- const namespaceUri =
175
- ctx.namespaces[prefix] ?? (prefix === "xsl" ? XSLT_NAMESPACE : null);
176
-
177
- return (
178
- namespaceUri === XSLT_NAMESPACE && isXsltElementAvailable(localName)
252
+ if (namespaceUri === XSLT_NAMESPACE) {
253
+ return isXsltElementAvailable(localName);
254
+ }
255
+ // Extension elements with an implementation (registerExtensionElement)
256
+ return Boolean(
257
+ engine.extensionElements?.has(`{${namespaceUri}}${localName}`),
179
258
  );
180
259
  },
181
260
 
package/src/xslt/index.js CHANGED
@@ -3,7 +3,13 @@
3
3
  * Based on W3C XSLT 1.0 Specification: http://www.w3.org/TR/1999/REC-xslt-19991116
4
4
  */
5
5
 
6
- export { XsltContext, XsltEngine } from "./engine.js";
6
+ export {
7
+ XSLT_MAX_EXPRESSION_DEPTH,
8
+ XSLT_MAX_RESULT_SIZE,
9
+ XSLT_MAX_TEMPLATE_DEPTH,
10
+ XsltContext,
11
+ XsltEngine,
12
+ } from "./engine.js";
7
13
 
8
14
  // XSLT vocabulary and function library (advanced usage)
9
15
  export {
package/src/xslt/keys.js CHANGED
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * `xsl:key` indexing for the XSLT `key()` function.
3
3
  *
4
- * Indexes are built lazily, once per (document, key name) pair, and cached in a
5
- * `WeakMap` so source documents stay garbage collectable. The registry is
4
+ * Indexes are built lazily, once per (root node, key name) pair, and cached in
5
+ * a `WeakMap` so source documents stay garbage collectable. The root node is
6
+ * the document of the context node, or the DocumentFragment of a result tree
7
+ * fragment converted with `exsl:node-set()` (see `rootNodeOf` in xpath/axes). The registry is
6
8
  * deliberately decoupled from the engine: pattern matching and `use` evaluation
7
9
  * are injected as callbacks.
8
10
  */
@@ -10,14 +12,27 @@
10
12
  "use strict";
11
13
 
12
14
  /**
13
- * Lazily built, per-document indexes for all declared keys.
15
+ * @typedef {Object} KeyDefinition
16
+ * @property {string} match - Pattern of the nodes to index
17
+ * @property {string} use - Expression computing the key values
18
+ * @property {Object<string, string>} [namespaces] - Prefixes in scope on the xsl:key element
19
+ */
20
+
21
+ /**
22
+ * @typedef {Object} KeyIndex
23
+ * @property {Map<string, Node[]>} buckets - Key value to nodes, in document order
24
+ * @property {Map<Node, number>} order - Document order position of every node
25
+ */
26
+
27
+ /**
28
+ * Lazily built, per-tree indexes for all declared keys.
14
29
  */
15
30
  export class KeyIndexRegistry {
16
31
  /**
17
32
  * @param {Object} options - Registry configuration
18
- * @param {Object<string, {match: string, use: string}>} options.keys - Declared keys by name
19
- * @param {(node: Node, pattern: string) => boolean} options.matchesPattern - XSLT pattern matcher
20
- * @param {(node: Node, expression: string) => string[]} options.evaluateUse - `use` evaluator returning key values
33
+ * @param {Object<string, (KeyDefinition|KeyDefinition[])>} options.keys - Declared keys by name; several xsl:key elements may share a name
34
+ * @param {(node: Node, pattern: string, definition: KeyDefinition) => boolean} options.matchesPattern - XSLT pattern matcher
35
+ * @param {(node: Node, expression: string, definition: KeyDefinition) => string[]} options.evaluateUse - `use` evaluator returning key values
21
36
  */
22
37
  constructor({ keys, matchesPattern, evaluateUse }) {
23
38
  this.keys = keys;
@@ -43,7 +58,7 @@ export class KeyIndexRegistry {
43
58
  *
44
59
  * @param {string} name - The key name
45
60
  * @param {string|string[]} values - One key value, or several to union
46
- * @param {Document} doc - The document to search
61
+ * @param {Node} doc - Root of the tree to search (Document or DocumentFragment)
47
62
  * @returns {Node[]} Matching nodes in document order, without duplicates
48
63
  * @throws {Error} When the key name was never declared
49
64
  *
@@ -55,25 +70,23 @@ export class KeyIndexRegistry {
55
70
  throw new Error(`Undefined key: ${name}`);
56
71
  }
57
72
 
58
- const index = this.getIndex(name, doc);
73
+ const { buckets, order } = this.getIndex(name, doc);
59
74
  const wanted = Array.isArray(values) ? values : [values];
60
- const result = [];
75
+ if (wanted.length === 1) return [...(buckets.get(wanted[0]) || [])];
61
76
 
77
+ const found = new Set();
62
78
  for (const value of wanted) {
63
- for (const node of index.get(value) || []) {
64
- if (!result.includes(node)) result.push(node);
65
- }
79
+ for (const node of buckets.get(value) || []) found.add(node);
66
80
  }
67
-
68
- return result;
81
+ return [...found].sort((a, b) => order.get(a) - order.get(b));
69
82
  }
70
83
 
71
84
  /**
72
85
  * Get (building if needed) the index of one key for one document.
73
86
  *
74
87
  * @param {string} name - The key name
75
- * @param {Document} doc - The document being indexed
76
- * @returns {Map<string, Node[]>} Key value to nodes
88
+ * @param {Node} doc - Root of the tree being indexed
89
+ * @returns {KeyIndex} The index
77
90
  */
78
91
  getIndex(name, doc) {
79
92
  let byName = this.cache.get(doc);
@@ -92,27 +105,37 @@ export class KeyIndexRegistry {
92
105
  }
93
106
 
94
107
  /**
95
- * Build the index of one key for one document.
108
+ * Build the index of one key for one document, merging every declaration
109
+ * of that key name.
96
110
  *
97
111
  * @param {string} name - The key name
98
- * @param {Document} doc - The document being indexed
99
- * @returns {Map<string, Node[]>} Key value to nodes
112
+ * @param {Node} doc - Root of the tree being indexed
113
+ * @returns {KeyIndex} The index
100
114
  */
101
115
  buildIndex(name, doc) {
102
- const { match, use } = this.keys[name];
103
- const index = new Map();
116
+ const definitions = [this.keys[name]].flat();
117
+ const buckets = new Map();
118
+ const order = new Map();
104
119
 
105
120
  for (const node of documentOrderNodes(doc)) {
106
- if (!this.matchesPattern(node, match)) continue;
107
-
108
- for (const value of this.evaluateUse(node, use)) {
109
- const bucket = index.get(value);
110
- if (bucket) bucket.push(node);
111
- else index.set(value, [node]);
121
+ order.set(node, order.size);
122
+
123
+ for (const definition of definitions) {
124
+ if (!this.matchesPattern(node, definition.match, definition)) continue;
125
+
126
+ for (const value of this.evaluateUse(
127
+ node,
128
+ definition.use,
129
+ definition,
130
+ )) {
131
+ const bucket = buckets.get(value);
132
+ if (!bucket) buckets.set(value, [node]);
133
+ else if (bucket[bucket.length - 1] !== node) bucket.push(node);
134
+ }
112
135
  }
113
136
  }
114
137
 
115
- return index;
138
+ return { buckets, order };
116
139
  }
117
140
  }
118
141
 
@@ -42,10 +42,20 @@ export function lookupNamespaceUri(node, prefix) {
42
42
 
43
43
  /**
44
44
  * The `xsl:namespace-alias` declarations of a stylesheet.
45
+ *
46
+ * Literal result names are aliased as libxslt (and so Chrome) does, see
47
+ * {@link NamespaceAliasMap#resolveLiteral}: the namespace URI is replaced and
48
+ * the prefix written in the stylesheet is kept (`axsl:stylesheet` with
49
+ * `xmlns:axsl` bound to the XSLT namespace); `result-prefix="#default"`
50
+ * without a default namespace in scope gives names in no namespace, and
51
+ * `stylesheet-prefix="#default"` without a default namespace in scope
52
+ * aliases the elements in no namespace, which then take the result prefix.
45
53
  */
46
54
  export class NamespaceAliasMap {
47
55
  constructor() {
48
56
  this.byUri = new Map();
57
+ /** Alias of elements in no namespace (`#default` stylesheet prefix). */
58
+ this.noNamespaceAlias = null;
49
59
  }
50
60
 
51
61
  /**
@@ -62,22 +72,22 @@ export class NamespaceAliasMap {
62
72
  const resultPrefix = node.getAttribute("result-prefix");
63
73
  if (!stylesheetPrefix || !resultPrefix) return;
64
74
 
75
+ const isDefaultSource = stylesheetPrefix === "#default";
65
76
  const fromUri = lookupNamespaceUri(
66
77
  node,
67
- stylesheetPrefix === "#default" ? null : stylesheetPrefix,
78
+ isDefaultSource ? null : stylesheetPrefix,
68
79
  );
69
- if (!fromUri) return;
80
+ if (!fromUri && !isDefaultSource) return;
70
81
 
71
82
  const isDefaultResult = resultPrefix === "#default";
72
83
  const toUri = lookupNamespaceUri(
73
84
  node,
74
85
  isDefaultResult ? null : resultPrefix,
75
86
  );
87
+ const alias = { uri: toUri, prefix: isDefaultResult ? null : resultPrefix };
76
88
 
77
- this.byUri.set(fromUri, {
78
- uri: toUri,
79
- prefix: isDefaultResult ? null : resultPrefix,
80
- });
89
+ if (fromUri) this.byUri.set(fromUri, alias);
90
+ else if (toUri) this.noNamespaceAlias = alias;
81
91
  }
82
92
 
83
93
  /**
@@ -89,7 +99,20 @@ export class NamespaceAliasMap {
89
99
  * aliases.isEmpty();
90
100
  */
91
101
  isEmpty() {
92
- return this.byUri.size === 0;
102
+ return this.byUri.size === 0 && this.noNamespaceAlias === null;
103
+ }
104
+
105
+ /**
106
+ * Whether a namespace is the stylesheet side of an alias.
107
+ *
108
+ * @param {string} uri - A namespace URI
109
+ * @returns {boolean} True when literal result names in it are aliased
110
+ *
111
+ * @example
112
+ * aliases.isAliased('http://www.w3.org/1999/XSL/TransformAlias'); // true
113
+ */
114
+ isAliased(uri) {
115
+ return this.byUri.has(uri);
93
116
  }
94
117
 
95
118
  /**
@@ -112,6 +135,39 @@ export class NamespaceAliasMap {
112
135
  qname: alias.prefix ? `${alias.prefix}:${localName}` : localName,
113
136
  };
114
137
  }
138
+
139
+ /**
140
+ * Apply aliasing to the name of a literal result element or attribute as
141
+ * libxslt does: the namespace URI is replaced and the stylesheet prefix
142
+ * kept. An element in no namespace takes the `#default` stylesheet-prefix
143
+ * alias with its result prefix; an alias to no namespace drops the prefix.
144
+ *
145
+ * @param {Element|Attr} node - The literal result element or attribute
146
+ * @returns {{namespaceUri: (string|null), qname: string}|null} The aliased
147
+ * name, or null when no alias applies
148
+ *
149
+ * @example
150
+ * // xmlns:axsl="urn:alias", <xsl:namespace-alias stylesheet-prefix="axsl"
151
+ * // result-prefix="xsl"/>
152
+ * aliases.resolveLiteral(axslStylesheetElement);
153
+ * // { namespaceUri: 'http://www.w3.org/1999/XSL/Transform', qname: 'axsl:stylesheet' }
154
+ */
155
+ resolveLiteral(node) {
156
+ const { localName } = node;
157
+ if (!node.namespaceURI) {
158
+ const alias = node.nodeType === 1 ? this.noNamespaceAlias : null;
159
+ return alias
160
+ ? { namespaceUri: alias.uri, qname: `${alias.prefix}:${localName}` }
161
+ : null;
162
+ }
163
+ const alias = this.byUri.get(node.namespaceURI);
164
+ if (!alias) return null;
165
+ if (!alias.uri) return { namespaceUri: null, qname: localName };
166
+ return {
167
+ namespaceUri: alias.uri,
168
+ qname: node.prefix ? `${node.prefix}:${localName}` : localName,
169
+ };
170
+ }
115
171
  }
116
172
 
117
173
  /**
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Helpers of the XSLT pattern matcher (see patterns.js): node relationships
3
+ * in the XPath data model and the per-call {@link MatchScope} that builds the
4
+ * XPath contexts predicates and `id()`/`key()` anchors are evaluated in.
5
+ *
6
+ * @module xslt/matchScope
7
+ */
8
+
9
+ "use strict";
10
+
11
+ import { XPathContext } from "../xpath/evaluator.js";
12
+ import { parentOf } from "../xpath/axes.js";
13
+
14
+ const XMLNS_NAMESPACE = "http://www.w3.org/2000/xmlns/";
15
+ const EMPTY_VARIABLES = Object.freeze({});
16
+ const EMPTY_NAMESPACES = Object.freeze({});
17
+
18
+ export { parentOf };
19
+
20
+ /**
21
+ * The root of the tree holding a node (its document, or the top of a
22
+ * detached subtree or result tree fragment).
23
+ *
24
+ * @param {Node} node - Any node
25
+ * @returns {Node} The outermost ancestor
26
+ */
27
+ export function rootOf(node) {
28
+ let root = node;
29
+ for (let parent = parentOf(root); parent; parent = parentOf(parent)) {
30
+ root = parent;
31
+ }
32
+ return root;
33
+ }
34
+
35
+ /**
36
+ * Whether a node is a root node (a document or a result tree fragment).
37
+ *
38
+ * @param {Node|null} node - Any node
39
+ * @returns {boolean} True for document and document fragment nodes
40
+ */
41
+ export function isRoot(node) {
42
+ return node !== null && (node.nodeType === 9 || node.nodeType === 11);
43
+ }
44
+
45
+ /**
46
+ * Whether an attribute is a namespace declaration, which XPath does not
47
+ * expose on the attribute axis.
48
+ *
49
+ * @param {Attr} attribute - An attribute node
50
+ * @returns {boolean} True for `xmlns` and `xmlns:*` attributes
51
+ */
52
+ export function isNamespaceDeclaration(attribute) {
53
+ return (
54
+ attribute.namespaceURI === XMLNS_NAMESPACE ||
55
+ attribute.name === "xmlns" ||
56
+ attribute.name.startsWith("xmlns:")
57
+ );
58
+ }
59
+
60
+ /**
61
+ * State of a single match call: lazily merges the XSLT variables once.
62
+ */
63
+ export class MatchScope {
64
+ /**
65
+ * @param {object|null} host - XSLT context (variables, parameters, namespaces)
66
+ * @param {Object<string, string>} [namespaces] - Prefix bindings, when they
67
+ * differ from the host's (e.g. those in scope on an xsl:template)
68
+ */
69
+ constructor(host, namespaces) {
70
+ this.host = host;
71
+ this.namespaces = namespaces ?? host?.namespaces ?? EMPTY_NAMESPACES;
72
+ this.variables = null;
73
+ }
74
+
75
+ /**
76
+ * Build an XPath context for evaluating a predicate or anchor call.
77
+ *
78
+ * @param {Node} node - Context node
79
+ * @param {number} position - Context position
80
+ * @param {number} size - Context size
81
+ * @returns {XPathContext} The evaluation context
82
+ */
83
+ context(node, position, size) {
84
+ if (!this.variables) {
85
+ this.variables = this.host?.xpathVariables ?? {
86
+ ...this.host?.variables,
87
+ ...this.host?.parameters,
88
+ };
89
+ }
90
+ return new XPathContext(
91
+ node,
92
+ position,
93
+ size,
94
+ this.variables,
95
+ this.namespaces,
96
+ this.host,
97
+ );
98
+ }
99
+
100
+ /**
101
+ * Build a cheap XPath context for node tests (namespaces only).
102
+ *
103
+ * @param {Node} node - Context node
104
+ * @returns {XPathContext} The evaluation context
105
+ */
106
+ testContext(node) {
107
+ return new XPathContext(
108
+ node,
109
+ 1,
110
+ 1,
111
+ EMPTY_VARIABLES,
112
+ this.namespaces,
113
+ this.host,
114
+ );
115
+ }
116
+ }