@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
|
@@ -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
|
+
}
|
package/src/xslt/formatNumber.js
CHANGED
|
@@ -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 >
|
|
167
|
+
while (fraction.length > minFraction && fraction.endsWith("0")) {
|
|
157
168
|
fraction = fraction.slice(0, -1);
|
|
158
169
|
}
|
|
159
170
|
|
|
160
|
-
let integer = rawInteger
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
}
|