@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
package/src/xslt/number.js
CHANGED
|
@@ -1,178 +1,271 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `xsl:number` counting
|
|
2
|
+
* `xsl:number` counting (XSLT 1.0 section 7.7).
|
|
3
3
|
*
|
|
4
4
|
* Counting is kept independent from the engine: callers pass a `matcher`
|
|
5
5
|
* callback that answers "does this node match this XSLT pattern", which keeps
|
|
6
|
-
* this module free of
|
|
6
|
+
* this module free of the XPath evaluator and easy to test in isolation.
|
|
7
|
+
*
|
|
8
|
+
* Numbering every node of a long list would be quadratic if each call counted
|
|
9
|
+
* from scratch, so a call may be given a memo (one per instruction and
|
|
10
|
+
* transformation, see {@link isMemoizable}) remembering the numbers already
|
|
11
|
+
* computed: a later call stops at the nearest numbered node. The tree is
|
|
12
|
+
* walked with previousSibling/lastChild/parentNode only.
|
|
7
13
|
*/
|
|
8
14
|
|
|
9
15
|
"use strict";
|
|
10
16
|
|
|
17
|
+
import { isParserArtifact } from "../xpath/axes.js";
|
|
18
|
+
|
|
11
19
|
/** Node types that participate in `xsl:number` counting. */
|
|
12
20
|
const COUNTABLE_NODE_TYPES = new Set([1, 3, 4, 7, 8]);
|
|
13
21
|
|
|
14
22
|
/**
|
|
15
|
-
*
|
|
23
|
+
* Whether a node takes part in counting: an element, text, processing
|
|
24
|
+
* instruction or comment of the data model (not a parser artifact, see
|
|
25
|
+
* axes.js).
|
|
26
|
+
*
|
|
27
|
+
* @param {Node} node - Any node
|
|
28
|
+
* @returns {boolean} True for countable nodes
|
|
29
|
+
*/
|
|
30
|
+
function isCountable(node) {
|
|
31
|
+
return COUNTABLE_NODE_TYPES.has(node.nodeType) && !isParserArtifact(node);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The XPath node kind of a DOM node: CDATA sections are text nodes.
|
|
36
|
+
*
|
|
37
|
+
* @param {Node} node - Any node
|
|
38
|
+
* @returns {number} The DOM node type, 3 for CDATA sections
|
|
39
|
+
*/
|
|
40
|
+
function nodeKind(node) {
|
|
41
|
+
return node.nodeType === 4 ? 3 : node.nodeType;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Default `count` pattern: nodes of the same kind as the numbered node and,
|
|
46
|
+
* for elements and attributes, the same expanded name; for processing
|
|
47
|
+
* instructions, the same target.
|
|
16
48
|
*
|
|
17
49
|
* @param {Node} candidate - The node being considered
|
|
18
50
|
* @param {Node} node - The node `xsl:number` is numbering
|
|
19
|
-
* @returns {boolean} True when the candidate is
|
|
51
|
+
* @returns {boolean} True when the candidate is counted
|
|
20
52
|
*/
|
|
21
53
|
function matchesDefaultCount(candidate, node) {
|
|
22
|
-
if (candidate
|
|
23
|
-
|
|
24
|
-
|
|
54
|
+
if (nodeKind(candidate) !== nodeKind(node)) return false;
|
|
55
|
+
switch (node.nodeType) {
|
|
56
|
+
case 1:
|
|
57
|
+
case 2:
|
|
58
|
+
return (
|
|
59
|
+
candidate.localName === node.localName &&
|
|
60
|
+
(candidate.namespaceURI ?? null) === (node.namespaceURI ?? null)
|
|
61
|
+
);
|
|
62
|
+
case 7:
|
|
63
|
+
return candidate.target === node.target;
|
|
64
|
+
default:
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
25
67
|
}
|
|
26
68
|
|
|
27
69
|
/**
|
|
28
|
-
*
|
|
70
|
+
* Key telling apart the node kinds the default `count` pattern selects, so
|
|
71
|
+
* memoized numbers are only reused for the same kind.
|
|
29
72
|
*
|
|
30
|
-
* @param {Node} node - The node
|
|
31
|
-
* @
|
|
32
|
-
* @param {(node: Node, pattern: string) => boolean} matcher - Pattern matcher
|
|
33
|
-
* @returns {(candidate: Node) => boolean} The predicate
|
|
73
|
+
* @param {Node} node - The numbered node
|
|
74
|
+
* @returns {string} e.g. `1|urn:x|item`
|
|
34
75
|
*/
|
|
35
|
-
function
|
|
36
|
-
if (
|
|
37
|
-
|
|
76
|
+
function defaultCountKey(node) {
|
|
77
|
+
if (node.nodeType === 7) return `7|${node.target}`;
|
|
78
|
+
if (node.nodeType === 1 || node.nodeType === 2) {
|
|
79
|
+
return `${node.nodeType}|${node.namespaceURI ?? ""}|${node.localName}`;
|
|
80
|
+
}
|
|
81
|
+
return String(nodeKind(node));
|
|
38
82
|
}
|
|
39
83
|
|
|
40
84
|
/**
|
|
41
|
-
*
|
|
85
|
+
* Whether the numbers of an instruction may be memoized: its patterns must
|
|
86
|
+
* not depend on variables or on the current node, whose values may differ
|
|
87
|
+
* between invocations of the same instruction.
|
|
42
88
|
*
|
|
43
|
-
* @param {string|null}
|
|
44
|
-
* @param {
|
|
45
|
-
* @returns {
|
|
89
|
+
* @param {string|null} count - The `count` pattern
|
|
90
|
+
* @param {string|null} from - The `from` pattern
|
|
91
|
+
* @returns {boolean} True when counting only depends on the source tree
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* isMemoizable("item", null); // true
|
|
95
|
+
* isMemoizable("item[@k=$k]", null); // false
|
|
46
96
|
*/
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
return (candidate) => matcher(candidate, from);
|
|
97
|
+
export function isMemoizable(count, from) {
|
|
98
|
+
return !/\$|current\s*\(/.test(`${count ?? ""} ${from ?? ""}`);
|
|
50
99
|
}
|
|
51
100
|
|
|
52
101
|
/**
|
|
53
|
-
* Count preceding siblings
|
|
102
|
+
* Count a node's preceding siblings satisfying the predicate, stopping at the
|
|
103
|
+
* nearest one whose position is memoized.
|
|
54
104
|
*
|
|
55
|
-
* @param {Node} node - The node whose position is computed
|
|
105
|
+
* @param {Node} node - The (counted) node whose position is computed
|
|
56
106
|
* @param {(candidate: Node) => boolean} isCounted - Counting predicate
|
|
107
|
+
* @param {WeakMap<Node, number>|null} positions - Memoized positions
|
|
57
108
|
* @returns {number} The 1-based position
|
|
58
109
|
*/
|
|
59
|
-
function siblingPosition(node, isCounted) {
|
|
110
|
+
function siblingPosition(node, isCounted, positions) {
|
|
111
|
+
const own = positions?.get(node);
|
|
112
|
+
if (own !== undefined) return own;
|
|
113
|
+
|
|
60
114
|
let position = 1;
|
|
61
|
-
let sibling = node.previousSibling;
|
|
62
|
-
|
|
63
|
-
if (
|
|
115
|
+
for (let sibling = node.previousSibling; sibling;) {
|
|
116
|
+
const known = positions?.get(sibling);
|
|
117
|
+
if (known !== undefined) {
|
|
118
|
+
position += known;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
if (isCountable(sibling) && isCounted(sibling)) {
|
|
64
122
|
position++;
|
|
65
123
|
}
|
|
66
124
|
sibling = sibling.previousSibling;
|
|
67
125
|
}
|
|
126
|
+
positions?.set(node, position);
|
|
68
127
|
return position;
|
|
69
128
|
}
|
|
70
129
|
|
|
71
130
|
/**
|
|
72
|
-
*
|
|
131
|
+
* Whether a node hangs off an element without being its child: an
|
|
132
|
+
* attribute or a namespace node, whose parent is `ownerElement`.
|
|
73
133
|
*
|
|
74
|
-
* @param {Node}
|
|
75
|
-
* @returns {
|
|
134
|
+
* @param {Node} node - Any node
|
|
135
|
+
* @returns {boolean} True for attribute and namespace nodes
|
|
76
136
|
*/
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
const result = [];
|
|
80
|
-
const stack = [root];
|
|
81
|
-
|
|
82
|
-
while (stack.length > 0) {
|
|
83
|
-
const current = stack.pop();
|
|
84
|
-
result.push(current);
|
|
85
|
-
if (current === target) break;
|
|
86
|
-
const children = current.childNodes;
|
|
87
|
-
if (children) {
|
|
88
|
-
for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return result;
|
|
137
|
+
function isAttachedNode(node) {
|
|
138
|
+
return node.nodeType === 2 || node.nodeType === 13;
|
|
93
139
|
}
|
|
94
140
|
|
|
95
141
|
/**
|
|
96
|
-
*
|
|
142
|
+
* The node before another one in document order (its preceding node or its
|
|
143
|
+
* parent).
|
|
97
144
|
*
|
|
98
|
-
* @param {Node} node -
|
|
99
|
-
* @
|
|
100
|
-
* @param {(candidate: Node) => boolean} isFrom - Boundary predicate
|
|
101
|
-
* @returns {number[]} A single number, or an empty list when nothing matches
|
|
145
|
+
* @param {Node} node - A child node
|
|
146
|
+
* @returns {Node|null} The previous node, null at the root
|
|
102
147
|
*/
|
|
103
|
-
function
|
|
104
|
-
let
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
current = current.parentNode;
|
|
109
|
-
}
|
|
110
|
-
return [];
|
|
148
|
+
function previousInDocumentOrder(node) {
|
|
149
|
+
let previous = node.previousSibling;
|
|
150
|
+
if (!previous) return node.parentNode;
|
|
151
|
+
while (previous.lastChild) previous = previous.lastChild;
|
|
152
|
+
return previous;
|
|
111
153
|
}
|
|
112
154
|
|
|
113
155
|
/**
|
|
114
|
-
* Count a node according to `level="
|
|
156
|
+
* Count the ancestors-or-self of a node according to `level="single"` or
|
|
157
|
+
* `level="multiple"`.
|
|
115
158
|
*
|
|
116
159
|
* @param {Node} node - The node being numbered
|
|
160
|
+
* @param {boolean} multiple - Whether every counted ancestor is numbered
|
|
117
161
|
* @param {(candidate: Node) => boolean} isCounted - Counting predicate
|
|
118
162
|
* @param {(candidate: Node) => boolean} isFrom - Boundary predicate
|
|
163
|
+
* @param {WeakMap<Node, number>|null} positions - Memoized positions
|
|
119
164
|
* @returns {number[]} Numbers from the outermost ancestor inwards
|
|
120
165
|
*/
|
|
121
|
-
function
|
|
166
|
+
function countAncestors(node, multiple, isCounted, isFrom, positions) {
|
|
122
167
|
const numbers = [];
|
|
123
168
|
let current = node;
|
|
124
169
|
|
|
125
170
|
while (current && current.nodeType !== 9) {
|
|
126
171
|
if (isFrom(current)) break;
|
|
127
172
|
if (isCounted(current)) {
|
|
128
|
-
numbers.unshift(siblingPosition(current, isCounted));
|
|
173
|
+
numbers.unshift(siblingPosition(current, isCounted, positions));
|
|
174
|
+
if (!multiple) break;
|
|
129
175
|
}
|
|
130
|
-
current = current
|
|
176
|
+
current = isAttachedNode(current)
|
|
177
|
+
? current.ownerElement
|
|
178
|
+
: current.parentNode;
|
|
131
179
|
}
|
|
132
180
|
|
|
133
181
|
return numbers;
|
|
134
182
|
}
|
|
135
183
|
|
|
136
184
|
/**
|
|
137
|
-
* Count a node according to `level="any"
|
|
185
|
+
* Count a node according to `level="any"`: walk backwards in document order
|
|
186
|
+
* until a `from` node, the root, or a node whose total is memoized. An
|
|
187
|
+
* attribute or namespace node counts itself, then its element and the nodes
|
|
188
|
+
* before it: other attributes are neither preceding nor ancestor nodes (as
|
|
189
|
+
* in libxslt).
|
|
138
190
|
*
|
|
139
191
|
* @param {Node} node - The node being numbered
|
|
140
192
|
* @param {(candidate: Node) => boolean} isCounted - Counting predicate
|
|
141
193
|
* @param {(candidate: Node) => boolean} isFrom - Boundary predicate
|
|
194
|
+
* @param {WeakMap<Node, number>|null} totals - Memoized totals
|
|
142
195
|
* @returns {number[]} A single number, or an empty list when nothing matches
|
|
143
196
|
*/
|
|
144
|
-
function countAny(node, isCounted, isFrom) {
|
|
197
|
+
function countAny(node, isCounted, isFrom, totals) {
|
|
145
198
|
let total = 0;
|
|
199
|
+
let current = node;
|
|
200
|
+
if (isAttachedNode(node)) {
|
|
201
|
+
if (isFrom(node)) return [];
|
|
202
|
+
if (isCounted(node)) total++;
|
|
203
|
+
current = node.ownerElement;
|
|
204
|
+
}
|
|
146
205
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (
|
|
150
|
-
total
|
|
151
|
-
|
|
206
|
+
while (current && current.nodeType !== 9) {
|
|
207
|
+
const known = totals?.get(current);
|
|
208
|
+
if (known !== undefined) {
|
|
209
|
+
total += known;
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
if (isCountable(current)) {
|
|
213
|
+
if (isFrom(current)) break;
|
|
214
|
+
if (isCounted(current)) total++;
|
|
152
215
|
}
|
|
153
|
-
|
|
216
|
+
current = previousInDocumentOrder(current);
|
|
154
217
|
}
|
|
155
218
|
|
|
219
|
+
totals?.set(node, total);
|
|
156
220
|
return total > 0 ? [total] : [];
|
|
157
221
|
}
|
|
158
222
|
|
|
223
|
+
/**
|
|
224
|
+
* The memo tables of one kind of counted node.
|
|
225
|
+
*
|
|
226
|
+
* @param {Map<string, {positions: WeakMap, totals: WeakMap}>|null} memo - The instruction's memo
|
|
227
|
+
* @param {string} key - The counted kind
|
|
228
|
+
* @returns {{positions: WeakMap, totals: WeakMap}|null} The tables, null without memo
|
|
229
|
+
*/
|
|
230
|
+
function memoTables(memo, key) {
|
|
231
|
+
if (!memo) return null;
|
|
232
|
+
let tables = memo.get(key);
|
|
233
|
+
if (!tables) {
|
|
234
|
+
tables = { positions: new WeakMap(), totals: new WeakMap() };
|
|
235
|
+
memo.set(key, tables);
|
|
236
|
+
}
|
|
237
|
+
return tables;
|
|
238
|
+
}
|
|
239
|
+
|
|
159
240
|
/**
|
|
160
241
|
* Compute the number sequence for an `xsl:number` instruction.
|
|
161
242
|
*
|
|
162
243
|
* @param {Node} node - The current node
|
|
163
244
|
* @param {{level?: string, count?: string|null, from?: string|null}} options - Instruction attributes
|
|
164
245
|
* @param {(node: Node, pattern: string) => boolean} matcher - XSLT pattern matcher
|
|
246
|
+
* @param {Map|null} [memo] - Memo of the instruction for the current
|
|
247
|
+
* transformation (a Map owned by the caller), or null to count from scratch
|
|
165
248
|
* @returns {number[]} The computed numbers, outermost first
|
|
166
249
|
*
|
|
167
250
|
* @example
|
|
168
251
|
* countXsltNumber(item, { level: 'any' }, matcher); // [2]
|
|
169
252
|
*/
|
|
170
|
-
export function countXsltNumber(node, options, matcher) {
|
|
253
|
+
export function countXsltNumber(node, options, matcher, memo = null) {
|
|
171
254
|
const { level = "single", count = null, from = null } = options;
|
|
172
|
-
const isCounted =
|
|
173
|
-
|
|
255
|
+
const isCounted = count
|
|
256
|
+
? (candidate) => matcher(candidate, count)
|
|
257
|
+
: (candidate) => matchesDefaultCount(candidate, node);
|
|
258
|
+
const isFrom = from ? (candidate) => matcher(candidate, from) : () => false;
|
|
259
|
+
const tables = memoTables(memo, count ? "" : defaultCountKey(node));
|
|
174
260
|
|
|
175
|
-
if (level === "any")
|
|
176
|
-
|
|
177
|
-
|
|
261
|
+
if (level === "any") {
|
|
262
|
+
return countAny(node, isCounted, isFrom, tables?.totals ?? null);
|
|
263
|
+
}
|
|
264
|
+
return countAncestors(
|
|
265
|
+
node,
|
|
266
|
+
level === "multiple",
|
|
267
|
+
isCounted,
|
|
268
|
+
isFrom,
|
|
269
|
+
tables?.positions ?? null,
|
|
270
|
+
);
|
|
178
271
|
}
|
package/src/xslt/numberFormat.js
CHANGED
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
* `xsl:number` number-to-string conversion.
|
|
3
3
|
*
|
|
4
4
|
* Renders the number sequence produced by {@link countXsltNumber} using the
|
|
5
|
-
* `format` attribute of `xsl:number`: numeric tokens (`1`, `01
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* `format` attribute of `xsl:number`: numeric tokens (`1`, `01`, and the
|
|
6
|
+
* same in any Unicode digit family, e.g. `٠١`), alphabetic tokens (`a`, `A`)
|
|
7
|
+
* and Roman numerals (`i`, `I`), together with the prefix, separators and
|
|
8
|
+
* suffix taken from the format string itself.
|
|
9
|
+
*
|
|
10
|
+
* Extreme values follow libxslt: a negative number is formatted as 0, NaN
|
|
11
|
+
* and Infinity as by `string()`, alphabetic and Roman tokens use decimals
|
|
12
|
+
* below 1 (and Roman ones above 5000), and every conversion takes O(log n)
|
|
13
|
+
* steps, so a huge value cannot stall the transformation.
|
|
8
14
|
*/
|
|
9
15
|
|
|
10
16
|
"use strict";
|
|
@@ -46,6 +52,9 @@ const ROMAN_NUMERALS = Object.freeze([
|
|
|
46
52
|
["I", 1],
|
|
47
53
|
]);
|
|
48
54
|
|
|
55
|
+
/** Largest number written with Roman numerals, as in libxslt. */
|
|
56
|
+
const MAX_ROMAN = 5000;
|
|
57
|
+
|
|
49
58
|
/**
|
|
50
59
|
* Convert a positive integer to a Roman numeral.
|
|
51
60
|
*
|
|
@@ -60,41 +69,124 @@ export function toRoman(value) {
|
|
|
60
69
|
let result = "";
|
|
61
70
|
|
|
62
71
|
for (const [numeral, amount] of ROMAN_NUMERALS) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
remaining -= amount;
|
|
66
|
-
}
|
|
72
|
+
result += numeral.repeat(Math.floor(remaining / amount));
|
|
73
|
+
remaining %= amount;
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
return result;
|
|
70
77
|
}
|
|
71
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Insert a grouping separator every `size` digits, counting from the right.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} digits - The decimal digits
|
|
83
|
+
* @param {{separator?: string, size?: number}} grouping - The grouping settings
|
|
84
|
+
* @returns {string} The grouped digits
|
|
85
|
+
*/
|
|
86
|
+
function groupDigits(digits, { separator, size }) {
|
|
87
|
+
if (!separator || Number.isNaN(size) || size <= 0) return digits;
|
|
88
|
+
|
|
89
|
+
let result = "";
|
|
90
|
+
for (let end = digits.length; end > 0; end -= size) {
|
|
91
|
+
const group = digits.slice(Math.max(0, end - size), end);
|
|
92
|
+
result = result ? `${group}${separator}${result}` : group;
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A Unicode decimal digit (general category Nd). */
|
|
98
|
+
const DECIMAL_DIGIT = /^\p{Nd}$/u;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Whether a code point is a Unicode decimal digit.
|
|
102
|
+
*
|
|
103
|
+
* @param {number} codePoint - Any code point
|
|
104
|
+
* @returns {boolean} True for characters of category Nd
|
|
105
|
+
*/
|
|
106
|
+
function isDecimalDigit(codePoint) {
|
|
107
|
+
return DECIMAL_DIGIT.test(String.fromCodePoint(codePoint));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The zero of the digit family of a decimal format token: a token whose
|
|
112
|
+
* last character has the digit value 1 and whose other characters are the
|
|
113
|
+
* zero of that family (XSLT 1.0 section 7.7.1), e.g. `1`, `01`, `٠١`.
|
|
114
|
+
* Digit families are runs of ten code points, which may follow each other
|
|
115
|
+
* (the mathematical digits), so the value is counted from the run start.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} token - A format token
|
|
118
|
+
* @returns {number|null} The code point of the family's zero, or null
|
|
119
|
+
*/
|
|
120
|
+
function decimalTokenZero(token) {
|
|
121
|
+
const digits = Array.from(token, (char) => char.codePointAt(0));
|
|
122
|
+
const one = digits.at(-1);
|
|
123
|
+
if (!isDecimalDigit(one)) return null;
|
|
124
|
+
let start = one;
|
|
125
|
+
while (isDecimalDigit(start - 1)) start--;
|
|
126
|
+
const zero = one - 1;
|
|
127
|
+
if ((one - start) % 10 !== 1) return null;
|
|
128
|
+
return digits.slice(0, -1).every((digit) => digit === zero) ? zero : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The decimal digits of a non-negative integer, without exponent notation.
|
|
133
|
+
*
|
|
134
|
+
* @param {number} value - A finite, non-negative integer
|
|
135
|
+
* @returns {string} Its ASCII decimal digits
|
|
136
|
+
*/
|
|
137
|
+
function decimalDigits(value) {
|
|
138
|
+
return Number.isSafeInteger(value) ? String(value) : BigInt(value).toString();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Write a number with decimal digits of a family, padded with zeros to a
|
|
143
|
+
* minimum width and grouped.
|
|
144
|
+
*
|
|
145
|
+
* @param {number} value - A finite, non-negative integer
|
|
146
|
+
* @param {number} zero - Code point of the family's zero
|
|
147
|
+
* @param {number} width - Minimum number of digits
|
|
148
|
+
* @param {{separator?: string, size?: number}} grouping - Digit grouping
|
|
149
|
+
* @returns {string} The rendered number
|
|
150
|
+
*/
|
|
151
|
+
function formatDecimal(value, zero, width, grouping) {
|
|
152
|
+
const ascii = decimalDigits(value).padStart(width, "0");
|
|
153
|
+
const digits =
|
|
154
|
+
zero === 0x30
|
|
155
|
+
? ascii
|
|
156
|
+
: Array.from(ascii, (digit) =>
|
|
157
|
+
String.fromCodePoint(zero + Number(digit)),
|
|
158
|
+
).join("");
|
|
159
|
+
return groupDigits(digits, grouping);
|
|
160
|
+
}
|
|
161
|
+
|
|
72
162
|
/**
|
|
73
163
|
* Render one number with a single `xsl:number` format token.
|
|
74
164
|
*
|
|
75
165
|
* @param {number} value - The number to render
|
|
76
166
|
* @param {string} token - The format token, e.g. `1`, `01`, `a`, `I`
|
|
167
|
+
* @param {{separator?: string, size?: number}} grouping - Digit grouping
|
|
77
168
|
* @returns {string} The rendered number
|
|
78
169
|
*/
|
|
79
|
-
function formatToken(value, token) {
|
|
170
|
+
function formatToken(value, token, grouping) {
|
|
171
|
+
if (Number.isNaN(value) || value === Infinity) return String(value);
|
|
172
|
+
// Negative numbers are an error that libxslt recovers from with 0
|
|
173
|
+
const number = value < 0 ? 0 : Math.round(value);
|
|
174
|
+
|
|
80
175
|
if (/^\d+$/.test(token)) {
|
|
81
|
-
return
|
|
176
|
+
return formatDecimal(number, 0x30, token.length, grouping);
|
|
177
|
+
}
|
|
178
|
+
const zero = decimalTokenZero(token);
|
|
179
|
+
if (zero !== null) {
|
|
180
|
+
return formatDecimal(number, zero, Array.from(token).length, grouping);
|
|
82
181
|
}
|
|
83
182
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return toAlphabetic(value, false);
|
|
89
|
-
case "A":
|
|
90
|
-
return toAlphabetic(value, true);
|
|
91
|
-
case "i":
|
|
92
|
-
return toRoman(value).toLowerCase();
|
|
93
|
-
case "I":
|
|
94
|
-
return toRoman(value);
|
|
95
|
-
default:
|
|
96
|
-
return String(value);
|
|
183
|
+
const alphabetic = token === "a" || token === "A";
|
|
184
|
+
const roman = token === "i" || token === "I";
|
|
185
|
+
if (number < 1 || (roman && number > MAX_ROMAN) || (!alphabetic && !roman)) {
|
|
186
|
+
return decimalDigits(number);
|
|
97
187
|
}
|
|
188
|
+
if (alphabetic) return toAlphabetic(number, token === "A");
|
|
189
|
+
return token === "I" ? toRoman(number) : toRoman(number).toLowerCase();
|
|
98
190
|
}
|
|
99
191
|
|
|
100
192
|
/**
|
|
@@ -104,8 +196,8 @@ function formatToken(value, token) {
|
|
|
104
196
|
* @returns {{prefix: string, suffix: string, tokens: string[], separators: string[]}} The parsed format
|
|
105
197
|
*/
|
|
106
198
|
function parseFormat(format) {
|
|
107
|
-
const parts = format.match(/[
|
|
108
|
-
const isToken = (part) => /^[
|
|
199
|
+
const parts = format.match(/[\p{L}\p{N}]+|[^\p{L}\p{N}]+/gu) || [];
|
|
200
|
+
const isToken = (part) => /^[\p{L}\p{N}]+$/u.test(part);
|
|
109
201
|
|
|
110
202
|
const tokens = [];
|
|
111
203
|
const separators = [];
|
|
@@ -130,14 +222,20 @@ function parseFormat(format) {
|
|
|
130
222
|
/**
|
|
131
223
|
* Format a number sequence produced by {@link countXsltNumber}.
|
|
132
224
|
*
|
|
225
|
+
* Decimal tokens are grouped when both `grouping.separator` and a positive
|
|
226
|
+
* `grouping.size` are given (the `grouping-separator` and `grouping-size`
|
|
227
|
+
* attributes).
|
|
228
|
+
*
|
|
133
229
|
* @param {number[]} numbers - The numbers, outermost first
|
|
134
230
|
* @param {string} [format] - The `format` attribute value
|
|
231
|
+
* @param {{separator?: string, size?: number}} [grouping] - Digit grouping
|
|
135
232
|
* @returns {string} The formatted string, empty when there is nothing to number
|
|
136
233
|
*
|
|
137
234
|
* @example
|
|
138
235
|
* formatXsltNumber([2, 3], '1.1'); // '2.3'
|
|
236
|
+
* formatXsltNumber([1234567], '1', { separator: ',', size: 3 }); // '1,234,567'
|
|
139
237
|
*/
|
|
140
|
-
export function formatXsltNumber(numbers, format = "1") {
|
|
238
|
+
export function formatXsltNumber(numbers, format = "1", grouping = {}) {
|
|
141
239
|
if (numbers.length === 0) return "";
|
|
142
240
|
|
|
143
241
|
const { prefix, suffix, tokens, separators } = parseFormat(format);
|
|
@@ -148,7 +246,7 @@ export function formatXsltNumber(numbers, format = "1") {
|
|
|
148
246
|
const separator = separators[index - 1] ?? separators.at(-1) ?? ".";
|
|
149
247
|
result += separator;
|
|
150
248
|
}
|
|
151
|
-
result += formatToken(value, tokens[index] ?? tokens.at(-1));
|
|
249
|
+
result += formatToken(value, tokens[index] ?? tokens.at(-1), grouping);
|
|
152
250
|
});
|
|
153
251
|
|
|
154
252
|
return result + suffix;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QName-valued attributes of `xsl:output` (XSLT 1.0 section 16).
|
|
3
|
+
*
|
|
4
|
+
* `cdata-section-elements` lists QNames that are expanded with the namespace
|
|
5
|
+
* declarations in scope on the `xsl:output` element, the default namespace
|
|
6
|
+
* included for unprefixed names (section 16.1). The serializer then compares
|
|
7
|
+
* expanded names, so the prefixes used in the result do not matter.
|
|
8
|
+
*
|
|
9
|
+
* @module xslt/outputNames
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
"use strict";
|
|
13
|
+
|
|
14
|
+
import { isQName } from "./qname.js";
|
|
15
|
+
import { splitQName } from "./resultNamespaces.js";
|
|
16
|
+
import { resolvePrefix } from "./stylesheetNamespaces.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {{namespaceUri: (string|null), localName: string}} ExpandedName
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Expand the QNames of a `cdata-section-elements` attribute and add them to
|
|
24
|
+
* the names already declared (several xsl:output elements are merged, so the
|
|
25
|
+
* lists are united). Invalid names and undeclared prefixes are reported and
|
|
26
|
+
* skipped.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} value - Whitespace separated QNames
|
|
29
|
+
* @param {Object<string, string>} scope - Namespaces in scope on xsl:output
|
|
30
|
+
* @param {ExpandedName[]} declared - Names declared by earlier xsl:output elements
|
|
31
|
+
* @param {(message: string) => void} warn - Reports a name that is skipped
|
|
32
|
+
* @returns {ExpandedName[]} The union, without duplicates
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* cdataSectionNames("p:c d", { p: "urn:p" }, [], console.warn);
|
|
36
|
+
* // [{ namespaceUri: "urn:p", localName: "c" },
|
|
37
|
+
* // { namespaceUri: null, localName: "d" }]
|
|
38
|
+
*/
|
|
39
|
+
export function cdataSectionNames(value, scope, declared, warn) {
|
|
40
|
+
const names = [...declared];
|
|
41
|
+
const seen = new Set(names.map((n) => `{${n.namespaceUri}}${n.localName}`));
|
|
42
|
+
|
|
43
|
+
for (const qname of value.split(/[ \t\r\n]+/).filter(Boolean)) {
|
|
44
|
+
const { prefix, localName } = splitQName(qname);
|
|
45
|
+
const namespaceUri = resolvePrefix(scope, prefix);
|
|
46
|
+
if (!isQName(qname) || (prefix && !namespaceUri)) {
|
|
47
|
+
warn(
|
|
48
|
+
`xsl:output cdata-section-elements: "${qname}" is not a QName with a declared prefix and is ignored`,
|
|
49
|
+
);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const key = `{${namespaceUri}}${localName}`;
|
|
53
|
+
if (seen.has(key)) continue;
|
|
54
|
+
seen.add(key);
|
|
55
|
+
names.push({ namespaceUri, localName });
|
|
56
|
+
}
|
|
57
|
+
return names;
|
|
58
|
+
}
|