@tradik/xslt-processor 1.0.3 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +1 -1
- package/README.md +110 -520
- package/bin/lib/decode.js +15 -0
- package/bin/lib/dom.js +177 -0
- package/bin/lib/loaders.js +127 -0
- package/bin/lib/options.js +131 -0
- package/bin/lib/output.js +114 -0
- package/bin/lib/paths.js +186 -0
- package/bin/lib/transform.js +206 -0
- package/bin/xslt.js +73 -168
- package/dist/xslt-processor.browser.js +9564 -1585
- package/dist/xslt-processor.browser.js.map +4 -4
- package/dist/xslt-processor.browser.min.js +13 -2
- package/dist/xslt-processor.browser.min.js.map +4 -4
- package/dist/xslt-processor.cjs +9572 -1586
- package/dist/xslt-processor.cjs.map +4 -4
- package/dist/xslt-processor.d.cts +658 -0
- package/dist/xslt-processor.d.ts +459 -12
- package/dist/xslt-processor.js +9546 -1582
- package/dist/xslt-processor.js.map +4 -4
- package/package.json +71 -20
- package/src/XSLTProcessor.js +494 -48
- package/src/async/abort.js +63 -0
- package/src/async/documentUris.js +128 -0
- package/src/async/loaders.js +134 -0
- package/src/async/preload.js +159 -0
- package/src/async/processor.js +206 -0
- package/src/async/stream.js +125 -0
- package/src/bridge/engine.js +221 -0
- package/src/bridge/loader.js +78 -0
- package/src/bridge/results.js +75 -0
- package/src/bridge/version.js +63 -0
- package/src/index.js +26 -8
- package/src/io/decode.js +140 -0
- package/src/io/readSource.js +167 -0
- package/src/xpath/axes.js +562 -0
- package/src/xpath/documentOrder.js +270 -0
- package/src/xpath/evaluator.js +518 -357
- package/src/xpath/index.js +8 -2
- package/src/xpath/namespaceNodes.js +172 -0
- package/src/xpath/nodeSetFunctions.js +169 -0
- package/src/xpath/parser.js +30 -5
- package/src/xpath/strings.js +183 -0
- package/src/xpath/tokenizer.js +37 -23
- package/src/xslt/attributeSets.js +95 -0
- package/src/xslt/avt.js +103 -0
- package/src/xslt/computedNames.js +91 -0
- package/src/xslt/copying.js +212 -0
- package/src/xslt/declarationNames.js +80 -0
- package/src/xslt/domParsing.js +95 -0
- package/src/xslt/elements.js +57 -0
- package/src/xslt/engine/bindings.js +195 -0
- package/src/xslt/engine/context.js +105 -0
- package/src/xslt/engine/controlFlow.js +145 -0
- package/src/xslt/engine/copyInstructions.js +133 -0
- package/src/xslt/engine/declarations.js +233 -0
- package/src/xslt/engine/functionSupport.js +103 -0
- package/src/xslt/engine/methods.js +33 -0
- package/src/xslt/engine/nodeConstruction.js +187 -0
- package/src/xslt/engine/numbering.js +104 -0
- package/src/xslt/engine/outputDeclaration.js +77 -0
- package/src/xslt/engine/sequenceConstructor.js +228 -0
- package/src/xslt/engine/stylesheetLoading.js +208 -0
- package/src/xslt/engine/templateInvocation.js +253 -0
- package/src/xslt/engine/templateRules.js +243 -0
- package/src/xslt/engine/textInstructions.js +171 -0
- package/src/xslt/engine/topLevel.js +130 -0
- package/src/xslt/engine/transformation.js +263 -0
- package/src/xslt/engine/workStack.js +245 -0
- package/src/xslt/engine.js +184 -1736
- package/src/xslt/exslt/arguments.js +99 -0
- package/src/xslt/exslt/calendar.js +120 -0
- package/src/xslt/exslt/common.js +44 -0
- package/src/xslt/exslt/dateCalc.js +261 -0
- package/src/xslt/exslt/dateFormat.js +150 -0
- package/src/xslt/exslt/dateParse.js +265 -0
- package/src/xslt/exslt/dates.js +259 -0
- package/src/xslt/exslt/duration.js +207 -0
- package/src/xslt/exslt/dynamic.js +59 -0
- package/src/xslt/exslt/index.js +59 -0
- package/src/xslt/exslt/math.js +177 -0
- package/src/xslt/exslt/sets.js +96 -0
- package/src/xslt/exslt/stringOps.js +163 -0
- package/src/xslt/exslt/strings.js +147 -0
- package/src/xslt/exslt/uri.js +92 -0
- package/src/xslt/formatNumber.js +233 -0
- package/src/xslt/forwardsCompatible.js +75 -0
- package/src/xslt/functions.js +270 -0
- package/src/xslt/index.js +38 -1
- package/src/xslt/keys.js +164 -0
- package/src/xslt/literalResult.js +223 -0
- package/src/xslt/matchScope.js +116 -0
- package/src/xslt/number.js +271 -0
- package/src/xslt/numberFormat.js +253 -0
- package/src/xslt/outputNames.js +58 -0
- package/src/xslt/patternCompiler.js +175 -0
- package/src/xslt/patterns.js +324 -0
- package/src/xslt/qname.js +90 -0
- package/src/xslt/resultDocument.js +98 -0
- package/src/xslt/resultNamespaces.js +219 -0
- package/src/xslt/resultTree.js +211 -0
- package/src/xslt/serializer/baseWriter.js +390 -0
- package/src/xslt/serializer/chunks.js +120 -0
- package/src/xslt/serializer/constants.js +92 -0
- package/src/xslt/serializer/encoding.js +327 -0
- package/src/xslt/serializer/escape.js +135 -0
- package/src/xslt/serializer/frames.js +168 -0
- package/src/xslt/serializer/htmlDoctype.js +102 -0
- package/src/xslt/serializer/htmlEntities.js +77 -0
- package/src/xslt/serializer/htmlSerializer.js +239 -0
- package/src/xslt/serializer/indent.js +51 -0
- package/src/xslt/serializer/namespaces.js +68 -0
- package/src/xslt/serializer/rawText.js +41 -0
- package/src/xslt/serializer/settings.js +179 -0
- package/src/xslt/serializer/textSerializer.js +77 -0
- package/src/xslt/serializer/xhtmlDocument.js +103 -0
- package/src/xslt/serializer/xmlSerializer.js +227 -0
- package/src/xslt/serializer.js +90 -0
- package/src/xslt/sort.js +151 -0
- package/src/xslt/spaceNameTests.js +115 -0
- package/src/xslt/stylesheetChecks.js +206 -0
- package/src/xslt/stylesheetNamespaces.js +266 -0
- package/src/xslt/templatePriority.js +45 -0
- package/src/xslt/uri.js +68 -0
- package/src/xslt/variables.js +152 -0
- package/src/xslt/whitespace.js +200 -0
- package/LICENSE +0 -29
- package/src/XSLTProcessor.test.js +0 -930
- package/src/xpath/evaluator.test.js +0 -1852
- package/src/xpath/tokenizer.test.js +0 -224
- package/src/xslt/engine.test.js +0 -3130
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* String algorithms of the EXSLT strings module, ported from libexslt
|
|
3
|
+
* `strings.c`. They work on plain strings; `strings.js` binds them to XPath.
|
|
4
|
+
* Lengths and positions count characters (Unicode code points).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
import { characters } from "./arguments.js";
|
|
10
|
+
|
|
11
|
+
/** Delimiters `str:tokenize()` uses without a second argument. */
|
|
12
|
+
export const DEFAULT_TOKENIZE_DELIMITERS = "\t\r\n ";
|
|
13
|
+
|
|
14
|
+
/** Longest string `str:padding()` builds (libexslt's limit). */
|
|
15
|
+
export const MAX_PADDING = 100000;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* `str:tokenize()`: split on any of the delimiter characters, dropping empty
|
|
19
|
+
* tokens; an empty delimiter string makes every character a token.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} str - The string to split
|
|
22
|
+
* @param {string} delimiters - Delimiter characters
|
|
23
|
+
* @returns {string[]} The tokens
|
|
24
|
+
*/
|
|
25
|
+
export function tokenizeString(str, delimiters) {
|
|
26
|
+
const chars = characters(str);
|
|
27
|
+
if (delimiters === "") return chars;
|
|
28
|
+
|
|
29
|
+
const separators = new Set(characters(delimiters));
|
|
30
|
+
const tokens = [];
|
|
31
|
+
let token = "";
|
|
32
|
+
for (const char of chars) {
|
|
33
|
+
if (!separators.has(char)) {
|
|
34
|
+
token += char;
|
|
35
|
+
} else if (token !== "") {
|
|
36
|
+
tokens.push(token);
|
|
37
|
+
token = "";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (token !== "") tokens.push(token);
|
|
41
|
+
return tokens;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Lower-case the ASCII letters of a string, as libxml2 `xmlStrncasecmp`.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} str - Any string
|
|
48
|
+
* @returns {string} The string with A-Z lower-cased
|
|
49
|
+
*/
|
|
50
|
+
function asciiLowerCase(str) {
|
|
51
|
+
return str.replace(/[A-Z]/g, (letter) => letter.toLowerCase());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* `str:split()`: split on a delimiter string, dropping empty tokens. As in
|
|
56
|
+
* libexslt the delimiter matches ASCII letters case-insensitively, and an
|
|
57
|
+
* empty delimiter makes every character a token.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} str - The string to split
|
|
60
|
+
* @param {string} delimiter - The delimiter
|
|
61
|
+
* @returns {string[]} The tokens
|
|
62
|
+
*/
|
|
63
|
+
export function splitString(str, delimiter) {
|
|
64
|
+
if (delimiter === "") return characters(str);
|
|
65
|
+
|
|
66
|
+
const haystack = asciiLowerCase(str);
|
|
67
|
+
const needle = asciiLowerCase(delimiter);
|
|
68
|
+
const tokens = [];
|
|
69
|
+
let start = 0;
|
|
70
|
+
let at = haystack.indexOf(needle);
|
|
71
|
+
while (at !== -1) {
|
|
72
|
+
if (at > start) tokens.push(str.substring(start, at));
|
|
73
|
+
start = at + needle.length;
|
|
74
|
+
at = haystack.indexOf(needle, start);
|
|
75
|
+
}
|
|
76
|
+
if (start < str.length) tokens.push(str.substring(start));
|
|
77
|
+
return tokens;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* `str:padding(length, chars)`: repeat `chars` (a space when empty) and
|
|
82
|
+
* truncate to `length` characters, capped at {@link MAX_PADDING}.
|
|
83
|
+
*
|
|
84
|
+
* @param {number} length - Requested length
|
|
85
|
+
* @param {string} chars - Padding characters
|
|
86
|
+
* @returns {string} The padding
|
|
87
|
+
*/
|
|
88
|
+
export function paddingString(length, chars) {
|
|
89
|
+
if (Number.isNaN(length) || length < 1) return "";
|
|
90
|
+
const count = Math.min(Math.trunc(length), MAX_PADDING);
|
|
91
|
+
const pattern = characters(chars === "" ? " " : chars);
|
|
92
|
+
const whole = Math.floor(count / pattern.length);
|
|
93
|
+
return (
|
|
94
|
+
pattern.join("").repeat(whole) +
|
|
95
|
+
pattern.slice(0, count - whole * pattern.length).join("")
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* `str:align(string, padding, alignment)`: overlay the string on the padding
|
|
101
|
+
* ("left" unless "right" or "center"), truncating a longer string.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} str - The string to align
|
|
104
|
+
* @param {string} padding - The padding it is placed on
|
|
105
|
+
* @param {string|null} alignment - "left", "right" or "center"
|
|
106
|
+
* @returns {string} The aligned string
|
|
107
|
+
*/
|
|
108
|
+
export function alignString(str, padding, alignment) {
|
|
109
|
+
const text = characters(str);
|
|
110
|
+
const pad = characters(padding);
|
|
111
|
+
if (text.length >= pad.length) return text.slice(0, pad.length).join("");
|
|
112
|
+
|
|
113
|
+
const free = pad.length - text.length;
|
|
114
|
+
let left = 0;
|
|
115
|
+
if (alignment === "right") left = free;
|
|
116
|
+
else if (alignment === "center") left = Math.floor(free / 2);
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
pad.slice(0, left).join("") + str + pad.slice(left + text.length).join("")
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* `str:replace()`: replace every occurrence of the search strings, longest
|
|
125
|
+
* match first (the earliest search string on ties). Search string `i` is
|
|
126
|
+
* replaced by replacement `i`, or removed when there is none. The first empty
|
|
127
|
+
* search string with a non-empty replacement inserts that replacement between
|
|
128
|
+
* the characters that no search string matches.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} str - The string to process
|
|
131
|
+
* @param {string[]} searches - Search strings
|
|
132
|
+
* @param {Array<string|null>} replacements - Replacements by search index
|
|
133
|
+
* @returns {string} The processed string
|
|
134
|
+
*/
|
|
135
|
+
export function replaceStrings(str, searches, replacements) {
|
|
136
|
+
const replacementOf = (index) => replacements[index] ?? "";
|
|
137
|
+
let emptyIndex = searches.indexOf("");
|
|
138
|
+
if (emptyIndex !== -1 && replacementOf(emptyIndex) === "") emptyIndex = -1;
|
|
139
|
+
|
|
140
|
+
let result = "";
|
|
141
|
+
let start = 0;
|
|
142
|
+
let at = 0;
|
|
143
|
+
while (at < str.length) {
|
|
144
|
+
let match = -1;
|
|
145
|
+
searches.forEach((search, index) => {
|
|
146
|
+
const longer = match === -1 || search.length > searches[match].length;
|
|
147
|
+
if (search !== "" && longer && str.startsWith(search, at)) match = index;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (match === -1) {
|
|
151
|
+
if (emptyIndex !== -1 && start < at) {
|
|
152
|
+
result += str.substring(start, at) + replacementOf(emptyIndex);
|
|
153
|
+
start = at;
|
|
154
|
+
}
|
|
155
|
+
at += str.codePointAt(at) > 0xffff ? 2 : 1;
|
|
156
|
+
} else {
|
|
157
|
+
result += str.substring(start, at) + replacementOf(match);
|
|
158
|
+
at += searches[match].length;
|
|
159
|
+
start = at;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return result + str.substring(start);
|
|
163
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EXSLT strings module (http://exslt.org/strings), following libexslt
|
|
3
|
+
* `strings.c`: tokenize, split, replace, padding, align, concat, encode-uri
|
|
4
|
+
* and decode-uri.
|
|
5
|
+
*
|
|
6
|
+
* `tokenize()` / `split()` return `token` elements (no namespace) and
|
|
7
|
+
* `replace()` returns one text node; like libexslt's result tree fragments,
|
|
8
|
+
* the nodes of one call are siblings in a new DocumentFragment.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
"use strict";
|
|
12
|
+
|
|
13
|
+
import { expandedFunctionName } from "../../xpath/evaluator.js";
|
|
14
|
+
import {
|
|
15
|
+
EXSLT_STRINGS,
|
|
16
|
+
checkArity,
|
|
17
|
+
createContainer,
|
|
18
|
+
isNodeSetValue,
|
|
19
|
+
toNodeSet,
|
|
20
|
+
} from "./arguments.js";
|
|
21
|
+
import {
|
|
22
|
+
DEFAULT_TOKENIZE_DELIMITERS,
|
|
23
|
+
alignString,
|
|
24
|
+
paddingString,
|
|
25
|
+
replaceStrings,
|
|
26
|
+
splitString,
|
|
27
|
+
tokenizeString,
|
|
28
|
+
} from "./stringOps.js";
|
|
29
|
+
import { decodeUri, encodeUri } from "./uri.js";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build the EXSLT strings functions.
|
|
33
|
+
*
|
|
34
|
+
* @param {import('../../xpath/evaluator.js').XPathEvaluator} evaluator - Evaluates the arguments
|
|
35
|
+
* @returns {Object<string, Function>} Functions keyed by expanded name
|
|
36
|
+
*/
|
|
37
|
+
export function createStringsFunctions(evaluator) {
|
|
38
|
+
const value = (arg, ctx) => evaluator.evaluate(arg, ctx);
|
|
39
|
+
const string = (arg, ctx) => evaluator.toString(value(arg, ctx));
|
|
40
|
+
const optionalString = (args, index, ctx, fallback) =>
|
|
41
|
+
args.length > index ? string(args[index], ctx) : fallback;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Turn tokens into `token` elements of a new fragment.
|
|
45
|
+
*
|
|
46
|
+
* @param {string[]} tokens - The tokens
|
|
47
|
+
* @param {object} ctx - Evaluation context, provides the owner document
|
|
48
|
+
* @returns {Element[]} The token elements
|
|
49
|
+
*/
|
|
50
|
+
const tokenElements = (tokens, ctx) => {
|
|
51
|
+
const container = createContainer(ctx);
|
|
52
|
+
const doc = container.ownerDocument;
|
|
53
|
+
return tokens.map((token) => {
|
|
54
|
+
const element = doc.createElementNS(null, "token");
|
|
55
|
+
element.appendChild(doc.createTextNode(token));
|
|
56
|
+
return container.appendChild(element);
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* String values of a node-set argument, or the string of any other value.
|
|
62
|
+
*
|
|
63
|
+
* @param {*} argValue - An evaluated argument
|
|
64
|
+
* @returns {string[]} The strings
|
|
65
|
+
*/
|
|
66
|
+
const stringList = (argValue) =>
|
|
67
|
+
isNodeSetValue(argValue)
|
|
68
|
+
? toNodeSet("str:replace", argValue).map((node) =>
|
|
69
|
+
evaluator.getStringValue(node),
|
|
70
|
+
)
|
|
71
|
+
: [evaluator.toString(argValue)];
|
|
72
|
+
|
|
73
|
+
const key = (local) => expandedFunctionName(EXSLT_STRINGS, local);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
[key("tokenize")]: (args, ctx) => {
|
|
77
|
+
checkArity("str:tokenize", args, 1, 2);
|
|
78
|
+
const delimiters = optionalString(
|
|
79
|
+
args,
|
|
80
|
+
1,
|
|
81
|
+
ctx,
|
|
82
|
+
DEFAULT_TOKENIZE_DELIMITERS,
|
|
83
|
+
);
|
|
84
|
+
return tokenElements(
|
|
85
|
+
tokenizeString(string(args[0], ctx), delimiters),
|
|
86
|
+
ctx,
|
|
87
|
+
);
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
[key("split")]: (args, ctx) => {
|
|
91
|
+
checkArity("str:split", args, 1, 2);
|
|
92
|
+
const delimiter = optionalString(args, 1, ctx, " ");
|
|
93
|
+
return tokenElements(splitString(string(args[0], ctx), delimiter), ctx);
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
[key("replace")]: (args, ctx) => {
|
|
97
|
+
checkArity("str:replace", args, 3);
|
|
98
|
+
const str = string(args[0], ctx);
|
|
99
|
+
const searches = stringList(value(args[1], ctx));
|
|
100
|
+
const replacements = stringList(value(args[2], ctx));
|
|
101
|
+
const container = createContainer(ctx);
|
|
102
|
+
const text = container.ownerDocument.createTextNode(
|
|
103
|
+
replaceStrings(str, searches, replacements),
|
|
104
|
+
);
|
|
105
|
+
return [container.appendChild(text)];
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
[key("padding")]: (args, ctx) => {
|
|
109
|
+
checkArity("str:padding", args, 1, 2);
|
|
110
|
+
const length = evaluator.toNumber(value(args[0], ctx));
|
|
111
|
+
return paddingString(length, optionalString(args, 1, ctx, ""));
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
[key("align")]: (args, ctx) => {
|
|
115
|
+
checkArity("str:align", args, 2, 3);
|
|
116
|
+
return alignString(
|
|
117
|
+
string(args[0], ctx),
|
|
118
|
+
string(args[1], ctx),
|
|
119
|
+
optionalString(args, 2, ctx, null),
|
|
120
|
+
);
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
[key("concat")]: (args, ctx) => {
|
|
124
|
+
checkArity("str:concat", args, 1);
|
|
125
|
+
return toNodeSet("str:concat", value(args[0], ctx))
|
|
126
|
+
.map((node) => evaluator.getStringValue(node))
|
|
127
|
+
.join("");
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
[key("encode-uri")]: (args, ctx) => {
|
|
131
|
+
checkArity("str:encode-uri", args, 2, 3);
|
|
132
|
+
return encodeUri(
|
|
133
|
+
string(args[0], ctx),
|
|
134
|
+
evaluator.toBoolean(value(args[1], ctx)),
|
|
135
|
+
optionalString(args, 2, ctx, undefined),
|
|
136
|
+
);
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
[key("decode-uri")]: (args, ctx) => {
|
|
140
|
+
checkArity("str:decode-uri", args, 1, 2);
|
|
141
|
+
return decodeUri(
|
|
142
|
+
string(args[0], ctx),
|
|
143
|
+
optionalString(args, 1, ctx, undefined),
|
|
144
|
+
);
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `str:encode-uri()` / `str:decode-uri()` algorithms, following libexslt
|
|
3
|
+
* `strings.c` and libxml2 `xmlURIEscapeStr()` / `xmlURIUnescapeString()`.
|
|
4
|
+
*
|
|
5
|
+
* Only UTF-8 is supported: any other encoding argument (the name is compared
|
|
6
|
+
* case-sensitively, as libexslt does) yields the empty string, as does a
|
|
7
|
+
* string that is not well-formed Unicode or decodes to malformed UTF-8.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
"use strict";
|
|
11
|
+
|
|
12
|
+
/** The only encoding name the functions accept. */
|
|
13
|
+
export const URI_ENCODING = "UTF-8";
|
|
14
|
+
|
|
15
|
+
/** Characters never escaped: RFC 2396 "mark" characters (letters and digits aside). */
|
|
16
|
+
const MARKS = "-_.!~*'()";
|
|
17
|
+
|
|
18
|
+
/** Reserved characters kept when `escape-reserved` is false. */
|
|
19
|
+
const RESERVED = ";/?:@&=+$,[]";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Whether a string contains an unpaired surrogate, which has no UTF-8 form.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} str - Any string
|
|
25
|
+
* @returns {boolean} True when the string is not well-formed
|
|
26
|
+
*/
|
|
27
|
+
function hasLoneSurrogate(str) {
|
|
28
|
+
return /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/.test(
|
|
29
|
+
str,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Percent-encode the UTF-8 bytes of one character, in upper case hex.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} char - One character (code point)
|
|
37
|
+
* @returns {string} The escaped bytes
|
|
38
|
+
*/
|
|
39
|
+
function escapeBytes(char) {
|
|
40
|
+
const code = char.codePointAt(0);
|
|
41
|
+
if (code < 0x80) {
|
|
42
|
+
return `%${code.toString(16).toUpperCase().padStart(2, "0")}`;
|
|
43
|
+
}
|
|
44
|
+
return encodeURIComponent(char);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `str:encode-uri(string, escape-reserved, encoding?)`.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} str - The string to escape
|
|
51
|
+
* @param {boolean} escapeReserved - Whether reserved characters are escaped too
|
|
52
|
+
* @param {string} [encoding] - Must be "UTF-8" when given
|
|
53
|
+
* @returns {string} The escaped string, "" on failure
|
|
54
|
+
*/
|
|
55
|
+
export function encodeUri(str, escapeReserved, encoding = URI_ENCODING) {
|
|
56
|
+
if (encoding !== URI_ENCODING || hasLoneSurrogate(str)) return "";
|
|
57
|
+
|
|
58
|
+
const kept = escapeReserved ? MARKS : MARKS + RESERVED;
|
|
59
|
+
let result = "";
|
|
60
|
+
for (const char of str) {
|
|
61
|
+
const unescaped = /^[A-Za-z0-9]$/.test(char) || kept.includes(char);
|
|
62
|
+
result += unescaped ? char : escapeBytes(char);
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* `str:decode-uri(string, encoding?)`: decode every `%XX` escape (a `%` not
|
|
69
|
+
* followed by two hex digits stays as is); the decoded bytes must be UTF-8.
|
|
70
|
+
* As the result is a C string in libexslt, it ends at a decoded NUL byte.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} str - The string to unescape
|
|
73
|
+
* @param {string} [encoding] - Must be "UTF-8" when given
|
|
74
|
+
* @returns {string} The unescaped string, "" on failure
|
|
75
|
+
*/
|
|
76
|
+
export function decodeUri(str, encoding = URI_ENCODING) {
|
|
77
|
+
if (encoding !== URI_ENCODING || hasLoneSurrogate(str)) return "";
|
|
78
|
+
|
|
79
|
+
// Re-escape everything but valid escapes, then let the platform decode the
|
|
80
|
+
// bytes: decodeURIComponent throws on malformed UTF-8.
|
|
81
|
+
const escaped = str.replace(/%(?![0-9A-Fa-f]{2})|[^%]+/gu, (text) =>
|
|
82
|
+
text === "%" ? "%25" : encodeURIComponent(text),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const decoded = decodeURIComponent(escaped);
|
|
87
|
+
const nul = decoded.indexOf("\0");
|
|
88
|
+
return nul === -1 ? decoded : decoded.substring(0, nul);
|
|
89
|
+
} catch {
|
|
90
|
+
return "";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XSLT 1.0 `format-number()` picture string formatting.
|
|
3
|
+
*
|
|
4
|
+
* Implements the subset of the JDK `DecimalFormat` picture syntax that XSLT 1.0
|
|
5
|
+
* requires: grouping separator, decimal separator, minimum/maximum fraction
|
|
6
|
+
* digits, minimum integer digits, percent and per-mille scaling and an optional
|
|
7
|
+
* negative subpattern. All symbols are taken from an `xsl:decimal-format`
|
|
8
|
+
* declaration so alternative digits and separators are honoured.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
"use strict";
|
|
12
|
+
|
|
13
|
+
/** Symbols of the unnamed, default `xsl:decimal-format`. */
|
|
14
|
+
export const DEFAULT_DECIMAL_FORMAT = Object.freeze({
|
|
15
|
+
decimalSeparator: ".",
|
|
16
|
+
groupingSeparator: ",",
|
|
17
|
+
percent: "%",
|
|
18
|
+
perMille: "‰",
|
|
19
|
+
zeroDigit: "0",
|
|
20
|
+
digit: "#",
|
|
21
|
+
patternSeparator: ";",
|
|
22
|
+
infinity: "Infinity",
|
|
23
|
+
nan: "NaN",
|
|
24
|
+
minusSign: "-",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Split a picture string into its positive and optional negative subpattern.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} pattern - The picture string
|
|
31
|
+
* @param {Object} format - Decimal format symbols
|
|
32
|
+
* @returns {{positive: string, negative: (string|null)}} The subpatterns
|
|
33
|
+
*/
|
|
34
|
+
function splitSubPatterns(pattern, format) {
|
|
35
|
+
const index = pattern.indexOf(format.patternSeparator);
|
|
36
|
+
if (index === -1) return { positive: pattern, negative: null };
|
|
37
|
+
return {
|
|
38
|
+
positive: pattern.substring(0, index),
|
|
39
|
+
negative: pattern.substring(index + format.patternSeparator.length),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parse a single subpattern into a formatting description.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} subPattern - One subpattern of a picture string
|
|
47
|
+
* @param {Object} format - Decimal format symbols
|
|
48
|
+
* @returns {Object} Prefix, suffix, digit counts, grouping size and multiplier
|
|
49
|
+
*/
|
|
50
|
+
function parseSubPattern(subPattern, format) {
|
|
51
|
+
const special = new Set([
|
|
52
|
+
format.digit,
|
|
53
|
+
format.zeroDigit,
|
|
54
|
+
format.groupingSeparator,
|
|
55
|
+
format.decimalSeparator,
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
let start = 0;
|
|
59
|
+
while (start < subPattern.length && !special.has(subPattern[start])) start++;
|
|
60
|
+
|
|
61
|
+
let end = start;
|
|
62
|
+
while (end < subPattern.length && special.has(subPattern[end])) end++;
|
|
63
|
+
|
|
64
|
+
const prefix = subPattern.substring(0, start);
|
|
65
|
+
const numeric = subPattern.substring(start, end);
|
|
66
|
+
const suffix = subPattern.substring(end);
|
|
67
|
+
|
|
68
|
+
const decimalIndex = numeric.indexOf(format.decimalSeparator);
|
|
69
|
+
const integerPart =
|
|
70
|
+
decimalIndex === -1 ? numeric : numeric.substring(0, decimalIndex);
|
|
71
|
+
const fractionPart =
|
|
72
|
+
decimalIndex === -1 ? "" : numeric.substring(decimalIndex + 1);
|
|
73
|
+
|
|
74
|
+
const groupingIndex = integerPart.lastIndexOf(format.groupingSeparator);
|
|
75
|
+
const affixes = prefix + suffix;
|
|
76
|
+
|
|
77
|
+
let multiplier = 1;
|
|
78
|
+
if (affixes.includes(format.percent)) multiplier = 100;
|
|
79
|
+
else if (affixes.includes(format.perMille)) multiplier = 1000;
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
prefix,
|
|
83
|
+
suffix,
|
|
84
|
+
multiplier,
|
|
85
|
+
minInteger: countOccurrences(integerPart, format.zeroDigit),
|
|
86
|
+
integerHash: countOccurrences(integerPart, format.digit),
|
|
87
|
+
hasDecimal: decimalIndex !== -1,
|
|
88
|
+
minFraction: countOccurrences(fractionPart, format.zeroDigit),
|
|
89
|
+
maxFraction: Math.min(fractionPart.length, 100),
|
|
90
|
+
groupingSize:
|
|
91
|
+
groupingIndex === -1 ? 0 : integerPart.length - groupingIndex - 1,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Count occurrences of a character inside a string.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} text - The text to scan
|
|
99
|
+
* @param {string} char - The character to count
|
|
100
|
+
* @returns {number} Number of occurrences
|
|
101
|
+
*/
|
|
102
|
+
function countOccurrences(text, char) {
|
|
103
|
+
let total = 0;
|
|
104
|
+
for (const current of text) {
|
|
105
|
+
if (current === char) total++;
|
|
106
|
+
}
|
|
107
|
+
return total;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Insert grouping separators into a run of integer digits.
|
|
112
|
+
*
|
|
113
|
+
* @param {string} digits - Integer digits, most significant first
|
|
114
|
+
* @param {number} size - Grouping size, 0 disables grouping
|
|
115
|
+
* @param {string} separator - The grouping separator
|
|
116
|
+
* @returns {string} The grouped digits
|
|
117
|
+
*/
|
|
118
|
+
function applyGrouping(digits, size, separator) {
|
|
119
|
+
if (size <= 0 || digits.length <= size) return digits;
|
|
120
|
+
|
|
121
|
+
let result = "";
|
|
122
|
+
for (let i = 0; i < digits.length; i++) {
|
|
123
|
+
const fromEnd = digits.length - i;
|
|
124
|
+
if (i > 0 && fromEnd % size === 0) result += separator;
|
|
125
|
+
result += digits[i];
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Translate ASCII digits to the digits of the decimal format.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} text - Text containing ASCII digits
|
|
134
|
+
* @param {string} zeroDigit - The format's zero digit
|
|
135
|
+
* @returns {string} Text using the format's digit family
|
|
136
|
+
*/
|
|
137
|
+
function translateDigits(text, zeroDigit) {
|
|
138
|
+
const offset = zeroDigit.codePointAt(0) - 48;
|
|
139
|
+
if (offset === 0) return text;
|
|
140
|
+
return text.replaceAll(/\d/g, (digit) =>
|
|
141
|
+
String.fromCodePoint(digit.codePointAt(0) + offset),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Format the magnitude of a finite number according to a parsed subpattern.
|
|
147
|
+
*
|
|
148
|
+
* Follows libxslt (and so Chrome) where the JDK rules leave room: a pattern
|
|
149
|
+
* without integer digits and zero fraction digits (`.#`) shows one fraction
|
|
150
|
+
* digit, as the JDK does; an integer part of zero is written as `0` unless
|
|
151
|
+
* the pattern asks for fraction zeros (`#.#` gives `0.5`, `.#` gives `.5`);
|
|
152
|
+
* and a decimal separator with no fraction digits after it is kept (`#.`
|
|
153
|
+
* gives `1.`).
|
|
154
|
+
*
|
|
155
|
+
* @param {number} magnitude - Absolute, already scaled value
|
|
156
|
+
* @param {Object} spec - Parsed subpattern
|
|
157
|
+
* @param {Object} format - Decimal format symbols
|
|
158
|
+
* @returns {string} The formatted number without prefix or suffix
|
|
159
|
+
*/
|
|
160
|
+
function formatMagnitude(magnitude, spec, format) {
|
|
161
|
+
const noDigits = spec.minInteger + spec.integerHash + spec.minFraction === 0;
|
|
162
|
+
const minFraction = noDigits && spec.maxFraction > 0 ? 1 : spec.minFraction;
|
|
163
|
+
const fixed = magnitude.toFixed(spec.maxFraction);
|
|
164
|
+
const [rawInteger, rawFraction = ""] = fixed.split(".");
|
|
165
|
+
|
|
166
|
+
let fraction = rawFraction;
|
|
167
|
+
while (fraction.length > minFraction && fraction.endsWith("0")) {
|
|
168
|
+
fraction = fraction.slice(0, -1);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let integer = (rawInteger === "0" ? "" : rawInteger).padStart(
|
|
172
|
+
spec.minInteger,
|
|
173
|
+
"0",
|
|
174
|
+
);
|
|
175
|
+
if (integer === "" && spec.minInteger + minFraction === 0) integer = "0";
|
|
176
|
+
|
|
177
|
+
integer = applyGrouping(integer, spec.groupingSize, format.groupingSeparator);
|
|
178
|
+
|
|
179
|
+
let body = integer;
|
|
180
|
+
if (fraction.length > 0) body += format.decimalSeparator + fraction;
|
|
181
|
+
else if (spec.maxFraction === 0 && spec.hasDecimal) {
|
|
182
|
+
body += format.decimalSeparator;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return translateDigits(body, format.zeroDigit);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Format a number using an XSLT 1.0 picture string.
|
|
190
|
+
*
|
|
191
|
+
* @param {number} value - The number to format
|
|
192
|
+
* @param {string} pattern - The picture string, e.g. `#,##0.00`
|
|
193
|
+
* @param {Object} [decimalFormat] - `xsl:decimal-format` symbols
|
|
194
|
+
* @returns {string} The formatted number
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* formatNumber(1234.5, '#,##0.00'); // '1,234.50'
|
|
198
|
+
* formatNumber(-1234, '#,##0;(#,##0)'); // '(1,234)'
|
|
199
|
+
*/
|
|
200
|
+
export function formatNumber(
|
|
201
|
+
value,
|
|
202
|
+
pattern,
|
|
203
|
+
decimalFormat = DEFAULT_DECIMAL_FORMAT,
|
|
204
|
+
) {
|
|
205
|
+
const format = { ...DEFAULT_DECIMAL_FORMAT, ...decimalFormat };
|
|
206
|
+
|
|
207
|
+
if (typeof value !== "number" || Number.isNaN(value)) return format.nan;
|
|
208
|
+
|
|
209
|
+
const subPatterns = splitSubPatterns(pattern, format);
|
|
210
|
+
const positive = parseSubPattern(subPatterns.positive, format);
|
|
211
|
+
const isNegative = value < 0;
|
|
212
|
+
|
|
213
|
+
let spec = positive;
|
|
214
|
+
let prefix = positive.prefix;
|
|
215
|
+
let suffix = positive.suffix;
|
|
216
|
+
|
|
217
|
+
if (isNegative) {
|
|
218
|
+
if (subPatterns.negative !== null) {
|
|
219
|
+
spec = parseSubPattern(subPatterns.negative, format);
|
|
220
|
+
prefix = spec.prefix;
|
|
221
|
+
suffix = spec.suffix;
|
|
222
|
+
} else {
|
|
223
|
+
prefix = format.minusSign + positive.prefix;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const magnitude = Math.abs(value) * spec.multiplier;
|
|
228
|
+
const body = Number.isFinite(magnitude)
|
|
229
|
+
? formatMagnitude(magnitude, spec, format)
|
|
230
|
+
: format.infinity;
|
|
231
|
+
|
|
232
|
+
return prefix + body + suffix;
|
|
233
|
+
}
|