@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,130 @@
1
+ /**
2
+ * Top-level elements of a stylesheet module: their dispatch to the
3
+ * declaration handlers, simplified stylesheets and the stylesheet-wide
4
+ * namespace table.
5
+ *
6
+ * Methods installed on XsltEngine.prototype (`this` is the engine).
7
+ */
8
+
9
+ import { XSLT_NAMESPACE } from "../elements.js";
10
+ import { inScopeNamespaces } from "../stylesheetNamespaces.js";
11
+ import {
12
+ checkLocalBindings,
13
+ checkNumberPatterns,
14
+ checkTopLevelText,
15
+ } from "../stylesheetChecks.js";
16
+ import { isForwardsCompatible } from "../forwardsCompatible.js";
17
+
18
+ /**
19
+ * Engine method handling each top-level XSLT element (xsl:import is handled
20
+ * first, separately, because imports must precede everything else).
21
+ */
22
+ const TOP_LEVEL_HANDLERS = Object.freeze({
23
+ template: "registerTemplate",
24
+ output: "processOutput",
25
+ variable: "processGlobalVariable",
26
+ param: "processGlobalParam",
27
+ key: "processKey",
28
+ "decimal-format": "processDecimalFormat",
29
+ "namespace-alias": "processNamespaceAlias",
30
+ "attribute-set": "processAttributeSet",
31
+ "strip-space": "processStripSpace",
32
+ "preserve-space": "processPreserveSpace",
33
+ include: "processInclude",
34
+ });
35
+
36
+ export const topLevelMethods = {
37
+ /**
38
+ * Process the top-level elements of a stylesheet module.
39
+ *
40
+ * xsl:import elements are processed first, so imported declarations get a
41
+ * lower import precedence than the ones of the importing stylesheet.
42
+ *
43
+ * @param {Element} root - The xsl:stylesheet element
44
+ * @param {string} stylesheetUri - URI used to resolve imports and includes
45
+ * @returns {void}
46
+ */
47
+ processTopLevelElements(root, stylesheetUri) {
48
+ checkTopLevelText(root);
49
+ this.collectNamespaces(root);
50
+
51
+ const imports = [];
52
+ const otherElements = [];
53
+ for (const child of root.childNodes) {
54
+ if (child.nodeType !== 1) continue;
55
+ if (this.isXsltElement(child, "import")) imports.push(child);
56
+ else otherElements.push(child);
57
+ }
58
+
59
+ for (const importNode of imports) {
60
+ this.processImport(importNode, stylesheetUri);
61
+ }
62
+
63
+ for (const child of otherElements) {
64
+ if (!this.isXsltNamespace(child)) continue;
65
+ const method = TOP_LEVEL_HANDLERS[child.localName];
66
+ if (method) this[method](child, stylesheetUri);
67
+ else this.unknownTopLevelElement(child);
68
+ }
69
+ checkNumberPatterns(this.patternMatcher, root);
70
+ },
71
+
72
+ /**
73
+ * Ignore an unknown top-level XSLT element: silently in forwards-compatible
74
+ * mode (XSLT 1.0 section 2.5), with a warning otherwise.
75
+ *
76
+ * @param {Element} node - The unknown element
77
+ * @returns {void}
78
+ */
79
+ unknownTopLevelElement(node) {
80
+ if (isForwardsCompatible(node)) return;
81
+ this.warnOnce(`unknown top-level element xsl:${node.localName} is ignored`);
82
+ },
83
+
84
+ /**
85
+ * Register a simplified stylesheet (XSLT 1.0 section 2.3): the literal result
86
+ * root element is the body of a template rule matching "/", so the root
87
+ * element itself is instantiated, not only its children.
88
+ *
89
+ * @param {Element} root - The literal result root element
90
+ * @returns {void}
91
+ */
92
+ processLiteralResultStylesheet(root) {
93
+ this.collectNamespaces(root);
94
+ checkNumberPatterns(this.patternMatcher, root);
95
+ checkLocalBindings(root, (message) => this.warnOnce(message));
96
+ this.templates.push({
97
+ match: "/",
98
+ name: null,
99
+ mode: null,
100
+ priority: 0.5,
101
+ importPrecedence: this.currentImportPrecedence,
102
+ namespaces: inScopeNamespaces(root),
103
+ node: { firstChild: root, childNodes: [root] },
104
+ });
105
+ },
106
+
107
+ /**
108
+ * Record the namespace declarations of a stylesheet document element in
109
+ * the stylesheet-wide fallback table. Instructions resolve prefixes against
110
+ * their own in-scope namespaces; this table only serves contexts without a
111
+ * stylesheet element. The first binding of a prefix wins, so a module
112
+ * loaded later cannot rebind the main stylesheet's prefixes.
113
+ *
114
+ * @param {Element} node - The stylesheet element
115
+ * @returns {void}
116
+ */
117
+ collectNamespaces(node) {
118
+ if (!node.attributes) return;
119
+
120
+ for (const attr of node.attributes) {
121
+ let prefix = null;
122
+ if (attr.name.startsWith("xmlns:")) prefix = attr.name.substring(6);
123
+ else if (attr.name === "xmlns") prefix = "";
124
+
125
+ if (prefix !== null && attr.value !== XSLT_NAMESPACE) {
126
+ this.namespaces[prefix] ??= attr.value;
127
+ }
128
+ }
129
+ },
130
+ };
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Transformation entry points: building the result tree and shaping it as
3
+ * a fragment, a document or a string.
4
+ *
5
+ * Methods installed on XsltEngine.prototype (`this` is the engine).
6
+ */
7
+
8
+ import { WhitespaceFilter, stripWhitespaceNodes } from "../whitespace.js";
9
+ import {
10
+ createResultDocument,
11
+ importResultFragment,
12
+ isHtmlDocument,
13
+ parseHtmlFragment,
14
+ wrapTextResult,
15
+ } from "../resultTree.js";
16
+ import { resolveOutputSettings, serializeResult } from "../serializer.js";
17
+ import { fillXmlDocument, parseHtmlDocument } from "../resultDocument.js";
18
+ import { XsltContext } from "./context.js";
19
+ import {
20
+ DocumentOrderIndex,
21
+ hasNativePositionComparison,
22
+ } from "../../xpath/documentOrder.js";
23
+
24
+ /**
25
+ * Turn a JavaScript stack overflow into a clear transformation error; any
26
+ * other error is returned unchanged.
27
+ *
28
+ * @param {Error} error - The error thrown by a transformation
29
+ * @returns {Error} The error to report
30
+ */
31
+ function recursionError(error) {
32
+ const isStackOverflow =
33
+ (error instanceof RangeError && /call stack/i.test(error.message)) ||
34
+ // Firefox reports "InternalError: too much recursion"
35
+ (error?.name === "InternalError" && /recursion/i.test(error.message));
36
+ if (!isStackOverflow) return error;
37
+
38
+ return new Error(
39
+ "Template recursion too deep: the transformation exceeded the JavaScript " +
40
+ "call stack (infinite recursion, or recursion deeper than the runtime allows)",
41
+ { cause: error },
42
+ );
43
+ }
44
+
45
+ export const transformationMethods = {
46
+ /**
47
+ * Transform a source node into a fragment owned by `ownerDocument`, built
48
+ * with the XML DOM (names and namespaces as in the result tree).
49
+ *
50
+ * @param {Node} sourceNode - Source document or element
51
+ * @param {Document} [ownerDocument] - Output document, default the global one
52
+ * @returns {DocumentFragment} The result
53
+ */
54
+ transform(sourceNode, ownerDocument) {
55
+ const doc = this.outputDocumentOf(ownerDocument);
56
+ return importResultFragment(this.buildResultTree(sourceNode, doc), doc);
57
+ },
58
+
59
+ /**
60
+ * Transform a source node into a fragment of `ownerDocument` as Chrome's
61
+ * `transformToFragment` does: into an HTML document, the output of the
62
+ * html method (declared or detected) is serialized and parsed as HTML, so
63
+ * it holds HTMLElements, and other output keeps its nodes except that
64
+ * elements in no namespace become XHTML elements (as in Chrome and
65
+ * Firefox); into an XML document the result nodes are kept.
66
+ *
67
+ * @param {Node} sourceNode - Source document or element
68
+ * @param {Document} ownerDocument - Output document
69
+ * @returns {DocumentFragment} The result
70
+ */
71
+ transformToFragment(sourceNode, ownerDocument) {
72
+ const doc = this.outputDocumentOf(ownerDocument);
73
+ const fragment = this.buildResultTree(sourceNode, doc);
74
+ const settings = resolveOutputSettings(this.outputSettings, fragment);
75
+ if (isHtmlDocument(doc) && settings.method === "html") {
76
+ return parseHtmlFragment(
77
+ serializeResult(fragment, this.outputSettings),
78
+ doc,
79
+ );
80
+ }
81
+ return importResultFragment(fragment, doc, { htmlElements: true });
82
+ },
83
+
84
+ /**
85
+ * The document that owns a transformation result.
86
+ *
87
+ * @param {Document} [ownerDocument] - Requested owner
88
+ * @returns {Document} The owner, else the global document
89
+ * @throws {Error} When there is no document at all
90
+ */
91
+ outputDocumentOf(ownerDocument) {
92
+ const doc =
93
+ ownerDocument || (typeof document !== "undefined" ? document : null);
94
+
95
+ if (!doc) {
96
+ throw new Error("No output document available");
97
+ }
98
+ return doc;
99
+ },
100
+
101
+ /**
102
+ * Run the transformation and return the result tree, built in a neutral
103
+ * XML document: creating nodes directly in an HTML owner document would
104
+ * lower case names and force the XHTML namespace on every element.
105
+ *
106
+ * The templates are applied to the document node (not the document
107
+ * element), so the "/" template has the document as context node and
108
+ * paths such as "RootElement/child" work.
109
+ *
110
+ * @param {Node} sourceNode - Source document or element
111
+ * @param {Document} doc - Document providing the DOM implementation
112
+ * @returns {DocumentFragment} The result tree
113
+ */
114
+ buildResultTree(sourceNode, doc) {
115
+ const resultDocument = createResultDocument(doc);
116
+ const source = this.initialNode(this.prepareSource(sourceNode, doc));
117
+
118
+ const context = new XsltContext({
119
+ currentNode: source,
120
+ currentNodeList: [source],
121
+ position: 1,
122
+ outputDocument: resultDocument,
123
+ stylesheet: this.stylesheetDoc,
124
+ namespaces: this.namespaces,
125
+ templates: this.templates,
126
+ keys: this.keys,
127
+ decimalFormats: this.decimalFormats,
128
+ outputMethod: this.outputSettings.method,
129
+ xpathEvaluator: this.xpathEvaluator,
130
+ });
131
+
132
+ this.rootContext = context;
133
+ // The source tree may have changed since the previous transformation
134
+ this.keyRegistry.clear();
135
+ this.patternMatcher.reset();
136
+ this.xpathEvaluator.resetNamespaceNodes();
137
+ // Document order from positions numbered once, unless the DOM compares
138
+ // positions natively (see documentOrder.js)
139
+ this.xpathEvaluator.resetDocumentOrder(
140
+ hasNativePositionComparison(source) ? null : new DocumentOrderIndex(),
141
+ );
142
+ this.numberMemos = new WeakMap();
143
+
144
+ const fragment = resultDocument.createDocumentFragment();
145
+ try {
146
+ context.globals = this.createGlobals(context);
147
+ context.globals.evaluateAll();
148
+ this.applyTemplates([source], null, context, fragment);
149
+ } catch (error) {
150
+ throw recursionError(error);
151
+ }
152
+ return fragment;
153
+ },
154
+
155
+ /**
156
+ * Choose the node the transformation starts from.
157
+ *
158
+ * A document element is transformed through its document, so that the "/"
159
+ * template rule applies as for a whole document (as browsers do); any other
160
+ * node is transformed as is.
161
+ *
162
+ * @param {Node} source - The (prepared) source node
163
+ * @returns {Node} The initial context node
164
+ */
165
+ initialNode(source) {
166
+ const owner = source.ownerDocument;
167
+ return source.nodeType === 1 && owner?.documentElement === source
168
+ ? owner
169
+ : source;
170
+ },
171
+
172
+ /**
173
+ * Apply `xsl:strip-space` to the source tree.
174
+ *
175
+ * Stripping produces a copy so the caller's document is never modified; when
176
+ * no `xsl:strip-space` is declared the original node is used unchanged.
177
+ *
178
+ * @param {Node} sourceNode - The source document or element
179
+ * @param {Document} ownerDocument - Document providing the DOM implementation
180
+ * @returns {Node} The source to transform
181
+ */
182
+ prepareSource(sourceNode, ownerDocument) {
183
+ const filter = new WhitespaceFilter(this.stripSpace, this.preserveSpace);
184
+ if (!filter.isActive()) return sourceNode;
185
+
186
+ return stripWhitespaceNodes(
187
+ sourceNode,
188
+ filter,
189
+ createResultDocument(ownerDocument),
190
+ );
191
+ },
192
+
193
+ /**
194
+ * Transform to a complete document, shaped as Chrome's XSLTProcessor
195
+ * returns it (see resultDocument.js): with `method="text"` an XHTML page
196
+ * holding the text in a `pre` element (see wrapTextResult), with the html
197
+ * method (declared or detected) an HTML document parsed from the html
198
+ * output, otherwise an XML document of the result nodes.
199
+ *
200
+ * @param {Node} sourceNode - Source document or element to transform
201
+ * @returns {Document} The result document
202
+ */
203
+ transformToDocument(sourceNode) {
204
+ const doc = this.createDocument(sourceNode);
205
+ const fragment = this.transform(sourceNode, doc);
206
+
207
+ if (this.outputSettings.method === "text") {
208
+ return wrapTextResult(doc, fragment.textContent);
209
+ }
210
+ const settings = resolveOutputSettings(this.outputSettings, fragment);
211
+ if (settings.method === "html") {
212
+ const markup = serializeResult(fragment, this.outputSettings);
213
+ const htmlDoc = parseHtmlDocument(markup, doc);
214
+ if (htmlDoc) return htmlDoc;
215
+ }
216
+ return fillXmlDocument(doc, fragment, settings);
217
+ },
218
+
219
+ /**
220
+ * Transform a source document and serialize the result to a string.
221
+ *
222
+ * Non-W3C convenience method: the result tree is serialized honoring the
223
+ * `xsl:output` settings of the stylesheet (XSLT 1.0 section 16).
224
+ *
225
+ * @param {Node} sourceNode - Source document or element to transform
226
+ * @returns {string} The serialized transformation result
227
+ */
228
+ transformToString(sourceNode) {
229
+ const fragment = this.buildResultTree(
230
+ sourceNode,
231
+ this.createDocument(sourceNode),
232
+ );
233
+ return serializeResult(fragment, this.outputSettings);
234
+ },
235
+
236
+ /**
237
+ * Create an empty XML document to hold a transformation result.
238
+ *
239
+ * Uses the global `document` when running in a browser and otherwise falls
240
+ * back to the DOM implementation owning `referenceNode` (e.g. a jsdom or
241
+ * xmldom document in Node.js).
242
+ *
243
+ * @param {Node} [referenceNode] - Any node whose DOM implementation can be reused
244
+ * @returns {Document} A new empty document
245
+ * @throws {Error} When no DOM implementation is available
246
+ */
247
+ createDocument(referenceNode) {
248
+ if (typeof document !== "undefined") {
249
+ return document.implementation.createDocument(null, null, null);
250
+ }
251
+
252
+ const ownerDocument =
253
+ referenceNode &&
254
+ (referenceNode.nodeType === 9
255
+ ? referenceNode
256
+ : referenceNode.ownerDocument);
257
+ if (ownerDocument?.implementation) {
258
+ return ownerDocument.implementation.createDocument(null, null, null);
259
+ }
260
+
261
+ throw new Error("Document creation not available in this environment");
262
+ },
263
+ };
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Explicit work stack for template instantiation.
3
+ *
4
+ * Recursive stylesheets nest template invocations thousands of levels deep
5
+ * (libxslt allows 3000). Instantiating each level with JavaScript recursion
6
+ * costs several native frames per level and overflows the call stack of
7
+ * browsers and Node long before that, so the engine keeps the pending work
8
+ * in frames on an explicit stack instead:
9
+ *
10
+ * - a SequenceFrame walks the children of a stylesheet element (a sequence
11
+ * constructor: a template body, the content of xsl:if, a literal result
12
+ * element, ...);
13
+ * - a LoopFrame visits the items of xsl:apply-templates or xsl:for-each.
14
+ *
15
+ * An instruction whose content comes last (xsl:if, xsl:choose,
16
+ * xsl:call-template, literal result elements, ...) schedules that content
17
+ * with {@link workStackMethods.continueWith} and returns; the driver loop
18
+ * runs it before the next sibling of the instruction. The native stack then
19
+ * stays flat however deep the templates nest. Instructions that need the
20
+ * result at once (xsl:attribute, xsl:with-param content, ...) run a nested
21
+ * driver with {@link workStackMethods.runFrame}.
22
+ *
23
+ * Methods installed on XsltEngine.prototype (`this` is the engine).
24
+ */
25
+
26
+ import { inScopeNamespaces } from "../stylesheetNamespaces.js";
27
+
28
+ /**
29
+ * Deepest nesting of template instantiations in a transformation, the
30
+ * default of libxslt's `xsltMaxDepth`. Deeper recursion is reported as a
31
+ * potential infinite recursion. Override it with the `maxTemplateDepth`
32
+ * engine option.
33
+ */
34
+ export const XSLT_MAX_TEMPLATE_DEPTH = 3000;
35
+
36
+ /**
37
+ * Pending instantiation of the children of a stylesheet element.
38
+ */
39
+ export class SequenceFrame {
40
+ /**
41
+ * A body declaring variables gets its own copy of the local bindings: a
42
+ * variable is visible to its following siblings and their descendants only.
43
+ *
44
+ * @param {object} engine - The engine
45
+ * @param {Element|object} node - The parent stylesheet element
46
+ * @param {XsltContext} context - The current context
47
+ * @param {Node} output - The result node receiving the output
48
+ * @param {(() => void)|null} [onDone] - Called once the children are done
49
+ */
50
+ constructor(engine, node, context, output, onDone = null) {
51
+ this.scope = engine.declaresVariables(node) ? context.clone() : context;
52
+ this.saved = this.scope.namespaces;
53
+ this.next = node.firstChild;
54
+ this.output = output;
55
+ // Not named `then`: an object with a `then` method is treated as a promise
56
+ this.onDone = onDone;
57
+ // Whether the frame is a template instantiation (counted in the depth)
58
+ this.template = false;
59
+ }
60
+
61
+ /**
62
+ * Instantiate children until one schedules further frames or none is left.
63
+ *
64
+ * @param {object} engine - The engine
65
+ * @param {object[]} frames - The work stack
66
+ * @returns {boolean} False when every child is done
67
+ */
68
+ advance(engine, frames) {
69
+ const height = frames.length;
70
+ const scope = this.scope;
71
+ for (let child = this.next; child; child = this.next) {
72
+ this.next = child.nextSibling;
73
+ const type = child.nodeType;
74
+ if (type === 1) {
75
+ // Prefixes resolve against the namespaces in scope on the element
76
+ scope.namespaces = inScopeNamespaces(child);
77
+ const method = engine.instructionMethod(child);
78
+ if (method) engine[method](child, scope, this.output);
79
+ } else if (type === 3 || type === 4) {
80
+ engine.processText(child, scope, this.output);
81
+ }
82
+ if (frames.length !== height) return true;
83
+ }
84
+ return false;
85
+ }
86
+
87
+ /**
88
+ * Leave the frame, completed or abandoned by an error.
89
+ *
90
+ * @param {object} engine - The engine
91
+ * @returns {void}
92
+ */
93
+ release(engine) {
94
+ this.scope.namespaces = this.saved;
95
+ if (this.template) engine.templateDepth--;
96
+ }
97
+
98
+ /**
99
+ * Complete the frame.
100
+ *
101
+ * @param {object} engine - The engine
102
+ * @returns {void}
103
+ */
104
+ finish(engine) {
105
+ this.release(engine);
106
+ if (this.onDone) this.onDone();
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Pending visits of the items of a list (the nodes of xsl:apply-templates
112
+ * or xsl:for-each), one at a time.
113
+ */
114
+ export class LoopFrame {
115
+ /**
116
+ * @param {number} count - Number of items
117
+ * @param {(index: number) => void} visit - Instantiates one item; it may
118
+ * schedule frames, which run before the next item
119
+ */
120
+ constructor(count, visit) {
121
+ this.count = count;
122
+ this.index = 0;
123
+ this.visit = visit;
124
+ this.template = false;
125
+ }
126
+
127
+ /**
128
+ * Visit items until one schedules further frames or none is left.
129
+ *
130
+ * @param {object} _engine - The engine
131
+ * @param {object[]} frames - The work stack
132
+ * @returns {boolean} False when every item is done
133
+ */
134
+ advance(_engine, frames) {
135
+ const height = frames.length;
136
+ while (this.index < this.count) {
137
+ this.visit(this.index++);
138
+ if (frames.length !== height) return true;
139
+ }
140
+ return false;
141
+ }
142
+
143
+ /**
144
+ * Leave the frame, completed or abandoned by an error.
145
+ *
146
+ * @param {object} engine - The engine
147
+ * @returns {void}
148
+ */
149
+ release(engine) {
150
+ if (this.template) engine.templateDepth--;
151
+ }
152
+
153
+ /**
154
+ * Complete the frame.
155
+ *
156
+ * @param {object} engine - The engine
157
+ * @returns {void}
158
+ */
159
+ finish(engine) {
160
+ this.release(engine);
161
+ }
162
+ }
163
+
164
+ export const workStackMethods = {
165
+ /**
166
+ * Run a frame, and the frames it schedules, to completion.
167
+ *
168
+ * The outermost call owns the work stack of the transformation; nested
169
+ * calls (content whose result is needed at once) run on top of it.
170
+ *
171
+ * @param {SequenceFrame|LoopFrame} frame - The frame
172
+ * @returns {void}
173
+ */
174
+ runFrame(frame) {
175
+ const outermost = this.frames === null;
176
+ if (outermost) {
177
+ this.frames = [];
178
+ this.templateDepth = 0;
179
+ }
180
+ const frames = this.frames;
181
+ const base = frames.length;
182
+ try {
183
+ this.pushFrame(frame);
184
+ while (frames.length > base) {
185
+ const top = frames[frames.length - 1];
186
+ if (!top.advance(this, frames)) {
187
+ frames.pop();
188
+ top.finish(this);
189
+ }
190
+ }
191
+ } catch (error) {
192
+ this.unwindFrames(base);
193
+ throw error;
194
+ } finally {
195
+ if (outermost) this.frames = null;
196
+ }
197
+ },
198
+
199
+ /**
200
+ * Schedule a frame to run once the current instruction returns, before
201
+ * its next sibling; without a running work stack, run it now.
202
+ *
203
+ * The instruction must not use the output of the frame afterwards.
204
+ *
205
+ * @param {SequenceFrame|LoopFrame} frame - The frame
206
+ * @returns {void}
207
+ */
208
+ continueWith(frame) {
209
+ if (this.frames === null) this.runFrame(frame);
210
+ else this.pushFrame(frame);
211
+ },
212
+
213
+ /**
214
+ * Push a frame on the work stack, counting template instantiations.
215
+ *
216
+ * @param {SequenceFrame|LoopFrame} frame - The frame
217
+ * @returns {void}
218
+ * @throws {Error} When the frame would nest templates deeper than
219
+ * `maxTemplateDepth`
220
+ */
221
+ pushFrame(frame) {
222
+ if (frame.template) {
223
+ if (this.templateDepth >= this.maxTemplateDepth) {
224
+ throw new Error(
225
+ `Template recursion too deep: more than ${this.maxTemplateDepth} ` +
226
+ "nested template invocations, a potential infinite recursion " +
227
+ "(raise the maxTemplateDepth option to allow deeper recursion)",
228
+ );
229
+ }
230
+ this.templateDepth++;
231
+ }
232
+ this.frames.push(frame);
233
+ },
234
+
235
+ /**
236
+ * Drop the frames above a height after an error.
237
+ *
238
+ * @param {number} base - The height to return to
239
+ * @returns {void}
240
+ */
241
+ unwindFrames(base) {
242
+ const frames = this.frames;
243
+ while (frames.length > base) frames.pop().release(this);
244
+ },
245
+ };