@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,133 @@
1
+ /**
2
+ * Copying source nodes to the result: xsl:copy and xsl:copy-of.
3
+ *
4
+ * Methods installed on XsltEngine.prototype (`this` is the engine).
5
+ */
6
+
7
+ import {
8
+ cloneNode,
9
+ copyAttribute,
10
+ copyNamespaceNode,
11
+ copyOf,
12
+ shallowCopyElement,
13
+ } from "../copying.js";
14
+
15
+ export const copyInstructionMethods = {
16
+ /**
17
+ * Instantiate `xsl:copy`: a shallow copy of the current node; the content
18
+ * is instantiated into a copied element, or directly for a root node.
19
+ *
20
+ * @param {Element} node - The xsl:copy instruction
21
+ * @param {XsltContext} context - The current context
22
+ * @param {Node} output - The result node receiving the copy
23
+ * @returns {void}
24
+ */
25
+ xslCopy(node, context, output) {
26
+ const currentNode = context.currentNode;
27
+ const useAttributeSets = node.getAttribute("use-attribute-sets");
28
+
29
+ switch (currentNode.nodeType) {
30
+ case 1: {
31
+ // Element, with its namespace declarations (XSLT 1.0 section 7.5)
32
+ const copy = shallowCopyElement(currentNode, context.outputDocument);
33
+ output.appendChild(copy);
34
+
35
+ if (useAttributeSets) {
36
+ this.applyAttributeSets(useAttributeSets, context, copy, node);
37
+ }
38
+
39
+ this.scheduleChildren(node, context, copy);
40
+ break;
41
+ }
42
+
43
+ case 2: // Attribute
44
+ copyAttribute(currentNode, output, (target) =>
45
+ this.canAddAttribute(target),
46
+ );
47
+ break;
48
+
49
+ case 13: // Namespace node: a namespace declaration
50
+ copyNamespaceNode(currentNode, output, (target) =>
51
+ this.canAddAttribute(target),
52
+ );
53
+ break;
54
+
55
+ case 3: // Text
56
+ case 4: // CDATA; a text node stands for its whole text run
57
+ output.appendChild(
58
+ context.outputDocument.createTextNode(
59
+ this.xpathEvaluator.getStringValue(currentNode),
60
+ ),
61
+ );
62
+ break;
63
+
64
+ case 7: // Processing Instruction
65
+ output.appendChild(
66
+ context.outputDocument.createProcessingInstruction(
67
+ currentNode.target,
68
+ currentNode.data,
69
+ ),
70
+ );
71
+ break;
72
+
73
+ case 8: // Comment
74
+ output.appendChild(
75
+ context.outputDocument.createComment(currentNode.nodeValue || ""),
76
+ );
77
+ break;
78
+
79
+ case 9: // Document
80
+ case 11: // Document Fragment
81
+ this.scheduleChildren(node, context, output);
82
+ break;
83
+ }
84
+ },
85
+
86
+ /**
87
+ * Instantiate `xsl:copy-of`: a deep copy of the selected value.
88
+ *
89
+ * @param {Element} node - The xsl:copy-of instruction
90
+ * @param {XsltContext} context - The current context
91
+ * @param {Node} output - The result node receiving the copy
92
+ * @returns {void}
93
+ */
94
+ xslCopyOf(node, context, output) {
95
+ const select = node.getAttribute("select");
96
+ const result = this.evaluateXPath(select, context);
97
+
98
+ this.copyToOutput(result, context, output);
99
+ },
100
+
101
+ /**
102
+ * Copy a value to the result tree as `xsl:copy-of` does (see copying.js).
103
+ *
104
+ * @param {*} value - The value to copy
105
+ * @param {XsltContext} context - The current context
106
+ * @param {Node} output - The result node receiving the copy
107
+ * @returns {void}
108
+ */
109
+ copyToOutput(value, context, output) {
110
+ copyOf(value, output, {
111
+ doc: context.outputDocument,
112
+ stringValue: (node) => this.xpathEvaluator.getStringValue(node),
113
+ toString: (item) => this.xpathEvaluator.toString(item),
114
+ canAddAttribute: (target) => this.canAddAttribute(target),
115
+ });
116
+ },
117
+
118
+ /**
119
+ * Deep copy of a node for the result tree (see copying.js); nodes that
120
+ * cannot be children become an empty text node.
121
+ *
122
+ * @param {Node} node - The node to copy
123
+ * @param {Document} targetDoc - The result document
124
+ * @returns {Node} The copy
125
+ */
126
+ deepCloneNode(node, targetDoc) {
127
+ return (
128
+ cloneNode(node, targetDoc, (text) =>
129
+ this.xpathEvaluator.getStringValue(text),
130
+ ) ?? targetDoc.createTextNode("")
131
+ );
132
+ },
133
+ };
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Top-level declarations other than templates and xsl:output: global
3
+ * variables and parameters, keys, decimal formats, namespace aliases,
4
+ * attribute sets and whitespace stripping.
5
+ *
6
+ * Methods installed on XsltEngine.prototype (`this` is the engine).
7
+ */
8
+
9
+ import { inScopeNamespaces } from "../stylesheetNamespaces.js";
10
+ import { compileSpaceNameTests } from "../spaceNameTests.js";
11
+ import { registerAttributeSet } from "../attributeSets.js";
12
+ import { expandName } from "../declarationNames.js";
13
+ import {
14
+ checkGlobalDuplicate,
15
+ checkLocalBindings,
16
+ checkPattern,
17
+ xsltLocalName,
18
+ } from "../stylesheetChecks.js";
19
+
20
+ /**
21
+ * The properties of an xsl:decimal-format declaration, with their defaults
22
+ * (XSLT 1.0 section 12.3).
23
+ *
24
+ * @param {Element} node - The xsl:decimal-format element
25
+ * @param {number} importPrecedence - Its import precedence
26
+ * @returns {object} The decimal format
27
+ */
28
+ function readDecimalFormat(node, importPrecedence) {
29
+ return {
30
+ importPrecedence,
31
+ decimalSeparator: node.getAttribute("decimal-separator") || ".",
32
+ groupingSeparator: node.getAttribute("grouping-separator") || ",",
33
+ percent: node.getAttribute("percent") || "%",
34
+ perMille: node.getAttribute("per-mille") || "‰",
35
+ zeroDigit: node.getAttribute("zero-digit") || "0",
36
+ digit: node.getAttribute("digit") || "#",
37
+ patternSeparator: node.getAttribute("pattern-separator") || ";",
38
+ infinity: node.getAttribute("infinity") || "Infinity",
39
+ nan: node.getAttribute("NaN") || "NaN",
40
+ minusSign: node.getAttribute("minus-sign") || "-",
41
+ };
42
+ }
43
+
44
+ export const declarationMethods = {
45
+ /**
46
+ * Register an `xsl:variable` top level declaration.
47
+ *
48
+ * @param {Element} node - The `xsl:variable` element
49
+ * @returns {void}
50
+ */
51
+ processGlobalVariable(node) {
52
+ const name = node.getAttribute("name");
53
+ this.checkGlobalBinding(name, node);
54
+ this.globalVariables[name] = {
55
+ node,
56
+ select: node.getAttribute("select"),
57
+ importPrecedence: this.currentImportPrecedence,
58
+ };
59
+ },
60
+
61
+ /**
62
+ * Warn about a global variable or parameter declared twice at the same
63
+ * import precedence (an error in XSLT 1.0 section 11.4 that libxslt
64
+ * reports) and about duplicate local bindings in its content. The later
65
+ * declaration is used, or the xsl:variable when a parameter has the same
66
+ * name and import precedence.
67
+ *
68
+ * @param {string} name - The declared name
69
+ * @param {Element} node - The xsl:variable or xsl:param element
70
+ * @returns {void}
71
+ */
72
+ checkGlobalBinding(name, node) {
73
+ const warn = (message) => this.warnOnce(message);
74
+ checkLocalBindings(node, warn);
75
+ checkGlobalDuplicate(
76
+ {
77
+ name,
78
+ kind: xsltLocalName(node),
79
+ precedence: this.currentImportPrecedence,
80
+ },
81
+ {
82
+ variable: this.globalVariables[name],
83
+ param: this.globalParameters[name],
84
+ },
85
+ warn,
86
+ );
87
+ },
88
+
89
+ /**
90
+ * Register an `xsl:param` top level declaration.
91
+ *
92
+ * A value supplied from outside (through `setParameter`) has precedence over
93
+ * the declared default, so it survives compilation of the stylesheet.
94
+ *
95
+ * @param {Element} node - The `xsl:param` element
96
+ * @returns {void}
97
+ */
98
+ processGlobalParam(node) {
99
+ const name = node.getAttribute("name");
100
+ this.checkGlobalBinding(name, node);
101
+ const existing = this.globalParameters[name];
102
+ const definition = {
103
+ node,
104
+ select: node.getAttribute("select"),
105
+ importPrecedence: this.currentImportPrecedence,
106
+ };
107
+
108
+ if (existing && "value" in existing) {
109
+ definition.value = existing.value;
110
+ }
111
+
112
+ this.globalParameters[name] = definition;
113
+ },
114
+
115
+ /**
116
+ * Register an xsl:key declaration. Declarations sharing a name all
117
+ * contribute to the same key (XSLT 1.0 section 12.2); a declaration seen
118
+ * again through another import branch is only kept once.
119
+ *
120
+ * @param {Element} node - The xsl:key element
121
+ * @returns {void}
122
+ */
123
+ processKey(node) {
124
+ const name = node.getAttribute("name");
125
+ const match = node.getAttribute("match");
126
+ const use = node.getAttribute("use");
127
+ if (match !== null) {
128
+ checkPattern(this.patternMatcher, match, "xsl:key match");
129
+ }
130
+ const definitions = (this.keys[name] ??= []);
131
+
132
+ if (!definitions.some((d) => d.match === match && d.use === use)) {
133
+ definitions.push({ match, use, namespaces: inScopeNamespaces(node) });
134
+ }
135
+ this.keyRegistry.clear();
136
+ },
137
+
138
+ /**
139
+ * Register an xsl:decimal-format declaration under the expanded name of
140
+ * its `name` (XSLT 1.0 section 12.3). Like libxslt, an invalid name or an
141
+ * undeclared prefix is reported and the declaration ignored, and a second
142
+ * declaration of the same name and import precedence is reported and
143
+ * ignored; one of a higher import precedence replaces an imported one.
144
+ *
145
+ * @param {Element} node - The xsl:decimal-format element
146
+ * @returns {void}
147
+ */
148
+ processDecimalFormat(node) {
149
+ const name = node.getAttribute("name");
150
+ let key = "";
151
+ if (name !== null) {
152
+ const expanded = expandName(name, node);
153
+ if (expanded.error) {
154
+ this.warnOnce(
155
+ `xsl:decimal-format: ${expanded.error}; the declaration is ignored`,
156
+ );
157
+ return;
158
+ }
159
+ key = expanded.key;
160
+ const existing = this.decimalFormats[key];
161
+ if (existing?.importPrecedence === this.currentImportPrecedence) {
162
+ this.warnOnce(
163
+ `xsl:decimal-format: "${name}" is already declared; the first declaration is used`,
164
+ );
165
+ return;
166
+ }
167
+ }
168
+
169
+ this.decimalFormats[key] = readDecimalFormat(
170
+ node,
171
+ this.currentImportPrecedence,
172
+ );
173
+ },
174
+
175
+ /**
176
+ * Register an xsl:namespace-alias declaration.
177
+ *
178
+ * @param {Element} node - The xsl:namespace-alias element
179
+ * @returns {void}
180
+ */
181
+ processNamespaceAlias(node) {
182
+ this.namespaceAliases.add(node);
183
+ },
184
+
185
+ /**
186
+ * Register an xsl:attribute-set declaration; declarations of the same
187
+ * expanded name are merged (see attributeSets.js).
188
+ *
189
+ * @param {Element} node - The xsl:attribute-set element
190
+ * @returns {void}
191
+ */
192
+ processAttributeSet(node) {
193
+ checkLocalBindings(node, (message) => this.warnOnce(message));
194
+ registerAttributeSet(
195
+ this.attributeSets,
196
+ node,
197
+ this.currentImportPrecedence,
198
+ );
199
+ },
200
+
201
+ /**
202
+ * Register the name tests of an xsl:strip-space element, expanded with
203
+ * its namespace declarations (see spaceNameTests.js).
204
+ *
205
+ * @param {Element} node - The xsl:strip-space element
206
+ * @returns {void}
207
+ */
208
+ processStripSpace(node) {
209
+ this.stripSpace.push(...this.spaceNameTests(node));
210
+ },
211
+
212
+ /**
213
+ * Register the name tests of an xsl:preserve-space element.
214
+ *
215
+ * @param {Element} node - The xsl:preserve-space element
216
+ * @returns {void}
217
+ */
218
+ processPreserveSpace(node) {
219
+ this.preserveSpace.push(...this.spaceNameTests(node));
220
+ },
221
+
222
+ /**
223
+ * Expand the `elements` name tests of xsl:strip-space/preserve-space.
224
+ *
225
+ * @param {Element} node - The declaring element
226
+ * @returns {object[]} The expanded name tests
227
+ */
228
+ spaceNameTests(node) {
229
+ return compileSpaceNameTests(node.getAttribute("elements"), node, (m) =>
230
+ this.warnOnce(m),
231
+ );
232
+ },
233
+ };
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Engine services behind the XSLT functions: document(), generate-id()
3
+ * and key() (see functions.js).
4
+ *
5
+ * Methods installed on XsltEngine.prototype (`this` is the engine).
6
+ */
7
+
8
+ import { resolveUri, stripFragment } from "../uri.js";
9
+
10
+ export const functionSupportMethods = {
11
+ /**
12
+ * Set the loader used by the XSLT `document()` function.
13
+ *
14
+ * The loader is synchronous and must return a `Document`, an XML string or
15
+ * null. Returning null (or configuring no loader at all) makes `document()`
16
+ * evaluate to an empty node-set instead of failing the transformation.
17
+ *
18
+ * @param {((uri: string, baseUri?: string) => (Document|string|null))|null} loader - The loader, or null to remove it
19
+ * @returns {XsltEngine} This engine, to allow chaining
20
+ *
21
+ * @example
22
+ * engine.setDocumentLoader((uri) => readFileSync(uri, 'utf8'));
23
+ */
24
+ setDocumentLoader(loader) {
25
+ this.documentLoader = loader ?? null;
26
+ this.loadedDocuments.clear();
27
+ return this;
28
+ },
29
+
30
+ /**
31
+ * Load an external document for the `document()` function.
32
+ *
33
+ * Results are cached per resolved URI for the life of the engine, so the same
34
+ * URI always yields the identical node-set.
35
+ *
36
+ * @param {string} uri - The requested URI, fragment identifiers are ignored
37
+ * @param {string} [baseUri] - Base URI used to resolve relative references
38
+ * @returns {Document|null} The loaded document, or null when unavailable
39
+ *
40
+ * @example
41
+ * engine.loadDocument('data.xml', '/styles/main.xsl');
42
+ */
43
+ loadDocument(uri, baseUri) {
44
+ const target = stripFragment(uri);
45
+
46
+ if (target === "") return this.stylesheetDoc;
47
+ if (!this.documentLoader) return null;
48
+
49
+ const resolved = resolveUri(target, baseUri);
50
+ if (this.loadedDocuments.has(resolved)) {
51
+ return this.loadedDocuments.get(resolved);
52
+ }
53
+
54
+ const loaded = this.documentLoader(resolved, baseUri);
55
+ const doc =
56
+ typeof loaded === "string" ? this.parseXmlString(loaded) : loaded || null;
57
+
58
+ this.loadedDocuments.set(resolved, doc);
59
+ return doc;
60
+ },
61
+
62
+ /**
63
+ * Return the stable identifier of a node for `generate-id()`.
64
+ *
65
+ * @param {Node} node - The node to identify
66
+ * @returns {string} An identifier starting with a letter
67
+ *
68
+ * @example
69
+ * engine.generateId(element); // 'N1'
70
+ */
71
+ generateId(node) {
72
+ let id = this.generatedIds.get(node);
73
+ if (!id) {
74
+ this.generatedIdCount++;
75
+ id = `N${this.generatedIdCount}`;
76
+ this.generatedIds.set(node, id);
77
+ }
78
+ return id;
79
+ },
80
+
81
+ /**
82
+ * Evaluate the `use` expression of an `xsl:key` for one node.
83
+ *
84
+ * @param {Node} node - The node being indexed
85
+ * @param {string} expression - The `use` expression
86
+ * @param {Object<string, string>} [namespaces] - Prefixes in scope on the xsl:key
87
+ * @returns {string[]} The key values contributed by the node
88
+ */
89
+ evaluateKeyValues(node, expression, namespaces) {
90
+ const context = this.rootContext.clone({
91
+ currentNode: node,
92
+ currentNodeList: [node],
93
+ position: 1,
94
+ });
95
+ if (namespaces) context.namespaces = namespaces;
96
+ const value = this.evaluateXPath(expression, context);
97
+
98
+ if (Array.isArray(value)) {
99
+ return value.map((item) => this.xpathEvaluator.getStringValue(item));
100
+ }
101
+ return [this.xpathEvaluator.toString(value)];
102
+ },
103
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Installation of the XsltEngine method groups.
3
+ *
4
+ * The engine is split by concern into modules that each export a plain
5
+ * object of methods (`this` is the engine). They are installed on
6
+ * `XsltEngine.prototype`, so every method stays callable on the engine under
7
+ * its old name and the instruction dispatch (`this[method](...)`) costs no
8
+ * extra stack frame per recursion level.
9
+ */
10
+
11
+ /**
12
+ * Define the methods (and accessors) of each group on a prototype, as
13
+ * non-enumerable properties like class methods.
14
+ *
15
+ * @param {object} prototype - The class prototype receiving the methods
16
+ * @param {...object} groups - Objects of methods, getters included
17
+ * @returns {void}
18
+ * @throws {Error} When two groups define the same name
19
+ */
20
+ export function installMethods(prototype, ...groups) {
21
+ for (const group of groups) {
22
+ const descriptors = Object.getOwnPropertyDescriptors(group);
23
+ for (const [name, descriptor] of Object.entries(descriptors)) {
24
+ if (Object.hasOwn(prototype, name)) {
25
+ throw new Error(`Engine method defined twice: ${name}`);
26
+ }
27
+ Object.defineProperty(prototype, name, {
28
+ ...descriptor,
29
+ enumerable: false,
30
+ });
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Creation of result elements and attributes: literal result elements,
3
+ * xsl:element, xsl:attribute and attribute sets.
4
+ *
5
+ * Methods installed on XsltEngine.prototype (`this` is the engine).
6
+ */
7
+
8
+ import { XSLT_NAMESPACE } from "../elements.js";
9
+ import { getXsltAttribute, shouldCopyAttribute } from "../literalResult.js";
10
+ import {
11
+ copyLiteralNamespaces,
12
+ setResultAttribute,
13
+ } from "../resultNamespaces.js";
14
+ import {
15
+ computedAttributeName,
16
+ computedElementName,
17
+ } from "../computedNames.js";
18
+ import { applyAttributeSets } from "../attributeSets.js";
19
+ import { requireExpandedNames } from "../declarationNames.js";
20
+
21
+ export const nodeConstructionMethods = {
22
+ /**
23
+ * Copy one attribute of a literal result element to the result, evaluating
24
+ * it as an attribute value template and applying xsl:namespace-alias.
25
+ *
26
+ * @param {Attr} attr - Attribute of the literal result element
27
+ * @param {XsltContext} context - The current XSLT context
28
+ * @param {Element} outputElement - The result element
29
+ * @returns {void}
30
+ */
31
+ copyLiteralAttribute(attr, context, outputElement) {
32
+ const value = this.processAttributeValueTemplate(attr.value, context);
33
+ const alias = this.namespaceAliases.resolveLiteral(attr);
34
+
35
+ if (alias) {
36
+ outputElement.setAttributeNS(alias.namespaceUri, alias.qname, value);
37
+ } else if (attr.namespaceURI) {
38
+ outputElement.setAttributeNS(attr.namespaceURI, attr.name, value);
39
+ } else {
40
+ outputElement.setAttribute(attr.name, value);
41
+ }
42
+ },
43
+
44
+ /**
45
+ * Process a literal result element (non-XSLT)
46
+ *
47
+ * Applies `xsl:namespace-alias` to the element and its attributes, honours
48
+ * `xsl:use-attribute-sets` and keeps XSLT-only attributes and namespace
49
+ * declarations out of the result tree.
50
+ *
51
+ * @param {Element} node - The literal result element in the stylesheet
52
+ * @param {XsltContext} context - The current XSLT context
53
+ * @param {Node} output - The result tree node receiving the element
54
+ * @returns {void}
55
+ */
56
+ processLiteralResultElement(node, context, output) {
57
+ const alias = this.namespaceAliases.resolveLiteral(node);
58
+ const namespaceUri = alias ? alias.namespaceUri : node.namespaceURI;
59
+ const qname = alias ? alias.qname : node.nodeName;
60
+
61
+ const outputElement =
62
+ namespaceUri && context.outputDocument.createElementNS
63
+ ? context.outputDocument.createElementNS(namespaceUri, qname)
64
+ : context.outputDocument.createElement(qname);
65
+ // Attached first, so namespace lookups see the result ancestors
66
+ output.appendChild(outputElement);
67
+ copyLiteralNamespaces(outputElement, node, output, (uri) =>
68
+ this.namespaceAliases.isAliased(uri),
69
+ );
70
+
71
+ // Attribute sets come first so literal attributes take precedence
72
+ const useAttributeSets = getXsltAttribute(
73
+ node,
74
+ "use-attribute-sets",
75
+ XSLT_NAMESPACE,
76
+ );
77
+ if (useAttributeSets) {
78
+ this.applyAttributeSets(useAttributeSets, context, outputElement, node);
79
+ }
80
+
81
+ for (const attr of node.attributes) {
82
+ if (shouldCopyAttribute(attr, XSLT_NAMESPACE)) {
83
+ this.copyLiteralAttribute(attr, context, outputElement);
84
+ }
85
+ }
86
+
87
+ this.scheduleChildren(node, context, outputElement);
88
+ },
89
+
90
+ /**
91
+ * Instantiate `xsl:element`. The name's prefix, or the default namespace
92
+ * for an unprefixed name, resolves against the namespaces in scope on the
93
+ * instruction; a `namespace` attribute wins (XSLT 1.0 section 7.1.2).
94
+ * An invalid name or an undeclared prefix is reported and, as in libxslt,
95
+ * neither the element nor its content is created.
96
+ *
97
+ * @param {Element} node - The xsl:element instruction
98
+ * @param {XsltContext} context - The current context
99
+ * @param {Node} output - The result node receiving the element
100
+ * @returns {void}
101
+ */
102
+ xslElement(node, context, output) {
103
+ const { name, error } = computedElementName(
104
+ this.processAttributeValueTemplate(node.getAttribute("name"), context),
105
+ this.optionalAvt(node, "namespace", context),
106
+ context.namespaces,
107
+ );
108
+ if (error) {
109
+ this.warnOnce(error);
110
+ return;
111
+ }
112
+ const { namespaceUri, qname } = name;
113
+ const element = namespaceUri
114
+ ? context.outputDocument.createElementNS(namespaceUri, qname)
115
+ : context.outputDocument.createElement(qname);
116
+ output.appendChild(element);
117
+
118
+ const useAttributeSets = node.getAttribute("use-attribute-sets");
119
+ if (useAttributeSets) {
120
+ this.applyAttributeSets(useAttributeSets, context, element, node);
121
+ }
122
+
123
+ this.scheduleChildren(node, context, element);
124
+ },
125
+
126
+ /**
127
+ * Instantiate `xsl:attribute` (XSLT 1.0 section 7.1.3): a prefixed name
128
+ * uses the namespace bound in scope, `namespace` wins, and a namespaced
129
+ * attribute without a usable prefix gets a generated one. An invalid name,
130
+ * an undeclared prefix or `xmlns` is reported and the attribute skipped.
131
+ *
132
+ * @param {Element} node - The xsl:attribute instruction
133
+ * @param {XsltContext} context - The current context
134
+ * @param {Node} output - The result element receiving the attribute
135
+ * @returns {void}
136
+ */
137
+ xslAttribute(node, context, output) {
138
+ if (!this.canAddAttribute(output)) return;
139
+
140
+ const { name, error } = computedAttributeName(
141
+ this.processAttributeValueTemplate(node.getAttribute("name"), context),
142
+ this.optionalAvt(node, "namespace", context),
143
+ context.namespaces,
144
+ );
145
+ if (error) {
146
+ this.warnOnce(error);
147
+ return;
148
+ }
149
+ setResultAttribute(output, name, this.instantiateText(node, context));
150
+ },
151
+
152
+ /**
153
+ * Whether an attribute may still be added to a result node: it must be an
154
+ * element without children. Attributes added after children are ignored
155
+ * (as libxslt does), with a single warning per engine.
156
+ *
157
+ * @param {Node} target - The result node
158
+ * @returns {boolean} True when the attribute can be added
159
+ */
160
+ canAddAttribute(target) {
161
+ if (target.nodeType !== 1) return false;
162
+ if (!target.firstChild) return true;
163
+ this.warnOnce(
164
+ "an attribute created after the children of an element is ignored (XSLT 1.0 section 7.1.3)",
165
+ );
166
+ return false;
167
+ },
168
+
169
+ /**
170
+ * Apply the attribute sets named by a `use-attribute-sets` attribute (see
171
+ * attributeSets.js). The xsl:attribute children of each declaration are
172
+ * instantiated with the prefixes in scope there.
173
+ *
174
+ * @param {string} names - Whitespace separated QNames
175
+ * @param {XsltContext} context - Context of the instruction
176
+ * @param {Element} element - The result element receiving the attributes
177
+ * @param {Element} instruction - The element carrying the attribute, whose
178
+ * namespace declarations expand the names
179
+ * @returns {void}
180
+ */
181
+ applyAttributeSets(names, context, element, instruction) {
182
+ const keys = requireExpandedNames(names, instruction, "use-attribute-sets");
183
+ applyAttributeSets(this.attributeSets, keys, (declaration) =>
184
+ this.processChildren(declaration, context, element),
185
+ );
186
+ },
187
+ };