@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.
Files changed (122) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +102 -757
  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 +17 -0
  7. package/bin/lib/output.js +114 -0
  8. package/bin/lib/paths.js +3 -3
  9. package/bin/lib/transform.js +124 -33
  10. package/bin/xslt.js +26 -27
  11. package/dist/xslt-processor.browser.js +8784 -2720
  12. package/dist/xslt-processor.browser.js.map +4 -4
  13. package/dist/xslt-processor.browser.min.js +13 -6
  14. package/dist/xslt-processor.browser.min.js.map +4 -4
  15. package/dist/xslt-processor.cjs +8789 -2723
  16. package/dist/xslt-processor.cjs.map +4 -4
  17. package/dist/xslt-processor.d.cts +380 -21
  18. package/dist/xslt-processor.d.ts +380 -21
  19. package/dist/xslt-processor.js +8770 -2722
  20. package/dist/xslt-processor.js.map +4 -4
  21. package/package.json +51 -11
  22. package/src/XSLTProcessor.js +343 -66
  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 +16 -4
  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 +475 -355
  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 +1 -1
  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 +176 -2020
  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 +22 -9
  87. package/src/xslt/forwardsCompatible.js +75 -0
  88. package/src/xslt/functions.js +94 -15
  89. package/src/xslt/index.js +7 -1
  90. package/src/xslt/keys.js +51 -28
  91. package/src/xslt/literalResult.js +63 -7
  92. package/src/xslt/matchScope.js +116 -0
  93. package/src/xslt/number.js +171 -78
  94. package/src/xslt/numberFormat.js +124 -26
  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 +143 -6
  102. package/src/xslt/serializer/baseWriter.js +173 -66
  103. package/src/xslt/serializer/chunks.js +120 -0
  104. package/src/xslt/serializer/constants.js +14 -0
  105. package/src/xslt/serializer/encoding.js +327 -0
  106. package/src/xslt/serializer/escape.js +49 -12
  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 +123 -25
  111. package/src/xslt/serializer/settings.js +89 -13
  112. package/src/xslt/serializer/textSerializer.js +58 -10
  113. package/src/xslt/serializer/xhtmlDocument.js +103 -0
  114. package/src/xslt/serializer/xmlSerializer.js +113 -13
  115. package/src/xslt/serializer.js +50 -17
  116. package/src/xslt/sort.js +151 -0
  117. package/src/xslt/spaceNameTests.js +115 -0
  118. package/src/xslt/stylesheetChecks.js +206 -0
  119. package/src/xslt/stylesheetNamespaces.js +266 -0
  120. package/src/xslt/variables.js +152 -0
  121. package/src/xslt/whitespace.js +43 -27
  122. package/LICENSE +0 -29
