@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
@@ -3,1808 +3,256 @@
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";
9
31
  import { XPathEvaluator, XPathContext } from "../xpath/evaluator.js";
10
-
11
- const XSLT_NS = "http://www.w3.org/1999/XSL/Transform";
32
+ import { XSLT_NAMESPACE } from "./elements.js";
33
+ import { createXsltFunctions } from "./functions.js";
34
+ import { KeyIndexRegistry } from "./keys.js";
35
+ import { NamespaceAliasMap } from "./literalResult.js";
36
+ import { PatternMatcher } from "./patterns.js";
37
+ import { installMethods } from "./engine/methods.js";
38
+ import {
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";
60
+
61
+ export { XsltContext } from "./engine/context.js";
62
+ export { XSLT_MAX_TEMPLATE_DEPTH };
12
63
 
13
64
  /**
14
- * 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.
15
72
  */
16
- export class XsltContext {
17
- constructor(options = {}) {
18
- this.currentNode = options.currentNode;
19
- this.currentNodeList = options.currentNodeList || [];
20
- this.position = options.position || 1;
21
- this.variables = { ...options.variables };
22
- this.parameters = { ...options.parameters };
23
- this.outputDocument = options.outputDocument;
24
- this.stylesheet = options.stylesheet;
25
- this.namespaces = { ...options.namespaces };
26
- this.templates = options.templates || [];
27
- this.keys = options.keys || {};
28
- this.decimalFormats = options.decimalFormats || {};
29
- this.outputMethod = options.outputMethod || "xml";
30
- this.xpathEvaluator = options.xpathEvaluator || new XPathEvaluator();
31
- }
73
+ export const XSLT_MAX_RESULT_SIZE = 5000000;
32
74
 
33
- clone(overrides = {}) {
34
- return new XsltContext({
35
- currentNode: overrides.currentNode ?? this.currentNode,
36
- currentNodeList: overrides.currentNodeList ?? this.currentNodeList,
37
- position: overrides.position ?? this.position,
38
- variables: overrides.variables
39
- ? { ...this.variables, ...overrides.variables }
40
- : { ...this.variables },
41
- parameters: overrides.parameters
42
- ? { ...this.parameters, ...overrides.parameters }
43
- : { ...this.parameters },
44
- outputDocument: this.outputDocument,
45
- stylesheet: this.stylesheet,
46
- namespaces: overrides.namespaces
47
- ? { ...this.namespaces, ...overrides.namespaces }
48
- : { ...this.namespaces },
49
- templates: this.templates,
50
- keys: this.keys,
51
- decimalFormats: this.decimalFormats,
52
- outputMethod: this.outputMethod,
53
- xpathEvaluator: this.xpathEvaluator,
54
- });
55
- }
56
-
57
- getVariable(name) {
58
- if (name in this.variables) {
59
- return this.variables[name];
60
- }
61
- if (name in this.parameters) {
62
- return this.parameters[name];
63
- }
64
- throw new Error(`Undefined variable: $${name}`);
65
- }
66
-
67
- setVariable(name, value) {
68
- this.variables[name] = value;
69
- }
70
- }
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;
71
84
 
72
85
  /**
73
86
  * XSLT Engine
74
87
  */
75
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
+ */
76
105
  constructor(options = {}) {
77
- 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
+
78
118
  this.templates = [];
79
119
  this.keys = {};
80
120
  this.globalVariables = {};
81
121
  this.globalParameters = {};
82
- this.outputSettings = {
83
- method: "xml",
84
- encoding: "UTF-8",
85
- indent: "no",
86
- omitXmlDeclaration: "no",
87
- doctypePublic: null,
88
- doctypeSystem: null,
89
- mediaType: null,
90
- cdataSectionElements: [],
91
- };
122
+ this.outputSettings = createOutputSettings();
92
123
  this.namespaces = {};
93
124
  this.decimalFormats = {};
94
125
  this.stylesheetDoc = null;
95
126
  this.attributeSets = {};
96
- this.namespaceAliases = {};
127
+ this.namespaceAliases = new NamespaceAliasMap();
97
128
  this.stripSpace = [];
98
129
  this.preserveSpace = [];
99
130
 
100
131
  // Import/Include support
101
132
  this.stylesheetLoader = options.stylesheetLoader || null;
102
133
  this.currentImportPrecedence = 0;
103
- this.processedStylesheets = new Set();
134
+ // URIs of the stylesheets being loaded, used to detect import cycles
135
+ this.stylesheetStack = [];
104
136
  this.baseUri = options.baseUri || "";
105
- }
106
-
107
- /**
108
- * Set the stylesheet loader function for xsl:import and xsl:include
109
- * @param {Function} loader - Function(href, baseUri) => Document or string (XML)
110
- */
111
- setStylesheetLoader(loader) {
112
- this.stylesheetLoader = loader;
113
- }
114
-
115
- /**
116
- * Resolve a relative URI against a base URI
117
- */
118
- resolveUri(href, baseUri) {
119
- if (
120
- !baseUri ||
121
- href.startsWith("http://") ||
122
- href.startsWith("https://") ||
123
- href.startsWith("/")
124
- ) {
125
- return href;
126
- }
127
-
128
- // Remove filename from baseUri to get directory
129
- const lastSlash = baseUri.lastIndexOf("/");
130
- const baseDir = lastSlash >= 0 ? baseUri.substring(0, lastSlash + 1) : "";
131
-
132
- return baseDir + href;
133
- }
134
-
135
- /**
136
- * Load an external stylesheet document
137
- */
138
- loadStylesheet(href, baseUri) {
139
- if (!this.stylesheetLoader) {
140
- throw new Error(
141
- `Cannot load stylesheet "${href}": no stylesheetLoader configured. ` +
142
- "Use engine.setStylesheetLoader(fn) to provide a loader function.",
143
- );
144
- }
145
-
146
- const resolvedUri = this.resolveUri(href, baseUri);
147
- const result = this.stylesheetLoader(resolvedUri, baseUri);
148
-
149
- // If result is a string, it needs to be parsed (caller should handle this)
150
- return { document: result, uri: resolvedUri };
151
- }
152
-
153
- /**
154
- * Parse XML string to document (helper for stylesheet loading)
155
- */
156
- parseXmlString(xmlString) {
157
- if (typeof DOMParser !== "undefined") {
158
- const parser = new DOMParser();
159
- const doc = parser.parseFromString(xmlString, "application/xml");
160
- const parseError = doc.querySelector("parsererror");
161
- if (parseError) {
162
- throw new Error(`XML parse error: ${parseError.textContent}`);
163
- }
164
- return doc;
165
- }
166
- throw new Error("XML parsing not available in this environment");
167
- }
168
-
169
- /**
170
- * Import and compile an XSLT stylesheet
171
- * @param {Document|Element} stylesheetNode - The stylesheet document or root element
172
- * @param {string} [stylesheetUri] - Optional URI of the stylesheet for resolving imports
173
- */
174
- importStylesheet(stylesheetNode, stylesheetUri) {
175
- const isMainStylesheet = this.stylesheetDoc === null;
176
-
177
- if (isMainStylesheet) {
178
- this.stylesheetDoc = stylesheetNode.ownerDocument || stylesheetNode;
179
- if (stylesheetUri) {
180
- this.baseUri = stylesheetUri;
181
- this.processedStylesheets.add(stylesheetUri);
182
- }
183
- }
184
-
185
- const root = stylesheetNode.documentElement || stylesheetNode;
186
-
187
- // Validate stylesheet
188
- if (
189
- !this.isXsltElement(root, "stylesheet") &&
190
- !this.isXsltElement(root, "transform")
191
- ) {
192
- // Check for literal result element (simplified stylesheet)
193
- if (root.getAttribute && root.getAttribute("xsl:version")) {
194
- this.processLiteralResultStylesheet(root);
195
- return;
196
- }
197
- throw new Error(
198
- "Invalid XSLT stylesheet: root element must be xsl:stylesheet or xsl:transform",
199
- );
200
- }
201
-
202
- // Collect namespaces from root
203
- this.collectNamespaces(root);
204
-
205
- // XSLT 1.0: xsl:import elements MUST come first and are processed with lower precedence
206
- // Process imports first (they have lower precedence than the importing stylesheet)
207
- const imports = [];
208
- const otherElements = [];
209
-
210
- for (const child of root.childNodes) {
211
- if (child.nodeType !== 1) continue;
212
-
213
- if (this.isXsltElement(child, "import")) {
214
- imports.push(child);
215
- } else {
216
- otherElements.push(child);
217
- }
218
- }
219
-
220
- // Process imports (lower precedence - process before current stylesheet)
221
- for (const importNode of imports) {
222
- this.processImport(importNode, stylesheetUri || this.baseUri);
223
- }
224
-
225
- // Process other top-level elements (including includes)
226
- for (const child of otherElements) {
227
- if (this.isXsltElement(child, "template")) {
228
- this.registerTemplate(child);
229
- } else if (this.isXsltElement(child, "output")) {
230
- this.processOutput(child);
231
- } else if (this.isXsltElement(child, "variable")) {
232
- this.processGlobalVariable(child);
233
- } else if (this.isXsltElement(child, "param")) {
234
- this.processGlobalParam(child);
235
- } else if (this.isXsltElement(child, "key")) {
236
- this.processKey(child);
237
- } else if (this.isXsltElement(child, "decimal-format")) {
238
- this.processDecimalFormat(child);
239
- } else if (this.isXsltElement(child, "namespace-alias")) {
240
- this.processNamespaceAlias(child);
241
- } else if (this.isXsltElement(child, "attribute-set")) {
242
- this.processAttributeSet(child);
243
- } else if (this.isXsltElement(child, "strip-space")) {
244
- this.processStripSpace(child);
245
- } else if (this.isXsltElement(child, "preserve-space")) {
246
- this.processPreserveSpace(child);
247
- } else if (this.isXsltElement(child, "include")) {
248
- this.processInclude(child, stylesheetUri || this.baseUri);
249
- }
250
- }
251
-
252
- // Increment import precedence after processing this stylesheet
253
- if (isMainStylesheet) {
254
- this.currentImportPrecedence++;
255
- }
256
- }
257
-
258
- /**
259
- * Process xsl:include element
260
- * Includes are merged at the same import precedence level
261
- */
262
- processInclude(node, baseUri) {
263
- const href = node.getAttribute("href");
264
- if (!href) {
265
- throw new Error("xsl:include requires an href attribute");
266
- }
267
-
268
- const resolvedUri = this.resolveUri(href, baseUri);
269
-
270
- // Check for circular includes
271
- if (this.processedStylesheets.has(resolvedUri)) {
272
- throw new Error(`Circular stylesheet reference detected: ${resolvedUri}`);
273
- }
274
-
275
- this.processedStylesheets.add(resolvedUri);
276
-
277
- try {
278
- const { document: stylesheetDoc } = this.loadStylesheet(href, baseUri);
279
-
280
- // Parse if string
281
- let doc = stylesheetDoc;
282
- if (typeof stylesheetDoc === "string") {
283
- doc = this.parseXmlString(stylesheetDoc);
284
- }
285
-
286
- // Process the included stylesheet at the same import precedence
287
- const savedPrecedence = this.currentImportPrecedence;
288
- this.processIncludedStylesheet(doc, resolvedUri);
289
- this.currentImportPrecedence = savedPrecedence;
290
- } catch (error) {
291
- throw new Error(
292
- `Failed to include stylesheet "${href}": ${error.message}`,
293
- );
294
- }
295
- }
296
-
297
- /**
298
- * Process xsl:import element
299
- * Imports have lower precedence than the importing stylesheet
300
- */
301
- processImport(node, baseUri) {
302
- const href = node.getAttribute("href");
303
- if (!href) {
304
- throw new Error("xsl:import requires an href attribute");
305
- }
306
-
307
- const resolvedUri = this.resolveUri(href, baseUri);
308
-
309
- // Check for circular imports
310
- if (this.processedStylesheets.has(resolvedUri)) {
311
- throw new Error(`Circular stylesheet reference detected: ${resolvedUri}`);
312
- }
313
-
314
- this.processedStylesheets.add(resolvedUri);
315
-
316
- try {
317
- const { document: stylesheetDoc } = this.loadStylesheet(href, baseUri);
318
-
319
- // Parse if string
320
- let doc = stylesheetDoc;
321
- if (typeof stylesheetDoc === "string") {
322
- doc = this.parseXmlString(stylesheetDoc);
323
- }
324
-
325
- // Process the imported stylesheet (imports have lower precedence)
326
- // Don't increment precedence yet - imported templates get current (lower) precedence
327
- this.processIncludedStylesheet(doc, resolvedUri);
328
-
329
- // After processing import, increment precedence for next imports and main stylesheet
330
- this.currentImportPrecedence++;
331
- } catch (error) {
332
- throw new Error(
333
- `Failed to import stylesheet "${href}": ${error.message}`,
334
- );
335
- }
336
- }
337
-
338
- /**
339
- * Process an included/imported stylesheet document
340
- */
341
- processIncludedStylesheet(stylesheetDoc, stylesheetUri) {
342
- const root = stylesheetDoc.documentElement || stylesheetDoc;
343
-
344
- // Validate stylesheet
345
- if (
346
- !this.isXsltElement(root, "stylesheet") &&
347
- !this.isXsltElement(root, "transform")
348
- ) {
349
- throw new Error(
350
- "Included/imported document is not a valid XSLT stylesheet",
351
- );
352
- }
353
-
354
- // Collect namespaces
355
- this.collectNamespaces(root);
356
-
357
- // Process imports first (they have lower precedence)
358
- const imports = [];
359
- const otherElements = [];
360
-
361
- for (const child of root.childNodes) {
362
- if (child.nodeType !== 1) continue;
363
-
364
- if (this.isXsltElement(child, "import")) {
365
- imports.push(child);
366
- } else {
367
- otherElements.push(child);
368
- }
369
- }
370
-
371
- // Process nested imports
372
- for (const importNode of imports) {
373
- this.processImport(importNode, stylesheetUri);
374
- }
375
-
376
- // Process other elements
377
- for (const child of otherElements) {
378
- if (this.isXsltElement(child, "template")) {
379
- this.registerTemplate(child);
380
- } else if (this.isXsltElement(child, "output")) {
381
- this.processOutput(child);
382
- } else if (this.isXsltElement(child, "variable")) {
383
- this.processGlobalVariable(child);
384
- } else if (this.isXsltElement(child, "param")) {
385
- this.processGlobalParam(child);
386
- } else if (this.isXsltElement(child, "key")) {
387
- this.processKey(child);
388
- } else if (this.isXsltElement(child, "decimal-format")) {
389
- this.processDecimalFormat(child);
390
- } else if (this.isXsltElement(child, "namespace-alias")) {
391
- this.processNamespaceAlias(child);
392
- } else if (this.isXsltElement(child, "attribute-set")) {
393
- this.processAttributeSet(child);
394
- } else if (this.isXsltElement(child, "strip-space")) {
395
- this.processStripSpace(child);
396
- } else if (this.isXsltElement(child, "preserve-space")) {
397
- this.processPreserveSpace(child);
398
- } else if (this.isXsltElement(child, "include")) {
399
- this.processInclude(child, stylesheetUri);
400
- }
401
- }
402
- }
403
-
404
- processLiteralResultStylesheet(root) {
405
- // Simplified stylesheet - entire document is one template matching /
406
- this.templates.push({
407
- match: "/",
408
- name: null,
409
- mode: null,
410
- priority: 0.5,
411
- node: root,
412
- });
413
- }
414
-
415
- collectNamespaces(node) {
416
- if (!node.attributes) return;
417
-
418
- for (const attr of node.attributes) {
419
- if (attr.name.startsWith("xmlns:")) {
420
- const prefix = attr.name.substring(6);
421
- if (attr.value !== XSLT_NS) {
422
- this.namespaces[prefix] = attr.value;
423
- }
424
- } else if (attr.name === "xmlns" && attr.value !== XSLT_NS) {
425
- this.namespaces[""] = attr.value;
426
- }
427
- }
428
- }
429
-
430
- registerTemplate(node) {
431
- const match = node.getAttribute("match");
432
- const name = node.getAttribute("name");
433
- const mode = node.getAttribute("mode") || null;
434
- const priorityAttr = node.getAttribute("priority");
435
- const priority = priorityAttr
436
- ? parseFloat(priorityAttr)
437
- : this.calculatePriority(match);
438
-
439
- this.templates.push({
440
- match,
441
- name,
442
- mode,
443
- priority,
444
- importPrecedence: this.currentImportPrecedence,
445
- node,
446
- });
447
- }
448
137
 
449
- calculatePriority(matchPattern) {
450
- if (!matchPattern) return 0.5;
138
+ // document() support
139
+ this.documentLoader = options.documentLoader || null;
140
+ this.loadedDocuments = new Map();
451
141
 
452
- // Simplified priority calculation based on XPath 1.0 spec
453
- // - NodeType or * have priority -0.5
454
- // - NCName:* has priority -0.25
455
- // - QName has priority 0
456
- // - Other patterns have priority 0.5
142
+ // Parser for XML strings returned by the loaders (see domParsing.js)
143
+ this.domParser = options.domParser ?? null;
457
144
 
458
- if (
459
- matchPattern === "*" ||
460
- matchPattern === "node()" ||
461
- matchPattern === "text()" ||
462
- matchPattern === "comment()" ||
463
- matchPattern === "processing-instruction()"
464
- ) {
465
- return -0.5;
466
- }
145
+ // generate-id() support
146
+ this.generatedIds = new WeakMap();
147
+ this.generatedIdCount = 0;
467
148
 
468
- if (matchPattern.includes(":*")) {
469
- return -0.25;
470
- }
149
+ // Compiled XSLT patterns (template match, xsl:key, xsl:number)
150
+ this.patternMatcher = new PatternMatcher(this.xpathEvaluator);
471
151
 
472
- if (/^[a-zA-Z_][\w.-]*$/.test(matchPattern)) {
473
- return 0;
474
- }
475
-
476
- return 0.5;
477
- }
478
-
479
- processOutput(node) {
480
- const method = node.getAttribute("method");
481
- if (method) this.outputSettings.method = method;
482
-
483
- const encoding = node.getAttribute("encoding");
484
- if (encoding) this.outputSettings.encoding = encoding;
485
-
486
- const indent = node.getAttribute("indent");
487
- if (indent) this.outputSettings.indent = indent;
488
-
489
- const omit = node.getAttribute("omit-xml-declaration");
490
- if (omit) this.outputSettings.omitXmlDeclaration = omit;
491
-
492
- const doctypePublic = node.getAttribute("doctype-public");
493
- if (doctypePublic) this.outputSettings.doctypePublic = doctypePublic;
494
-
495
- const doctypeSystem = node.getAttribute("doctype-system");
496
- if (doctypeSystem) this.outputSettings.doctypeSystem = doctypeSystem;
497
-
498
- const mediaType = node.getAttribute("media-type");
499
- if (mediaType) this.outputSettings.mediaType = mediaType;
500
-
501
- const cdataElements = node.getAttribute("cdata-section-elements");
502
- if (cdataElements) {
503
- this.outputSettings.cdataSectionElements = cdataElements
504
- .split(/\s+/)
505
- .filter(Boolean);
506
- }
507
- }
508
-
509
- processGlobalVariable(node) {
510
- const name = node.getAttribute("name");
511
- const select = node.getAttribute("select");
512
-
513
- this.globalVariables[name] = { node, select };
514
- }
515
-
516
- processGlobalParam(node) {
517
- const name = node.getAttribute("name");
518
- const select = node.getAttribute("select");
519
-
520
- this.globalParameters[name] = { node, select };
521
- }
522
-
523
- processKey(node) {
524
- const name = node.getAttribute("name");
525
- const match = node.getAttribute("match");
526
- const use = node.getAttribute("use");
527
-
528
- this.keys[name] = { match, use };
529
- }
530
-
531
- processDecimalFormat(node) {
532
- const name = node.getAttribute("name") || "";
533
-
534
- this.decimalFormats[name] = {
535
- decimalSeparator: node.getAttribute("decimal-separator") || ".",
536
- groupingSeparator: node.getAttribute("grouping-separator") || ",",
537
- percent: node.getAttribute("percent") || "%",
538
- perMille: node.getAttribute("per-mille") || "\u2030",
539
- zeroDigit: node.getAttribute("zero-digit") || "0",
540
- digit: node.getAttribute("digit") || "#",
541
- patternSeparator: node.getAttribute("pattern-separator") || ";",
542
- infinity: node.getAttribute("infinity") || "Infinity",
543
- nan: node.getAttribute("NaN") || "NaN",
544
- minusSign: node.getAttribute("minus-sign") || "-",
545
- };
546
- }
547
-
548
- processNamespaceAlias(node) {
549
- const stylesheet = node.getAttribute("stylesheet-prefix");
550
- const result = node.getAttribute("result-prefix");
551
- this.namespaceAliases[stylesheet] = result;
552
- }
553
-
554
- processAttributeSet(node) {
555
- const name = node.getAttribute("name");
556
- const useAttributeSets = node.getAttribute("use-attribute-sets");
557
-
558
- this.attributeSets[name] = {
559
- node,
560
- useAttributeSets: useAttributeSets
561
- ? useAttributeSets.split(/\s+/).filter(Boolean)
562
- : [],
563
- };
564
- }
565
-
566
- processStripSpace(node) {
567
- const elements = node.getAttribute("elements");
568
- if (elements) {
569
- this.stripSpace.push(...elements.split(/\s+/).filter(Boolean));
570
- }
571
- }
572
-
573
- processPreserveSpace(node) {
574
- const elements = node.getAttribute("elements");
575
- if (elements) {
576
- this.preserveSpace.push(...elements.split(/\s+/).filter(Boolean));
577
- }
578
- }
579
-
580
- /**
581
- * Transform a source document
582
- */
583
- transform(sourceNode, ownerDocument) {
584
- const doc =
585
- ownerDocument || (typeof document !== "undefined" ? document : null);
586
-
587
- if (!doc) {
588
- throw new Error("No output document available");
589
- }
590
-
591
- // Create context - use document node as initial context for "/" template matching
592
- // XPath paths like "RootElement/child" expect to start from document node
593
- const context = new XsltContext({
594
- currentNode: sourceNode,
595
- currentNodeList: [sourceNode],
596
- position: 1,
597
- outputDocument: doc,
598
- stylesheet: this.stylesheetDoc,
599
- namespaces: { ...this.namespaces },
600
- templates: this.templates,
152
+ // key() support
153
+ this.rootContext = null;
154
+ this.keyRegistry = new KeyIndexRegistry({
601
155
  keys: this.keys,
602
- decimalFormats: this.decimalFormats,
603
- outputMethod: this.outputSettings.method,
604
- 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),
605
165
  });
606
166
 
607
- // Evaluate global variables
608
- for (const [name, def] of Object.entries(this.globalParameters)) {
609
- if (!(name in context.parameters)) {
610
- context.parameters[name] = this.evaluateVariable(def, context);
611
- }
612
- }
613
-
614
- for (const [name, def] of Object.entries(this.globalVariables)) {
615
- context.variables[name] = this.evaluateVariable(def, context);
616
- }
617
-
618
- // Create result document fragment
619
- const fragment = doc.createDocumentFragment();
620
-
621
- // Apply templates to document node (not documentElement)
622
- // This ensures "/" template has document as context, so paths like
623
- // "RootElement/child" work correctly
624
- this.applyTemplates([sourceNode], null, context, fragment);
625
-
626
- return fragment;
627
- }
628
-
629
- /**
630
- * Transform to a complete document
631
- */
632
- transformToDocument(sourceNode) {
633
- // For Node.js environments, we need a document implementation
634
- const doc = this.createDocument();
635
- const fragment = this.transform(sourceNode, doc);
636
-
637
- // Move fragment contents to document
638
- while (fragment.firstChild) {
639
- doc.appendChild(fragment.firstChild);
640
- }
641
-
642
- return doc;
643
- }
644
-
645
- createDocument() {
646
- if (typeof document !== "undefined") {
647
- return document.implementation.createDocument(null, null, null);
648
- }
649
-
650
- // For Node.js - would need JSDOM or similar
651
- throw new Error("Document creation not available in this environment");
652
- }
653
-
654
- evaluateVariable(def, context) {
655
- if (def.select) {
656
- return this.evaluateXPath(def.select, context);
657
- }
658
-
659
- // If no select, evaluate content as result tree fragment
660
- const fragment = context.outputDocument.createDocumentFragment();
661
- this.processChildren(def.node, context, fragment);
662
- return fragment;
663
- }
664
-
665
- /**
666
- * Apply templates to a node list
667
- */
668
- applyTemplates(nodes, mode, context, output) {
669
- const nodeList = Array.isArray(nodes) ? nodes : [nodes];
670
-
671
- for (let i = 0; i < nodeList.length; i++) {
672
- const node = nodeList[i];
673
- const template = this.findMatchingTemplate(node, mode, context);
674
-
675
- if (template) {
676
- const newContext = context.clone({
677
- currentNode: node,
678
- currentNodeList: nodeList,
679
- position: i + 1,
680
- });
681
-
682
- this.processTemplate(template.node, newContext, output);
683
- } else {
684
- // Built-in templates
685
- this.applyBuiltinTemplate(node, mode, context, output);
686
- }
687
- }
688
- }
689
-
690
- /**
691
- * Find the best matching template for a node
692
- */
693
- findMatchingTemplate(node, mode, context) {
694
- let bestMatch = null;
695
- let bestPriority = -Infinity;
696
- let bestImportPrecedence = -Infinity;
697
-
698
- for (const template of this.templates) {
699
- if (template.mode !== mode) continue;
700
- if (!template.match) continue;
701
-
702
- if (this.matchesPattern(node, template.match, context)) {
703
- const priority = template.priority;
704
- const importPrecedence = template.importPrecedence || 0;
705
-
706
- if (
707
- importPrecedence > bestImportPrecedence ||
708
- (importPrecedence === bestImportPrecedence && priority > bestPriority)
709
- ) {
710
- bestMatch = template;
711
- bestPriority = priority;
712
- bestImportPrecedence = importPrecedence;
713
- }
714
- }
715
- }
716
-
717
- return bestMatch;
718
- }
719
-
720
- /**
721
- * Check if a node matches an XSLT pattern
722
- */
723
- matchesPattern(node, pattern, context) {
724
- // Split union patterns
725
- const patterns = this.splitUnionPattern(pattern);
726
-
727
- for (const p of patterns) {
728
- if (this.matchesSinglePattern(node, p.trim(), context)) {
729
- return true;
730
- }
731
- }
732
-
733
- return false;
734
- }
735
-
736
- splitUnionPattern(pattern) {
737
- // Simple split on | not inside predicates or strings
738
- const parts = [];
739
- let current = "";
740
- let depth = 0;
741
- let inString = false;
742
- let stringChar = "";
743
-
744
- for (let i = 0; i < pattern.length; i++) {
745
- const char = pattern[i];
746
-
747
- if (inString) {
748
- current += char;
749
- if (char === stringChar) {
750
- inString = false;
751
- }
752
- } else if (char === '"' || char === "'") {
753
- inString = true;
754
- stringChar = char;
755
- current += char;
756
- } else if (char === "[") {
757
- depth++;
758
- current += char;
759
- } else if (char === "]") {
760
- depth--;
761
- current += char;
762
- } else if (char === "|" && depth === 0) {
763
- parts.push(current);
764
- current = "";
765
- } else {
766
- current += char;
767
- }
768
- }
769
-
770
- if (current) {
771
- parts.push(current);
772
- }
773
-
774
- return parts;
775
- }
776
-
777
- matchesSinglePattern(node, pattern, context) {
778
- try {
779
- // Handle root pattern
780
- if (pattern === "/") {
781
- return (
782
- node.nodeType === 9 || node === node.ownerDocument?.documentElement
783
- );
784
- }
785
-
786
- // Handle patterns like "item" (child match)
787
- // Need to check if node would be selected by pattern from parent
788
- const ast = parseXPath(pattern);
789
-
790
- // For patterns starting with /, evaluate from root
791
- if (pattern.startsWith("/")) {
792
- const doc = node.ownerDocument || node;
793
- const xpathContext = new XPathContext(
794
- doc,
795
- 1,
796
- 1,
797
- { ...context.variables, ...context.parameters },
798
- context.namespaces,
799
- );
800
- const result = this.xpathEvaluator.evaluate(ast, xpathContext);
801
- const nodes = Array.isArray(result) ? result : [result];
802
- return nodes.includes(node);
803
- }
804
-
805
- // For relative patterns, check if this node matches when evaluated from parent
806
- if (node.parentNode) {
807
- const xpathContext = new XPathContext(
808
- node.parentNode,
809
- 1,
810
- 1,
811
- { ...context.variables, ...context.parameters },
812
- context.namespaces,
813
- );
814
- const result = this.xpathEvaluator.evaluate(ast, xpathContext);
815
- const nodes = Array.isArray(result) ? result : [result];
816
- return nodes.includes(node);
817
- }
818
-
819
- // For document node without parent
820
- const xpathContext = new XPathContext(
821
- node,
822
- 1,
823
- 1,
824
- { ...context.variables, ...context.parameters },
825
- context.namespaces,
826
- );
827
- const result = this.xpathEvaluator.evaluate(ast, xpathContext);
828
- const nodes = Array.isArray(result) ? result : [result];
829
- return nodes.includes(node);
830
- } catch {
831
- return false;
832
- }
833
- }
834
-
835
- /**
836
- * Apply built-in template rules
837
- */
838
- applyBuiltinTemplate(node, mode, context, output) {
839
- switch (node.nodeType) {
840
- case 1: // Element
841
- case 9: // Document
842
- case 11: // Document Fragment
843
- // Process children
844
- this.applyTemplates(Array.from(node.childNodes), mode, context, output);
845
- break;
846
-
847
- case 3: // Text
848
- case 4: {
849
- // CDATA
850
- // Copy text value
851
- const text = context.outputDocument.createTextNode(
852
- node.nodeValue || "",
853
- );
854
- output.appendChild(text);
855
- break;
856
- }
857
-
858
- case 2: {
859
- // Attribute
860
- // Copy attribute value as text
861
- const attrText = context.outputDocument.createTextNode(
862
- node.nodeValue || "",
863
- );
864
- output.appendChild(attrText);
865
- break;
866
- }
867
-
868
- // Comments and PIs have no built-in template
869
- }
870
- }
871
-
872
- /**
873
- * Process template content
874
- */
875
- processTemplate(templateNode, context, output) {
876
- // Process template parameters first
877
- const localContext = context.clone();
878
-
879
- for (const child of templateNode.childNodes) {
880
- if (child.nodeType === 1 && this.isXsltElement(child, "param")) {
881
- const name = child.getAttribute("name");
882
- if (!(name in localContext.parameters)) {
883
- localContext.parameters[name] = this.evaluateVariable(
884
- { node: child, select: child.getAttribute("select") },
885
- localContext,
886
- );
887
- }
888
- }
889
- }
890
-
891
- this.processChildren(templateNode, localContext, output);
892
- }
893
-
894
- /**
895
- * Process child nodes of an XSLT element
896
- */
897
- processChildren(node, context, output) {
898
- for (const child of node.childNodes) {
899
- this.processNode(child, context, output);
900
- }
901
- }
902
-
903
- /**
904
- * Process a single node in the stylesheet
905
- */
906
- processNode(node, context, output) {
907
- switch (node.nodeType) {
908
- case 1: // Element
909
- this.processElement(node, context, output);
910
- break;
911
-
912
- case 3: // Text
913
- case 4: {
914
- // CDATA
915
- // Output text if not whitespace only (or if preserving space)
916
- const text = node.nodeValue;
917
- if (text && (text.trim() || this.shouldPreserveSpace(node))) {
918
- const textNode = context.outputDocument.createTextNode(text);
919
- output.appendChild(textNode);
920
- }
921
- break;
922
- }
923
- }
924
- }
925
-
926
- shouldPreserveSpace(node) {
927
- // Check xml:space attribute on ancestors
928
- let current = node.parentNode;
929
- while (current && current.nodeType === 1) {
930
- const space = current.getAttribute("xml:space");
931
- if (space === "preserve") return true;
932
- if (space === "default") return false;
933
- current = current.parentNode;
934
- }
935
- return false;
936
- }
937
-
938
- /**
939
- * Process an element in the stylesheet
940
- */
941
- processElement(node, context, output) {
942
- // Check if XSLT element
943
- if (this.isXsltNamespace(node)) {
944
- this.processXsltElement(node, context, output);
945
- } else {
946
- // Literal result element
947
- this.processLiteralResultElement(node, context, output);
948
- }
949
- }
950
-
951
- /**
952
- * Process an XSLT instruction element
953
- */
954
- processXsltElement(node, context, output) {
955
- const localName = node.localName || node.nodeName.replace(/^xsl:/, "");
956
-
957
- switch (localName) {
958
- case "apply-templates":
959
- this.xslApplyTemplates(node, context, output);
960
- break;
961
-
962
- case "call-template":
963
- this.xslCallTemplate(node, context, output);
964
- break;
965
-
966
- case "value-of":
967
- this.xslValueOf(node, context, output);
968
- break;
969
-
970
- case "text":
971
- this.xslText(node, context, output);
972
- break;
973
-
974
- case "element":
975
- this.xslElement(node, context, output);
976
- break;
977
-
978
- case "attribute":
979
- this.xslAttribute(node, context, output);
980
- break;
981
-
982
- case "if":
983
- this.xslIf(node, context, output);
984
- break;
985
-
986
- case "choose":
987
- this.xslChoose(node, context, output);
988
- break;
989
-
990
- case "for-each":
991
- this.xslForEach(node, context, output);
992
- break;
993
-
994
- case "copy":
995
- this.xslCopy(node, context, output);
996
- break;
997
-
998
- case "copy-of":
999
- this.xslCopyOf(node, context, output);
1000
- break;
1001
-
1002
- case "variable":
1003
- this.xslVariable(node, context, output);
1004
- break;
1005
-
1006
- case "param":
1007
- // Params are processed at template start
1008
- break;
1009
-
1010
- case "comment":
1011
- this.xslComment(node, context, output);
1012
- break;
1013
-
1014
- case "processing-instruction":
1015
- this.xslProcessingInstruction(node, context, output);
1016
- break;
1017
-
1018
- case "number":
1019
- this.xslNumber(node, context, output);
1020
- break;
1021
-
1022
- case "sort":
1023
- // Handled by apply-templates and for-each
1024
- break;
1025
-
1026
- case "with-param":
1027
- // Handled by call-template and apply-templates
1028
- break;
1029
-
1030
- case "message":
1031
- this.xslMessage(node, context, output);
1032
- break;
1033
-
1034
- case "fallback":
1035
- // Used for forward compatibility
1036
- break;
1037
-
1038
- default:
1039
- console.warn(`Unknown XSLT element: ${localName}`);
1040
- }
1041
- }
1042
-
1043
- /**
1044
- * Process a literal result element (non-XSLT)
1045
- */
1046
- processLiteralResultElement(node, context, output) {
1047
- // Create element in output
1048
- let outputElement;
1049
- const namespaceURI = node.namespaceURI;
1050
- const nodeName = node.nodeName;
1051
-
1052
- // Apply namespace aliases
1053
- let resolvedNS = namespaceURI;
1054
- if (namespaceURI) {
1055
- for (const [from, to] of Object.entries(this.namespaceAliases)) {
1056
- if (this.namespaces[from] === namespaceURI) {
1057
- resolvedNS = this.namespaces[to] || to;
1058
- break;
1059
- }
1060
- }
1061
- }
1062
-
1063
- if (resolvedNS && context.outputDocument.createElementNS) {
1064
- outputElement = context.outputDocument.createElementNS(
1065
- resolvedNS,
1066
- nodeName,
1067
- );
1068
- } else {
1069
- outputElement = context.outputDocument.createElement(nodeName);
1070
- }
1071
-
1072
- // Copy attributes (except XSLT namespace)
1073
- if (node.attributes) {
1074
- for (const attr of node.attributes) {
1075
- if (attr.namespaceURI === XSLT_NS) continue;
1076
- if (attr.name.startsWith("xmlns")) continue;
167
+ // Recoverable errors already reported, so each is reported once
168
+ this.reportedWarnings = new Set();
1077
169
 
1078
- // Process attribute value templates
1079
- const value = this.processAttributeValueTemplate(attr.value, context);
1080
- outputElement.setAttribute(attr.name, value);
1081
- }
1082
- }
170
+ // Extension element implementations by expanded name `{uri}local`
171
+ this.extensionElements = new Map();
1083
172
 
1084
- // Process children
1085
- this.processChildren(node, context, outputElement);
1086
-
1087
- output.appendChild(outputElement);
173
+ this.xpathEvaluator.registerFunctions(createXsltFunctions(this));
1088
174
  }
1089
175
 
1090
176
  /**
1091
- * Process attribute value templates (expressions in curly braces)
177
+ * Evaluate an XPath expression in an XSLT context.
178
+ *
179
+ * @param {string} expr - The expression
180
+ * @param {XsltContext} context - The current context
181
+ * @returns {*} The value
1092
182
  */
1093
- processAttributeValueTemplate(value, context) {
1094
- if (!value.includes("{")) return value;
1095
-
1096
- let result = "";
1097
- let i = 0;
1098
-
1099
- while (i < value.length) {
1100
- if (value[i] === "{") {
1101
- if (value[i + 1] === "{") {
1102
- // Escaped brace
1103
- result += "{";
1104
- i += 2;
1105
- } else {
1106
- // Find closing brace
1107
- let depth = 1;
1108
- let j = i + 1;
1109
- while (j < value.length && depth > 0) {
1110
- if (value[j] === "{") depth++;
1111
- else if (value[j] === "}") depth--;
1112
- j++;
1113
- }
1114
-
1115
- const expr = value.substring(i + 1, j - 1);
1116
- const evalResult = this.evaluateXPath(expr, context);
1117
- result += this.xpathEvaluator.toString(evalResult);
1118
- i = j;
1119
- }
1120
- } else if (value[i] === "}") {
1121
- if (value[i + 1] === "}") {
1122
- // Escaped brace
1123
- result += "}";
1124
- i += 2;
1125
- } else {
1126
- throw new Error("Unmatched } in attribute value template");
1127
- }
1128
- } else {
1129
- result += value[i];
1130
- i++;
1131
- }
1132
- }
1133
-
1134
- return result;
1135
- }
1136
-
1137
- // XSLT Instructions
1138
- xslApplyTemplates(node, context, output) {
1139
- const select = node.getAttribute("select") || "node()";
1140
- const mode = node.getAttribute("mode") || null;
1141
-
1142
- // Evaluate select expression
1143
- let nodes = this.evaluateXPath(select, context);
1144
- if (!Array.isArray(nodes)) {
1145
- nodes = nodes ? [nodes] : [];
1146
- }
1147
-
1148
- // Collect sort specifications
1149
- const sortSpecs = [];
1150
- for (const child of node.childNodes) {
1151
- if (child.nodeType === 1 && this.isXsltElement(child, "sort")) {
1152
- sortSpecs.push({
1153
- select: child.getAttribute("select") || ".",
1154
- order: child.getAttribute("order") || "ascending",
1155
- dataType: child.getAttribute("data-type") || "text",
1156
- caseOrder: child.getAttribute("case-order") || "upper-first",
1157
- lang: child.getAttribute("lang"),
1158
- });
1159
- }
1160
- }
1161
-
1162
- // Apply sorting
1163
- if (sortSpecs.length > 0) {
1164
- nodes = this.sortNodes(nodes, sortSpecs, context);
1165
- }
1166
-
1167
- // Collect with-param values
1168
- const params = {};
1169
- for (const child of node.childNodes) {
1170
- if (child.nodeType === 1 && this.isXsltElement(child, "with-param")) {
1171
- const name = child.getAttribute("name");
1172
- const selectAttr = child.getAttribute("select");
1173
- if (selectAttr) {
1174
- params[name] = this.evaluateXPath(selectAttr, context);
1175
- } else {
1176
- const fragment = context.outputDocument.createDocumentFragment();
1177
- this.processChildren(child, context, fragment);
1178
- params[name] = fragment;
1179
- }
1180
- }
1181
- }
1182
-
1183
- // Apply templates with new context including params
1184
- const newContext = context.clone({
1185
- parameters: { ...context.parameters, ...params },
1186
- });
1187
- this.applyTemplates(nodes, mode, newContext, output);
1188
- }
1189
-
1190
- xslCallTemplate(node, context, output) {
1191
- const name = node.getAttribute("name");
1192
-
1193
- // Find named template
1194
- const template = this.templates.find((t) => t.name === name);
1195
- if (!template) {
1196
- throw new Error(`Template not found: ${name}`);
1197
- }
1198
-
1199
- // Collect with-param values
1200
- const params = {};
1201
- for (const child of node.childNodes) {
1202
- if (child.nodeType === 1 && this.isXsltElement(child, "with-param")) {
1203
- const paramName = child.getAttribute("name");
1204
- const selectAttr = child.getAttribute("select");
1205
- if (selectAttr) {
1206
- params[paramName] = this.evaluateXPath(selectAttr, context);
1207
- } else {
1208
- const fragment = context.outputDocument.createDocumentFragment();
1209
- this.processChildren(child, context, fragment);
1210
- params[paramName] = fragment;
1211
- }
1212
- }
1213
- }
1214
-
1215
- // Call template with params
1216
- const newContext = context.clone({
1217
- parameters: { ...context.parameters, ...params },
1218
- });
1219
- this.processTemplate(template.node, newContext, output);
1220
- }
1221
-
1222
- xslValueOf(node, context, output) {
1223
- const select = node.getAttribute("select");
1224
- const disableOutputEscaping =
1225
- node.getAttribute("disable-output-escaping") === "yes";
1226
-
1227
- const result = this.evaluateXPath(select, context);
1228
- const text = this.xpathEvaluator.toString(result);
1229
-
1230
- if (text) {
1231
- const textNode = context.outputDocument.createTextNode(text);
1232
- if (disableOutputEscaping) {
1233
- textNode._disableOutputEscaping = true;
1234
- }
1235
- output.appendChild(textNode);
1236
- }
1237
- }
1238
-
1239
- xslText(node, context, output) {
1240
- const disableOutputEscaping =
1241
- node.getAttribute("disable-output-escaping") === "yes";
1242
- let text = "";
1243
-
1244
- for (const child of node.childNodes) {
1245
- if (child.nodeType === 3 || child.nodeType === 4) {
1246
- text += child.nodeValue || "";
1247
- }
1248
- }
1249
-
1250
- if (text) {
1251
- const textNode = context.outputDocument.createTextNode(text);
1252
- if (disableOutputEscaping) {
1253
- textNode._disableOutputEscaping = true;
1254
- }
1255
- output.appendChild(textNode);
1256
- }
1257
- }
1258
-
1259
- xslElement(node, context, output) {
1260
- const name = this.processAttributeValueTemplate(
1261
- node.getAttribute("name"),
1262
- context,
1263
- );
1264
- const namespace = node.getAttribute("namespace");
1265
- const useAttributeSets = node.getAttribute("use-attribute-sets");
1266
-
1267
- let element;
1268
- if (namespace) {
1269
- const ns = this.processAttributeValueTemplate(namespace, context);
1270
- element = context.outputDocument.createElementNS(ns, name);
1271
- } else {
1272
- element = context.outputDocument.createElement(name);
1273
- }
1274
-
1275
- // Apply attribute sets
1276
- if (useAttributeSets) {
1277
- this.applyAttributeSets(useAttributeSets, context, element);
1278
- }
1279
-
1280
- this.processChildren(node, context, element);
1281
- output.appendChild(element);
1282
- }
1283
-
1284
- xslAttribute(node, context, output) {
1285
- const name = this.processAttributeValueTemplate(
1286
- node.getAttribute("name"),
1287
- context,
1288
- );
1289
- const namespace = node.getAttribute("namespace");
1290
-
1291
- // Collect content
1292
- const fragment = context.outputDocument.createDocumentFragment();
1293
- this.processChildren(node, context, fragment);
1294
-
1295
- // Get text content
1296
- let value = "";
1297
- const getText = (n) => {
1298
- if (n.nodeType === 3 || n.nodeType === 4) {
1299
- value += n.nodeValue || "";
1300
- } else if (n.childNodes) {
1301
- for (const child of n.childNodes) {
1302
- getText(child);
1303
- }
1304
- }
1305
- };
1306
- getText(fragment);
1307
-
1308
- // Add attribute to parent element
1309
- if (output.nodeType === 1) {
1310
- if (namespace) {
1311
- const ns = this.processAttributeValueTemplate(namespace, context);
1312
- output.setAttributeNS(ns, name, value);
1313
- } else {
1314
- output.setAttribute(name, value);
1315
- }
1316
- }
1317
- }
1318
-
1319
- xslIf(node, context, output) {
1320
- const test = node.getAttribute("test");
1321
- const result = this.evaluateXPath(test, context);
1322
-
1323
- if (this.xpathEvaluator.toBoolean(result)) {
1324
- this.processChildren(node, context, output);
1325
- }
1326
- }
1327
-
1328
- xslChoose(node, context, output) {
1329
- for (const child of node.childNodes) {
1330
- if (child.nodeType !== 1) continue;
1331
-
1332
- if (this.isXsltElement(child, "when")) {
1333
- const test = child.getAttribute("test");
1334
- const result = this.evaluateXPath(test, context);
1335
-
1336
- if (this.xpathEvaluator.toBoolean(result)) {
1337
- this.processChildren(child, context, output);
1338
- return;
1339
- }
1340
- } else if (this.isXsltElement(child, "otherwise")) {
1341
- this.processChildren(child, context, output);
1342
- return;
1343
- }
1344
- }
1345
- }
1346
-
1347
- xslForEach(node, context, output) {
1348
- const select = node.getAttribute("select");
1349
-
1350
- let nodes = this.evaluateXPath(select, context);
1351
- if (!Array.isArray(nodes)) {
1352
- nodes = nodes ? [nodes] : [];
1353
- }
1354
-
1355
- // Collect sort specifications
1356
- const sortSpecs = [];
1357
- for (const child of node.childNodes) {
1358
- if (child.nodeType === 1 && this.isXsltElement(child, "sort")) {
1359
- sortSpecs.push({
1360
- select: child.getAttribute("select") || ".",
1361
- order: child.getAttribute("order") || "ascending",
1362
- dataType: child.getAttribute("data-type") || "text",
1363
- caseOrder: child.getAttribute("case-order") || "upper-first",
1364
- lang: child.getAttribute("lang"),
1365
- });
1366
- }
1367
- }
1368
-
1369
- // Apply sorting
1370
- if (sortSpecs.length > 0) {
1371
- nodes = this.sortNodes(nodes, sortSpecs, context);
1372
- }
1373
-
1374
- // Process each node
1375
- for (let i = 0; i < nodes.length; i++) {
1376
- const newContext = context.clone({
1377
- currentNode: nodes[i],
1378
- currentNodeList: nodes,
1379
- position: i + 1,
1380
- });
1381
-
1382
- this.processChildren(node, newContext, output);
1383
- }
1384
- }
1385
-
1386
- xslCopy(node, context, output) {
1387
- const currentNode = context.currentNode;
1388
- const useAttributeSets = node.getAttribute("use-attribute-sets");
1389
-
1390
- switch (currentNode.nodeType) {
1391
- case 1: {
1392
- // Element
1393
- let copy;
1394
- if (currentNode.namespaceURI) {
1395
- copy = context.outputDocument.createElementNS(
1396
- currentNode.namespaceURI,
1397
- currentNode.nodeName,
1398
- );
1399
- } else {
1400
- copy = context.outputDocument.createElement(currentNode.nodeName);
1401
- }
1402
-
1403
- if (useAttributeSets) {
1404
- this.applyAttributeSets(useAttributeSets, context, copy);
1405
- }
1406
-
1407
- this.processChildren(node, context, copy);
1408
- output.appendChild(copy);
1409
- break;
1410
- }
1411
-
1412
- case 2: // Attribute
1413
- if (output.nodeType === 1) {
1414
- output.setAttribute(currentNode.name, currentNode.value);
1415
- }
1416
- break;
1417
-
1418
- case 3: // Text
1419
- case 4: {
1420
- // CDATA
1421
- const textCopy = context.outputDocument.createTextNode(
1422
- currentNode.nodeValue || "",
1423
- );
1424
- output.appendChild(textCopy);
1425
- break;
1426
- }
1427
-
1428
- case 7: {
1429
- // Processing Instruction
1430
- const piCopy = context.outputDocument.createProcessingInstruction(
1431
- currentNode.target,
1432
- currentNode.data,
1433
- );
1434
- output.appendChild(piCopy);
1435
- break;
1436
- }
1437
-
1438
- case 8: {
1439
- // Comment
1440
- const commentCopy = context.outputDocument.createComment(
1441
- currentNode.nodeValue || "",
1442
- );
1443
- output.appendChild(commentCopy);
1444
- break;
1445
- }
1446
-
1447
- case 9: // Document
1448
- case 11: // Document Fragment
1449
- this.processChildren(node, context, output);
1450
- break;
1451
- }
1452
- }
1453
-
1454
- xslCopyOf(node, context, output) {
1455
- const select = node.getAttribute("select");
1456
- const result = this.evaluateXPath(select, context);
1457
-
1458
- this.copyToOutput(result, context, output);
1459
- }
1460
-
1461
- copyToOutput(value, context, output) {
1462
- if (Array.isArray(value)) {
1463
- for (const item of value) {
1464
- this.copyToOutput(item, context, output);
1465
- }
1466
- return;
1467
- }
1468
-
1469
- if (value && value.nodeType) {
1470
- // Deep copy node
1471
- const clone = this.deepCloneNode(value, context.outputDocument);
1472
- output.appendChild(clone);
1473
- } else if (
1474
- typeof value === "string" ||
1475
- typeof value === "number" ||
1476
- typeof value === "boolean"
1477
- ) {
1478
- const text = context.outputDocument.createTextNode(String(value));
1479
- output.appendChild(text);
1480
- }
1481
- }
1482
-
1483
- deepCloneNode(node, targetDoc) {
1484
- switch (node.nodeType) {
1485
- case 1: {
1486
- // Element
1487
- let clone;
1488
- if (node.namespaceURI && targetDoc.createElementNS) {
1489
- clone = targetDoc.createElementNS(node.namespaceURI, node.nodeName);
1490
- } else {
1491
- clone = targetDoc.createElement(node.nodeName);
1492
- }
1493
-
1494
- if (node.attributes) {
1495
- for (const attr of node.attributes) {
1496
- clone.setAttribute(attr.name, attr.value);
1497
- }
1498
- }
1499
-
1500
- for (const child of node.childNodes) {
1501
- clone.appendChild(this.deepCloneNode(child, targetDoc));
1502
- }
1503
-
1504
- return clone;
1505
- }
1506
-
1507
- case 3: // Text
1508
- case 4: // CDATA
1509
- return targetDoc.createTextNode(node.nodeValue || "");
1510
-
1511
- case 7: // Processing Instruction
1512
- return targetDoc.createProcessingInstruction(node.target, node.data);
1513
-
1514
- case 8: // Comment
1515
- return targetDoc.createComment(node.nodeValue || "");
1516
-
1517
- case 11: {
1518
- // Document Fragment
1519
- const frag = targetDoc.createDocumentFragment();
1520
- for (const child of node.childNodes) {
1521
- frag.appendChild(this.deepCloneNode(child, targetDoc));
1522
- }
1523
- return frag;
1524
- }
1525
-
1526
- default:
1527
- return targetDoc.createTextNode("");
1528
- }
1529
- }
1530
-
1531
- xslVariable(node, context, _output) {
1532
- const name = node.getAttribute("name");
1533
- const select = node.getAttribute("select");
1534
-
1535
- let value;
1536
- if (select) {
1537
- value = this.evaluateXPath(select, context);
1538
- } else {
1539
- const fragment = context.outputDocument.createDocumentFragment();
1540
- this.processChildren(node, context, fragment);
1541
- value = fragment;
1542
- }
1543
-
1544
- context.setVariable(name, value);
1545
- }
1546
-
1547
- xslComment(node, context, output) {
1548
- const fragment = context.outputDocument.createDocumentFragment();
1549
- this.processChildren(node, context, fragment);
1550
-
1551
- let text = "";
1552
- const getText = (n) => {
1553
- if (n.nodeType === 3 || n.nodeType === 4) {
1554
- text += n.nodeValue || "";
1555
- } else if (n.childNodes) {
1556
- for (const child of n.childNodes) {
1557
- getText(child);
1558
- }
1559
- }
1560
- };
1561
- getText(fragment);
1562
-
1563
- const comment = context.outputDocument.createComment(text);
1564
- output.appendChild(comment);
1565
- }
1566
-
1567
- xslProcessingInstruction(node, context, output) {
1568
- const name = this.processAttributeValueTemplate(
1569
- node.getAttribute("name"),
1570
- context,
1571
- );
1572
-
1573
- const fragment = context.outputDocument.createDocumentFragment();
1574
- this.processChildren(node, context, fragment);
1575
-
1576
- let data = "";
1577
- const getText = (n) => {
1578
- if (n.nodeType === 3 || n.nodeType === 4) {
1579
- data += n.nodeValue || "";
1580
- } else if (n.childNodes) {
1581
- for (const child of n.childNodes) {
1582
- getText(child);
1583
- }
1584
- }
1585
- };
1586
- getText(fragment);
1587
-
1588
- const pi = context.outputDocument.createProcessingInstruction(name, data);
1589
- output.appendChild(pi);
1590
- }
1591
-
1592
- xslNumber(node, context, output) {
1593
- const value = node.getAttribute("value");
1594
- const format = node.getAttribute("format") || "1";
1595
- const level = node.getAttribute("level") || "single";
1596
-
1597
- let number;
1598
- if (value) {
1599
- number = Math.round(
1600
- this.xpathEvaluator.toNumber(this.evaluateXPath(value, context)),
1601
- );
1602
- } else {
1603
- // Count based on level
1604
- number = this.countNumber(context.currentNode, level, node, context);
1605
- }
1606
-
1607
- const formatted = this.formatNumber(number, format);
1608
- const text = context.outputDocument.createTextNode(formatted);
1609
- output.appendChild(text);
1610
- }
1611
-
1612
- countNumber(node, level, spec, context) {
1613
- const count = spec.getAttribute("count");
1614
- const _from = spec.getAttribute("from");
1615
-
1616
- // Simplified implementation
1617
- if (level === "single") {
1618
- // Count preceding siblings matching pattern
1619
- let n = 1;
1620
- let sibling = node.previousSibling;
1621
- while (sibling) {
1622
- if (sibling.nodeType === 1) {
1623
- if (!count || this.matchesPattern(sibling, count, context)) {
1624
- n++;
1625
- }
1626
- }
1627
- sibling = sibling.previousSibling;
1628
- }
1629
- return n;
1630
- }
1631
-
1632
- return 1;
1633
- }
1634
-
1635
- formatNumber(number, format) {
1636
- // Simple format implementation
1637
- if (/^[0-9]+$/.test(format)) {
1638
- return String(number).padStart(format.length, "0");
1639
- }
1640
-
1641
- if (format === "a") {
1642
- return String.fromCharCode(96 + ((number - 1) % 26) + 1);
1643
- }
1644
-
1645
- if (format === "A") {
1646
- return String.fromCharCode(64 + ((number - 1) % 26) + 1);
1647
- }
1648
-
1649
- if (format === "i") {
1650
- return this.toRoman(number).toLowerCase();
1651
- }
1652
-
1653
- if (format === "I") {
1654
- return this.toRoman(number);
1655
- }
1656
-
1657
- return String(number);
1658
- }
1659
-
1660
- toRoman(num) {
1661
- const romanNumerals = [
1662
- ["M", 1000],
1663
- ["CM", 900],
1664
- ["D", 500],
1665
- ["CD", 400],
1666
- ["C", 100],
1667
- ["XC", 90],
1668
- ["L", 50],
1669
- ["XL", 40],
1670
- ["X", 10],
1671
- ["IX", 9],
1672
- ["V", 5],
1673
- ["IV", 4],
1674
- ["I", 1],
1675
- ];
1676
-
1677
- let result = "";
1678
- for (const [numeral, value] of romanNumerals) {
1679
- while (num >= value) {
1680
- result += numeral;
1681
- num -= value;
1682
- }
1683
- }
1684
- return result;
1685
- }
1686
-
1687
- xslMessage(node, context, _output) {
1688
- const terminate = node.getAttribute("terminate") === "yes";
1689
-
1690
- const fragment = context.outputDocument.createDocumentFragment();
1691
- this.processChildren(node, context, fragment);
1692
-
1693
- let text = "";
1694
- const getText = (n) => {
1695
- if (n.nodeType === 3 || n.nodeType === 4) {
1696
- text += n.nodeValue || "";
1697
- } else if (n.childNodes) {
1698
- for (const child of n.childNodes) {
1699
- getText(child);
1700
- }
1701
- }
1702
- };
1703
- getText(fragment);
1704
-
1705
- console.log("XSLT Message:", text);
1706
-
1707
- if (terminate) {
1708
- throw new Error(`XSLT terminated: ${text}`);
1709
- }
1710
- }
1711
-
1712
- applyAttributeSets(names, context, element) {
1713
- const setNames = names.split(/\s+/).filter(Boolean);
1714
-
1715
- for (const name of setNames) {
1716
- const attrSet = this.attributeSets[name];
1717
- if (attrSet) {
1718
- // Apply inherited sets first
1719
- if (attrSet.useAttributeSets.length > 0) {
1720
- this.applyAttributeSets(
1721
- attrSet.useAttributeSets.join(" "),
1722
- context,
1723
- element,
1724
- );
1725
- }
1726
-
1727
- // Apply attributes from this set
1728
- for (const child of attrSet.node.childNodes) {
1729
- if (child.nodeType === 1 && this.isXsltElement(child, "attribute")) {
1730
- this.xslAttribute(child, context, element);
1731
- }
1732
- }
1733
- }
1734
- }
1735
- }
1736
-
1737
- sortNodes(nodes, sortSpecs, context) {
1738
- return [...nodes].sort((a, b) => {
1739
- for (const spec of sortSpecs) {
1740
- const contextA = context.clone({ currentNode: a });
1741
- const contextB = context.clone({ currentNode: b });
1742
-
1743
- let valueA = this.evaluateXPath(spec.select, contextA);
1744
- let valueB = this.evaluateXPath(spec.select, contextB);
1745
-
1746
- // Convert to string for comparison (ensure non-null values)
1747
- valueA = this.xpathEvaluator.toString(valueA) || "";
1748
- valueB = this.xpathEvaluator.toString(valueB) || "";
1749
-
1750
- if (spec.dataType === "number") {
1751
- valueA = parseFloat(valueA) || 0;
1752
- valueB = parseFloat(valueB) || 0;
1753
- } else {
1754
- // Text comparison
1755
- if (spec.caseOrder === "lower-first") {
1756
- valueA = valueA.toLowerCase();
1757
- valueB = valueB.toLowerCase();
1758
- } else {
1759
- valueA = valueA.toUpperCase();
1760
- valueB = valueB.toUpperCase();
1761
- }
1762
- }
1763
-
1764
- let cmp;
1765
- if (typeof valueA === "number") {
1766
- cmp = valueA - valueB;
1767
- } else {
1768
- cmp = valueA.localeCompare(valueB, spec.lang || undefined);
1769
- }
1770
-
1771
- if (spec.order === "descending") {
1772
- cmp = -cmp;
1773
- }
1774
-
1775
- if (cmp !== 0) return cmp;
1776
- }
1777
-
1778
- return 0;
1779
- });
1780
- }
1781
-
1782
183
  evaluateXPath(expr, context) {
1783
184
  const ast = parseXPath(expr);
1784
185
  const xpathContext = new XPathContext(
1785
186
  context.currentNode,
1786
187
  context.position,
1787
188
  context.currentNodeList.length,
1788
- { ...context.variables, ...context.parameters },
189
+ context.xpathVariables,
1789
190
  context.namespaces,
191
+ context,
1790
192
  );
1791
193
  return this.xpathEvaluator.evaluate(ast, xpathContext);
1792
194
  }
1793
195
 
196
+ /**
197
+ * Whether a node is in the XSLT namespace (or uses the xsl prefix).
198
+ *
199
+ * @param {Node} node - A stylesheet node
200
+ * @returns {boolean} True for an XSLT element
201
+ */
1794
202
  isXsltNamespace(node) {
1795
203
  return (
1796
- node.namespaceURI === XSLT_NS ||
204
+ node.namespaceURI === XSLT_NAMESPACE ||
1797
205
  (node.nodeName && node.nodeName.startsWith("xsl:"))
1798
206
  );
1799
207
  }
1800
208
 
209
+ /**
210
+ * Whether a node is the XSLT element of a local name.
211
+ *
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
215
+ */
1801
216
  isXsltElement(node, localName) {
1802
217
  if (node.nodeType !== 1) return false;
1803
218
 
1804
219
  const nodeName = node.localName || node.nodeName;
1805
220
  return (
1806
- (node.namespaceURI === XSLT_NS && nodeName === localName) ||
221
+ (node.namespaceURI === XSLT_NAMESPACE && nodeName === localName) ||
1807
222
  node.nodeName === `xsl:${localName}`
1808
223
  );
1809
224
  }
225
+
226
+ /**
227
+ * Report a recoverable stylesheet error with console.warn, once per engine
228
+ * and message (libxslt reports these errors and goes on).
229
+ *
230
+ * @param {string} message - The error description
231
+ * @returns {void}
232
+ */
233
+ warnOnce(message) {
234
+ if (this.reportedWarnings.has(message)) return;
235
+ this.reportedWarnings.add(message);
236
+ console.warn(`XSLT: ${message}`);
237
+ }
1810
238
  }
239
+
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
+ );