@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.
- package/LICENSE.md +1 -1
- package/README.md +110 -520
- package/bin/lib/decode.js +15 -0
- package/bin/lib/dom.js +177 -0
- package/bin/lib/loaders.js +127 -0
- package/bin/lib/options.js +131 -0
- package/bin/lib/output.js +114 -0
- package/bin/lib/paths.js +186 -0
- package/bin/lib/transform.js +206 -0
- package/bin/xslt.js +73 -168
- package/dist/xslt-processor.browser.js +9564 -1585
- package/dist/xslt-processor.browser.js.map +4 -4
- package/dist/xslt-processor.browser.min.js +13 -2
- package/dist/xslt-processor.browser.min.js.map +4 -4
- package/dist/xslt-processor.cjs +9572 -1586
- package/dist/xslt-processor.cjs.map +4 -4
- package/dist/xslt-processor.d.cts +658 -0
- package/dist/xslt-processor.d.ts +459 -12
- package/dist/xslt-processor.js +9546 -1582
- package/dist/xslt-processor.js.map +4 -4
- package/package.json +71 -20
- package/src/XSLTProcessor.js +494 -48
- package/src/async/abort.js +63 -0
- package/src/async/documentUris.js +128 -0
- package/src/async/loaders.js +134 -0
- package/src/async/preload.js +159 -0
- package/src/async/processor.js +206 -0
- package/src/async/stream.js +125 -0
- package/src/bridge/engine.js +221 -0
- package/src/bridge/loader.js +78 -0
- package/src/bridge/results.js +75 -0
- package/src/bridge/version.js +63 -0
- package/src/index.js +26 -8
- package/src/io/decode.js +140 -0
- package/src/io/readSource.js +167 -0
- package/src/xpath/axes.js +562 -0
- package/src/xpath/documentOrder.js +270 -0
- package/src/xpath/evaluator.js +518 -357
- package/src/xpath/index.js +8 -2
- package/src/xpath/namespaceNodes.js +172 -0
- package/src/xpath/nodeSetFunctions.js +169 -0
- package/src/xpath/parser.js +30 -5
- package/src/xpath/strings.js +183 -0
- package/src/xpath/tokenizer.js +37 -23
- package/src/xslt/attributeSets.js +95 -0
- package/src/xslt/avt.js +103 -0
- package/src/xslt/computedNames.js +91 -0
- package/src/xslt/copying.js +212 -0
- package/src/xslt/declarationNames.js +80 -0
- package/src/xslt/domParsing.js +95 -0
- package/src/xslt/elements.js +57 -0
- package/src/xslt/engine/bindings.js +195 -0
- package/src/xslt/engine/context.js +105 -0
- package/src/xslt/engine/controlFlow.js +145 -0
- package/src/xslt/engine/copyInstructions.js +133 -0
- package/src/xslt/engine/declarations.js +233 -0
- package/src/xslt/engine/functionSupport.js +103 -0
- package/src/xslt/engine/methods.js +33 -0
- package/src/xslt/engine/nodeConstruction.js +187 -0
- package/src/xslt/engine/numbering.js +104 -0
- package/src/xslt/engine/outputDeclaration.js +77 -0
- package/src/xslt/engine/sequenceConstructor.js +228 -0
- package/src/xslt/engine/stylesheetLoading.js +208 -0
- package/src/xslt/engine/templateInvocation.js +253 -0
- package/src/xslt/engine/templateRules.js +243 -0
- package/src/xslt/engine/textInstructions.js +171 -0
- package/src/xslt/engine/topLevel.js +130 -0
- package/src/xslt/engine/transformation.js +263 -0
- package/src/xslt/engine/workStack.js +245 -0
- package/src/xslt/engine.js +184 -1736
- package/src/xslt/exslt/arguments.js +99 -0
- package/src/xslt/exslt/calendar.js +120 -0
- package/src/xslt/exslt/common.js +44 -0
- package/src/xslt/exslt/dateCalc.js +261 -0
- package/src/xslt/exslt/dateFormat.js +150 -0
- package/src/xslt/exslt/dateParse.js +265 -0
- package/src/xslt/exslt/dates.js +259 -0
- package/src/xslt/exslt/duration.js +207 -0
- package/src/xslt/exslt/dynamic.js +59 -0
- package/src/xslt/exslt/index.js +59 -0
- package/src/xslt/exslt/math.js +177 -0
- package/src/xslt/exslt/sets.js +96 -0
- package/src/xslt/exslt/stringOps.js +163 -0
- package/src/xslt/exslt/strings.js +147 -0
- package/src/xslt/exslt/uri.js +92 -0
- package/src/xslt/formatNumber.js +233 -0
- package/src/xslt/forwardsCompatible.js +75 -0
- package/src/xslt/functions.js +270 -0
- package/src/xslt/index.js +38 -1
- package/src/xslt/keys.js +164 -0
- package/src/xslt/literalResult.js +223 -0
- package/src/xslt/matchScope.js +116 -0
- package/src/xslt/number.js +271 -0
- package/src/xslt/numberFormat.js +253 -0
- package/src/xslt/outputNames.js +58 -0
- package/src/xslt/patternCompiler.js +175 -0
- package/src/xslt/patterns.js +324 -0
- package/src/xslt/qname.js +90 -0
- package/src/xslt/resultDocument.js +98 -0
- package/src/xslt/resultNamespaces.js +219 -0
- package/src/xslt/resultTree.js +211 -0
- package/src/xslt/serializer/baseWriter.js +390 -0
- package/src/xslt/serializer/chunks.js +120 -0
- package/src/xslt/serializer/constants.js +92 -0
- package/src/xslt/serializer/encoding.js +327 -0
- package/src/xslt/serializer/escape.js +135 -0
- package/src/xslt/serializer/frames.js +168 -0
- package/src/xslt/serializer/htmlDoctype.js +102 -0
- package/src/xslt/serializer/htmlEntities.js +77 -0
- package/src/xslt/serializer/htmlSerializer.js +239 -0
- package/src/xslt/serializer/indent.js +51 -0
- package/src/xslt/serializer/namespaces.js +68 -0
- package/src/xslt/serializer/rawText.js +41 -0
- package/src/xslt/serializer/settings.js +179 -0
- package/src/xslt/serializer/textSerializer.js +77 -0
- package/src/xslt/serializer/xhtmlDocument.js +103 -0
- package/src/xslt/serializer/xmlSerializer.js +227 -0
- package/src/xslt/serializer.js +90 -0
- package/src/xslt/sort.js +151 -0
- package/src/xslt/spaceNameTests.js +115 -0
- package/src/xslt/stylesheetChecks.js +206 -0
- package/src/xslt/stylesheetNamespaces.js +266 -0
- package/src/xslt/templatePriority.js +45 -0
- package/src/xslt/uri.js +68 -0
- package/src/xslt/variables.js +152 -0
- package/src/xslt/whitespace.js +200 -0
- package/LICENSE +0 -29
- package/src/XSLTProcessor.test.js +0 -930
- package/src/xpath/evaluator.test.js +0 -1852
- package/src/xpath/tokenizer.test.js +0 -224
- 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
|
+
};
|