@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,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
+ }