@tradik/xslt-processor 1.1.1 → 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 +102 -757
- 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 +17 -0
- package/bin/lib/output.js +114 -0
- package/bin/lib/paths.js +3 -3
- package/bin/lib/transform.js +124 -33
- package/bin/xslt.js +26 -27
- package/dist/xslt-processor.browser.js +8784 -2720
- package/dist/xslt-processor.browser.js.map +4 -4
- package/dist/xslt-processor.browser.min.js +13 -6
- package/dist/xslt-processor.browser.min.js.map +4 -4
- package/dist/xslt-processor.cjs +8789 -2723
- package/dist/xslt-processor.cjs.map +4 -4
- package/dist/xslt-processor.d.cts +380 -21
- package/dist/xslt-processor.d.ts +380 -21
- package/dist/xslt-processor.js +8770 -2722
- package/dist/xslt-processor.js.map +4 -4
- package/package.json +51 -11
- package/src/XSLTProcessor.js +343 -66
- 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 +16 -4
- 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 +475 -355
- 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 +1 -1
- 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 +176 -2020
- 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 +22 -9
- package/src/xslt/forwardsCompatible.js +75 -0
- package/src/xslt/functions.js +94 -15
- package/src/xslt/index.js +7 -1
- package/src/xslt/keys.js +51 -28
- package/src/xslt/literalResult.js +63 -7
- package/src/xslt/matchScope.js +116 -0
- package/src/xslt/number.js +171 -78
- package/src/xslt/numberFormat.js +124 -26
- 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 +143 -6
- package/src/xslt/serializer/baseWriter.js +173 -66
- package/src/xslt/serializer/chunks.js +120 -0
- package/src/xslt/serializer/constants.js +14 -0
- package/src/xslt/serializer/encoding.js +327 -0
- package/src/xslt/serializer/escape.js +49 -12
- 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 +123 -25
- package/src/xslt/serializer/settings.js +89 -13
- package/src/xslt/serializer/textSerializer.js +58 -10
- package/src/xslt/serializer/xhtmlDocument.js +103 -0
- package/src/xslt/serializer/xmlSerializer.js +113 -13
- package/src/xslt/serializer.js +50 -17
- 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/variables.js +152 -0
- package/src/xslt/whitespace.js +43 -27
- package/LICENSE +0 -29
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static checks run while a stylesheet is compiled.
|
|
3
|
+
*
|
|
4
|
+
* - Patterns (XSLT 1.0 section 5.2) of `xsl:template match`, `xsl:key match`
|
|
5
|
+
* and `xsl:number count`/`from` are compiled at import time, so an invalid
|
|
6
|
+
* pattern makes `importStylesheet` fail with an error naming it, as libxslt
|
|
7
|
+
* (and so the browsers' XSLTProcessor) rejects such a stylesheet.
|
|
8
|
+
* - Two global xsl:variable elements of the same name and import precedence
|
|
9
|
+
* (section 11.4) and text at the top level of a stylesheet (section 2.2)
|
|
10
|
+
* make `importStylesheet` fail, as libxslt rejects such a stylesheet.
|
|
11
|
+
* - Two local variables or parameters of the same name where one is in the
|
|
12
|
+
* scope of the other (section 11.5), and a global xsl:param clashing with
|
|
13
|
+
* another global binding of the same import precedence (section 11.4),
|
|
14
|
+
* are errors libxslt only reports; they are warned about here, and the
|
|
15
|
+
* binding used so far keeps being used.
|
|
16
|
+
*
|
|
17
|
+
* @module xslt/stylesheetChecks
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
"use strict";
|
|
21
|
+
|
|
22
|
+
import { XSLT_NAMESPACE } from "./elements.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The local name of an XSLT element, or null for any other node.
|
|
26
|
+
*
|
|
27
|
+
* @param {Node} node - A stylesheet node
|
|
28
|
+
* @returns {string|null} e.g. "number" for xsl:number
|
|
29
|
+
*/
|
|
30
|
+
export function xsltLocalName(node) {
|
|
31
|
+
if (node.nodeType !== 1) return null;
|
|
32
|
+
if (node.namespaceURI === XSLT_NAMESPACE) return node.localName;
|
|
33
|
+
return node.nodeName.startsWith("xsl:") ? node.nodeName.slice(4) : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Visit every element below a node, in document order.
|
|
38
|
+
*
|
|
39
|
+
* @param {Node} root - The subtree root (not visited itself)
|
|
40
|
+
* @param {(element: Element) => void} visit - Called for each element
|
|
41
|
+
* @returns {void}
|
|
42
|
+
*/
|
|
43
|
+
function forEachDescendant(root, visit) {
|
|
44
|
+
for (let child = root.firstChild; child; child = child.nextSibling) {
|
|
45
|
+
if (child.nodeType !== 1) continue;
|
|
46
|
+
visit(child);
|
|
47
|
+
forEachDescendant(child, visit);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Compile a pattern of the stylesheet, reporting where it comes from when
|
|
53
|
+
* it is invalid.
|
|
54
|
+
*
|
|
55
|
+
* @param {{compile: (pattern: string) => object[]}} matcher - The pattern matcher
|
|
56
|
+
* @param {string} pattern - The pattern
|
|
57
|
+
* @param {string} where - The attribute holding it, e.g. "xsl:template match"
|
|
58
|
+
* @returns {void}
|
|
59
|
+
* @throws {Error} When the pattern is invalid
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* checkPattern(matcher, "a/..", "xsl:template match");
|
|
63
|
+
* // Error: xsl:template match: Invalid pattern "a/..": Axis not allowed ...
|
|
64
|
+
*/
|
|
65
|
+
export function checkPattern(matcher, pattern, where) {
|
|
66
|
+
try {
|
|
67
|
+
matcher.compile(pattern);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
throw new Error(`${where}: ${error.message}`, { cause: error });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Compile the `count` and `from` patterns of every `xsl:number` below a
|
|
75
|
+
* stylesheet element (they are not attribute value templates, so they are
|
|
76
|
+
* known at compile time).
|
|
77
|
+
*
|
|
78
|
+
* @param {{compile: (pattern: string) => object[]}} matcher - The pattern matcher
|
|
79
|
+
* @param {Element} root - The stylesheet (or simplified stylesheet) element
|
|
80
|
+
* @returns {void}
|
|
81
|
+
* @throws {Error} When one of the patterns is invalid
|
|
82
|
+
*/
|
|
83
|
+
export function checkNumberPatterns(matcher, root) {
|
|
84
|
+
forEachDescendant(root, (element) => {
|
|
85
|
+
if (xsltLocalName(element) !== "number") return;
|
|
86
|
+
for (const attribute of ["count", "from"]) {
|
|
87
|
+
const pattern = element.getAttribute(attribute);
|
|
88
|
+
if (pattern) checkPattern(matcher, pattern, `xsl:number ${attribute}`);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A short description of the declaration holding local bindings.
|
|
95
|
+
*
|
|
96
|
+
* @param {Element} declaration - xsl:template, xsl:variable, ...
|
|
97
|
+
* @returns {string} e.g. `template match="/"`
|
|
98
|
+
*/
|
|
99
|
+
function describeDeclaration(declaration) {
|
|
100
|
+
const kind = xsltLocalName(declaration) ?? declaration.nodeName;
|
|
101
|
+
for (const attribute of ["match", "name"]) {
|
|
102
|
+
const value = declaration.getAttribute(attribute);
|
|
103
|
+
if (value !== null) return `${kind} ${attribute}="${value}"`;
|
|
104
|
+
}
|
|
105
|
+
return kind;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Walk a sequence constructor, reporting bindings that shadow another local
|
|
110
|
+
* binding in scope. A binding is in scope for its following siblings and
|
|
111
|
+
* their descendants, not for its own content.
|
|
112
|
+
*
|
|
113
|
+
* @param {Element} parent - The element whose children are walked
|
|
114
|
+
* @param {Set<string>} inScope - Local names bound around `parent`
|
|
115
|
+
* @param {(name: string) => void} report - Called for each duplicate
|
|
116
|
+
* @returns {void}
|
|
117
|
+
*/
|
|
118
|
+
function walkBindings(parent, inScope, report) {
|
|
119
|
+
let scope = inScope;
|
|
120
|
+
for (let child = parent.firstChild; child; child = child.nextSibling) {
|
|
121
|
+
if (child.nodeType !== 1) continue;
|
|
122
|
+
const kind = xsltLocalName(child);
|
|
123
|
+
walkBindings(child, scope, report);
|
|
124
|
+
if (kind !== "variable" && kind !== "param") continue;
|
|
125
|
+
|
|
126
|
+
const name = child.getAttribute("name");
|
|
127
|
+
if (scope.has(name)) report(name);
|
|
128
|
+
scope = new Set(scope).add(name);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Warn about local variables and parameters that shadow another local
|
|
134
|
+
* binding of the same declaration (XSLT 1.0 section 11.5). A local binding
|
|
135
|
+
* shadowing a global one is allowed and not reported.
|
|
136
|
+
*
|
|
137
|
+
* @param {Element} declaration - A top-level element (xsl:template,
|
|
138
|
+
* xsl:variable, xsl:param, xsl:attribute-set)
|
|
139
|
+
* @param {(message: string) => void} warn - Reports one duplicate
|
|
140
|
+
* @returns {void}
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* checkLocalBindings(templateElement, (m) => console.warn(m));
|
|
144
|
+
*/
|
|
145
|
+
export function checkLocalBindings(declaration, warn) {
|
|
146
|
+
walkBindings(declaration, new Set(), (name) =>
|
|
147
|
+
warn(
|
|
148
|
+
`duplicate binding of variable $${name} in ${describeDeclaration(declaration)}; the later one is used (XSLT 1.0 section 11.5)`,
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Check a global variable or parameter repeating the name of an earlier one
|
|
155
|
+
* with the same import precedence (XSLT 1.0 section 11.4). Two
|
|
156
|
+
* xsl:variable elements are an error, as in libxslt; a clash involving an
|
|
157
|
+
* xsl:param is warned about, naming the binding the engine uses: the later
|
|
158
|
+
* declaration, except that a global xsl:variable wins over an xsl:param.
|
|
159
|
+
*
|
|
160
|
+
* @param {{name: string, kind: string, precedence: number}} declaration - The
|
|
161
|
+
* new declaration: its name, "variable" or "param", and import precedence
|
|
162
|
+
* @param {{variable?: object, param?: object}} earlier - The definitions
|
|
163
|
+
* registered so far under that name (with `node` and `importPrecedence`)
|
|
164
|
+
* @param {(message: string) => void} warn - Reports the duplicate
|
|
165
|
+
* @returns {void}
|
|
166
|
+
* @throws {Error} When an xsl:variable repeats an xsl:variable
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* checkGlobalDuplicate({ name: "v", kind: "param", precedence: 1 },
|
|
170
|
+
* { variable: { node, importPrecedence: 1 } }, console.warn);
|
|
171
|
+
*/
|
|
172
|
+
export function checkGlobalDuplicate(declaration, earlier, warn) {
|
|
173
|
+
const { name, kind, precedence } = declaration;
|
|
174
|
+
const clashes = (definition) =>
|
|
175
|
+
Boolean(definition?.node) && definition.importPrecedence === precedence;
|
|
176
|
+
if (kind === "variable" && clashes(earlier.variable)) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`redefinition of global variable $${name} at the same import precedence (XSLT 1.0 section 11.4)`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
if (!clashes(earlier.variable) && !clashes(earlier.param)) return;
|
|
182
|
+
const used =
|
|
183
|
+
kind === "param" && earlier.variable ? "the xsl:variable" : "the later one";
|
|
184
|
+
warn(
|
|
185
|
+
`duplicate global binding of variable $${name} at the same import precedence; ${used} is used (XSLT 1.0 section 11.4)`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Reject text other than whitespace among the top-level elements of a
|
|
191
|
+
* stylesheet (XSLT 1.0 section 2.2), as libxslt does.
|
|
192
|
+
*
|
|
193
|
+
* @param {Element} root - The xsl:stylesheet or xsl:transform element
|
|
194
|
+
* @returns {void}
|
|
195
|
+
* @throws {Error} When a text node holds non-whitespace characters
|
|
196
|
+
*/
|
|
197
|
+
export function checkTopLevelText(root) {
|
|
198
|
+
for (let child = root.firstChild; child; child = child.nextSibling) {
|
|
199
|
+
const isText = child.nodeType === 3 || child.nodeType === 4;
|
|
200
|
+
if (isText && /[^ \t\r\n]/.test(child.nodeValue)) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`misplaced text at the top level of the stylesheet: "${child.nodeValue.trim()}" (XSLT 1.0 section 2.2)`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-scope namespaces of stylesheet elements.
|
|
3
|
+
*
|
|
4
|
+
* Prefixes in XPath expressions, patterns and QName-valued attributes resolve
|
|
5
|
+
* against the namespace declarations in scope on the element that carries
|
|
6
|
+
* them (XSLT 1.0 section 2.4), not against one stylesheet-wide table: a
|
|
7
|
+
* declaration on `xsl:template` or on a literal result element applies inside
|
|
8
|
+
* it only, and an included stylesheet may bind a prefix differently from the
|
|
9
|
+
* including one. The same bindings are the namespace nodes that literal
|
|
10
|
+
* result elements copy to the result tree (section 7.1.1), minus the XSLT
|
|
11
|
+
* namespace and the excluded and extension prefixes.
|
|
12
|
+
*
|
|
13
|
+
* Every map is computed once per element and cached; an element without
|
|
14
|
+
* declarations of its own shares its parent's (frozen) map, so identical
|
|
15
|
+
* scopes can be recognised by identity.
|
|
16
|
+
*
|
|
17
|
+
* @module xslt/stylesheetNamespaces
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
"use strict";
|
|
21
|
+
|
|
22
|
+
import { XSLT_NAMESPACE } from "./elements.js";
|
|
23
|
+
|
|
24
|
+
/** Namespace of `xmlns` attributes. */
|
|
25
|
+
export const XMLNS_NAMESPACE = "http://www.w3.org/2000/xmlns/";
|
|
26
|
+
|
|
27
|
+
/** Namespace bound to the `xml` prefix. */
|
|
28
|
+
export const XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace";
|
|
29
|
+
|
|
30
|
+
/** The scope above the stylesheet document element. */
|
|
31
|
+
const EMPTY_SCOPE = Object.freeze({});
|
|
32
|
+
|
|
33
|
+
/** Namespaces excluded above the stylesheet document element. */
|
|
34
|
+
const BASE_EXCLUSIONS = new Set([XSLT_NAMESPACE]);
|
|
35
|
+
|
|
36
|
+
/** No extension namespaces, above the stylesheet document element. */
|
|
37
|
+
const NO_EXTENSIONS = new Set();
|
|
38
|
+
|
|
39
|
+
const scopes = new WeakMap();
|
|
40
|
+
const exclusions = new WeakMap();
|
|
41
|
+
const extensions = new WeakMap();
|
|
42
|
+
const namespaceNodes = new WeakMap();
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The prefix a namespace declaration attribute declares, or null when the
|
|
46
|
+
* attribute is not a declaration.
|
|
47
|
+
*
|
|
48
|
+
* @param {Attr} attribute - Any attribute
|
|
49
|
+
* @returns {string|null} The prefix, "" for a default namespace declaration
|
|
50
|
+
*/
|
|
51
|
+
export function declaredPrefix(attribute) {
|
|
52
|
+
const name = attribute.name;
|
|
53
|
+
if (name === "xmlns") return "";
|
|
54
|
+
if (name.startsWith("xmlns:")) return name.slice(6);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The namespace bindings in scope on a stylesheet element.
|
|
60
|
+
*
|
|
61
|
+
* @param {Node|null} node - A stylesheet element (other nodes have no scope)
|
|
62
|
+
* @returns {Readonly<Object<string, string>>} URIs by prefix; "" holds the
|
|
63
|
+
* default namespace, an empty URI means "undeclared"
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* inScopeNamespaces(templateElement).f; // "urn:f"
|
|
67
|
+
*/
|
|
68
|
+
export function inScopeNamespaces(node) {
|
|
69
|
+
if (node?.nodeType !== 1) return EMPTY_SCOPE;
|
|
70
|
+
|
|
71
|
+
let scope = scopes.get(node);
|
|
72
|
+
if (scope) return scope;
|
|
73
|
+
|
|
74
|
+
const parentScope = inScopeNamespaces(node.parentNode);
|
|
75
|
+
let own = null;
|
|
76
|
+
for (const attribute of node.attributes) {
|
|
77
|
+
const prefix = declaredPrefix(attribute);
|
|
78
|
+
if (prefix === null) continue;
|
|
79
|
+
own ??= {};
|
|
80
|
+
own[prefix] = attribute.value;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
scope = own ? Object.freeze({ ...parentScope, ...own }) : parentScope;
|
|
84
|
+
scopes.set(node, scope);
|
|
85
|
+
return scope;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Split an XSLT attribute holding whitespace separated prefixes.
|
|
90
|
+
*
|
|
91
|
+
* @param {string|null} value - e.g. `"a b #default"`
|
|
92
|
+
* @returns {string[]} The prefixes, "" standing for `#default`
|
|
93
|
+
*/
|
|
94
|
+
function prefixList(value) {
|
|
95
|
+
if (!value) return [];
|
|
96
|
+
return value
|
|
97
|
+
.split(/[ \t\r\n]+/)
|
|
98
|
+
.filter(Boolean)
|
|
99
|
+
.map((prefix) => (prefix === "#default" ? "" : prefix));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Attributes whose prefixes are excluded from the result tree. */
|
|
103
|
+
const EXCLUSION_ATTRIBUTES = [
|
|
104
|
+
"exclude-result-prefixes",
|
|
105
|
+
"extension-element-prefixes",
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
/** Attributes declaring extension namespaces (XSLT 1.0 section 14.1). */
|
|
109
|
+
const EXTENSION_ATTRIBUTES = ["extension-element-prefixes"];
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Read prefix list attributes of one stylesheet element: unqualified on
|
|
113
|
+
* `xsl:stylesheet`, `xsl:`-qualified on literal result elements.
|
|
114
|
+
*
|
|
115
|
+
* @param {Element} element - A stylesheet element
|
|
116
|
+
* @param {string[]} attributes - Local names of the attributes to read
|
|
117
|
+
* @returns {Array<{prefix: string, attribute: string}>} Prefixes declared on
|
|
118
|
+
* this element, with the attribute declaring each
|
|
119
|
+
*/
|
|
120
|
+
function ownPrefixes(element, attributes) {
|
|
121
|
+
const isXslt = element.namespaceURI === XSLT_NAMESPACE;
|
|
122
|
+
const read = (localName) =>
|
|
123
|
+
isXslt
|
|
124
|
+
? element.getAttribute(localName)
|
|
125
|
+
: element.getAttributeNS(XSLT_NAMESPACE, localName);
|
|
126
|
+
return attributes.flatMap((attribute) =>
|
|
127
|
+
prefixList(read(attribute)).map((prefix) => ({ prefix, attribute })),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Namespace URIs named by prefix list attributes on a stylesheet element or
|
|
133
|
+
* its ancestors, cached per element.
|
|
134
|
+
*
|
|
135
|
+
* @param {Node|null} node - A stylesheet element
|
|
136
|
+
* @param {object} kind - What to collect
|
|
137
|
+
* @param {string[]} kind.attributes - The prefix list attributes
|
|
138
|
+
* @param {WeakMap<Element, Set<string>>} kind.cache - Results by element
|
|
139
|
+
* @param {Set<string>} kind.base - The set above the document element
|
|
140
|
+
* @param {boolean} kind.report - Whether undeclared prefixes are reported
|
|
141
|
+
* @returns {Set<string>} The namespace URIs
|
|
142
|
+
*/
|
|
143
|
+
function declaredNamespaces(node, kind) {
|
|
144
|
+
if (node?.nodeType !== 1) return kind.base;
|
|
145
|
+
|
|
146
|
+
let found = kind.cache.get(node);
|
|
147
|
+
if (found) return found;
|
|
148
|
+
|
|
149
|
+
found = declaredNamespaces(node.parentNode, kind);
|
|
150
|
+
const prefixes = ownPrefixes(node, kind.attributes);
|
|
151
|
+
if (prefixes.length > 0) {
|
|
152
|
+
const scope = inScopeNamespaces(node);
|
|
153
|
+
found = new Set(found);
|
|
154
|
+
for (const { prefix, attribute } of prefixes) {
|
|
155
|
+
const uri = resolvePrefix(scope, prefix);
|
|
156
|
+
if (uri) {
|
|
157
|
+
found.add(uri);
|
|
158
|
+
} else if (kind.report) {
|
|
159
|
+
// An error in XSLT 1.0 (section 7.1.1) that libxslt reports and
|
|
160
|
+
// recovers from; reported once, as the result is cached per element
|
|
161
|
+
console.warn(
|
|
162
|
+
`XSLT: ${attribute}: undefined namespace prefix "${prefix || "#default"}" is ignored`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
kind.cache.set(node, found);
|
|
168
|
+
return found;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Excluded namespaces: excluded and extension prefixes, plus XSLT. */
|
|
172
|
+
const EXCLUDED = {
|
|
173
|
+
attributes: EXCLUSION_ATTRIBUTES,
|
|
174
|
+
cache: exclusions,
|
|
175
|
+
base: BASE_EXCLUSIONS,
|
|
176
|
+
report: true,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/** Extension namespaces (reported through EXCLUDED already). */
|
|
180
|
+
const EXTENSIONS = {
|
|
181
|
+
attributes: EXTENSION_ATTRIBUTES,
|
|
182
|
+
cache: extensions,
|
|
183
|
+
base: NO_EXTENSIONS,
|
|
184
|
+
report: false,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Namespace URIs excluded from the result tree around a stylesheet element:
|
|
189
|
+
* the XSLT namespace, plus the namespaces of every excluded or extension
|
|
190
|
+
* prefix declared on the element or its ancestors. A prefix that is not
|
|
191
|
+
* declared (including `#all`, which XSLT 1.0 does not know) is reported with
|
|
192
|
+
* console.warn and ignored.
|
|
193
|
+
*
|
|
194
|
+
* @param {Node|null} node - A stylesheet element
|
|
195
|
+
* @returns {Set<string>} Excluded namespace URIs
|
|
196
|
+
*/
|
|
197
|
+
function excludedNamespaces(node) {
|
|
198
|
+
return declaredNamespaces(node, EXCLUDED);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Whether a stylesheet element is an extension element: an element in a
|
|
203
|
+
* namespace that an `extension-element-prefixes` attribute on it or on an
|
|
204
|
+
* ancestor declares as an extension namespace (XSLT 1.0 section 14.1).
|
|
205
|
+
*
|
|
206
|
+
* @param {Element} element - A stylesheet element that is not in the XSLT namespace
|
|
207
|
+
* @returns {boolean} True for extension elements
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* // <xsl:stylesheet xmlns:e="urn:e" extension-element-prefixes="e">
|
|
211
|
+
* isExtensionElement(eElement); // true
|
|
212
|
+
*/
|
|
213
|
+
export function isExtensionElement(element) {
|
|
214
|
+
const uri = element.namespaceURI;
|
|
215
|
+
if (!uri) return false;
|
|
216
|
+
// Resolving the exclusions first reports undeclared prefixes once
|
|
217
|
+
excludedNamespaces(element);
|
|
218
|
+
return declaredNamespaces(element, EXTENSIONS).has(uri);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* The namespace nodes a literal result element copies to the result tree
|
|
223
|
+
* (XSLT 1.0 section 7.1.1).
|
|
224
|
+
*
|
|
225
|
+
* @param {Element} node - The literal result element
|
|
226
|
+
* The list is shared by every element with the same scope and exclusions,
|
|
227
|
+
* so equal lists can be compared by identity.
|
|
228
|
+
*
|
|
229
|
+
* @param {Element} node - The literal result element
|
|
230
|
+
* @returns {Array<[string, string]>} `[prefix, uri]` pairs, "" for the default namespace
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* resultNamespaceNodes(literalElement); // [["q", "urn:q"]]
|
|
234
|
+
*/
|
|
235
|
+
export function resultNamespaceNodes(node) {
|
|
236
|
+
const scope = inScopeNamespaces(node);
|
|
237
|
+
const excluded = excludedNamespaces(node);
|
|
238
|
+
|
|
239
|
+
let byExclusions = namespaceNodes.get(scope);
|
|
240
|
+
if (!byExclusions) {
|
|
241
|
+
byExclusions = new WeakMap();
|
|
242
|
+
namespaceNodes.set(scope, byExclusions);
|
|
243
|
+
}
|
|
244
|
+
let nodes = byExclusions.get(excluded);
|
|
245
|
+
if (!nodes) {
|
|
246
|
+
nodes = Object.entries(scope).filter(
|
|
247
|
+
([, uri]) => uri !== "" && !excluded.has(uri),
|
|
248
|
+
);
|
|
249
|
+
byExclusions.set(excluded, nodes);
|
|
250
|
+
}
|
|
251
|
+
return nodes;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Resolve a QName prefix against a scope.
|
|
256
|
+
*
|
|
257
|
+
* @param {Object<string, string>} scope - Bindings from {@link inScopeNamespaces}
|
|
258
|
+
* @param {string} prefix - The prefix, "" for the default namespace
|
|
259
|
+
* @returns {string|null} The namespace URI, or null when unbound
|
|
260
|
+
*/
|
|
261
|
+
export function resolvePrefix(scope, prefix) {
|
|
262
|
+
if (prefix === "xml") return XML_NAMESPACE;
|
|
263
|
+
return Object.hasOwn(scope, prefix) && scope[prefix] !== ""
|
|
264
|
+
? scope[prefix]
|
|
265
|
+
: null;
|
|
266
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Variable and parameter bindings (XSLT 1.0 section 11).
|
|
3
|
+
*
|
|
4
|
+
* Local bindings live in the `variables` and `parameters` objects of an
|
|
5
|
+
* XSLT context; the engine gives every template invocation empty ones and a
|
|
6
|
+
* new copy to every sequence constructor that declares a variable, which
|
|
7
|
+
* makes local variables lexically scoped. Global variables and parameters are
|
|
8
|
+
* kept in a {@link GlobalBindings} table shared by all contexts of one
|
|
9
|
+
* transformation and are evaluated lazily, so they may refer to each other in
|
|
10
|
+
* any order (section 11.4).
|
|
11
|
+
*
|
|
12
|
+
* @module xslt/variables
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
"use strict";
|
|
16
|
+
|
|
17
|
+
/** Evaluation states of a global binding. */
|
|
18
|
+
const PENDING = 0;
|
|
19
|
+
const EVALUATING = 1;
|
|
20
|
+
const DONE = 2;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The global variables and parameters of one transformation.
|
|
24
|
+
*/
|
|
25
|
+
export class GlobalBindings {
|
|
26
|
+
/**
|
|
27
|
+
* @param {(definition: object) => *} evaluate - Computes the value of a
|
|
28
|
+
* definition; called at most once per name
|
|
29
|
+
*/
|
|
30
|
+
constructor(evaluate) {
|
|
31
|
+
this.evaluate = evaluate;
|
|
32
|
+
this.entries = new Map();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Declare a global binding. A later declaration of the same name replaces
|
|
37
|
+
* an earlier one.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} name - The variable or parameter name
|
|
40
|
+
* @param {object} definition - Passed to the evaluate callback
|
|
41
|
+
* @returns {void}
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* globals.define('title', { select: "'Report'" });
|
|
45
|
+
*/
|
|
46
|
+
define(name, definition) {
|
|
47
|
+
this.entries.set(name, { definition, state: PENDING, value: undefined });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Whether a global binding with that name exists.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} name - The variable name
|
|
54
|
+
* @returns {boolean} True when declared
|
|
55
|
+
*/
|
|
56
|
+
has(name) {
|
|
57
|
+
return this.entries.has(name);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The value of a global binding, evaluating it on first use.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} name - The variable name
|
|
64
|
+
* @returns {*} The value, undefined when the name is not declared
|
|
65
|
+
* @throws {Error} When the definition (indirectly) refers to itself
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* globals.get('title'); // "Report"
|
|
69
|
+
*/
|
|
70
|
+
get(name) {
|
|
71
|
+
const entry = this.entries.get(name);
|
|
72
|
+
if (!entry) return undefined;
|
|
73
|
+
if (entry.state === DONE) return entry.value;
|
|
74
|
+
if (entry.state === EVALUATING) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Circular definition of global variable $${name} (XSLT 1.0 section 11.4)`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
entry.state = EVALUATING;
|
|
81
|
+
try {
|
|
82
|
+
entry.value = this.evaluate(entry.definition);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
entry.state = PENDING;
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
entry.state = DONE;
|
|
88
|
+
return entry.value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Evaluate every binding, in declaration order, so that errors and
|
|
93
|
+
* `xsl:message` side effects of unused globals still surface.
|
|
94
|
+
*
|
|
95
|
+
* @returns {void}
|
|
96
|
+
*/
|
|
97
|
+
evaluateAll() {
|
|
98
|
+
for (const name of this.entries.keys()) this.get(name);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Look a variable up in an XSLT context: local variables, then local
|
|
104
|
+
* parameters, then the globals.
|
|
105
|
+
*
|
|
106
|
+
* @param {{variables: object, parameters: object, globals?: GlobalBindings|null}} context - The XSLT context
|
|
107
|
+
* @param {string} name - The variable name
|
|
108
|
+
* @returns {{found: boolean, value: *}} Whether it is bound, and its value
|
|
109
|
+
*/
|
|
110
|
+
export function lookupVariable(context, name) {
|
|
111
|
+
if (Object.hasOwn(context.variables, name)) {
|
|
112
|
+
return { found: true, value: context.variables[name] };
|
|
113
|
+
}
|
|
114
|
+
if (Object.hasOwn(context.parameters, name)) {
|
|
115
|
+
return { found: true, value: context.parameters[name] };
|
|
116
|
+
}
|
|
117
|
+
const globals = context.globals;
|
|
118
|
+
if (globals?.has(name)) return { found: true, value: globals.get(name) };
|
|
119
|
+
return { found: false, value: undefined };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Read-only object view of the variables in scope of an XSLT context, in the
|
|
124
|
+
* shape the XPath evaluator expects (`hasOwnProperty` + property access).
|
|
125
|
+
* Nothing is copied: lookups read the context live, and globals are only
|
|
126
|
+
* evaluated when an expression actually refers to them.
|
|
127
|
+
*
|
|
128
|
+
* @param {object} context - The XSLT context
|
|
129
|
+
* @returns {object} A proxy mapping variable names to values
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* new XPathContext(node, 1, 1, createVariableView(xsltContext), namespaces);
|
|
133
|
+
*/
|
|
134
|
+
export function createVariableView(context) {
|
|
135
|
+
const binding = (name) =>
|
|
136
|
+
typeof name === "string" ? lookupVariable(context, name) : null;
|
|
137
|
+
|
|
138
|
+
return new Proxy(Object.create(null), {
|
|
139
|
+
has: (_target, name) => binding(name)?.found === true,
|
|
140
|
+
get: (_target, name) => binding(name)?.value,
|
|
141
|
+
getOwnPropertyDescriptor: (_target, name) => {
|
|
142
|
+
const found = binding(name);
|
|
143
|
+
if (!found?.found) return undefined;
|
|
144
|
+
return {
|
|
145
|
+
value: found.value,
|
|
146
|
+
writable: false,
|
|
147
|
+
enumerable: true,
|
|
148
|
+
configurable: true,
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|