@tradik/xslt-processor 1.0.3 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +110 -520
  3. package/bin/lib/decode.js +15 -0
  4. package/bin/lib/dom.js +177 -0
  5. package/bin/lib/loaders.js +127 -0
  6. package/bin/lib/options.js +131 -0
  7. package/bin/lib/output.js +114 -0
  8. package/bin/lib/paths.js +186 -0
  9. package/bin/lib/transform.js +206 -0
  10. package/bin/xslt.js +73 -168
  11. package/dist/xslt-processor.browser.js +9564 -1585
  12. package/dist/xslt-processor.browser.js.map +4 -4
  13. package/dist/xslt-processor.browser.min.js +13 -2
  14. package/dist/xslt-processor.browser.min.js.map +4 -4
  15. package/dist/xslt-processor.cjs +9572 -1586
  16. package/dist/xslt-processor.cjs.map +4 -4
  17. package/dist/xslt-processor.d.cts +658 -0
  18. package/dist/xslt-processor.d.ts +459 -12
  19. package/dist/xslt-processor.js +9546 -1582
  20. package/dist/xslt-processor.js.map +4 -4
  21. package/package.json +71 -20
  22. package/src/XSLTProcessor.js +494 -48
  23. package/src/async/abort.js +63 -0
  24. package/src/async/documentUris.js +128 -0
  25. package/src/async/loaders.js +134 -0
  26. package/src/async/preload.js +159 -0
  27. package/src/async/processor.js +206 -0
  28. package/src/async/stream.js +125 -0
  29. package/src/bridge/engine.js +221 -0
  30. package/src/bridge/loader.js +78 -0
  31. package/src/bridge/results.js +75 -0
  32. package/src/bridge/version.js +63 -0
  33. package/src/index.js +26 -8
  34. package/src/io/decode.js +140 -0
  35. package/src/io/readSource.js +167 -0
  36. package/src/xpath/axes.js +562 -0
  37. package/src/xpath/documentOrder.js +270 -0
  38. package/src/xpath/evaluator.js +518 -357
  39. package/src/xpath/index.js +8 -2
  40. package/src/xpath/namespaceNodes.js +172 -0
  41. package/src/xpath/nodeSetFunctions.js +169 -0
  42. package/src/xpath/parser.js +30 -5
  43. package/src/xpath/strings.js +183 -0
  44. package/src/xpath/tokenizer.js +37 -23
  45. package/src/xslt/attributeSets.js +95 -0
  46. package/src/xslt/avt.js +103 -0
  47. package/src/xslt/computedNames.js +91 -0
  48. package/src/xslt/copying.js +212 -0
  49. package/src/xslt/declarationNames.js +80 -0
  50. package/src/xslt/domParsing.js +95 -0
  51. package/src/xslt/elements.js +57 -0
  52. package/src/xslt/engine/bindings.js +195 -0
  53. package/src/xslt/engine/context.js +105 -0
  54. package/src/xslt/engine/controlFlow.js +145 -0
  55. package/src/xslt/engine/copyInstructions.js +133 -0
  56. package/src/xslt/engine/declarations.js +233 -0
  57. package/src/xslt/engine/functionSupport.js +103 -0
  58. package/src/xslt/engine/methods.js +33 -0
  59. package/src/xslt/engine/nodeConstruction.js +187 -0
  60. package/src/xslt/engine/numbering.js +104 -0
  61. package/src/xslt/engine/outputDeclaration.js +77 -0
  62. package/src/xslt/engine/sequenceConstructor.js +228 -0
  63. package/src/xslt/engine/stylesheetLoading.js +208 -0
  64. package/src/xslt/engine/templateInvocation.js +253 -0
  65. package/src/xslt/engine/templateRules.js +243 -0
  66. package/src/xslt/engine/textInstructions.js +171 -0
  67. package/src/xslt/engine/topLevel.js +130 -0
  68. package/src/xslt/engine/transformation.js +263 -0
  69. package/src/xslt/engine/workStack.js +245 -0
  70. package/src/xslt/engine.js +184 -1736
  71. package/src/xslt/exslt/arguments.js +99 -0
  72. package/src/xslt/exslt/calendar.js +120 -0
  73. package/src/xslt/exslt/common.js +44 -0
  74. package/src/xslt/exslt/dateCalc.js +261 -0
  75. package/src/xslt/exslt/dateFormat.js +150 -0
  76. package/src/xslt/exslt/dateParse.js +265 -0
  77. package/src/xslt/exslt/dates.js +259 -0
  78. package/src/xslt/exslt/duration.js +207 -0
  79. package/src/xslt/exslt/dynamic.js +59 -0
  80. package/src/xslt/exslt/index.js +59 -0
  81. package/src/xslt/exslt/math.js +177 -0
  82. package/src/xslt/exslt/sets.js +96 -0
  83. package/src/xslt/exslt/stringOps.js +163 -0
  84. package/src/xslt/exslt/strings.js +147 -0
  85. package/src/xslt/exslt/uri.js +92 -0
  86. package/src/xslt/formatNumber.js +233 -0
  87. package/src/xslt/forwardsCompatible.js +75 -0
  88. package/src/xslt/functions.js +270 -0
  89. package/src/xslt/index.js +38 -1
  90. package/src/xslt/keys.js +164 -0
  91. package/src/xslt/literalResult.js +223 -0
  92. package/src/xslt/matchScope.js +116 -0
  93. package/src/xslt/number.js +271 -0
  94. package/src/xslt/numberFormat.js +253 -0
  95. package/src/xslt/outputNames.js +58 -0
  96. package/src/xslt/patternCompiler.js +175 -0
  97. package/src/xslt/patterns.js +324 -0
  98. package/src/xslt/qname.js +90 -0
  99. package/src/xslt/resultDocument.js +98 -0
  100. package/src/xslt/resultNamespaces.js +219 -0
  101. package/src/xslt/resultTree.js +211 -0
  102. package/src/xslt/serializer/baseWriter.js +390 -0
  103. package/src/xslt/serializer/chunks.js +120 -0
  104. package/src/xslt/serializer/constants.js +92 -0
  105. package/src/xslt/serializer/encoding.js +327 -0
  106. package/src/xslt/serializer/escape.js +135 -0
  107. package/src/xslt/serializer/frames.js +168 -0
  108. package/src/xslt/serializer/htmlDoctype.js +102 -0
  109. package/src/xslt/serializer/htmlEntities.js +77 -0
  110. package/src/xslt/serializer/htmlSerializer.js +239 -0
  111. package/src/xslt/serializer/indent.js +51 -0
  112. package/src/xslt/serializer/namespaces.js +68 -0
  113. package/src/xslt/serializer/rawText.js +41 -0
  114. package/src/xslt/serializer/settings.js +179 -0
  115. package/src/xslt/serializer/textSerializer.js +77 -0
  116. package/src/xslt/serializer/xhtmlDocument.js +103 -0
  117. package/src/xslt/serializer/xmlSerializer.js +227 -0
  118. package/src/xslt/serializer.js +90 -0
  119. package/src/xslt/sort.js +151 -0
  120. package/src/xslt/spaceNameTests.js +115 -0
  121. package/src/xslt/stylesheetChecks.js +206 -0
  122. package/src/xslt/stylesheetNamespaces.js +266 -0
  123. package/src/xslt/templatePriority.js +45 -0
  124. package/src/xslt/uri.js +68 -0
  125. package/src/xslt/variables.js +152 -0
  126. package/src/xslt/whitespace.js +200 -0
  127. package/LICENSE +0 -29
  128. package/src/XSLTProcessor.test.js +0 -930
  129. package/src/xpath/evaluator.test.js +0 -1852
  130. package/src/xpath/tokenizer.test.js +0 -224
  131. package/src/xslt/engine.test.js +0 -3130
