@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
package/src/xslt/sort.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* xsl:sort (XSLT 1.0 section 10).
|
|
3
|
+
*
|
|
4
|
+
* Sort keys are computed once per node (decorate, sort, undecorate) instead of
|
|
5
|
+
* once per comparison, and the attributes `order`, `lang`, `data-type` and
|
|
6
|
+
* `case-order` are evaluated as attribute value templates.
|
|
7
|
+
*
|
|
8
|
+
* Text keys are compared by Unicode code point unless `lang` or `case-order`
|
|
9
|
+
* is given. That matches libxslt, the engine behind the native XSLTProcessor
|
|
10
|
+
* in Chromium browsers, so a stylesheet sorts the same way with this library
|
|
11
|
+
* as it did natively. With `lang` or `case-order` an `Intl.Collator` is used.
|
|
12
|
+
*
|
|
13
|
+
* @module xslt/sort
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Compare two strings by Unicode code point (not by UTF-16 code unit).
|
|
18
|
+
*
|
|
19
|
+
* @param {string} a - First string
|
|
20
|
+
* @param {string} b - Second string
|
|
21
|
+
* @returns {number} Negative, zero or positive
|
|
22
|
+
*/
|
|
23
|
+
export function compareCodePoints(a, b) {
|
|
24
|
+
const left = a[Symbol.iterator]();
|
|
25
|
+
const right = b[Symbol.iterator]();
|
|
26
|
+
|
|
27
|
+
for (;;) {
|
|
28
|
+
const x = left.next();
|
|
29
|
+
const y = right.next();
|
|
30
|
+
if (x.done || y.done) return Number(y.done) - Number(x.done) || 0;
|
|
31
|
+
const diff = x.value.codePointAt(0) - y.value.codePointAt(0);
|
|
32
|
+
if (diff !== 0) return diff;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Compare two numeric sort keys; NaN precedes every number (section 10).
|
|
38
|
+
*
|
|
39
|
+
* @param {number} a - First key
|
|
40
|
+
* @param {number} b - Second key
|
|
41
|
+
* @returns {number} Negative, zero or positive
|
|
42
|
+
*/
|
|
43
|
+
export function compareNumbers(a, b) {
|
|
44
|
+
const aNaN = Number.isNaN(a);
|
|
45
|
+
const bNaN = Number.isNaN(b);
|
|
46
|
+
if (aNaN || bNaN) return Number(bNaN) - Number(aNaN) || 0;
|
|
47
|
+
if (a === b) return 0;
|
|
48
|
+
return a < b ? -1 : 1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build the text comparator for one sort key.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} lang - Evaluated `lang` attribute (may be empty)
|
|
55
|
+
* @param {string} caseOrder - Evaluated `case-order` attribute (may be empty)
|
|
56
|
+
* @returns {(a: string, b: string) => number} The comparator
|
|
57
|
+
*/
|
|
58
|
+
function textComparator(lang, caseOrder) {
|
|
59
|
+
if (!lang && !caseOrder) return compareCodePoints;
|
|
60
|
+
|
|
61
|
+
let collator;
|
|
62
|
+
try {
|
|
63
|
+
collator = new Intl.Collator(lang || undefined, {
|
|
64
|
+
caseFirst: caseOrder === "lower-first" ? "lower" : "upper",
|
|
65
|
+
sensitivity: "variant",
|
|
66
|
+
});
|
|
67
|
+
} catch {
|
|
68
|
+
collator = new Intl.Collator(undefined, {
|
|
69
|
+
caseFirst: caseOrder === "lower-first" ? "lower" : "upper",
|
|
70
|
+
sensitivity: "variant",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return collator.compare;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @typedef {object} SortHost
|
|
78
|
+
* @property {(expr: string, context: object) => *} evaluate - Evaluate an XPath expression
|
|
79
|
+
* @property {(value: string, context: object) => string} avt - Evaluate an attribute value template
|
|
80
|
+
* @property {(value: *) => string} toString - XPath string() conversion
|
|
81
|
+
* @property {(value: *) => number} toNumber - XPath number() conversion
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Resolve one xsl:sort element into a key specification.
|
|
86
|
+
*
|
|
87
|
+
* @param {Element} element - The xsl:sort element
|
|
88
|
+
* @param {object} context - Context of the sorting instruction (for AVTs)
|
|
89
|
+
* @param {SortHost} host - Engine callbacks
|
|
90
|
+
* @returns {{select: string, descending: boolean, numeric: boolean, compare: Function}} Specification
|
|
91
|
+
*/
|
|
92
|
+
export function resolveSortSpec(element, context, host) {
|
|
93
|
+
const attribute = (name) => {
|
|
94
|
+
const raw = element.getAttribute(name);
|
|
95
|
+
return raw ? host.avt(raw, context).trim() : "";
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const dataType = attribute("data-type");
|
|
99
|
+
const numeric = dataType === "number";
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
select: element.getAttribute("select") || ".",
|
|
103
|
+
descending: attribute("order") === "descending",
|
|
104
|
+
numeric,
|
|
105
|
+
compare: numeric
|
|
106
|
+
? compareNumbers
|
|
107
|
+
: textComparator(attribute("lang"), attribute("case-order")),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Sort a node list by the given xsl:sort elements.
|
|
113
|
+
*
|
|
114
|
+
* Each key is evaluated with the node as current node, the unsorted list as
|
|
115
|
+
* current node list and the node's position in that list, as the
|
|
116
|
+
* specification requires. Ties keep the original order.
|
|
117
|
+
*
|
|
118
|
+
* @param {Node[]} nodes - Nodes in their original (document) order
|
|
119
|
+
* @param {Element[]} sortElements - The xsl:sort children, in order
|
|
120
|
+
* @param {object} context - XSLT context of the sorting instruction
|
|
121
|
+
* @param {SortHost} host - Engine callbacks
|
|
122
|
+
* @returns {Node[]} A new, sorted array
|
|
123
|
+
*/
|
|
124
|
+
export function sortNodes(nodes, sortElements, context, host) {
|
|
125
|
+
if (sortElements.length === 0 || nodes.length < 2) return [...nodes];
|
|
126
|
+
|
|
127
|
+
const specs = sortElements.map((el) => resolveSortSpec(el, context, host));
|
|
128
|
+
|
|
129
|
+
const rows = nodes.map((node, index) => {
|
|
130
|
+
const nodeContext = context.clone({
|
|
131
|
+
currentNode: node,
|
|
132
|
+
currentNodeList: nodes,
|
|
133
|
+
position: index + 1,
|
|
134
|
+
});
|
|
135
|
+
const keys = specs.map((spec) => {
|
|
136
|
+
const text = host.toString(host.evaluate(spec.select, nodeContext));
|
|
137
|
+
return spec.numeric ? host.toNumber(text) : text;
|
|
138
|
+
});
|
|
139
|
+
return { node, index, keys };
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
rows.sort((a, b) => {
|
|
143
|
+
for (let k = 0; k < specs.length; k++) {
|
|
144
|
+
const cmp = specs[k].compare(a.keys[k], b.keys[k]);
|
|
145
|
+
if (cmp !== 0) return specs[k].descending ? -cmp : cmp;
|
|
146
|
+
}
|
|
147
|
+
return a.index - b.index;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return rows.map((row) => row.node);
|
|
151
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Name tests of `xsl:strip-space` and `xsl:preserve-space` (XSLT 1.0
|
|
3
|
+
* section 3.4).
|
|
4
|
+
*
|
|
5
|
+
* The `elements` attribute lists XPath name tests: `*`, `prefix:*` or a
|
|
6
|
+
* QName. The prefixes are expanded with the namespace declarations in scope
|
|
7
|
+
* on the declaring element, and an unprefixed name only matches elements in
|
|
8
|
+
* no namespace (XPath 1.0 section 2.3), so `p:*` matches by namespace URI
|
|
9
|
+
* whatever prefix the source document uses. Name tests given as plain
|
|
10
|
+
* strings (the {@link WhitespaceFilter} API before 1.2.0) are still matched
|
|
11
|
+
* by their lexical name.
|
|
12
|
+
*
|
|
13
|
+
* @module xslt/spaceNameTests
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
"use strict";
|
|
17
|
+
|
|
18
|
+
import { isQName } from "./qname.js";
|
|
19
|
+
import { splitQName } from "./resultNamespaces.js";
|
|
20
|
+
import { inScopeNamespaces, resolvePrefix } from "./stylesheetNamespaces.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} ElementNameTest
|
|
24
|
+
* @property {string|null|undefined} namespaceUri - The namespace to match,
|
|
25
|
+
* null for no namespace, undefined for any (the `*` test)
|
|
26
|
+
* @property {string} localName - The local name, `*` for any
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Expand the name tests of an `elements` attribute.
|
|
31
|
+
*
|
|
32
|
+
* @param {string|null} value - Whitespace separated name tests
|
|
33
|
+
* @param {Element} element - The xsl:strip-space or xsl:preserve-space element
|
|
34
|
+
* @param {(message: string) => void} warn - Reports a skipped name test
|
|
35
|
+
* @returns {ElementNameTest[]} The expanded name tests
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* // <xsl:strip-space xmlns:m="urn:m" elements="m:* p *"/>
|
|
39
|
+
* compileSpaceNameTests("m:* p *", element, console.warn);
|
|
40
|
+
* // [{ namespaceUri: "urn:m", localName: "*" },
|
|
41
|
+
* // { namespaceUri: null, localName: "p" },
|
|
42
|
+
* // { namespaceUri: undefined, localName: "*" }]
|
|
43
|
+
*/
|
|
44
|
+
export function compileSpaceNameTests(value, element, warn) {
|
|
45
|
+
const tests = [];
|
|
46
|
+
for (const token of (value ?? "").split(/[ \t\r\n]+/).filter(Boolean)) {
|
|
47
|
+
if (token === "*") {
|
|
48
|
+
tests.push({ namespaceUri: undefined, localName: "*" });
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const { prefix, localName } = splitQName(token);
|
|
52
|
+
const qname = localName === "*" ? `${prefix}:x` : token;
|
|
53
|
+
const namespaceUri = prefix
|
|
54
|
+
? resolvePrefix(inScopeNamespaces(element), prefix)
|
|
55
|
+
: null;
|
|
56
|
+
if (!isQName(qname) || (prefix && !namespaceUri)) {
|
|
57
|
+
warn(
|
|
58
|
+
`xsl:${element.localName} elements: "${token}" is not a name test with a declared prefix and is ignored`,
|
|
59
|
+
);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
tests.push({ namespaceUri, localName });
|
|
63
|
+
}
|
|
64
|
+
return tests;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The XSLT default priority of an element name test (section 5.5).
|
|
69
|
+
*
|
|
70
|
+
* @param {string|ElementNameTest} nameTest - A name test
|
|
71
|
+
* @returns {number} -0.5 for `*`, -0.25 for `prefix:*`, 0 for a name
|
|
72
|
+
*/
|
|
73
|
+
export function nameTestPriority(nameTest) {
|
|
74
|
+
if (typeof nameTest === "string") {
|
|
75
|
+
if (nameTest === "*") return -0.5;
|
|
76
|
+
return nameTest.endsWith(":*") ? -0.25 : 0;
|
|
77
|
+
}
|
|
78
|
+
if (nameTest.namespaceUri === undefined) return -0.5;
|
|
79
|
+
return nameTest.localName === "*" ? -0.25 : 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Whether an element matches a lexical name test (the string API).
|
|
84
|
+
*
|
|
85
|
+
* @param {Element} element - The element to test
|
|
86
|
+
* @param {string} nameTest - `*`, `prefix:*` or a name
|
|
87
|
+
* @returns {boolean} True when the element matches
|
|
88
|
+
*/
|
|
89
|
+
function matchesLexicalTest(element, nameTest) {
|
|
90
|
+
if (nameTest === "*") return true;
|
|
91
|
+
if (nameTest.endsWith(":*")) {
|
|
92
|
+
return element.nodeName.startsWith(`${nameTest.slice(0, -2)}:`);
|
|
93
|
+
}
|
|
94
|
+
return element.nodeName === nameTest || element.localName === nameTest;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Whether an element matches a name test.
|
|
99
|
+
*
|
|
100
|
+
* @param {Element} element - The element to test
|
|
101
|
+
* @param {string|ElementNameTest} nameTest - A name test
|
|
102
|
+
* @returns {boolean} True when the element matches
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* matchesNameTest(pElement, { namespaceUri: null, localName: "p" }); // true
|
|
106
|
+
*/
|
|
107
|
+
export function matchesNameTest(element, nameTest) {
|
|
108
|
+
if (typeof nameTest === "string") {
|
|
109
|
+
return matchesLexicalTest(element, nameTest);
|
|
110
|
+
}
|
|
111
|
+
const { namespaceUri, localName } = nameTest;
|
|
112
|
+
if (namespaceUri === undefined) return true;
|
|
113
|
+
if ((element.namespaceURI || null) !== namespaceUri) return false;
|
|
114
|
+
return localName === "*" || element.localName === localName;
|
|
115
|
+
}
|
|
@@ -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
|
+
}
|