@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
@@ -3,6 +3,28 @@
3
3
  * Based on W3C XSLT 1.0 Specification: http://www.w3.org/TR/1999/REC-xslt-19991116
4
4
  *
5
5
  * Processes XSLT stylesheets and transforms XML documents.
6
+ *
7
+ * XsltEngine is a facade: it owns the engine state and the helpers every
8
+ * part uses (XPath evaluation, XSLT element tests, warnings), while the
9
+ * methods live in modules under ./engine/ by concern and are installed on
10
+ * its prototype (see engine/methods.js):
11
+ *
12
+ * - stylesheetLoading.js: importStylesheet, xsl:import/xsl:include
13
+ * - topLevel.js: dispatch of top-level elements, simplified stylesheets
14
+ * - declarations.js, outputDeclaration.js: top-level declarations
15
+ * - templateRules.js: template registry and matching
16
+ * - templateInvocation.js: apply-templates, apply-imports, call-template
17
+ * - sequenceConstructor.js: instruction dispatch, fallback, extensions
18
+ * - workStack.js: the explicit stack instantiating templates and their
19
+ * content without deep JavaScript recursion, and the template depth limit
20
+ * - controlFlow.js: if, choose, for-each, sorting
21
+ * - bindings.js: variables and parameters
22
+ * - textInstructions.js, numbering.js: value-of, text, comment, PI,
23
+ * message, number
24
+ * - nodeConstruction.js, copyInstructions.js: literal result elements,
25
+ * element, attribute, attribute sets, copy, copy-of
26
+ * - transformation.js: transform entry points and the result tree
27
+ * - functionSupport.js: document(), generate-id() and key() services
6
28
  */
7
29
 
8
30
  import { parse as parseXPath } from "../xpath/parser.js";
@@ -10,106 +32,94 @@ import { XPathEvaluator, XPathContext } from "../xpath/evaluator.js";
10
32
  import { XSLT_NAMESPACE } from "./elements.js";
11
33
  import { createXsltFunctions } from "./functions.js";
12
34
  import { KeyIndexRegistry } from "./keys.js";
13
- import { countXsltNumber } from "./number.js";
14
- import { formatXsltNumber, toRoman } from "./numberFormat.js";
15
- import { resolveUri, stripFragment } from "./uri.js";
16
- import { WhitespaceFilter, stripWhitespaceNodes } from "./whitespace.js";
35
+ import { NamespaceAliasMap } from "./literalResult.js";
36
+ import { PatternMatcher } from "./patterns.js";
37
+ import { installMethods } from "./engine/methods.js";
17
38
  import {
18
- NamespaceAliasMap,
19
- getXsltAttribute,
20
- shouldCopyAttribute,
21
- } from "./literalResult.js";
22
- import { createResultDocument, importResultFragment } from "./resultTree.js";
23
- import { calculatePriority } from "./templatePriority.js";
24
- import { serializeResult } from "./serializer.js";
39
+ createOutputSettings,
40
+ outputDeclarationMethods,
41
+ } from "./engine/outputDeclaration.js";
42
+ import { stylesheetLoadingMethods } from "./engine/stylesheetLoading.js";
43
+ import { topLevelMethods } from "./engine/topLevel.js";
44
+ import { declarationMethods } from "./engine/declarations.js";
45
+ import { templateRuleMethods } from "./engine/templateRules.js";
46
+ import { templateInvocationMethods } from "./engine/templateInvocation.js";
47
+ import { sequenceConstructorMethods } from "./engine/sequenceConstructor.js";
48
+ import { controlFlowMethods } from "./engine/controlFlow.js";
49
+ import { bindingMethods } from "./engine/bindings.js";
50
+ import { textInstructionMethods } from "./engine/textInstructions.js";
51
+ import { numberingMethods } from "./engine/numbering.js";
52
+ import { nodeConstructionMethods } from "./engine/nodeConstruction.js";
53
+ import { copyInstructionMethods } from "./engine/copyInstructions.js";
54
+ import { transformationMethods } from "./engine/transformation.js";
55
+ import { functionSupportMethods } from "./engine/functionSupport.js";
56
+ import {
57
+ XSLT_MAX_TEMPLATE_DEPTH,
58
+ workStackMethods,
59
+ } from "./engine/workStack.js";
25
60
 
26
- const XSLT_NS = XSLT_NAMESPACE;
61
+ export { XsltContext } from "./engine/context.js";
62
+ export { XSLT_MAX_TEMPLATE_DEPTH };
27
63
 
28
64
  /**
29
- * XSLT Processing Context
65
+ * Largest node-set a single XPath step may produce inside a transformation.
66
+ *
67
+ * The standalone XPath API keeps its low default (XPathLimits.MAX_RESULT_SIZE)
68
+ * as a guard against untrusted expressions. A stylesheet is trusted program
69
+ * code and commonly walks documents with tens of thousands of nodes, which the
70
+ * native XSLTProcessor handles without any limit, so the engine uses a much
71
+ * higher bound. Override it with the `maxResultSize` engine option.
30
72
  */
31
- export class XsltContext {
32
- constructor(options = {}) {
33
- this.currentNode = options.currentNode;
34
- this.currentNodeList = options.currentNodeList || [];
35
- this.position = options.position || 1;
36
- this.variables = { ...options.variables };
37
- this.parameters = { ...options.parameters };
38
- this.outputDocument = options.outputDocument;
39
- this.stylesheet = options.stylesheet;
40
- this.namespaces = { ...options.namespaces };
41
- this.templates = options.templates || [];
42
- this.keys = options.keys || {};
43
- this.decimalFormats = options.decimalFormats || {};
44
- this.outputMethod = options.outputMethod || "xml";
45
- this.xpathEvaluator = options.xpathEvaluator || new XPathEvaluator();
46
- this.currentTemplate = options.currentTemplate || null;
47
- this.currentMode = options.currentMode ?? null;
48
- }
49
-
50
- clone(overrides = {}) {
51
- return new XsltContext({
52
- currentNode: overrides.currentNode ?? this.currentNode,
53
- currentNodeList: overrides.currentNodeList ?? this.currentNodeList,
54
- position: overrides.position ?? this.position,
55
- variables: overrides.variables
56
- ? { ...this.variables, ...overrides.variables }
57
- : { ...this.variables },
58
- parameters: overrides.parameters
59
- ? { ...this.parameters, ...overrides.parameters }
60
- : { ...this.parameters },
61
- outputDocument: this.outputDocument,
62
- stylesheet: this.stylesheet,
63
- namespaces: overrides.namespaces
64
- ? { ...this.namespaces, ...overrides.namespaces }
65
- : { ...this.namespaces },
66
- templates: this.templates,
67
- keys: this.keys,
68
- decimalFormats: this.decimalFormats,
69
- outputMethod: this.outputMethod,
70
- xpathEvaluator: this.xpathEvaluator,
71
- currentTemplate: overrides.currentTemplate ?? this.currentTemplate,
72
- currentMode: overrides.currentMode ?? this.currentMode,
73
- });
74
- }
75
-
76
- getVariable(name) {
77
- if (name in this.variables) {
78
- return this.variables[name];
79
- }
80
- if (name in this.parameters) {
81
- return this.parameters[name];
82
- }
83
- throw new Error(`Undefined variable: $${name}`);
84
- }
73
+ export const XSLT_MAX_RESULT_SIZE = 5000000;
85
74
 
86
- setVariable(name, value) {
87
- this.variables[name] = value;
88
- }
89
- }
75
+ /**
76
+ * Deepest XPath expression nesting allowed inside a transformation.
77
+ *
78
+ * Like XSLT_MAX_RESULT_SIZE, this is higher than the standalone XPath default
79
+ * (XPathLimits.MAX_RECURSION_DEPTH = 100) because stylesheets are trusted and
80
+ * generated ones often contain long chains such as `a + b + c + ...`.
81
+ * Override it with the `maxRecursionDepth` engine option.
82
+ */
83
+ export const XSLT_MAX_EXPRESSION_DEPTH = 1000;
90
84
 
91
85
  /**
92
86
  * XSLT Engine
93
87
  */
