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