@@ -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
+ }
@@ -83,6 +83,8 @@ function parseSubPattern(subPattern, format) {
83
83
  suffix,
84
84
  multiplier,
85
85
  minInteger: countOccurrences(integerPart, format.zeroDigit),
86
+ integerHash: countOccurrences(integerPart, format.digit),
87
+ hasDecimal: decimalIndex !== -1,
86
88
  minFraction: countOccurrences(fractionPart, format.zeroDigit),
87
89
  maxFraction: Math.min(fractionPart.length, 100),
88
90
  groupingSize:
@@ -143,31 +145,42 @@ function translateDigits(text, zeroDigit) {
143
145
  /**
144
146
  * Format the magnitude of a finite number according to a parsed subpattern.
145
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
+ *
146
155
  * @param {number} magnitude - Absolute, already scaled value
147
156
  * @param {Object} spec - Parsed subpattern
148
157
  * @param {Object} format - Decimal format symbols
149
158
  * @returns {string} The formatted number without prefix or suffix
150
159
  */
151
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;
152
163
  const fixed = magnitude.toFixed(spec.maxFraction);
153
164
  const [rawInteger, rawFraction = ""] = fixed.split(".");
154
165
 
155
166
  let fraction = rawFraction;
156
- while (fraction.length > spec.minFraction && fraction.endsWith("0")) {
167
+ while (fraction.length > minFraction && fraction.endsWith("0")) {
157
168
  fraction = fraction.slice(0, -1);
158
169
  }
159
170
 
160
- let integer = rawInteger.padStart(spec.minInteger, "0");
161
- if (spec.minInteger === 0 && integer === "0" && fraction.length > 0) {
162
- integer = "";
163
- }
171
+ let integer = (rawInteger === "0" ? "" : rawInteger).padStart(
172
+ spec.minInteger,
173
+ "0",
174
+ );
175
+ if (integer === "" && spec.minInteger + minFraction === 0) integer = "0";
164
176
 
165
177
  integer = applyGrouping(integer, spec.groupingSize, format.groupingSeparator);
166
178
 
167
- const body =
168
- fraction.length > 0
169
- ? integer + format.decimalSeparator + fraction
170
- : integer;
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
+ }
171
184
 
172
185
  return translateDigits(body, format.zeroDigit);
173
186
  }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Forwards-compatible processing and xsl:fallback (XSLT 1.0 sections 2.5
3
+ * and 15).
4
+ *
5
+ * An element is processed in forwards-compatible mode when the nearest
6
+ * enclosing `version` declaration (the `version` attribute of
7
+ * `xsl:stylesheet`/`xsl:transform`, or the `xsl:version` attribute of a
8
+ * literal result element) is not "1.0". In that mode unknown top-level XSLT
9
+ * elements are ignored. An unknown XSLT instruction is replaced by its
10
+ * `xsl:fallback` children when it has some (in any mode); otherwise it is an
11
+ * error only if it is actually instantiated.
12
+ *
13
+ * @module xslt/forwardsCompatible
14
+ */
15
+
16
+ "use strict";
17
+
18
+ import { XSLT_NAMESPACE } from "./elements.js";
19
+ import { xsltLocalName } from "./stylesheetChecks.js";
20
+
21
+ const modes = new WeakMap();
22
+
23
+ /**
24
+ * The version declared on one stylesheet element, if any.
25
+ *
26
+ * @param {Element} element - A stylesheet element
27
+ * @returns {string|null} The declared version, null when none
28
+ */
29
+ export function declaredVersion(element) {
30
+ const localName = xsltLocalName(element);
31
+ if (localName === "stylesheet" || localName === "transform") {
32
+ return element.getAttribute("version");
33
+ }
34
+ if (localName !== null) return null;
35
+ return element.getAttributeNS?.(XSLT_NAMESPACE, "version") ?? null;
36
+ }
37
+
38
+ /**
39
+ * Whether a stylesheet element is processed in forwards-compatible mode.
40
+ * Cached per element.
41
+ *
42
+ * @param {Node|null} node - A stylesheet node
43
+ * @returns {boolean} True when the version in effect is not "1.0"
44
+ *
45
+ * @example
46
+ * isForwardsCompatible(instruction); // true under version="2.0"
47
+ */
48
+ export function isForwardsCompatible(node) {
49
+ if (node?.nodeType !== 1) return false;
50
+
51
+ let forwards = modes.get(node);
52
+ if (forwards === undefined) {
53
+ const version = declaredVersion(node);
54
+ forwards =
55
+ version === null || version === ""
56
+ ? isForwardsCompatible(node.parentNode)
57
+ : version.trim() !== "1.0";
58
+ modes.set(node, forwards);
59
+ }
60
+ return forwards;
61
+ }
62
+
63
+ /**
64
+ * The `xsl:fallback` children of an instruction, in document order.
65
+ *
66
+ * @param {Element} instruction - An XSLT instruction
67
+ * @returns {Element[]} The fallback elements
68
+ */
69
+ export function fallbackChildren(instruction) {
70
+ const fallbacks = [];
71
+ for (let child = instruction.firstChild; child; child = child.nextSibling) {
72
+ if (xsltLocalName(child) === "fallback") fallbacks.push(child);
73
+ }
74
+ return fallbacks;
75
+ }