94
88
  export class XsltEngine {
89
+ /**
90
+ * @param {object} [options] - Engine options
91
+ * @param {number} [options.maxResultSize] - Largest node-set of one XPath step
92
+ * @param {number} [options.maxRecursionDepth] - Deepest XPath expression nesting
93
+ * @param {number} [options.maxTemplateDepth] - Deepest nesting of template
94
+ * instantiations (default XSLT_MAX_TEMPLATE_DEPTH, 3000 as in libxslt)
95
+ * @param {boolean} [options.legacyNameTests] - Deprecated: unprefixed name
96
+ * tests also match nodes in a namespace, as before 1.2.0
97
+ * @param {Function} [options.stylesheetLoader] - Loader for xsl:import/include
98
+ * @param {Function} [options.documentLoader] - Loader for document()
99
+ * @param {string} [options.baseUri] - Base URI of the stylesheet
100
+ * @param {object} [options.domParser] - Parser for loaded XML strings
101
+ * @param {boolean} [options.enableDynamicEvaluate] - Allow EXSLT
102
+ * `dyn:evaluate()` (off by default: it evaluates XPath built from data)
103
+ * @param {() => Date} [options.clock] - Clock for EXSLT current-time functions
104
+ */
95
105
  constructor(options = {}) {
96
- this.xpathEvaluator = new XPathEvaluator();
106
+ this.enableDynamicEvaluate = options.enableDynamicEvaluate === true;
107
+ this.clock = options.clock ?? null;
108
+ this.xpathEvaluator = new XPathEvaluator({
109
+ maxResultSize: options.maxResultSize ?? XSLT_MAX_RESULT_SIZE,
110
+ maxRecursionDepth: options.maxRecursionDepth ?? XSLT_MAX_EXPRESSION_DEPTH,
111
+ legacyNameTests: options.legacyNameTests,
112
+ });
113
+ // Template instantiation (see engine/workStack.js)
114
+ this.maxTemplateDepth = options.maxTemplateDepth ?? XSLT_MAX_TEMPLATE_DEPTH;
115
+ this.frames = null;
116
+ this.templateDepth = 0;
117
+
97
118
  this.templates = [];
98
119
  this.keys = {};
99
120
  this.globalVariables = {};
100
121
  this.globalParameters = {};
101
- this.outputSettings = {
102
- method: "xml",
103
- version: "1.0",
104
- encoding: "UTF-8",
105
- standalone: null,
106
- indent: "no",
107
- omitXmlDeclaration: "no",
108
- doctypePublic: null,
109
- doctypeSystem: null,
110
- mediaType: null,
111
- cdataSectionElements: [],
112
- };
122
+ this.outputSettings = createOutputSettings();
113
123
  this.namespaces = {};
114
124
  this.decimalFormats = {};
115
125
  this.stylesheetDoc = null;
@@ -121,1982 +131,128 @@ export class XsltEngine {
121
131
  // Import/Include support
122
132
  this.stylesheetLoader = options.stylesheetLoader || null;
123
133
  this.currentImportPrecedence = 0;
124
- this.processedStylesheets = new Set();
134
+ // URIs of the stylesheets being loaded, used to detect import cycles
135
+ this.stylesheetStack = [];
125
136
  this.baseUri = options.baseUri || "";
126
137
 
127
138
  // document() support
128
139
  this.documentLoader = options.documentLoader || null;
129
140
  this.loadedDocuments = new Map();
130
141
 
142
+ // Parser for XML strings returned by the loaders (see domParsing.js)
143
+ this.domParser = options.domParser ?? null;
144
+
131
145
  // generate-id() support
132
146
  this.generatedIds = new WeakMap();
133
147
  this.generatedIdCount = 0;
134
148
 
149
+ // Compiled XSLT patterns (template match, xsl:key, xsl:number)
150
+ this.patternMatcher = new PatternMatcher(this.xpathEvaluator);
151
+
135
152
  // key() support
136
153
  this.rootContext = null;
137
154
  this.keyRegistry = new KeyIndexRegistry({
138
155
  keys: this.keys,
139
- matchesPattern: (node, pattern) =>
140
- this.matchesPattern(node, pattern, this.rootContext),
141
- evaluateUse: (node, expression) =>
142
- this.evaluateKeyValues(node, expression),
143
- });
144
-
145
- this.xpathEvaluator.registerFunctions(createXsltFunctions(this));
146
- }
147
-
148
- /**
149
- * Set the loader used by the XSLT `document()` function.
150
- *
151
- * The loader is synchronous and must return a `Document`, an XML string or
152
- * null. Returning null (or configuring no loader at all) makes `document()`
153
- * evaluate to an empty node-set instead of failing the transformation.
154
- *
155
- * @param {((uri: string, baseUri?: string) => (Document|string|null))|null} loader - The loader, or null to remove it
156
- * @returns {XsltEngine} This engine, to allow chaining
157
- *
158
- * @example
159
- * engine.setDocumentLoader((uri) => readFileSync(uri, 'utf8'));
160
- */
161
- setDocumentLoader(loader) {
162
- this.documentLoader = loader ?? null;
163
- this.loadedDocuments.clear();
164
- return this;
165
- }
166
-
167
- /**
168
- * Load an external document for the `document()` function.
169
- *
170
- * Results are cached per resolved URI for the life of the engine, so the same
171
- * URI always yields the identical node-set.
172
- *
173
- * @param {string} uri - The requested URI, fragment identifiers are ignored
174
- * @param {string} [baseUri] - Base URI used to resolve relative references
175
- * @returns {Document|null} The loaded document, or null when unavailable
176
- *
177
- * @example
178
- * engine.loadDocument('data.xml', '/styles/main.xsl');
179
- */
180
- loadDocument(uri, baseUri) {
181
- const target = stripFragment(uri);
182
-
183
- if (target === "") return this.stylesheetDoc;
184
- if (!this.documentLoader) return null;
185
-
186
- const resolved = resolveUri(target, baseUri);
187
- if (this.loadedDocuments.has(resolved)) {
188
- return this.loadedDocuments.get(resolved);
189
- }
190
-
191
- const loaded = this.documentLoader(resolved, baseUri);
192
- const doc =
193
- typeof loaded === "string" ? this.parseXmlString(loaded) : loaded || null;
194
-
195
- this.loadedDocuments.set(resolved, doc);
196
- return doc;
197
- }
198
-
199
- /**
200
- * Return the stable identifier of a node for `generate-id()`.
201
- *
202
- * @param {Node} node - The node to identify
203
- * @returns {string} An identifier starting with a letter
204
- *
205
- * @example
206
- * engine.generateId(element); // 'N1'
207
- */
208
- generateId(node) {
209
- let id = this.generatedIds.get(node);
210
- if (!id) {
211
- this.generatedIdCount++;
212
- id = `N${this.generatedIdCount}`;
213
- this.generatedIds.set(node, id);
214
- }
215
- return id;
216
- }
217
-
218
- /**
219
- * Evaluate the `use` expression of an `xsl:key` for one node.
220
- *
221
- * @param {Node} node - The node being indexed
222
- * @param {string} expression - The `use` expression
223
- * @returns {string[]} The key values contributed by the node
224
- */
225
- evaluateKeyValues(node, expression) {
226
- const context = this.rootContext.clone({
227
- currentNode: node,
228
- currentNodeList: [node],
229
- position: 1,
230
- });
231
- const value = this.evaluateXPath(expression, context);
232
-
233
- if (Array.isArray(value)) {
234
- return value.map((item) => this.xpathEvaluator.getStringValue(item));
235
- }
236
- return [this.xpathEvaluator.toString(value)];
237
- }
238
-
239
- /**
240
- * Set the stylesheet loader function for xsl:import and xsl:include
241
- * @param {Function} loader - Function(href, baseUri) => Document or string (XML)
242
- */
243
- setStylesheetLoader(loader) {
244
- this.stylesheetLoader = loader;
245
- }
246
-
247
- /**
248
- * Resolve a relative URI against a base URI
249
- *
250
- * @param {string} href - The URI to resolve
251
- * @param {string} [baseUri] - The base URI
252
- * @returns {string} The resolved URI
253
- */
254
- resolveUri(href, baseUri) {
255
- return resolveUri(href, baseUri);
256
- }
257
-
258
- /**
259
- * Load an external stylesheet document
260
- */
261
- loadStylesheet(href, baseUri) {
262
- if (!this.stylesheetLoader) {
263
- throw new Error(
264
- `Cannot load stylesheet "${href}": no stylesheetLoader configured. ` +
265
- "Use engine.setStylesheetLoader(fn) to provide a loader function.",
266
- );
267
- }
268
-
269
- const resolvedUri = this.resolveUri(href, baseUri);
270
- const result = this.stylesheetLoader(resolvedUri, baseUri);
271
-
272
- // If result is a string, it needs to be parsed (caller should handle this)
273
- return { document: result, uri: resolvedUri };
274
- }
275
-
276
- /**
277
- * Parse XML string to document (helper for stylesheet loading)
278
- */
279
- parseXmlString(xmlString) {
280
- if (typeof DOMParser !== "undefined") {
281
- const parser = new DOMParser();
282
- const doc = parser.parseFromString(xmlString, "application/xml");
283
- const parseError = doc.querySelector("parsererror");
284
- if (parseError) {
285
- throw new Error(`XML parse error: ${parseError.textContent}`);
286
- }
287
- return doc;
288
- }
289
- throw new Error("XML parsing not available in this environment");
290
- }
291
-
292
- /**
293
- * Import and compile an XSLT stylesheet
294
- * @param {Document|Element} stylesheetNode - The stylesheet document or root element
295
- * @param {string} [stylesheetUri] - Optional URI of the stylesheet for resolving imports
296
- */
297
- importStylesheet(stylesheetNode, stylesheetUri) {
298
- const isMainStylesheet = this.stylesheetDoc === null;
299
-
300
- if (isMainStylesheet) {
301
- this.stylesheetDoc = stylesheetNode.ownerDocument || stylesheetNode;
302
- if (stylesheetUri) {
303
- this.baseUri = stylesheetUri;
304
- this.processedStylesheets.add(stylesheetUri);
305
- }
306
- }
307
-
308
- const root = stylesheetNode.documentElement || stylesheetNode;
309
-
310
- // Validate stylesheet
311
- if (
312
- !this.isXsltElement(root, "stylesheet") &&
313
- !this.isXsltElement(root, "transform")
314
- ) {
315
- // Check for literal result element (simplified stylesheet)
316
- if (root.getAttribute && root.getAttribute("xsl:version")) {
317
- this.processLiteralResultStylesheet(root);
318
- return;
319
- }
320
- throw new Error(
321
- "Invalid XSLT stylesheet: root element must be xsl:stylesheet or xsl:transform",
322
- );
323
- }
324
-
325
- // Collect namespaces from root
326
- this.collectNamespaces(root);
327
-
328
- // XSLT 1.0: xsl:import elements MUST come first and are processed with lower precedence
329
- // Process imports first (they have lower precedence than the importing stylesheet)
330
- const imports = [];
331
- const otherElements = [];
332
-
333
- for (const child of root.childNodes) {
334
- if (child.nodeType !== 1) continue;
335
-
336
- if (this.isXsltElement(child, "import")) {
337
- imports.push(child);
338
- } else {
339
- otherElements.push(child);
340
- }
341
- }
342
-
343
- // Process imports (lower precedence - process before current stylesheet)
344
- for (const importNode of imports) {
345
- this.processImport(importNode, stylesheetUri || this.baseUri);
346
- }
347
-
348
- // Process other top-level elements (including includes)
349
- for (const child of otherElements) {
350
- if (this.isXsltElement(child, "template")) {
351
- this.registerTemplate(child);
352
- } else if (this.isXsltElement(child, "output")) {
353
- this.processOutput(child);
354
- } else if (this.isXsltElement(child, "variable")) {
355
- this.processGlobalVariable(child);
356
- } else if (this.isXsltElement(child, "param")) {
357
- this.processGlobalParam(child);
358
- } else if (this.isXsltElement(child, "key")) {
359
- this.processKey(child);
360
- } else if (this.isXsltElement(child, "decimal-format")) {
361
- this.processDecimalFormat(child);
362
- } else if (this.isXsltElement(child, "namespace-alias")) {
363
- this.processNamespaceAlias(child);
364
- } else if (this.isXsltElement(child, "attribute-set")) {
365
- this.processAttributeSet(child);
366
- } else if (this.isXsltElement(child, "strip-space")) {
367
- this.processStripSpace(child);
368
- } else if (this.isXsltElement(child, "preserve-space")) {
369
- this.processPreserveSpace(child);
370
- } else if (this.isXsltElement(child, "include")) {
371
- this.processInclude(child, stylesheetUri || this.baseUri);
372
- }
373
- }
374
-
375
- // Increment import precedence after processing this stylesheet
376
- if (isMainStylesheet) {
377
- this.currentImportPrecedence++;
378
- }
379
- }
380
-
381
- /**
382
- * Process xsl:include element
383
- * Includes are merged at the same import precedence level
384
- */
385
- processInclude(node, baseUri) {
386
- const href = node.getAttribute("href");
387
- if (!href) {
388
- throw new Error("xsl:include requires an href attribute");
389
- }
390
-
391
- const resolvedUri = this.resolveUri(href, baseUri);
392
-
393
- // Check for circular includes
394
- if (this.processedStylesheets.has(resolvedUri)) {
395
- throw new Error(`Circular stylesheet reference detected: ${resolvedUri}`);
396
- }
397
-
398
- this.processedStylesheets.add(resolvedUri);
399
-
400
- try {
401
- const { document: stylesheetDoc } = this.loadStylesheet(href, baseUri);
402
-
403
- // Parse if string
404
- let doc = stylesheetDoc;
405
- if (typeof stylesheetDoc === "string") {
406
- doc = this.parseXmlString(stylesheetDoc);
407
- }
408
-
409
- // Process the included stylesheet at the same import precedence
410
- const savedPrecedence = this.currentImportPrecedence;
411
- this.processIncludedStylesheet(doc, resolvedUri);
412
- this.currentImportPrecedence = savedPrecedence;
413
- } catch (error) {
414
- throw new Error(
415
- `Failed to include stylesheet "${href}": ${error.message}`,
416
- { cause: error },
417
- );
418
- }
419
- }
420
-
421
- /**
422
- * Process xsl:import element
423
- * Imports have lower precedence than the importing stylesheet
424
- */
425
- processImport(node, baseUri) {
426
- const href = node.getAttribute("href");
427
- if (!href) {
428
- throw new Error("xsl:import requires an href attribute");
429
- }
430
-
431
- const resolvedUri = this.resolveUri(href, baseUri);
432
-
433
- // Check for circular imports
434
- if (this.processedStylesheets.has(resolvedUri)) {
435
- throw new Error(`Circular stylesheet reference detected: ${resolvedUri}`);
436
- }
437
-
438
- this.processedStylesheets.add(resolvedUri);
439
-
440
- try {
441
- const { document: stylesheetDoc } = this.loadStylesheet(href, baseUri);
442
-
443
- // Parse if string
444
- let doc = stylesheetDoc;
445
- if (typeof stylesheetDoc === "string") {
446
- doc = this.parseXmlString(stylesheetDoc);
447
- }
448
-
449
- // Process the imported stylesheet (imports have lower precedence)
450
- // Don't increment precedence yet - imported templates get current (lower) precedence
451
- this.processIncludedStylesheet(doc, resolvedUri);
452
-
453
- // After processing import, increment precedence for next imports and main stylesheet
454
- this.currentImportPrecedence++;
455
- } catch (error) {
456
- throw new Error(
457
- `Failed to import stylesheet "${href}": ${error.message}`,
458
- { cause: error },
459
- );
460
- }
461
- }
462
-
463
- /**
464
- * Process an included/imported stylesheet document
465
- */
466
- processIncludedStylesheet(stylesheetDoc, stylesheetUri) {
467
- const root = stylesheetDoc.documentElement || stylesheetDoc;
468
-
469
- // Validate stylesheet
470
- if (
471
- !this.isXsltElement(root, "stylesheet") &&
472
- !this.isXsltElement(root, "transform")
473
- ) {
474
- throw new Error(
475
- "Included/imported document is not a valid XSLT stylesheet",
476
- );
477
- }
478
-
479
- // Collect namespaces
480
- this.collectNamespaces(root);
481
-
482
- // Process imports first (they have lower precedence)
483
- const imports = [];
484
- const otherElements = [];
485
-
486
- for (const child of root.childNodes) {
487
- if (child.nodeType !== 1) continue;
488
-
489
- if (this.isXsltElement(child, "import")) {
490
- imports.push(child);
491
- } else {
492
- otherElements.push(child);
493
- }
494
- }
495
-
496
- // Process nested imports
497
- for (const importNode of imports) {
498
- this.processImport(importNode, stylesheetUri);
499
- }
500
-
501
- // Process other elements
502
- for (const child of otherElements) {
503
- if (this.isXsltElement(child, "template")) {
504
- this.registerTemplate(child);
505
- } else if (this.isXsltElement(child, "output")) {
506
- this.processOutput(child);
507
- } else if (this.isXsltElement(child, "variable")) {
508
- this.processGlobalVariable(child);
509
- } else if (this.isXsltElement(child, "param")) {
510
- this.processGlobalParam(child);
511
- } else if (this.isXsltElement(child, "key")) {
512
- this.processKey(child);
513
- } else if (this.isXsltElement(child, "decimal-format")) {
514
- this.processDecimalFormat(child);
515
- } else if (this.isXsltElement(child, "namespace-alias")) {
516
- this.processNamespaceAlias(child);
517
- } else if (this.isXsltElement(child, "attribute-set")) {
518
- this.processAttributeSet(child);
519
- } else if (this.isXsltElement(child, "strip-space")) {
520
- this.processStripSpace(child);
521
- } else if (this.isXsltElement(child, "preserve-space")) {
522
- this.processPreserveSpace(child);
523
- } else if (this.isXsltElement(child, "include")) {
524
- this.processInclude(child, stylesheetUri);
525
- }
526
- }
527
- }
528
-
529
- processLiteralResultStylesheet(root) {
530
- // Simplified stylesheet - entire document is one template matching /
531
- this.templates.push({
532
- match: "/",
533
- name: null,
534
- mode: null,
535
- priority: 0.5,
536
- node: root,
537
- });
538
- }
539
-
540
- collectNamespaces(node) {
541
- if (!node.attributes) return;
542
-
543
- for (const attr of node.attributes) {
544
- if (attr.name.startsWith("xmlns:")) {
545
- const prefix = attr.name.substring(6);
546
- if (attr.value !== XSLT_NS) {
547
- this.namespaces[prefix] = attr.value;
548
- }
549
- } else if (attr.name === "xmlns" && attr.value !== XSLT_NS) {
550
- this.namespaces[""] = attr.value;
551
- }
552
- }
553
- }
554
-
555
- /**
556
- * Register a template rule.
557
- *
558
- * A union match pattern is equivalent to a set of template rules, one per
559
- * alternative (XSLT 1.0 section 5.5), so each alternative is registered
560
- * separately with its own default priority.
561
- *
562
- * @param {Element} node - The xsl:template element
563
- */
564
- registerTemplate(node) {
565
- const match = node.getAttribute("match");
566
- const name = node.getAttribute("name");
567
- const mode = node.getAttribute("mode") || null;
568
- const priorityAttr = node.getAttribute("priority");
569
- const alternatives = match
570
- ? this.splitUnionPattern(match).map((p) => p.trim())
571
- : [null];
572
-
573
- for (const alternative of alternatives) {
574
- this.templates.push({
575
- match: alternative,
576
- name,
577
- mode,
578
- priority: priorityAttr
579
- ? parseFloat(priorityAttr)
580
- : this.calculatePriority(alternative),
581
- importPrecedence: this.currentImportPrecedence,
582
- node,
583
- });
584
- }
585
- }
586
-
587
- /**
588
- * Default priority of a single match pattern (see templatePriority.js).
589
- *
590
- * @param {string|null} matchPattern - The match pattern
591
- * @returns {number} The default priority
592
- */
593
- calculatePriority(matchPattern) {
594
- return calculatePriority(matchPattern ? matchPattern.trim() : matchPattern);
595
- }
596
-
597
- processOutput(node) {
598
- const method = node.getAttribute("method");
599
- if (method) this.outputSettings.method = method;
600
-
601
- const version = node.getAttribute("version");
602
- if (version) this.outputSettings.version = version;
603
-
604
- const encoding = node.getAttribute("encoding");
605
- if (encoding) this.outputSettings.encoding = encoding;
606
-
607
- const standalone = node.getAttribute("standalone");
608
- if (standalone) this.outputSettings.standalone = standalone;
609
-
610
- const indent = node.getAttribute("indent");
611
- if (indent) this.outputSettings.indent = indent;
612
-
613
- const omit = node.getAttribute("omit-xml-declaration");
614
- if (omit) this.outputSettings.omitXmlDeclaration = omit;
615
-
616
- const doctypePublic = node.getAttribute("doctype-public");
617
- if (doctypePublic) this.outputSettings.doctypePublic = doctypePublic;
618
-
619
- const doctypeSystem = node.getAttribute("doctype-system");
620
- if (doctypeSystem) this.outputSettings.doctypeSystem = doctypeSystem;
621
-
622
- const mediaType = node.getAttribute("media-type");
623
- if (mediaType) this.outputSettings.mediaType = mediaType;
624
-
625
- const cdataElements = node.getAttribute("cdata-section-elements");
626
- if (cdataElements) {
627
- this.outputSettings.cdataSectionElements = cdataElements
628
- .split(/\s+/)
629
- .filter(Boolean);
630
- }
631
- }
632
-
633
- processGlobalVariable(node) {
634
- const name = node.getAttribute("name");
635
- const select = node.getAttribute("select");
636
-
637
- this.globalVariables[name] = { node, select };
638
- }
639
-
640
- /**
641
- * Register an `xsl:param` top level declaration.
642
- *
643
- * A value supplied from outside (through `setParameter`) has precedence over
644
- * the declared default, so it survives compilation of the stylesheet.
645
- *
646
- * @param {Element} node - The `xsl:param` element
647
- * @returns {void}
648
- */
649
- processGlobalParam(node) {
650
- const name = node.getAttribute("name");
651
- const select = node.getAttribute("select");
652
- const existing = this.globalParameters[name];
653
- const definition = { node, select };
654
-
655
- if (existing && "value" in existing) {
656
- definition.value = existing.value;
657
- }
658
-
659
- this.globalParameters[name] = definition;
660
- }
661
-
662
- /**
663
- * Supply the value of a global parameter from outside the stylesheet.
664
- *
665
- * The value is merged into the `xsl:param` declaration when there is one, so
666
- * removing the value later restores the declared default.
667
- *
668
- * @param {string} name - The parameter name, `{uri}local` when namespaced
669
- * @param {*} value - The value to use
670
- * @returns {void}
671
- *
672
- * @example
673
- * engine.setParameterValue('sortOrder', 'ascending');
674
- */
675
- setParameterValue(name, value) {
676
- const definition = this.globalParameters[name];
677
-
678
- if (definition) definition.value = value;
679
- else this.globalParameters[name] = { value };
680
- }
681
-
682
- /**
683
- * Remove an externally supplied parameter value.
684
- *
685
- * The `xsl:param` declaration of the stylesheet is kept, so the parameter
686
- * falls back to its declared default instead of becoming undefined.
687
- *
688
- * @param {string} name - The parameter name, `{uri}local` when namespaced
689
- * @returns {void}
690
- *
691
- * @example
692
- * engine.clearParameterValue('sortOrder');
693
- */
694
- clearParameterValue(name) {
695
- const definition = this.globalParameters[name];
696
- if (!definition) return;
697
-
698
- if (definition.node) delete definition.value;
699
- else delete this.globalParameters[name];
700
- }
701
-
702
- /**
703
- * Remove every externally supplied parameter value.
704
- *
705
- * @returns {void}
706
- *
707
- * @example
708
- * engine.clearParameterValues();
709
- */
710
- clearParameterValues() {
711
- for (const name of Object.keys(this.globalParameters)) {
712
- this.clearParameterValue(name);
713
- }
714
- }
715
-
716
- processKey(node) {
717
- const name = node.getAttribute("name");
718
- const match = node.getAttribute("match");
719
- const use = node.getAttribute("use");
720
-
721
- this.keys[name] = { match, use };
722
- this.keyRegistry.clear();
723
- }
724
-
725
- processDecimalFormat(node) {
726
- const name = node.getAttribute("name") || "";
727
-
728
- this.decimalFormats[name] = {
729
- decimalSeparator: node.getAttribute("decimal-separator") || ".",
730
- groupingSeparator: node.getAttribute("grouping-separator") || ",",
731
- percent: node.getAttribute("percent") || "%",
732
- perMille: node.getAttribute("per-mille") || "\u2030",
733
- zeroDigit: node.getAttribute("zero-digit") || "0",
734
- digit: node.getAttribute("digit") || "#",
735
- patternSeparator: node.getAttribute("pattern-separator") || ";",
736
- infinity: node.getAttribute("infinity") || "Infinity",
737
- nan: node.getAttribute("NaN") || "NaN",
738
- minusSign: node.getAttribute("minus-sign") || "-",
739
- };
740
- }
741
-
742
- processNamespaceAlias(node) {
743
- this.namespaceAliases.add(node);
744
- }
745
-
746
- processAttributeSet(node) {
747
- const name = node.getAttribute("name");
748
- const useAttributeSets = node.getAttribute("use-attribute-sets");
749
-
750
- this.attributeSets[name] = {
751
- node,
752
- useAttributeSets: useAttributeSets
753
- ? useAttributeSets.split(/\s+/).filter(Boolean)
754
- : [],
755
- };
756
- }
757
-
758
- processStripSpace(node) {
759
- const elements = node.getAttribute("elements");
760
- if (elements) {
761
- this.stripSpace.push(...elements.split(/\s+/).filter(Boolean));
762
- }
763
- }
764
-
765
- processPreserveSpace(node) {
766
- const elements = node.getAttribute("elements");
767
- if (elements) {
768
- this.preserveSpace.push(...elements.split(/\s+/).filter(Boolean));
769
- }
770
- }
771
-
772
- /**
773
- * Transform a source document
774
- */
775
- transform(sourceNode, ownerDocument) {
776
- const doc =
777
- ownerDocument || (typeof document !== "undefined" ? document : null);
778
-
779
- if (!doc) {
780
- throw new Error("No output document available");
781
- }
782
-
783
- // Build the result tree in a neutral XML document: creating nodes directly
784
- // in an HTML owner document would lower case names and force the XHTML
785
- // namespace on every element.
786
- const resultDocument = createResultDocument(doc);
787
- const source = this.prepareSource(sourceNode, doc);
788
-
789
- // Create context - use document node as initial context for "/" template matching
790
- // XPath paths like "RootElement/child" expect to start from document node
791
- const context = new XsltContext({
792
- currentNode: source,
793
- currentNodeList: [source],
794
- position: 1,
795
- outputDocument: resultDocument,
796
- stylesheet: this.stylesheetDoc,
797
- namespaces: { ...this.namespaces },
798
- templates: this.templates,
799
- keys: this.keys,
800
- decimalFormats: this.decimalFormats,
801
- outputMethod: this.outputSettings.method,
802
- xpathEvaluator: this.xpathEvaluator,
156
+ matchesPattern: (node, pattern, definition) =>
157
+ this.matchesPattern(
158
+ node,
159
+ pattern,
160
+ this.rootContext,
161
+ definition?.namespaces,
162
+ ),
163
+ evaluateUse: (node, expression, definition) =>
164
+ this.evaluateKeyValues(node, expression, definition?.namespaces),
803
165
  });
804
166
 
805
- this.rootContext = context;
167
+ // Recoverable errors already reported, so each is reported once
168
+ this.reportedWarnings = new Set();
806
169
 
807
- // Evaluate global variables
808
- for (const [name, def] of Object.entries(this.globalParameters)) {
809
- if (!(name in context.parameters)) {
810
- context.parameters[name] = this.evaluateVariable(def, context);
811
- }
812
- }
170
+ // Extension element implementations by expanded name `{uri}local`
171
+ this.extensionElements = new Map();
813
172
 
814
- for (const [name, def] of Object.entries(this.globalVariables)) {
815
- context.variables[name] = this.evaluateVariable(def, context);
816
- }
817
-
818
- // Create result document fragment
819
- const fragment = resultDocument.createDocumentFragment();
820
-
821
- // Apply templates to document node (not documentElement)
822
- // This ensures "/" template has document as context, so paths like
823
- // "RootElement/child" work correctly
824
- this.applyTemplates([source], null, context, fragment);
825
-
826
- return importResultFragment(fragment, doc);
173
+ this.xpathEvaluator.registerFunctions(createXsltFunctions(this));
827
174
  }
828
175
 
829
176
  /**
830
- * Apply `xsl:strip-space` to the source tree.
177
+ * Evaluate an XPath expression in an XSLT context.
831
178
  *
832
- * Stripping produces a copy so the caller's document is never modified; when
833
- * no `xsl:strip-space` is declared the original node is used unchanged.
834
- *
835
- * @param {Node} sourceNode - The source document or element
836
- * @param {Document} ownerDocument - Document providing the DOM implementation
837
- * @returns {Node} The source to transform
179
+ * @param {string} expr - The expression
180
+ * @param {XsltContext} context - The current context
181
+ * @returns {*} The value
838
182
  */
839
- prepareSource(sourceNode, ownerDocument) {
840
- const filter = new WhitespaceFilter(this.stripSpace, this.preserveSpace);
841
- if (!filter.isActive()) return sourceNode;
842
-
843
- return stripWhitespaceNodes(
844
- sourceNode,
845
- filter,
846
- createResultDocument(ownerDocument),
183
+ evaluateXPath(expr, context) {
184
+ const ast = parseXPath(expr);
185
+ const xpathContext = new XPathContext(
186
+ context.currentNode,
187
+ context.position,
188
+ context.currentNodeList.length,
189
+ context.xpathVariables,
190
+ context.namespaces,
191
+ context,
847
192
  );
193
+ return this.xpathEvaluator.evaluate(ast, xpathContext);
848
194
  }
849
195
 
850
196
  /**
851
- * Transform to a complete document
852
- */
853
- transformToDocument(sourceNode) {
854
- // For Node.js environments, we need a document implementation
855
- const doc = this.createDocument(sourceNode);
856
- const fragment = this.transform(sourceNode, doc);
857
-
858
- // Move fragment contents to document
859
- while (fragment.firstChild) {
860
- doc.appendChild(fragment.firstChild);
861
- }
862
-
863
- return doc;
864
- }
865
-
866
- /**
867
- * Transform a source document and serialize the result to a string.
868
- *
869
- * Non-W3C convenience method: the result tree is serialized honoring the
870
- * `xsl:output` settings of the stylesheet (XSLT 1.0 section 16).
197
+ * Whether a node is in the XSLT namespace (or uses the xsl prefix).
871
198
  *
872
- * @param {Node} sourceNode - Source document or element to transform
873
- * @returns {string} The serialized transformation result
199
+ * @param {Node} node - A stylesheet node
200
+ * @returns {boolean} True for an XSLT element
874
201
  */
875
- transformToString(sourceNode) {
876
- const fragment = this.transform(
877
- sourceNode,
878
- this.createDocument(sourceNode),
202
+ isXsltNamespace(node) {
203
+ return (
204
+ node.namespaceURI === XSLT_NAMESPACE ||
205
+ (node.nodeName && node.nodeName.startsWith("xsl:"))
879
206
  );
880
- return serializeResult(fragment, this.outputSettings);
881
207
  }
882
208
 
883
209
  /**
884
- * Create an empty XML document to hold a transformation result.
885
- *
886
- * Uses the global `document` when running in a browser and otherwise falls
887
- * back to the DOM implementation owning `referenceNode` (e.g. a jsdom or
888
- * xmldom document in Node.js).
210
+ * Whether a node is the XSLT element of a local name.
889
211
  *
890
- * @param {Node} [referenceNode] - Any node whose DOM implementation can be reused
891
- * @returns {Document} A new empty document
892
- * @throws {Error} When no DOM implementation is available
212
+ * @param {Node} node - A stylesheet node
213
+ * @param {string} localName - The XSLT element name, e.g. "template"
214
+ * @returns {boolean} True for that element
893
215
  */
894
- createDocument(referenceNode) {
895
- if (typeof document !== "undefined") {
896
- return document.implementation.createDocument(null, null, null);
897
- }
898
-
899
- const ownerDocument =
900
- referenceNode &&
901
- (referenceNode.nodeType === 9
902
- ? referenceNode
903
- : referenceNode.ownerDocument);
904
- if (ownerDocument?.implementation) {
905
- return ownerDocument.implementation.createDocument(null, null, null);
906
- }
216
+ isXsltElement(node, localName) {
217
+ if (node.nodeType !== 1) return false;
907
218
 
908
- throw new Error("Document creation not available in this environment");
219
+ const nodeName = node.localName || node.nodeName;
220
+ return (
221
+ (node.namespaceURI === XSLT_NAMESPACE && nodeName === localName) ||
222
+ node.nodeName === `xsl:${localName}`
223
+ );
909
224
  }
910
225
 
911
226
  /**
912
- * Compute the value of a variable or parameter definition.
913
- *
914
- * A value supplied from outside (`setParameter`) wins over the `select`
915
- * expression and over the instantiated content of the declaration.
227
+ * Report a recoverable stylesheet error with console.warn, once per engine
228
+ * and message (libxslt reports these errors and goes on).
916
229
  *
917
- * @param {{value?: *, select?: string, node?: Element}} def - The definition
918
- * @param {XsltContext} context - The context used for evaluation
919
- * @returns {*} The variable value
230
+ * @param {string} message - The error description
231
+ * @returns {void}
920
232
  */
921
- evaluateVariable(def, context) {
922
- if ("value" in def) {
923
- return def.value;
924
- }
925
-
926
- if (def.select) {
927
- return this.evaluateXPath(def.select, context);
928
- }
929
-
930
- // If no select, evaluate content as result tree fragment
931
- const fragment = context.outputDocument.createDocumentFragment();
932
- this.processChildren(def.node, context, fragment);
933
- return fragment;
233
+ warnOnce(message) {
234
+ if (this.reportedWarnings.has(message)) return;
235
+ this.reportedWarnings.add(message);
236
+ console.warn(`XSLT: ${message}`);
934
237
  }
238
+ }
935
239
 
936
- /**
937
- * Apply templates to a node list
938
- */
939
- applyTemplates(nodes, mode, context, output) {
940
- const nodeList = Array.isArray(nodes) ? nodes : [nodes];
941
-
942
- for (let i = 0; i < nodeList.length; i++) {
943
- const node = nodeList[i];
944
- const template = this.findMatchingTemplate(node, mode, context);
945
-
946
- if (template) {
947
- const newContext = context.clone({
948
- currentNode: node,
949
- currentNodeList: nodeList,
950
- position: i + 1,
951
- currentTemplate: template,
952
- currentMode: mode,
953
- });
954
-
955
- this.processTemplate(template.node, newContext, output);
956
- } else {
957
- // Built-in templates
958
- this.applyBuiltinTemplate(node, mode, context, output);
959
- }
960
- }
961
- }
962
-
963
- /**
964
- * Find the best matching template for a node
965
- */
966
- findMatchingTemplate(node, mode, context, maxImportPrecedence = Infinity) {
967
- let bestMatch = null;
968
- let bestPriority = -Infinity;
969
- let bestImportPrecedence = -Infinity;
970
-
971
- for (const template of this.templates) {
972
- if (template.mode !== mode) continue;
973
- if (!template.match) continue;
974
- if ((template.importPrecedence || 0) >= maxImportPrecedence) continue;
975
-
976
- if (this.matchesPattern(node, template.match, context)) {
977
- const priority = template.priority;
978
- const importPrecedence = template.importPrecedence || 0;
979
-
980
- if (
981
- importPrecedence > bestImportPrecedence ||
982
- (importPrecedence === bestImportPrecedence && priority > bestPriority)
983
- ) {
984
- bestMatch = template;
985
- bestPriority = priority;
986
- bestImportPrecedence = importPrecedence;
987
- }
988
- }
989
- }
990
-
991
- return bestMatch;
992
- }
993
-
994
- /**
995
- * Check if a node matches an XSLT pattern
996
- */
997
- matchesPattern(node, pattern, context) {
998
- // Split union patterns
999
- const patterns = this.splitUnionPattern(pattern);
1000
-
1001
- for (const p of patterns) {
1002
- if (this.matchesSinglePattern(node, p.trim(), context)) {
1003
- return true;
1004
- }
1005
- }
1006
-
1007
- return false;
1008
- }
1009
-
1010
- splitUnionPattern(pattern) {
1011
- // Simple split on | not inside predicates or strings
1012
- const parts = [];
1013
- let current = "";
1014
- let depth = 0;
1015
- let inString = false;
1016
- let stringChar = "";
1017
-
1018
- for (let i = 0; i < pattern.length; i++) {
1019
- const char = pattern[i];
1020
-
1021
- if (inString) {
1022
- current += char;
1023
- if (char === stringChar) {
1024
- inString = false;
1025
- }
1026
- } else if (char === '"' || char === "'") {
1027
- inString = true;
1028
- stringChar = char;
1029
- current += char;
1030
- } else if (char === "[") {
1031
- depth++;
1032
- current += char;
1033
- } else if (char === "]") {
1034
- depth--;
1035
- current += char;
1036
- } else if (char === "|" && depth === 0) {
1037
- parts.push(current);
1038
- current = "";
1039
- } else {
1040
- current += char;
1041
- }
1042
- }
1043
-
1044
- if (current) {
1045
- parts.push(current);
1046
- }
1047
-
1048
- return parts;
1049
- }
1050
-
1051
- matchesSinglePattern(node, pattern, context) {
1052
- try {
1053
- // Handle root pattern
1054
- if (pattern === "/") {
1055
- return (
1056
- node.nodeType === 9 || node === node.ownerDocument?.documentElement
1057
- );
1058
- }
1059
-
1060
- // Handle patterns like "item" (child match)
1061
- // Need to check if node would be selected by pattern from parent
1062
- const ast = parseXPath(pattern);
1063
-
1064
- // For patterns starting with /, evaluate from root
1065
- if (pattern.startsWith("/")) {
1066
- const doc = node.ownerDocument || node;
1067
- const xpathContext = new XPathContext(
1068
- doc,
1069
- 1,
1070
- 1,
1071
- { ...context.variables, ...context.parameters },
1072
- context.namespaces,
1073
- context,
1074
- );
1075
- const result = this.xpathEvaluator.evaluate(ast, xpathContext);
1076
- const nodes = Array.isArray(result) ? result : [result];
1077
- return nodes.includes(node);
1078
- }
1079
-
1080
- // For relative patterns, check if this node matches when evaluated from
1081
- // its parent; attribute nodes are reached through their owner element
1082
- const parent = node.nodeType === 2 ? node.ownerElement : node.parentNode;
1083
- if (parent) {
1084
- const xpathContext = new XPathContext(
1085
- parent,
1086
- 1,
1087
- 1,
1088
- { ...context.variables, ...context.parameters },
1089
- context.namespaces,
1090
- context,
1091
- );
1092
- const result = this.xpathEvaluator.evaluate(ast, xpathContext);
1093
- const nodes = Array.isArray(result) ? result : [result];
1094
- return nodes.includes(node);
1095
- }
1096
-
1097
- // For document node without parent
1098
- const xpathContext = new XPathContext(
1099
- node,
1100
- 1,
1101
- 1,
1102
- { ...context.variables, ...context.parameters },
1103
- context.namespaces,
1104
- context,
1105
- );
1106
- const result = this.xpathEvaluator.evaluate(ast, xpathContext);
1107
- const nodes = Array.isArray(result) ? result : [result];
1108
- return nodes.includes(node);
1109
- } catch {
1110
- return false;
1111
- }
1112
- }
1113
-
1114
- /**
1115
- * Apply built-in template rules
1116
- */
1117
- applyBuiltinTemplate(node, mode, context, output) {
1118
- switch (node.nodeType) {
1119
- case 1: // Element
1120
- case 9: // Document
1121
- case 11: // Document Fragment
1122
- // Process children
1123
- this.applyTemplates(Array.from(node.childNodes), mode, context, output);
1124
- break;
1125
-
1126
- case 3: // Text
1127
- case 4: {
1128
- // CDATA
1129
- // Copy text value
1130
- const text = context.outputDocument.createTextNode(
1131
- node.nodeValue || "",
1132
- );
1133
- output.appendChild(text);
1134
- break;
1135
- }
1136
-
1137
- case 2: {
1138
- // Attribute
1139
- // Copy attribute value as text
1140
- const attrText = context.outputDocument.createTextNode(
1141
- node.nodeValue || "",
1142
- );
1143
- output.appendChild(attrText);
1144
- break;
1145
- }
1146
-
1147
- // Comments and PIs have no built-in template
1148
- }
1149
- }
1150
-
1151
- /**
1152
- * Process template content
1153
- */
1154
- processTemplate(templateNode, context, output) {
1155
- // Process template parameters first
1156
- const localContext = context.clone();
1157
-
1158
- for (const child of templateNode.childNodes) {
1159
- if (child.nodeType === 1 && this.isXsltElement(child, "param")) {
1160
- const name = child.getAttribute("name");
1161
- if (!(name in localContext.parameters)) {
1162
- localContext.parameters[name] = this.evaluateVariable(
1163
- { node: child, select: child.getAttribute("select") },
1164
- localContext,
1165
- );
1166
- }
1167
- }
1168
- }
1169
-
1170
- this.processChildren(templateNode, localContext, output);
1171
- }
1172
-
1173
- /**
1174
- * Process child nodes of an XSLT element
1175
- */
1176
- processChildren(node, context, output) {
1177
- for (const child of node.childNodes) {
1178
- this.processNode(child, context, output);
1179
- }
1180
- }
1181
-
1182
- /**
1183
- * Process a single node in the stylesheet
1184
- */
1185
- processNode(node, context, output) {
1186
- switch (node.nodeType) {
1187
- case 1: // Element
1188
- this.processElement(node, context, output);
1189
- break;
1190
-
1191
- case 3: // Text
1192
- case 4: {
1193
- // CDATA
1194
- // Output text if not whitespace only (or if preserving space)
1195
- const text = node.nodeValue;
1196
- if (text && (text.trim() || this.shouldPreserveSpace(node))) {
1197
- const textNode = context.outputDocument.createTextNode(text);
1198
- output.appendChild(textNode);
1199
- }
1200
- break;
1201
- }
1202
- }
1203
- }
1204
-
1205
- shouldPreserveSpace(node) {
1206
- // Check xml:space attribute on ancestors
1207
- let current = node.parentNode;
1208
- while (current && current.nodeType === 1) {
1209
- const space = current.getAttribute("xml:space");
1210
- if (space === "preserve") return true;
1211
- if (space === "default") return false;
1212
- current = current.parentNode;
1213
- }
1214
- return false;
1215
- }
1216
-
1217
- /**
1218
- * Process an element in the stylesheet
1219
- */
1220
- processElement(node, context, output) {
1221
- // Check if XSLT element
1222
- if (this.isXsltNamespace(node)) {
1223
- this.processXsltElement(node, context, output);
1224
- } else {
1225
- // Literal result element
1226
- this.processLiteralResultElement(node, context, output);
1227
- }
1228
- }
1229
-
1230
- /**
1231
- * Process an XSLT instruction element
1232
- */
1233
- processXsltElement(node, context, output) {
1234
- const localName = node.localName || node.nodeName.replace(/^xsl:/, "");
1235
-
1236
- switch (localName) {
1237
- case "apply-templates":
1238
- this.xslApplyTemplates(node, context, output);
1239
- break;
1240
-
1241
- case "apply-imports":
1242
- this.xslApplyImports(node, context, output);
1243
- break;
1244
-
1245
- case "call-template":
1246
- this.xslCallTemplate(node, context, output);
1247
- break;
1248
-
1249
- case "value-of":
1250
- this.xslValueOf(node, context, output);
1251
- break;
1252
-
1253
- case "text":
1254
- this.xslText(node, context, output);
1255
- break;
1256
-
1257
- case "element":
1258
- this.xslElement(node, context, output);
1259
- break;
1260
-
1261
- case "attribute":
1262
- this.xslAttribute(node, context, output);
1263
- break;
1264
-
1265
- case "if":
1266
- this.xslIf(node, context, output);
1267
- break;
1268
-
1269
- case "choose":
1270
- this.xslChoose(node, context, output);
1271
- break;
1272
-
1273
- case "for-each":
1274
- this.xslForEach(node, context, output);
1275
- break;
1276
-
1277
- case "copy":
1278
- this.xslCopy(node, context, output);
1279
- break;
1280
-
1281
- case "copy-of":
1282
- this.xslCopyOf(node, context, output);
1283
- break;
1284
-
1285
- case "variable":
1286
- this.xslVariable(node, context, output);
1287
- break;
1288
-
1289
- case "param":
1290
- // Params are processed at template start
1291
- break;
1292
-
1293
- case "comment":
1294
- this.xslComment(node, context, output);
1295
- break;
1296
-
1297
- case "processing-instruction":
1298
- this.xslProcessingInstruction(node, context, output);
1299
- break;
1300
-
1301
- case "number":
1302
- this.xslNumber(node, context, output);
1303
- break;
1304
-
1305
- case "sort":
1306
- // Handled by apply-templates and for-each
1307
- break;
1308
-
1309
- case "with-param":
1310
- // Handled by call-template and apply-templates
1311
- break;
1312
-
1313
- case "message":
1314
- this.xslMessage(node, context, output);
1315
- break;
1316
-
1317
- case "fallback":
1318
- // Used for forward compatibility
1319
- break;
1320
-
1321
- default:
1322
- console.warn(`Unknown XSLT element: ${localName}`);
1323
- }
1324
- }
1325
-
1326
- /**
1327
- * Process a literal result element (non-XSLT)
1328
- *
1329
- * Applies `xsl:namespace-alias` to the element and its attributes, honours
1330
- * `xsl:use-attribute-sets` and keeps XSLT-only attributes and namespace
1331
- * declarations out of the result tree.
1332
- *
1333
- * @param {Element} node - The literal result element in the stylesheet
1334
- * @param {XsltContext} context - The current XSLT context
1335
- * @param {Node} output - The result tree node receiving the element
1336
- * @returns {void}
1337
- */
1338
- processLiteralResultElement(node, context, output) {
1339
- const localName = node.localName || node.nodeName;
1340
- const alias = this.namespaceAliases.resolve(node.namespaceURI, localName);
1341
- const namespaceUri = alias ? alias.namespaceUri : node.namespaceURI;
1342
- const qname = alias ? alias.qname : node.nodeName;
1343
-
1344
- const outputElement =
1345
- namespaceUri && context.outputDocument.createElementNS
1346
- ? context.outputDocument.createElementNS(namespaceUri, qname)
1347
- : context.outputDocument.createElement(qname);
1348
-
1349
- // Attribute sets come first so literal attributes take precedence
1350
- const useAttributeSets = getXsltAttribute(
1351
- node,
1352
- "use-attribute-sets",
1353
- XSLT_NS,
1354
- );
1355
- if (useAttributeSets) {
1356
- this.applyAttributeSets(useAttributeSets, context, outputElement);
1357
- }
1358
-
1359
- if (node.attributes) {
1360
- for (const attr of node.attributes) {
1361
- if (!shouldCopyAttribute(attr, XSLT_NS)) continue;
1362
-
1363
- const value = this.processAttributeValueTemplate(attr.value, context);
1364
- const attrAlias = this.namespaceAliases.resolve(
1365
- attr.namespaceURI,
1366
- attr.localName || attr.name,
1367
- );
1368
-
1369
- if (attrAlias) {
1370
- outputElement.setAttributeNS(
1371
- attrAlias.namespaceUri,
1372
- attrAlias.qname,
1373
- value,
1374
- );
1375
- } else {
1376
- outputElement.setAttribute(attr.name, value);
1377
- }
1378
- }
1379
- }
1380
-
1381
- // Process children
1382
- this.processChildren(node, context, outputElement);
1383
-
1384
- output.appendChild(outputElement);
1385
- }
1386
-
1387
- /**
1388
- * Process attribute value templates (expressions in curly braces)
1389
- */
1390
- processAttributeValueTemplate(value, context) {
1391
- if (!value.includes("{")) return value;
1392
-
1393
- let result = "";
1394
- let i = 0;
1395
-
1396
- while (i < value.length) {
1397
- if (value[i] === "{") {
1398
- if (value[i + 1] === "{") {
1399
- // Escaped brace
1400
- result += "{";
1401
- i += 2;
1402
- } else {
1403
- // Find closing brace
1404
- let depth = 1;
1405
- let j = i + 1;
1406
- while (j < value.length && depth > 0) {
1407
- if (value[j] === "{") depth++;
1408
- else if (value[j] === "}") depth--;
1409
- j++;
1410
- }
1411
-
1412
- const expr = value.substring(i + 1, j - 1);
1413
- const evalResult = this.evaluateXPath(expr, context);
1414
- result += this.xpathEvaluator.toString(evalResult);
1415
- i = j;
1416
- }
1417
- } else if (value[i] === "}") {
1418
- if (value[i + 1] === "}") {
1419
- // Escaped brace
1420
- result += "}";
1421
- i += 2;
1422
- } else {
1423
- throw new Error("Unmatched } in attribute value template");
1424
- }
1425
- } else {
1426
- result += value[i];
1427
- i++;
1428
- }
1429
- }
1430
-
1431
- return result;
1432
- }
1433
-
1434
- // XSLT Instructions
1435
- xslApplyTemplates(node, context, output) {
1436
- const select = node.getAttribute("select") || "node()";
1437
- const mode = node.getAttribute("mode") || null;
1438
-
1439
- // Evaluate select expression
1440
- let nodes = this.evaluateXPath(select, context);
1441
- if (!Array.isArray(nodes)) {
1442
- nodes = nodes ? [nodes] : [];
1443
- }
1444
-
1445
- // Collect sort specifications
1446
- const sortSpecs = [];
1447
- for (const child of node.childNodes) {
1448
- if (child.nodeType === 1 && this.isXsltElement(child, "sort")) {
1449
- sortSpecs.push({
1450
- select: child.getAttribute("select") || ".",
1451
- order: child.getAttribute("order") || "ascending",
1452
- dataType: child.getAttribute("data-type") || "text",
1453
- caseOrder: child.getAttribute("case-order") || "upper-first",
1454
- lang: child.getAttribute("lang"),
1455
- });
1456
- }
1457
- }
1458
-
1459
- // Apply sorting
1460
- if (sortSpecs.length > 0) {
1461
- nodes = this.sortNodes(nodes, sortSpecs, context);
1462
- }
1463
-
1464
- // Collect with-param values
1465
- const params = {};
1466
- for (const child of node.childNodes) {
1467
- if (child.nodeType === 1 && this.isXsltElement(child, "with-param")) {
1468
- const name = child.getAttribute("name");
1469
- const selectAttr = child.getAttribute("select");
1470
- if (selectAttr) {
1471
- params[name] = this.evaluateXPath(selectAttr, context);
1472
- } else {
1473
- const fragment = context.outputDocument.createDocumentFragment();
1474
- this.processChildren(child, context, fragment);
1475
- params[name] = fragment;
1476
- }
1477
- }
1478
- }
1479
-
1480
- // Apply templates with new context including params
1481
- const newContext = context.clone({
1482
- parameters: { ...context.parameters, ...params },
1483
- });
1484
- this.applyTemplates(nodes, mode, newContext, output);
1485
- }
1486
-
1487
- /**
1488
- * Instantiate `xsl:apply-imports`.
1489
- *
1490
- * Only templates with a lower import precedence than the template being
1491
- * instantiated are considered; when none matches, the built-in template rules
1492
- * apply, exactly as for `xsl:apply-templates`.
1493
- *
1494
- * @param {Element} node - The `xsl:apply-imports` element
1495
- * @param {XsltContext} context - The current XSLT context
1496
- * @param {Node} output - The result tree node receiving the output
1497
- * @returns {void}
1498
- */
1499
- xslApplyImports(node, context, output) {
1500
- const currentNode = context.currentNode;
1501
- const mode = context.currentMode ?? null;
1502
- const precedence = context.currentTemplate
1503
- ? context.currentTemplate.importPrecedence || 0
1504
- : 0;
1505
-
1506
- const template = this.findMatchingTemplate(
1507
- currentNode,
1508
- mode,
1509
- context,
1510
- precedence,
1511
- );
1512
-
1513
- if (!template) {
1514
- this.applyBuiltinTemplate(currentNode, mode, context, output);
1515
- return;
1516
- }
1517
-
1518
- this.processTemplate(
1519
- template.node,
1520
- context.clone({ currentTemplate: template }),
1521
- output,
1522
- );
1523
- }
1524
-
1525
- xslCallTemplate(node, context, output) {
1526
- const name = node.getAttribute("name");
1527
-
1528
- // Find named template
1529
- const template = this.templates.find((t) => t.name === name);
1530
- if (!template) {
1531
- throw new Error(`Template not found: ${name}`);
1532
- }
1533
-
1534
- // Collect with-param values
1535
- const params = {};
1536
- for (const child of node.childNodes) {
1537
- if (child.nodeType === 1 && this.isXsltElement(child, "with-param")) {
1538
- const paramName = child.getAttribute("name");
1539
- const selectAttr = child.getAttribute("select");
1540
- if (selectAttr) {
1541
- params[paramName] = this.evaluateXPath(selectAttr, context);
1542
- } else {
1543
- const fragment = context.outputDocument.createDocumentFragment();
1544
- this.processChildren(child, context, fragment);
1545
- params[paramName] = fragment;
1546
- }
1547
- }
1548
- }
1549
-
1550
- // Call template with params
1551
- const newContext = context.clone({
1552
- parameters: { ...context.parameters, ...params },
1553
- });
1554
- this.processTemplate(template.node, newContext, output);
1555
- }
1556
-
1557
- xslValueOf(node, context, output) {
1558
- const select = node.getAttribute("select");
1559
- const disableOutputEscaping =
1560
- node.getAttribute("disable-output-escaping") === "yes";
1561
-
1562
- const result = this.evaluateXPath(select, context);
1563
- const text = this.xpathEvaluator.toString(result);
1564
-
1565
- if (text) {
1566
- const textNode = context.outputDocument.createTextNode(text);
1567
- if (disableOutputEscaping) {
1568
- textNode._disableOutputEscaping = true;
1569
- }
1570
- output.appendChild(textNode);
1571
- }
1572
- }
1573
-
1574
- xslText(node, context, output) {
1575
- const disableOutputEscaping =
1576
- node.getAttribute("disable-output-escaping") === "yes";
1577
- let text = "";
1578
-
1579
- for (const child of node.childNodes) {
1580
- if (child.nodeType === 3 || child.nodeType === 4) {
1581
- text += child.nodeValue || "";
1582
- }
1583
- }
1584
-
1585
- if (text) {
1586
- const textNode = context.outputDocument.createTextNode(text);
1587
- if (disableOutputEscaping) {
1588
- textNode._disableOutputEscaping = true;
1589
- }
1590
- output.appendChild(textNode);
1591
- }
1592
- }
1593
-
1594
- xslElement(node, context, output) {
1595
- const name = this.processAttributeValueTemplate(
1596
- node.getAttribute("name"),
1597
- context,
1598
- );
1599
- const namespace = node.getAttribute("namespace");
1600
- const useAttributeSets = node.getAttribute("use-attribute-sets");
1601
-
1602
- let element;
1603
- if (namespace) {
1604
- const ns = this.processAttributeValueTemplate(namespace, context);
1605
- element = context.outputDocument.createElementNS(ns, name);
1606
- } else {
1607
- element = context.outputDocument.createElement(name);
1608
- }
1609
-
1610
- // Apply attribute sets
1611
- if (useAttributeSets) {
1612
- this.applyAttributeSets(useAttributeSets, context, element);
1613
- }
1614
-
1615
- this.processChildren(node, context, element);
1616
- output.appendChild(element);
1617
- }
1618
-
1619
- xslAttribute(node, context, output) {
1620
- const name = this.processAttributeValueTemplate(
1621
- node.getAttribute("name"),
1622
- context,
1623
- );
1624
- const namespace = node.getAttribute("namespace");
1625
-
1626
- // Collect content
1627
- const fragment = context.outputDocument.createDocumentFragment();
1628
- this.processChildren(node, context, fragment);
1629
-
1630
- // Get text content
1631
- let value = "";
1632
- const getText = (n) => {
1633
- if (n.nodeType === 3 || n.nodeType === 4) {
1634
- value += n.nodeValue || "";
1635
- } else if (n.childNodes) {
1636
- for (const child of n.childNodes) {
1637
- getText(child);
1638
- }
1639
- }
1640
- };
1641
- getText(fragment);
1642
-
1643
- // Add attribute to parent element
1644
- if (output.nodeType === 1) {
1645
- if (namespace) {
1646
- const ns = this.processAttributeValueTemplate(namespace, context);
1647
- output.setAttributeNS(ns, name, value);
1648
- } else {
1649
- output.setAttribute(name, value);
1650
- }
1651
- }
1652
- }
1653
-
1654
- xslIf(node, context, output) {
1655
- const test = node.getAttribute("test");
1656
- const result = this.evaluateXPath(test, context);
1657
-
1658
- if (this.xpathEvaluator.toBoolean(result)) {
1659
- this.processChildren(node, context, output);
1660
- }
1661
- }
1662
-
1663
- xslChoose(node, context, output) {
1664
- for (const child of node.childNodes) {
1665
- if (child.nodeType !== 1) continue;
1666
-
1667
- if (this.isXsltElement(child, "when")) {
1668
- const test = child.getAttribute("test");
1669
- const result = this.evaluateXPath(test, context);
1670
-
1671
- if (this.xpathEvaluator.toBoolean(result)) {
1672
- this.processChildren(child, context, output);
1673
- return;
1674
- }
1675
- } else if (this.isXsltElement(child, "otherwise")) {
1676
- this.processChildren(child, context, output);
1677
- return;
1678
- }
1679
- }
1680
- }
1681
-
1682
- xslForEach(node, context, output) {
1683
- const select = node.getAttribute("select");
1684
-
1685
- let nodes = this.evaluateXPath(select, context);
1686
- if (!Array.isArray(nodes)) {
1687
- nodes = nodes ? [nodes] : [];
1688
- }
1689
-
1690
- // Collect sort specifications
1691
- const sortSpecs = [];
1692
- for (const child of node.childNodes) {
1693
- if (child.nodeType === 1 && this.isXsltElement(child, "sort")) {
1694
- sortSpecs.push({
1695
- select: child.getAttribute("select") || ".",
1696
- order: child.getAttribute("order") || "ascending",
1697
- dataType: child.getAttribute("data-type") || "text",
1698
- caseOrder: child.getAttribute("case-order") || "upper-first",
1699
- lang: child.getAttribute("lang"),
1700
- });
1701
- }
1702
- }
1703
-
1704
- // Apply sorting
1705
- if (sortSpecs.length > 0) {
1706
- nodes = this.sortNodes(nodes, sortSpecs, context);
1707
- }
1708
-
1709
- // Process each node
1710
- for (let i = 0; i < nodes.length; i++) {
1711
- const newContext = context.clone({
1712
- currentNode: nodes[i],
1713
- currentNodeList: nodes,
1714
- position: i + 1,
1715
- });
1716
-
1717
- this.processChildren(node, newContext, output);
1718
- }
1719
- }
1720
-
1721
- xslCopy(node, context, output) {
1722
- const currentNode = context.currentNode;
1723
- const useAttributeSets = node.getAttribute("use-attribute-sets");
1724
-
1725
- switch (currentNode.nodeType) {
1726
- case 1: {
1727
- // Element
1728
- let copy;
1729
- if (currentNode.namespaceURI) {
1730
- copy = context.outputDocument.createElementNS(
1731
- currentNode.namespaceURI,
1732
- currentNode.nodeName,
1733
- );
1734
- } else {
1735
- copy = context.outputDocument.createElement(currentNode.nodeName);
1736
- }
1737
-
1738
- if (useAttributeSets) {
1739
- this.applyAttributeSets(useAttributeSets, context, copy);
1740
- }
1741
-
1742
- this.processChildren(node, context, copy);
1743
- output.appendChild(copy);
1744
- break;
1745
- }
1746
-
1747
- case 2: // Attribute
1748
- if (output.nodeType === 1) {
1749
- output.setAttribute(currentNode.name, currentNode.value);
1750
- }
1751
- break;
1752
-
1753
- case 3: // Text
1754
- case 4: {
1755
- // CDATA
1756
- const textCopy = context.outputDocument.createTextNode(
1757
- currentNode.nodeValue || "",
1758
- );
1759
- output.appendChild(textCopy);
1760
- break;
1761
- }
1762
-
1763
- case 7: {
1764
- // Processing Instruction
1765
- const piCopy = context.outputDocument.createProcessingInstruction(
1766
- currentNode.target,
1767
- currentNode.data,
1768
- );
1769
- output.appendChild(piCopy);
1770
- break;
1771
- }
1772
-
1773
- case 8: {
1774
- // Comment
1775
- const commentCopy = context.outputDocument.createComment(
1776
- currentNode.nodeValue || "",
1777
- );
1778
- output.appendChild(commentCopy);
1779
- break;
1780
- }
1781
-
1782
- case 9: // Document
1783
- case 11: // Document Fragment
1784
- this.processChildren(node, context, output);
1785
- break;
1786
- }
1787
- }
1788
-
1789
- xslCopyOf(node, context, output) {
1790
- const select = node.getAttribute("select");
1791
- const result = this.evaluateXPath(select, context);
1792
-
1793
- this.copyToOutput(result, context, output);
1794
- }
1795
-
1796
- copyToOutput(value, context, output) {
1797
- if (Array.isArray(value)) {
1798
- for (const item of value) {
1799
- this.copyToOutput(item, context, output);
1800
- }
1801
- return;
1802
- }
1803
-
1804
- if (value && value.nodeType) {
1805
- // Deep copy node
1806
- const clone = this.deepCloneNode(value, context.outputDocument);
1807
- output.appendChild(clone);
1808
- } else if (
1809
- typeof value === "string" ||
1810
- typeof value === "number" ||
1811
- typeof value === "boolean"
1812
- ) {
1813
- const text = context.outputDocument.createTextNode(String(value));
1814
- output.appendChild(text);
1815
- }
1816
- }
1817
-
1818
- deepCloneNode(node, targetDoc) {
1819
- switch (node.nodeType) {
1820
- case 1: {
1821
- // Element
1822
- let clone;
1823
- if (node.namespaceURI && targetDoc.createElementNS) {
1824
- clone = targetDoc.createElementNS(node.namespaceURI, node.nodeName);
1825
- } else {
1826
- clone = targetDoc.createElement(node.nodeName);
1827
- }
1828
-
1829
- if (node.attributes) {
1830
- for (const attr of node.attributes) {
1831
- clone.setAttribute(attr.name, attr.value);
1832
- }
1833
- }
1834
-
1835
- for (const child of node.childNodes) {
1836
- clone.appendChild(this.deepCloneNode(child, targetDoc));
1837
- }
1838
-
1839
- return clone;
1840
- }
1841
-
1842
- case 3: // Text
1843
- case 4: // CDATA
1844
- return targetDoc.createTextNode(node.nodeValue || "");
1845
-
1846
- case 7: // Processing Instruction
1847
- return targetDoc.createProcessingInstruction(node.target, node.data);
1848
-
1849
- case 8: // Comment
1850
- return targetDoc.createComment(node.nodeValue || "");
1851
-
1852
- case 11: {
1853
- // Document Fragment
1854
- const frag = targetDoc.createDocumentFragment();
1855
- for (const child of node.childNodes) {
1856
- frag.appendChild(this.deepCloneNode(child, targetDoc));
1857
- }
1858
- return frag;
1859
- }
1860
-
1861
- default:
1862
- return targetDoc.createTextNode("");
1863
- }
1864
- }
1865
-
1866
- xslVariable(node, context, _output) {
1867
- const name = node.getAttribute("name");
1868
- const select = node.getAttribute("select");
1869
-
1870
- let value;
1871
- if (select) {
1872
- value = this.evaluateXPath(select, context);
1873
- } else {
1874
- const fragment = context.outputDocument.createDocumentFragment();
1875
- this.processChildren(node, context, fragment);
1876
- value = fragment;
1877
- }
1878
-
1879
- context.setVariable(name, value);
1880
- }
1881
-
1882
- xslComment(node, context, output) {
1883
- const fragment = context.outputDocument.createDocumentFragment();
1884
- this.processChildren(node, context, fragment);
1885
-
1886
- let text = "";
1887
- const getText = (n) => {
1888
- if (n.nodeType === 3 || n.nodeType === 4) {
1889
- text += n.nodeValue || "";
1890
- } else if (n.childNodes) {
1891
- for (const child of n.childNodes) {
1892
- getText(child);
1893
- }
1894
- }
1895
- };
1896
- getText(fragment);
1897
-
1898
- const comment = context.outputDocument.createComment(text);
1899
- output.appendChild(comment);
1900
- }
1901
-
1902
- xslProcessingInstruction(node, context, output) {
1903
- const name = this.processAttributeValueTemplate(
1904
- node.getAttribute("name"),
1905
- context,
1906
- );
1907
-
1908
- const fragment = context.outputDocument.createDocumentFragment();
1909
- this.processChildren(node, context, fragment);
1910
-
1911
- let data = "";
1912
- const getText = (n) => {
1913
- if (n.nodeType === 3 || n.nodeType === 4) {
1914
- data += n.nodeValue || "";
1915
- } else if (n.childNodes) {
1916
- for (const child of n.childNodes) {
1917
- getText(child);
1918
- }
1919
- }
1920
- };
1921
- getText(fragment);
1922
-
1923
- const pi = context.outputDocument.createProcessingInstruction(name, data);
1924
- output.appendChild(pi);
1925
- }
1926
-
1927
- xslNumber(node, context, output) {
1928
- const value = node.getAttribute("value");
1929
- const format = node.getAttribute("format") || "1";
1930
-
1931
- let numbers;
1932
- if (value) {
1933
- numbers = [
1934
- Math.round(
1935
- this.xpathEvaluator.toNumber(this.evaluateXPath(value, context)),
1936
- ),
1937
- ];
1938
- } else {
1939
- numbers = countXsltNumber(
1940
- context.currentNode,
1941
- {
1942
- level: node.getAttribute("level") || "single",
1943
- count: node.getAttribute("count"),
1944
- from: node.getAttribute("from"),
1945
- },
1946
- (candidate, pattern) =>
1947
- this.matchesPattern(candidate, pattern, context),
1948
- );
1949
- }
1950
-
1951
- const text = context.outputDocument.createTextNode(
1952
- formatXsltNumber(numbers, format),
1953
- );
1954
- output.appendChild(text);
1955
- }
1956
-
1957
- /**
1958
- * Format a single number with an `xsl:number` format token.
1959
- *
1960
- * @param {number} number - The number to format
1961
- * @param {string} format - The format token, e.g. `1`, `01`, `a`, `I`
1962
- * @returns {string} The formatted number
1963
- */
1964
- formatNumber(number, format) {
1965
- return formatXsltNumber([number], format);
1966
- }
1967
-
1968
- /**
1969
- * Convert a number to an upper case Roman numeral.
1970
- *
1971
- * @param {number} num - The number to convert
1972
- * @returns {string} The Roman numeral
1973
- */
1974
- toRoman(num) {
1975
- return toRoman(num);
1976
- }
1977
-
1978
- xslMessage(node, context, _output) {
1979
- const terminate = node.getAttribute("terminate") === "yes";
1980
-
1981
- const fragment = context.outputDocument.createDocumentFragment();
1982
- this.processChildren(node, context, fragment);
1983
-
1984
- let text = "";
1985
- const getText = (n) => {
1986
- if (n.nodeType === 3 || n.nodeType === 4) {
1987
- text += n.nodeValue || "";
1988
- } else if (n.childNodes) {
1989
- for (const child of n.childNodes) {
1990
- getText(child);
1991
- }
1992
- }
1993
- };
1994
- getText(fragment);
1995
-
1996
- console.log("XSLT Message:", text);
1997
-
1998
- if (terminate) {
1999
- throw new Error(`XSLT terminated: ${text}`);
2000
- }
2001
- }
2002
-
2003
- applyAttributeSets(names, context, element) {
2004
- const setNames = names.split(/\s+/).filter(Boolean);
2005
-
2006
- for (const name of setNames) {
2007
- const attrSet = this.attributeSets[name];
2008
- if (attrSet) {
2009
- // Apply inherited sets first
2010
- if (attrSet.useAttributeSets.length > 0) {
2011
- this.applyAttributeSets(
2012
- attrSet.useAttributeSets.join(" "),
2013
- context,
2014
- element,
2015
- );
2016
- }
2017
-
2018
- // Apply attributes from this set
2019
- for (const child of attrSet.node.childNodes) {
2020
- if (child.nodeType === 1 && this.isXsltElement(child, "attribute")) {
2021
- this.xslAttribute(child, context, element);
2022
- }
2023
- }
2024
- }
2025
- }
2026
- }
2027
-
2028
- sortNodes(nodes, sortSpecs, context) {
2029
- return [...nodes].sort((a, b) => {
2030
- for (const spec of sortSpecs) {
2031
- const contextA = context.clone({ currentNode: a });
2032
- const contextB = context.clone({ currentNode: b });
2033
-
2034
- let valueA = this.evaluateXPath(spec.select, contextA);
2035
- let valueB = this.evaluateXPath(spec.select, contextB);
2036
-
2037
- // Convert to string for comparison (ensure non-null values)
2038
- valueA = this.xpathEvaluator.toString(valueA) || "";
2039
- valueB = this.xpathEvaluator.toString(valueB) || "";
2040
-
2041
- if (spec.dataType === "number") {
2042
- valueA = parseFloat(valueA) || 0;
2043
- valueB = parseFloat(valueB) || 0;
2044
- } else {
2045
- // Text comparison
2046
- if (spec.caseOrder === "lower-first") {
2047
- valueA = valueA.toLowerCase();
2048
- valueB = valueB.toLowerCase();
2049
- } else {
2050
- valueA = valueA.toUpperCase();
2051
- valueB = valueB.toUpperCase();
2052
- }
2053
- }
2054
-
2055
- let cmp;
2056
- if (typeof valueA === "number") {
2057
- cmp = valueA - valueB;
2058
- } else {
2059
- cmp = valueA.localeCompare(valueB, spec.lang || undefined);
2060
- }
2061
-
2062
- if (spec.order === "descending") {
2063
- cmp = -cmp;
2064
- }
2065
-
2066
- if (cmp !== 0) return cmp;
2067
- }
2068
-
2069
- return 0;
2070
- });
2071
- }
2072
-
2073
- evaluateXPath(expr, context) {
2074
- const ast = parseXPath(expr);
2075
- const xpathContext = new XPathContext(
2076
- context.currentNode,
2077
- context.position,
2078
- context.currentNodeList.length,
2079
- { ...context.variables, ...context.parameters },
2080
- context.namespaces,
2081
- context,
2082
- );
2083
- return this.xpathEvaluator.evaluate(ast, xpathContext);
2084
- }
2085
-
2086
- isXsltNamespace(node) {
2087
- return (
2088
- node.namespaceURI === XSLT_NS ||
2089
- (node.nodeName && node.nodeName.startsWith("xsl:"))
2090
- );
2091
- }
2092
-
2093
- isXsltElement(node, localName) {
2094
- if (node.nodeType !== 1) return false;
2095
-
2096
- const nodeName = node.localName || node.nodeName;
2097
- return (
2098
- (node.namespaceURI === XSLT_NS && nodeName === localName) ||
2099
- node.nodeName === `xsl:${localName}`
2100
- );
2101
- }
2102
- }
240
+ installMethods(
241
+ XsltEngine.prototype,
242
+ stylesheetLoadingMethods,
243
+ topLevelMethods,
244
+ declarationMethods,
245
+ outputDeclarationMethods,
246
+ templateRuleMethods,
247
+ templateInvocationMethods,
248
+ sequenceConstructorMethods,
249
+ workStackMethods,
250
+ controlFlowMethods,
251
+ bindingMethods,
252
+ textInstructionMethods,
253
+ numberingMethods,
254
+ nodeConstructionMethods,
255
+ copyInstructionMethods,
256
+ transformationMethods,
257
+ functionSupportMethods,
258
+ );