@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,95 @@
1
+ /**
2
+ * XML parsing through whatever DOM implementation is available.
3
+ *
4
+ * Stylesheets pulled in by xsl:import/xsl:include and documents returned by a
5
+ * `document()` loader may be XML strings. They are parsed with, in order: a
6
+ * DOMParser given to the engine (`domParser` option), the global
7
+ * `DOMParser` (browsers, or a jsdom/xmldom global installed by the host), or
8
+ * the DOMParser of the window owning the stylesheet document. Parse errors are
9
+ * detected through `getElementsByTagName`, which every DOM implementation
10
+ * has (xmldom, for one, has no `querySelector`).
11
+ *
12
+ * @module xslt/domParsing
13
+ */
14
+
15
+ "use strict";
16
+
17
+ /**
18
+ * Namespace Gecko puts its `parsererror` element in. A namespace name is an
19
+ * identifier compared as a string, never fetched, so http is correct here.
20
+ */
21
+ const GECKO_PARSER_ERROR_NS =
22
+ "http://www.mozilla.org/newlayout/xml/parsererror.xml"; // NOSONAR
23
+
24
+ /**
25
+ * @typedef {{parseFromString: (text: string, type: string) => Document}} DomParserLike
26
+ */
27
+
28
+ /**
29
+ * The first `parsererror` element of a document parsed by a DOMParser, if
30
+ * any. Browsers and jsdom report malformed XML this way instead of throwing
31
+ * (xmldom throws, see {@link parseXml}).
32
+ *
33
+ * @param {Node|null|undefined} doc - A parsed document (or element)
34
+ * @returns {Element|null} The error element, or null when parsing succeeded
35
+ *
36
+ * @example
37
+ * findParseError(new DOMParser().parseFromString("<a>", "application/xml"));
38
+ * // <parsererror>…</parsererror>
39
+ */
40
+ export function findParseError(doc) {
41
+ if (!doc?.getElementsByTagName) return null;
42
+ const plain = doc.getElementsByTagName("parsererror");
43
+ if (plain.length > 0) return plain[0];
44
+ const gecko = doc.getElementsByTagNameNS?.(
45
+ GECKO_PARSER_ERROR_NS,
46
+ "parsererror",
47
+ );
48
+ return gecko?.length > 0 ? gecko[0] : null;
49
+ }
50
+
51
+ /**
52
+ * Pick the DOMParser to use.
53
+ *
54
+ * @param {DomParserLike|null|undefined} configured - The engine's `domParser` option
55
+ * @param {Document|null|undefined} referenceDoc - A document whose window may
56
+ * provide a DOMParser (usually the stylesheet)
57
+ * @returns {DomParserLike|null} A parser, or null when none is available
58
+ */
59
+ export function resolveDomParser(configured, referenceDoc) {
60
+ if (configured) return configured;
61
+ const Parser =
62
+ globalThis.DOMParser ?? referenceDoc?.defaultView?.DOMParser ?? null;
63
+ return Parser ? new Parser() : null;
64
+ }
65
+
66
+ /**
67
+ * Parse an XML string into a document.
68
+ *
69
+ * @param {string} xml - The markup
70
+ * @param {DomParserLike|null} parser - The parser (see {@link resolveDomParser})
71
+ * @returns {Document} The parsed document
72
+ * @throws {Error} When no parser is available or the markup is not well formed
73
+ *
74
+ * @example
75
+ * parseXml("<a/>", new DOMParser()).documentElement.nodeName; // "a"
76
+ */
77
+ export function parseXml(xml, parser) {
78
+ if (!parser) {
79
+ throw new Error(
80
+ "XML parsing not available in this environment: pass a domParser option or install a global DOMParser",
81
+ );
82
+ }
83
+ let doc;
84
+ try {
85
+ doc = parser.parseFromString(xml, "application/xml");
86
+ } catch (thrown) {
87
+ // xmldom's own DOMParser throws a ParseError instead
88
+ throw new Error(`XML parse error: ${thrown.message}`, {
89
+ cause: thrown,
90
+ });
91
+ }
92
+ const error = findParseError(doc);
93
+ if (error) throw new Error(`XML parse error: ${error.textContent}`);
94
+ return doc;
95
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * XSLT element vocabulary.
3
+ *
4
+ * Single source of truth for the XSLT namespace URI and for the element names
5
+ * the engine can instantiate. `element-available()` reports against this table,
6
+ * so it stays in step with what {@link XsltEngine#instructionMethod} dispatches.
7
+ */
8
+
9
+ "use strict";
10
+
11
+ /** The XSLT 1.0 namespace URI. */
12
+ export const XSLT_NAMESPACE = "http://www.w3.org/1999/XSL/Transform";
13
+
14
+ /**
15
+ * Element names the engine dispatches, i.e. every element that
16
+ * `element-available()` must report as supported.
17
+ */
18
+ export const XSLT_ELEMENTS = Object.freeze([
19
+ "apply-imports",
20
+ "apply-templates",
21
+ "attribute",
22
+ "call-template",
23
+ "choose",
24
+ "comment",
25
+ "copy",
26
+ "copy-of",
27
+ "element",
28
+ "fallback",
29
+ "for-each",
30
+ "if",
31
+ "message",
32
+ "number",
33
+ "otherwise",
34
+ "param",
35
+ "processing-instruction",
36
+ "sort",
37
+ "text",
38
+ "value-of",
39
+ "variable",
40
+ "when",
41
+ "with-param",
42
+ ]);
43
+
44
+ const ELEMENT_SET = new Set(XSLT_ELEMENTS);
45
+
46
+ /**
47
+ * Check whether an XSLT element local name is supported by the engine.
48
+ *
49
+ * @param {string} localName - Element local name, e.g. `for-each`
50
+ * @returns {boolean} True when the engine instantiates the element
51
+ *
52
+ * @example
53
+ * isXsltElementAvailable('for-each'); // true
54
+ */
55
+ export function isXsltElementAvailable(localName) {
56
+ return ELEMENT_SET.has(localName);
57
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Variables and parameters: external parameter values, the global bindings
3
+ * of a transformation, variable values, xsl:variable and xsl:with-param.
4
+ *
5
+ * Methods installed on XsltEngine.prototype (`this` is the engine).
6
+ */
7
+
8
+ import { trimXmlSpace } from "../../xpath/strings.js";
9
+ import { inScopeNamespaces } from "../stylesheetNamespaces.js";
10
+ import { GlobalBindings } from "../variables.js";
11
+
12
+ /**
13
+ * Whether a variable-binding element has content (XSLT 1.0 section 11.2).
14
+ *
15
+ * Whitespace-only text is stripped from stylesheets, and comments and
16
+ * processing instructions are not part of a template, so they do not count.
17
+ *
18
+ * @param {Element|undefined} node - xsl:variable, xsl:param or xsl:with-param
19
+ * @returns {boolean} True when instantiating the element produces a fragment
20
+ */
21
+ function hasTemplateContent(node) {
22
+ for (let child = node?.firstChild; child; child = child.nextSibling) {
23
+ if (child.nodeType === 1) return true;
24
+ const isText = child.nodeType === 3 || child.nodeType === 4;
25
+ if (isText && trimXmlSpace(child.nodeValue) !== "") return true;
26
+ }
27
+ return false;
28
+ }
29
+
30
+ export const bindingMethods = {
31
+ /**
32
+ * Supply the value of a global parameter from outside the stylesheet.
33
+ *
34
+ * The value is merged into the `xsl:param` declaration when there is one, so
35
+ * removing the value later restores the declared default.
36
+ *
37
+ * @param {string} name - The parameter name, `{uri}local` when namespaced
38
+ * @param {*} value - The value to use
39
+ * @returns {void}
40
+ *
41
+ * @example
42
+ * engine.setParameterValue('sortOrder', 'ascending');
43
+ */
44
+ setParameterValue(name, value) {
45
+ const definition = this.globalParameters[name];
46
+
47
+ if (definition) definition.value = value;
48
+ else this.globalParameters[name] = { value };
49
+ },
50
+
51
+ /**
52
+ * Remove an externally supplied parameter value.
53
+ *
54
+ * The `xsl:param` declaration of the stylesheet is kept, so the parameter
55
+ * falls back to its declared default instead of becoming undefined.
56
+ *
57
+ * @param {string} name - The parameter name, `{uri}local` when namespaced
58
+ * @returns {void}
59
+ *
60
+ * @example
61
+ * engine.clearParameterValue('sortOrder');
62
+ */
63
+ clearParameterValue(name) {
64
+ const definition = this.globalParameters[name];
65
+ if (!definition) return;
66
+
67
+ if (definition.node) delete definition.value;
68
+ else delete this.globalParameters[name];
69
+ },
70
+
71
+ /**
72
+ * Remove every externally supplied parameter value.
73
+ *
74
+ * @returns {void}
75
+ *
76
+ * @example
77
+ * engine.clearParameterValues();
78
+ */
79
+ clearParameterValues() {
80
+ for (const name of Object.keys(this.globalParameters)) {
81
+ this.clearParameterValue(name);
82
+ }
83
+ },
84
+
85
+ /**
86
+ * Declare the global variables and parameters of the stylesheet for one
87
+ * transformation. They are evaluated lazily with the root node as context
88
+ * node (XSLT 1.0 section 11.4), so they may refer to each other in any
89
+ * order. Of a variable and a parameter of the same name, the one with the
90
+ * higher import precedence wins (section 11.4), the variable when the
91
+ * precedences are equal.
92
+ *
93
+ * @param {XsltContext} rootContext - The initial context
94
+ * @returns {GlobalBindings} The global bindings
95
+ */
96
+ createGlobals(rootContext) {
97
+ const globals = new GlobalBindings((def) => {
98
+ const context = rootContext.clone();
99
+ if (def.node) context.namespaces = inScopeNamespaces(def.node);
100
+ return this.evaluateVariable(def, context);
101
+ });
102
+
103
+ for (const [name, def] of Object.entries(this.globalParameters)) {
104
+ globals.define(name, def);
105
+ }
106
+ for (const [name, def] of Object.entries(this.globalVariables)) {
107
+ const param = this.globalParameters[name];
108
+ if (param?.node && param.importPrecedence > def.importPrecedence) {
109
+ continue;
110
+ }
111
+ globals.define(name, def);
112
+ }
113
+ return globals;
114
+ },
115
+
116
+ /**
117
+ * Compute the value of a variable or parameter definition.
118
+ *
119
+ * A value supplied from outside (`setParameter`) wins over the `select`
120
+ * expression and over the instantiated content of the declaration.
121
+ *
122
+ * @param {{value?: *, select?: string, node?: Element}} def - The definition
123
+ * @param {XsltContext} context - The context used for evaluation
124
+ * @returns {*} The variable value
125
+ */
126
+ evaluateVariable(def, context) {
127
+ if ("value" in def) {
128
+ return def.value;
129
+ }
130
+
131
+ if (def.select) {
132
+ return this.evaluateXPath(def.select, context);
133
+ }
134
+
135
+ // Empty content and no select: the value is an empty string (XSLT 11.2),
136
+ // which is false in a boolean test, unlike an (always true) fragment.
137
+ if (!hasTemplateContent(def.node)) {
138
+ return "";
139
+ }
140
+
141
+ // Otherwise the content is instantiated as a result tree fragment
142
+ const fragment = context.outputDocument.createDocumentFragment();
143
+ this.processChildren(def.node, context, fragment);
144
+ return fragment;
145
+ },
146
+
147
+ /**
148
+ * Evaluate the xsl:with-param children of an apply-templates or
149
+ * call-template instruction.
150
+ *
151
+ * @param {Element} node - The invoking instruction
152
+ * @param {XsltContext} context - Its context
153
+ * @returns {Object<string, *>} Values by parameter name
154
+ */
155
+ withParams(node, context) {
156
+ const params = {};
157
+ for (let child = node.firstChild; child; child = child.nextSibling) {
158
+ if (this.isXsltElement(child, "with-param")) {
159
+ params[child.getAttribute("name")] = this.evaluateVariable(
160
+ { node: child, select: child.getAttribute("select") },
161
+ context,
162
+ );
163
+ }
164
+ }
165
+ return params;
166
+ },
167
+
168
+ /**
169
+ * Instantiate xsl:variable: bind the variable for the following siblings.
170
+ * Content is instantiated from the work stack (see workStack.js), so
171
+ * recursion inside a variable keeps the JavaScript stack flat, and the
172
+ * fragment is bound once it is complete.
173
+ *
174
+ * @param {Element} node - The xsl:variable element
175
+ * @param {XsltContext} context - The current context
176
+ * @param {Node} _output - Unused: a variable produces no output
177
+ * @returns {void}
178
+ */
179
+ xslVariable(node, context, _output) {
180
+ const name = node.getAttribute("name");
181
+ const select = node.getAttribute("select");
182
+ if (select || !hasTemplateContent(node)) {
183
+ context.setVariable(
184
+ name,
185
+ this.evaluateVariable({ node, select }, context),
186
+ );
187
+ return;
188
+ }
189
+
190
+ const fragment = context.outputDocument.createDocumentFragment();
191
+ this.scheduleChildren(node, context, fragment, () =>
192
+ context.setVariable(name, fragment),
193
+ );
194
+ },
195
+ };
@@ -0,0 +1,105 @@
1
+ /**
2
+ * XSLT processing context (the state of one instruction's evaluation).
3
+ */
4
+
5
+ import { XPathEvaluator } from "../../xpath/evaluator.js";
6
+ import { createVariableView, lookupVariable } from "../variables.js";
7
+
8
+ /**
9
+ * XSLT Processing Context
10
+ *
11
+ * `variables` and `parameters` hold the local bindings in scope; the global
12
+ * variables and parameters of the transformation are in `globals`. The engine
13
+ * starts every template invocation with empty local bindings (and the
14
+ * template's own parameters), so local variables are lexically scoped.
15
+ * `namespaces` holds the prefixes in scope on the stylesheet element being
16
+ * instantiated.
17
+ */
18
+ export class XsltContext {
19
+ constructor(options = {}) {
20
+ this.currentNode = options.currentNode;
21
+ this.currentNodeList = options.currentNodeList || [];
22
+ this.position = options.position || 1;
23
+ this.variables = { ...options.variables };
24
+ this.parameters = { ...options.parameters };
25
+ this.globals = options.globals ?? null;
26
+ this.outputDocument = options.outputDocument;
27
+ this.stylesheet = options.stylesheet;
28
+ this.namespaces = options.namespaces ?? {};
29
+ this.templates = options.templates || [];
30
+ this.keys = options.keys || {};
31
+ this.decimalFormats = options.decimalFormats || {};
32
+ this.outputMethod = options.outputMethod || "xml";
33
+ this.xpathEvaluator = options.xpathEvaluator || new XPathEvaluator();
34
+ this.currentTemplate = options.currentTemplate || null;
35
+ this.currentMode = options.currentMode ?? null;
36
+ this.variableView = null;
37
+ }
38
+
39
+ /**
40
+ * Copy the context, changing some of its properties.
41
+ *
42
+ * `variables`, `parameters` and `namespaces` overrides are merged into the
43
+ * current ones; with `fresh: true` the copy starts without local bindings
44
+ * (a new template invocation).
45
+ *
46
+ * @param {object} [overrides] - Properties to change
47
+ * @returns {XsltContext} The copy
48
+ */
49
+ clone(overrides = {}) {
50
+ const fresh = overrides.fresh === true;
51
+ const merge = (own, extra) => (extra ? { ...own, ...extra } : own);
52
+ return new XsltContext({
53
+ currentNode: overrides.currentNode ?? this.currentNode,
54
+ currentNodeList: overrides.currentNodeList ?? this.currentNodeList,
55
+ position: overrides.position ?? this.position,
56
+ variables: fresh ? null : merge(this.variables, overrides.variables),
57
+ parameters: fresh ? null : merge(this.parameters, overrides.parameters),
58
+ globals: this.globals,
59
+ outputDocument: this.outputDocument,
60
+ stylesheet: this.stylesheet,
61
+ namespaces: merge(this.namespaces, overrides.namespaces),
62
+ templates: this.templates,
63
+ keys: this.keys,
64
+ decimalFormats: this.decimalFormats,
65
+ outputMethod: this.outputMethod,
66
+ xpathEvaluator: this.xpathEvaluator,
67
+ currentTemplate: overrides.currentTemplate ?? this.currentTemplate,
68
+ currentMode: overrides.currentMode ?? this.currentMode,
69
+ });
70
+ }
71
+
72
+ /**
73
+ * The variables in scope, as the object view the XPath evaluator reads.
74
+ *
75
+ * @returns {object} A live, read-only view (see createVariableView)
76
+ */
77
+ get xpathVariables() {
78
+ this.variableView ??= createVariableView(this);
79
+ return this.variableView;
80
+ }
81
+
82
+ /**
83
+ * The value of a variable or parameter in scope.
84
+ *
85
+ * @param {string} name - The variable name
86
+ * @returns {*} The value
87
+ * @throws {Error} When no such variable is in scope
88
+ */
89
+ getVariable(name) {
90
+ const { found, value } = lookupVariable(this, name);
91
+ if (!found) throw new Error(`Undefined variable: $${name}`);
92
+ return value;
93
+ }
94
+
95
+ /**
96
+ * Bind a local variable in this context.
97
+ *
98
+ * @param {string} name - The variable name
99
+ * @param {*} value - Its value
100
+ * @returns {void}
101
+ */
102
+ setVariable(name, value) {
103
+ this.variables[name] = value;
104
+ }
105
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Conditional and repetition instructions: xsl:if, xsl:choose,
3
+ * xsl:for-each, and the xsl:sort glue shared with xsl:apply-templates.
4
+ *
5
+ * Methods installed on XsltEngine.prototype (`this` is the engine).
6
+ */
7
+
8
+ import { sortNodes } from "../sort.js";
9
+ import { LoopFrame } from "./workStack.js";
10
+
11
+ /**
12
+ * Evaluate the `select` expression of xsl:for-each or xsl:apply-templates
13
+ * as a node list.
14
+ *
15
+ * @param {object} engine - The engine
16
+ * @param {string} select - The expression
17
+ * @param {XsltContext} context - The current context
18
+ * @returns {Node[]} The selected nodes (a single value becomes a list of
19
+ * one, an empty value an empty list)
20
+ */
21
+ export function selectNodes(engine, select, context) {
22
+ const nodes = engine.evaluateXPath(select, context);
23
+ if (Array.isArray(nodes)) return nodes;
24
+ return nodes ? [nodes] : [];
25
+ }
26
+
27
+ export const controlFlowMethods = {
28
+ /**
29
+ * Instantiate `xsl:if`: the content when the test is true.
30
+ *
31
+ * @param {Element} node - The xsl:if instruction
32
+ * @param {XsltContext} context - The current context
33
+ * @param {Node} output - The result node receiving the output
34
+ * @returns {void}
35
+ */
36
+ xslIf(node, context, output) {
37
+ const test = node.getAttribute("test");
38
+ const result = this.evaluateXPath(test, context);
39
+
40
+ if (this.xpathEvaluator.toBoolean(result)) {
41
+ this.scheduleChildren(node, context, output);
42
+ }
43
+ },
44
+
45
+ /**
46
+ * Instantiate `xsl:choose`: the first xsl:when whose test is true, else
47
+ * the xsl:otherwise.
48
+ *
49
+ * @param {Element} node - The xsl:choose instruction
50
+ * @param {XsltContext} context - The current context
51
+ * @param {Node} output - The result node receiving the output
52
+ * @returns {void}
53
+ */
54
+ xslChoose(node, context, output) {
55
+ for (const child of node.childNodes) {
56
+ if (child.nodeType !== 1) continue;
57
+
58
+ if (this.isXsltElement(child, "when")) {
59
+ const test = child.getAttribute("test");
60
+ const result = this.evaluateXPath(test, context);
61
+
62
+ if (this.xpathEvaluator.toBoolean(result)) {
63
+ this.scheduleChildren(child, context, output);
64
+ return;
65
+ }
66
+ } else if (this.isXsltElement(child, "otherwise")) {
67
+ this.scheduleChildren(child, context, output);
68
+ return;
69
+ }
70
+ }
71
+ },
72
+
73
+ /**
74
+ * Instantiate `xsl:for-each`: the content once per selected node, in
75
+ * xsl:sort order, with that node as current node.
76
+ *
77
+ * @param {Element} node - The xsl:for-each instruction
78
+ * @param {XsltContext} context - The current context
79
+ * @param {Node} output - The result node receiving the output
80
+ * @returns {void}
81
+ */
82
+ xslForEach(node, context, output) {
83
+ const select = node.getAttribute("select");
84
+ const nodes = this.sortNodes(
85
+ selectNodes(this, select, context),
86
+ this.sortElementsOf(node),
87
+ context,
88
+ );
89
+
90
+ if (nodes.length === 0) return;
91
+
92
+ // One node at a time from the work stack (see workStack.js)
93
+ this.continueWith(
94
+ new LoopFrame(nodes.length, (i) => {
95
+ const newContext = context.clone({
96
+ currentNode: nodes[i],
97
+ currentNodeList: nodes,
98
+ position: i + 1,
99
+ });
100
+ this.scheduleChildren(node, newContext, output);
101
+ }),
102
+ );
103
+ },
104
+
105
+ /**
106
+ * The xsl:sort children of a sorting instruction, in document order.
107
+ *
108
+ * @param {Element} instruction - xsl:for-each or xsl:apply-templates
109
+ * @returns {Element[]} The xsl:sort elements
110
+ */
111
+ sortElementsOf(instruction) {
112
+ return Array.from(instruction.childNodes).filter((child) =>
113
+ this.isXsltElement(child, "sort"),
114
+ );
115
+ },
116
+
117
+ /**
118
+ * Sort a node list by xsl:sort elements (see sort.js).
119
+ *
120
+ * @param {Node[]} nodes - Nodes in document order
121
+ * @param {Element[]} sortElements - The xsl:sort elements
122
+ * @param {XsltContext} context - Context of the sorting instruction
123
+ * @returns {Node[]} The sorted nodes
124
+ */
125
+ sortNodes(nodes, sortElements, context) {
126
+ return sortNodes(nodes, sortElements, context, this.sortHost);
127
+ },
128
+
129
+ /**
130
+ * Engine callbacks used by the sort module, created once per engine.
131
+ *
132
+ * @returns {import('../sort.js').SortHost} The callbacks
133
+ */
134
+ get sortHost() {
135
+ if (!this._sortHost) {
136
+ this._sortHost = {
137
+ evaluate: (expr, ctx) => this.evaluateXPath(expr, ctx),
138
+ avt: (value, ctx) => this.processAttributeValueTemplate(value, ctx),
139
+ toString: (value) => this.xpathEvaluator.toString(value),
140
+ toNumber: (value) => this.xpathEvaluator.toNumber(value),
141
+ };
142
+ }
143
+ return this._sortHost;
144
+ },
145
+ };