@@ -0,0 +1,104 @@
1
+ /**
2
+ * xsl:number (XSLT 1.0 section 7.7) and the number formatting helpers.
3
+ *
4
+ * Methods installed on XsltEngine.prototype (`this` is the engine).
5
+ */
6
+
7
+ import { countXsltNumber, isMemoizable } from "../number.js";
8
+ import { formatXsltNumber, toRoman } from "../numberFormat.js";
9
+
10
+ /**
11
+ * The numbers an xsl:number instruction formats: its rounded `value`, else
12
+ * the position of the current node counted by level, count and from.
13
+ *
14
+ * @param {object} engine - The engine
15
+ * @param {Element} node - The xsl:number instruction
16
+ * @param {XsltContext} context - The current context
17
+ * @returns {number[]} The numbers
18
+ */
19
+ function numberValues(engine, node, context) {
20
+ const value = node.getAttribute("value");
21
+ if (value) {
22
+ const number = Math.round(
23
+ engine.xpathEvaluator.toNumber(engine.evaluateXPath(value, context)),
24
+ );
25
+ // An error that libxslt reports and recovers from by numbering 0
26
+ if (number < 0) {
27
+ engine.warnOnce("xsl:number: negative value, 0 is used");
28
+ }
29
+ return [number];
30
+ }
31
+
32
+ const count = node.getAttribute("count");
33
+ const from = node.getAttribute("from");
34
+ return countXsltNumber(
35
+ context.currentNode,
36
+ { level: node.getAttribute("level") || "single", count, from },
37
+ (candidate, pattern) => engine.matchesPattern(candidate, pattern, context),
38
+ isMemoizable(count, from) ? engine.numberMemo(node) : null,
39
+ );
40
+ }
41
+
42
+ export const numberingMethods = {
43
+ /**
44
+ * Instantiate `xsl:number` (XSLT 1.0 section 7.7). The formatting
45
+ * attributes format, grouping-separator and grouping-size are attribute
46
+ * value templates; lang and letter-value have no effect.
47
+ *
48
+ * @param {Element} node - The xsl:number instruction
49
+ * @param {XsltContext} context - The current context
50
+ * @param {Node} output - The result node receiving the number
51
+ * @returns {void}
52
+ */
53
+ xslNumber(node, context, output) {
54
+ const format = this.optionalAvt(node, "format", context) || "1";
55
+ const grouping = {
56
+ separator: this.optionalAvt(node, "grouping-separator", context),
57
+ size: Number(this.optionalAvt(node, "grouping-size", context)),
58
+ };
59
+ const numbers = numberValues(this, node, context);
60
+
61
+ const text = context.outputDocument.createTextNode(
62
+ formatXsltNumber(numbers, format, grouping),
63
+ );
64
+ output.appendChild(text);
65
+ },
66
+
67
+ /**
68
+ * The memo of an xsl:number instruction for the current transformation
69
+ * (see number.js), so numbering a long list stays linear.
70
+ *
71
+ * @param {Element} node - The xsl:number instruction
72
+ * @returns {Map} The instruction's memo
73
+ */
74
+ numberMemo(node) {
75
+ this.numberMemos ??= new WeakMap();
76
+ let memo = this.numberMemos.get(node);
77
+ if (!memo) {
78
+ memo = new Map();
79
+ this.numberMemos.set(node, memo);
80
+ }
81
+ return memo;
82
+ },
83
+
84
+ /**
85
+ * Format a single number with an `xsl:number` format token.
86
+ *
87
+ * @param {number} number - The number to format
88
+ * @param {string} format - The format token, e.g. `1`, `01`, `a`, `I`
89
+ * @returns {string} The formatted number
90
+ */
91
+ formatNumber(number, format) {
92
+ return formatXsltNumber([number], format);
93
+ },
94
+
95
+ /**
96
+ * Convert a number to an upper case Roman numeral.
97
+ *
98
+ * @param {number} num - The number to convert
99
+ * @returns {string} The Roman numeral
100
+ */
101
+ toRoman(num) {
102
+ return toRoman(num);
103
+ },
104
+ };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * The xsl:output declaration and the engine's output settings
3
+ * (XSLT 1.0 section 16).
4
+ *
5
+ * Methods installed on XsltEngine.prototype (`this` is the engine).
6
+ */
7
+
8
+ import { cdataSectionNames } from "../outputNames.js";
9
+ import { inScopeNamespaces } from "../stylesheetNamespaces.js";
10
+
11
+ /**
12
+ * The xsl:output attributes copied as they are, with their setting names.
13
+ * A later non-empty attribute replaces the setting.
14
+ */
15
+ const OUTPUT_ATTRIBUTES = Object.freeze([
16
+ ["method", "method"],
17
+ ["version", "version"],
18
+ ["encoding", "encoding"],
19
+ ["standalone", "standalone"],
20
+ ["indent", "indent"],
21
+ ["omit-xml-declaration", "omitXmlDeclaration"],
22
+ ["doctype-public", "doctypePublic"],
23
+ ["doctype-system", "doctypeSystem"],
24
+ ["media-type", "mediaType"],
25
+ ]);
26
+
27
+ /**
28
+ * The output settings of a stylesheet without xsl:output.
29
+ *
30
+ * A null method means "not declared": the serializer then picks html or xml
31
+ * from the result tree (XSLT 1.0 section 16). An undefined indent is "no"
32
+ * too, except for the line breaks libxslt writes between comments and the
33
+ * document element (see serializer/settings.js).
34
+ *
35
+ * @returns {object} New default output settings
36
+ */
37
+ export function createOutputSettings() {
38
+ return {
39
+ method: null,
40
+ version: "1.0",
41
+ encoding: "UTF-8",
42
+ standalone: null,
43
+ indent: undefined,
44
+ omitXmlDeclaration: "no",
45
+ doctypePublic: null,
46
+ doctypeSystem: null,
47
+ mediaType: null,
48
+ cdataSectionElements: [],
49
+ };
50
+ }
51
+
52
+ export const outputDeclarationMethods = {
53
+ /**
54
+ * Merge an xsl:output element into the output settings (XSLT 1.0
55
+ * section 16): a later attribute wins, except cdata-section-elements whose
56
+ * expanded names are united.
57
+ *
58
+ * @param {Element} node - The xsl:output element
59
+ * @returns {void}
60
+ */
61
+ processOutput(node) {
62
+ for (const [attribute, setting] of OUTPUT_ATTRIBUTES) {
63
+ const value = node.getAttribute(attribute);
64
+ if (value) this.outputSettings[setting] = value;
65
+ }
66
+
67
+ const cdataElements = node.getAttribute("cdata-section-elements");
68
+ if (cdataElements) {
69
+ this.outputSettings.cdataSectionElements = cdataSectionNames(
70
+ cdataElements,
71
+ inScopeNamespaces(node),
72
+ this.outputSettings.cdataSectionElements,
73
+ (message) => this.warnOnce(message),
74
+ );
75
+ }
76
+ },
77
+ };
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Instantiation of sequence constructors: the dispatch of each child of a
3
+ * template body to its instruction, stylesheet text, unknown XSLT elements,
4
+ * xsl:fallback and extension elements.
5
+ *
6
+ * Methods installed on XsltEngine.prototype (`this` is the engine).
7
+ */
8
+
9
+ import { isTextContinuation } from "../../xpath/axes.js";
10
+ import { isXmlWhitespace } from "../whitespace.js";
11
+ import { isExtensionElement } from "../stylesheetNamespaces.js";
12
+ import { xsltLocalName } from "../stylesheetChecks.js";
13
+ import { fallbackChildren } from "../forwardsCompatible.js";
14
+ import { SequenceFrame } from "./workStack.js";
15
+
16
+ /**
17
+ * Engine method instantiating each XSLT element that may occur in a sequence
18
+ * constructor; null marks elements that produce nothing there.
19
+ */
20
+ const INSTRUCTION_METHODS = Object.freeze({
21
+ "apply-templates": "xslApplyTemplates",
22
+ "apply-imports": "xslApplyImports",
23
+ "call-template": "xslCallTemplate",
24
+ "value-of": "xslValueOf",
25
+ text: "xslText",
26
+ element: "xslElement",
27
+ attribute: "xslAttribute",
28
+ if: "xslIf",
29
+ choose: "xslChoose",
30
+ "for-each": "xslForEach",
31
+ copy: "xslCopy",
32
+ "copy-of": "xslCopyOf",
33
+ variable: "xslVariable",
34
+ comment: "xslComment",
35
+ "processing-instruction": "xslProcessingInstruction",
36
+ number: "xslNumber",
37
+ message: "xslMessage",
38
+ // Handled by their parent instruction, or at template start
39
+ param: null,
40
+ sort: null,
41
+ "with-param": null,
42
+ // Used for forward compatibility
43
+ fallback: null,
44
+ });
45
+
46
+ export const sequenceConstructorMethods = {
47
+ /**
48
+ * Instantiate the children of a stylesheet element (a sequence
49
+ * constructor) now, for instructions that use the result at once.
50
+ *
51
+ * @param {Element|object} node - The parent stylesheet element
52
+ * @param {XsltContext} context - The current context
53
+ * @param {Node} output - The result node receiving the output
54
+ * @returns {void}
55
+ */
56
+ processChildren(node, context, output) {
57
+ this.runFrame(new SequenceFrame(this, node, context, output));
58
+ },
59
+
60
+ /**
61
+ * Instantiate the children of a stylesheet element once the current
62
+ * instruction returns (see workStack.js), keeping the JavaScript stack flat
63
+ * in deep recursion. For content that comes last in an instruction.
64
+ *
65
+ * @param {Element|object} node - The parent stylesheet element
66
+ * @param {XsltContext} context - The current context
67
+ * @param {Node} output - The result node receiving the output
68
+ * @param {(() => void)|null} [then] - Called once the children are done
69
+ * @returns {void}
70
+ */
71
+ scheduleChildren(node, context, output, then = null) {
72
+ this.continueWith(new SequenceFrame(this, node, context, output, then));
73
+ },
74
+
75
+ /**
76
+ * Whether an element has an xsl:variable child. Cached per element.
77
+ *
78
+ * @param {Element|object} node - A stylesheet element
79
+ * @returns {boolean} True when a child declares a variable
80
+ */
81
+ declaresVariables(node) {
82
+ this.variableDeclarations ??= new WeakMap();
83
+ let declares = this.variableDeclarations.get(node);
84
+ if (declares === undefined) {
85
+ declares = false;
86
+ for (let child = node.firstChild; child; child = child.nextSibling) {
87
+ if (this.isXsltElement(child, "variable")) declares = true;
88
+ }
89
+ this.variableDeclarations.set(node, declares);
90
+ }
91
+ return declares;
92
+ },
93
+
94
+ /**
95
+ * Instantiate a stylesheet text node. Adjacent text and CDATA nodes form
96
+ * one text node, which is dropped when it only holds XML whitespace (unless
97
+ * xml:space="preserve" is in scope, XSLT 1.0 section 3.4).
98
+ *
99
+ * @param {Text} node - A text or CDATA node of the stylesheet
100
+ * @param {XsltContext} context - The current context
101
+ * @param {Node} output - The result node receiving the output
102
+ * @returns {void}
103
+ */
104
+ processText(node, context, output) {
105
+ if (isTextContinuation(node)) return;
106
+ const text = this.xpathEvaluator.getStringValue(node);
107
+ if (text && (!isXmlWhitespace(text) || this.shouldPreserveSpace(node))) {
108
+ output.appendChild(context.outputDocument.createTextNode(text));
109
+ }
110
+ },
111
+
112
+ /**
113
+ * Whether xml:space="preserve" is in scope on a stylesheet text node.
114
+ *
115
+ * @param {Node} node - The text node
116
+ * @returns {boolean} True when the nearest xml:space says preserve
117
+ */
118
+ shouldPreserveSpace(node) {
119
+ let current = node.parentNode;
120
+ while (current && current.nodeType === 1) {
121
+ const space = current.getAttribute("xml:space");
122
+ if (space === "preserve") return true;
123
+ if (space === "default") return false;
124
+ current = current.parentNode;
125
+ }
126
+ return false;
127
+ },
128
+
129
+ /**
130
+ * Name of the engine method instantiating a stylesheet element: the
131
+ * handler of an XSLT instruction, processLiteralResultElement, or
132
+ * instantiateUnknown for an XSLT element the engine does not implement.
133
+ * Returns null for elements that are not instructions (xsl:param,
134
+ * xsl:sort, xsl:with-param, xsl:fallback).
135
+ *
136
+ * @param {Element} node - A stylesheet element in a sequence constructor
137
+ * @returns {string|null} The method name
138
+ */
139
+ instructionMethod(node) {
140
+ if (!this.isXsltNamespace(node)) {
141
+ return isExtensionElement(node)
142
+ ? "instantiateExtension"
143
+ : "processLiteralResultElement";
144
+ }
145
+
146
+ const localName = node.localName || node.nodeName.replace(/^xsl:/, "");
147
+ if (Object.hasOwn(INSTRUCTION_METHODS, localName)) {
148
+ return INSTRUCTION_METHODS[localName];
149
+ }
150
+ return "instantiateUnknown";
151
+ },
152
+
153
+ /**
154
+ * Instantiate an XSLT element the engine does not implement (XSLT 1.0
155
+ * section 15): its xsl:fallback children are instantiated in order; without
156
+ * any, the error is reported and nothing is produced.
157
+ *
158
+ * @param {Element} node - The unknown XSLT element
159
+ * @param {XsltContext} context - The current context
160
+ * @param {Node} output - The result node receiving the output
161
+ * @returns {void}
162
+ */
163
+ instantiateUnknown(node, context, output) {
164
+ if (!this.instantiateFallbacks(node, context, output)) {
165
+ console.warn(`Unknown XSLT element: ${xsltLocalName(node)}`);
166
+ }
167
+ },
168
+
169
+ /**
170
+ * Instantiate the xsl:fallback children of an element in order.
171
+ *
172
+ * @param {Element} node - An unknown XSLT element or extension element
173
+ * @param {XsltContext} context - The current context
174
+ * @param {Node} output - The result node receiving the output
175
+ * @returns {boolean} Whether the element had any xsl:fallback child
176
+ */
177
+ instantiateFallbacks(node, context, output) {
178
+ const fallbacks = fallbackChildren(node);
179
+ for (const fallback of fallbacks) {
180
+ this.processChildren(fallback, context, output);
181
+ }
182
+ return fallbacks.length > 0;
183
+ },
184
+
185
+ /**
186
+ * Register the implementation of an extension element (XSLT 1.0 section
187
+ * 14.1). It is called as `handler(node, context, output, engine)` where
188
+ * an element of that name is instantiated in a namespace declared with
189
+ * `extension-element-prefixes`.
190
+ *
191
+ * @param {string} namespaceUri - The extension namespace
192
+ * @param {string} localName - The element's local name
193
+ * @param {(node: Element, context: XsltContext, output: Node, engine: XsltEngine) => void} handler - The implementation
194
+ * @returns {XsltEngine} This engine, to allow chaining
195
+ *
196
+ * @example
197
+ * engine.registerExtensionElement("urn:my", "log", (node) => console.log(node.textContent));
198
+ */
199
+ registerExtensionElement(namespaceUri, localName, handler) {
200
+ this.extensionElements.set(`{${namespaceUri}}${localName}`, handler);
201
+ return this;
202
+ },
203
+
204
+ /**
205
+ * Instantiate an extension element: its registered implementation, else
206
+ * its xsl:fallback children (XSLT 1.0 sections 14.1 and 15); without
207
+ * either, the error is reported once and nothing is produced, as libxslt
208
+ * does. An extension element is never copied to the result.
209
+ *
210
+ * @param {Element} node - The extension element
211
+ * @param {XsltContext} context - The current context
212
+ * @param {Node} output - The result node receiving the output
213
+ * @returns {void}
214
+ */
215
+ instantiateExtension(node, context, output) {
216
+ const name = `{${node.namespaceURI}}${node.localName}`;
217
+ const handler = this.extensionElements.get(name);
218
+ if (handler) {
219
+ handler(node, context, output, this);
220
+ return;
221
+ }
222
+ if (!this.instantiateFallbacks(node, context, output)) {
223
+ this.warnOnce(
224
+ `extension element ${node.nodeName} (${name}) is not supported and has no xsl:fallback`,
225
+ );
226
+ }
227
+ },
228
+ };
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Stylesheet loading: the main stylesheet and the xsl:import and
3
+ * xsl:include modules (the top-level elements are in topLevel.js).
4
+ *
5
+ * Methods installed on XsltEngine.prototype (`this` is the engine).
6
+ */
7
+
8
+ import { resolveUri } from "../uri.js";
9
+ import { parseXml, resolveDomParser } from "../domParsing.js";
10
+
11
+ /**
12
+ * Whether an element is an xsl:stylesheet or xsl:transform element.
13
+ *
14
+ * @param {object} engine - The engine
15
+ * @param {Element} root - The document element of a stylesheet module
16
+ * @returns {boolean} True for a stylesheet element
17
+ */
18
+ function isStylesheetElement(engine, root) {
19
+ return (
20
+ engine.isXsltElement(root, "stylesheet") ||
21
+ engine.isXsltElement(root, "transform")
22
+ );
23
+ }
24
+
25
+ export const stylesheetLoadingMethods = {
26
+ /**
27
+ * Set the loader used by xsl:import and xsl:include.
28
+ *
29
+ * @param {((href: string, baseUri?: string) => (Document|string))|null} loader - The loader, or null to remove it
30
+ * @returns {XsltEngine} This engine, to allow chaining
31
+ *
32
+ * @example
33
+ * engine.setStylesheetLoader((href) => readFileSync(href, 'utf8'));
34
+ */
35
+ setStylesheetLoader(loader) {
36
+ this.stylesheetLoader = loader ?? null;
37
+ return this;
38
+ },
39
+
40
+ /**
41
+ * Resolve a relative URI against a base URI
42
+ *
43
+ * @param {string} href - The URI to resolve
44
+ * @param {string} [baseUri] - The base URI
45
+ * @returns {string} The resolved URI
46
+ */
47
+ resolveUri(href, baseUri) {
48
+ return resolveUri(href, baseUri);
49
+ },
50
+
51
+ /**
52
+ * Load an external stylesheet document with the stylesheet loader.
53
+ *
54
+ * @param {string} href - The referenced URI
55
+ * @param {string} [baseUri] - Base URI of the referencing stylesheet
56
+ * @returns {{document: (Document|string), uri: string}} What the loader
57
+ * returned (a string is parsed by the caller) and the resolved URI
58
+ * @throws {Error} When no stylesheet loader is configured
59
+ */
60
+ loadStylesheet(href, baseUri) {
61
+ if (!this.stylesheetLoader) {
62
+ throw new Error(
63
+ `Cannot load stylesheet "${href}": no stylesheetLoader configured. ` +
64
+ "Use engine.setStylesheetLoader(fn) to provide a loader function.",
65
+ );
66
+ }
67
+
68
+ const resolvedUri = this.resolveUri(href, baseUri);
69
+ const result = this.stylesheetLoader(resolvedUri, baseUri);
70
+ return { document: result, uri: resolvedUri };
71
+ },
72
+
73
+ /**
74
+ * Parse an XML string returned by a stylesheet or document loader, with
75
+ * the `domParser` option, else the global DOMParser, else the DOMParser of
76
+ * the stylesheet's window (see domParsing.js).
77
+ *
78
+ * @param {string} xmlString - The markup
79
+ * @returns {Document} The parsed document
80
+ * @throws {Error} When no parser is available or the markup is malformed
81
+ */
82
+ parseXmlString(xmlString) {
83
+ return parseXml(
84
+ xmlString,
85
+ resolveDomParser(this.domParser, this.stylesheetDoc),
86
+ );
87
+ },
88
+
89
+ /**
90
+ * Import and compile an XSLT stylesheet
91
+ * @param {Document|Element} stylesheetNode - The stylesheet document or root element
92
+ * @param {string} [stylesheetUri] - Optional URI of the stylesheet for resolving imports
93
+ */
94
+ importStylesheet(stylesheetNode, stylesheetUri) {
95
+ const isMainStylesheet = this.stylesheetDoc === null;
96
+
97
+ if (isMainStylesheet) {
98
+ this.stylesheetDoc = stylesheetNode.ownerDocument || stylesheetNode;
99
+ if (stylesheetUri) this.baseUri = stylesheetUri;
100
+ if (this.baseUri) this.stylesheetStack.push(this.baseUri);
101
+ }
102
+
103
+ const root = stylesheetNode.documentElement || stylesheetNode;
104
+
105
+ if (!isStylesheetElement(this, root)) {
106
+ // A literal result element as document element: simplified stylesheet
107
+ if (root.getAttribute && root.getAttribute("xsl:version")) {
108
+ this.processLiteralResultStylesheet(root);
109
+ return;
110
+ }
111
+ throw new Error(
112
+ "Invalid XSLT stylesheet: root element must be xsl:stylesheet or xsl:transform",
113
+ );
114
+ }
115
+
116
+ this.processTopLevelElements(root, stylesheetUri || this.baseUri);
117
+
118
+ // Increment import precedence after processing this stylesheet
119
+ if (isMainStylesheet) {
120
+ this.currentImportPrecedence++;
121
+ }
122
+ },
123
+
124
+ /**
125
+ * Process an xsl:include element: the included stylesheet is merged at the
126
+ * import precedence of the including stylesheet.
127
+ *
128
+ * @param {Element} node - The xsl:include element
129
+ * @param {string} baseUri - URI of the including stylesheet
130
+ * @returns {void}
131
+ */
132
+ processInclude(node, baseUri) {
133
+ const savedPrecedence = this.currentImportPrecedence;
134
+ this.loadStylesheetModule(node, baseUri, "include");
135
+ this.currentImportPrecedence = savedPrecedence;
136
+ },
137
+
138
+ /**
139
+ * Process an xsl:import element: the imported stylesheet gets a lower import
140
+ * precedence than everything processed after it.
141
+ *
142
+ * @param {Element} node - The xsl:import element
143
+ * @param {string} baseUri - URI of the importing stylesheet
144
+ * @returns {void}
145
+ */
146
+ processImport(node, baseUri) {
147
+ this.loadStylesheetModule(node, baseUri, "import");
148
+ this.currentImportPrecedence++;
149
+ },
150
+
151
+ /**
152
+ * Load and process the stylesheet referenced by xsl:import or xsl:include.
153
+ *
154
+ * Only a stylesheet that (directly or indirectly) references itself is an
155
+ * error; the same stylesheet may be reached through several branches of the
156
+ * import tree ("diamond" imports), as in libxslt.
157
+ *
158
+ * @param {Element} node - The xsl:import or xsl:include element
159
+ * @param {string} baseUri - URI of the referencing stylesheet
160
+ * @param {"import"|"include"} kind - The referencing instruction
161
+ * @returns {void}
162
+ * @throws {Error} When href is missing, loading fails or a cycle is found
163
+ */
164
+ loadStylesheetModule(node, baseUri, kind) {
165
+ const href = node.getAttribute("href");
166
+ if (!href) {
167
+ throw new Error(`xsl:${kind} requires an href attribute`);
168
+ }
169
+
170
+ const resolvedUri = this.resolveUri(href, baseUri);
171
+ if (this.stylesheetStack.includes(resolvedUri)) {
172
+ throw new Error(`Circular stylesheet reference detected: ${resolvedUri}`);
173
+ }
174
+
175
+ this.stylesheetStack.push(resolvedUri);
176
+ try {
177
+ const { document: loaded } = this.loadStylesheet(href, baseUri);
178
+ const doc =
179
+ typeof loaded === "string" ? this.parseXmlString(loaded) : loaded;
180
+ this.processIncludedStylesheet(doc, resolvedUri);
181
+ } catch (error) {
182
+ throw new Error(
183
+ `Failed to ${kind} stylesheet "${href}": ${error.message}`,
184
+ { cause: error },
185
+ );
186
+ } finally {
187
+ this.stylesheetStack.pop();
188
+ }
189
+ },
190
+
191
+ /**
192
+ * Process an included/imported stylesheet document.
193
+ *
194
+ * @param {Document|Element} stylesheetDoc - The loaded stylesheet module
195
+ * @param {string} stylesheetUri - Its resolved URI
196
+ * @returns {void}
197
+ * @throws {Error} When the document is not an XSLT stylesheet
198
+ */
199
+ processIncludedStylesheet(stylesheetDoc, stylesheetUri) {
200
+ const root = stylesheetDoc.documentElement || stylesheetDoc;
201
+ if (!isStylesheetElement(this, root)) {
202
+ throw new Error(
203
+ "Included/imported document is not a valid XSLT stylesheet",
204
+ );
205
+ }
206
+ this.processTopLevelElements(root, stylesheetUri);
207
+ },
208
+ };