@tradik/xslt3 0.0.0-stage → 1.0.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 (286) hide show
  1. package/LICENSE.md +28 -0
  2. package/README.md +98 -2
  3. package/package.json +43 -4
  4. package/src/errors.js +20 -0
  5. package/src/functions/aggregates.js +144 -0
  6. package/src/functions/arraySort.js +33 -0
  7. package/src/functions/arrays.js +181 -0
  8. package/src/functions/attributeTypes.js +58 -0
  9. package/src/functions/baseUris.js +82 -0
  10. package/src/functions/collations.js +127 -0
  11. package/src/functions/collections.js +84 -0
  12. package/src/functions/context.js +83 -0
  13. package/src/functions/core.js +167 -0
  14. package/src/functions/datetime.js +203 -0
  15. package/src/functions/deepEqual.js +132 -0
  16. package/src/functions/diagnostics.js +88 -0
  17. package/src/functions/focus.js +34 -0
  18. package/src/functions/format/dateComponents.js +176 -0
  19. package/src/functions/format/datePicture.js +115 -0
  20. package/src/functions/format/decimalFormat.js +65 -0
  21. package/src/functions/format/digitPattern.js +145 -0
  22. package/src/functions/format/formatDateTime.js +204 -0
  23. package/src/functions/format/formatInteger.js +109 -0
  24. package/src/functions/format/formatNumber.js +165 -0
  25. package/src/functions/format/fractionFormat.js +80 -0
  26. package/src/functions/format/germanWords.js +89 -0
  27. package/src/functions/format/numberLanguages.js +41 -0
  28. package/src/functions/format/numberPicture.js +202 -0
  29. package/src/functions/format/numberWords.js +155 -0
  30. package/src/functions/format/place.js +74 -0
  31. package/src/functions/format/timezoneFormat.js +89 -0
  32. package/src/functions/higherOrder.js +106 -0
  33. package/src/functions/ids.js +127 -0
  34. package/src/functions/json/index.js +17 -0
  35. package/src/functions/json/jsonChars.js +110 -0
  36. package/src/functions/json/jsonParser.js +203 -0
  37. package/src/functions/json/jsonStrings.js +116 -0
  38. package/src/functions/json/jsonToXml.js +116 -0
  39. package/src/functions/json/parseJson.js +119 -0
  40. package/src/functions/json/xmlRepresentation.js +103 -0
  41. package/src/functions/json/xmlToJson.js +147 -0
  42. package/src/functions/library.js +47 -0
  43. package/src/functions/mapArrayEqual.js +41 -0
  44. package/src/functions/maps.js +152 -0
  45. package/src/functions/math.js +73 -0
  46. package/src/functions/nodePaths.js +181 -0
  47. package/src/functions/nodes.js +199 -0
  48. package/src/functions/numeric.js +148 -0
  49. package/src/functions/options.js +58 -0
  50. package/src/functions/parseIetfDate.js +104 -0
  51. package/src/functions/parseXml.js +77 -0
  52. package/src/functions/qnames.js +68 -0
  53. package/src/functions/random.js +128 -0
  54. package/src/functions/regex/analyzeString.js +144 -0
  55. package/src/functions/regex/charClass.js +112 -0
  56. package/src/functions/regex/compile.js +81 -0
  57. package/src/functions/regex/escapes.js +109 -0
  58. package/src/functions/regex/parser.js +169 -0
  59. package/src/functions/regex/reader.js +37 -0
  60. package/src/functions/regex/regexFunctions.js +140 -0
  61. package/src/functions/regex/translate.js +102 -0
  62. package/src/functions/regex/unicodeBlocks.js +194 -0
  63. package/src/functions/registry.js +140 -0
  64. package/src/functions/sequences.js +173 -0
  65. package/src/functions/serializeFunction.js +53 -0
  66. package/src/functions/signatures.js +105 -0
  67. package/src/functions/sort.js +102 -0
  68. package/src/functions/stringMatch.js +45 -0
  69. package/src/functions/strings.js +194 -0
  70. package/src/functions/stringsCore.js +66 -0
  71. package/src/functions/support.js +106 -0
  72. package/src/functions/textDecoding.js +117 -0
  73. package/src/functions/ucaCollation.js +127 -0
  74. package/src/functions/unparsedText.js +71 -0
  75. package/src/functions/uri.js +166 -0
  76. package/src/functions/xqueryModule.js +24 -0
  77. package/src/index.js +38 -0
  78. package/src/items/array.js +84 -0
  79. package/src/items/function.js +45 -0
  80. package/src/items/map.js +120 -0
  81. package/src/items/mapKey.js +82 -0
  82. package/src/serialize/index.js +129 -0
  83. package/src/serialize/markup/content.js +74 -0
  84. package/src/serialize/markup/dialect.js +178 -0
  85. package/src/serialize/markup/htmlData.js +70 -0
  86. package/src/serialize/markup/namespaces.js +145 -0
  87. package/src/serialize/markup/prolog.js +91 -0
  88. package/src/serialize/markup/writer.js +183 -0
  89. package/src/serialize/methods/adaptive.js +131 -0
  90. package/src/serialize/methods/json.js +158 -0
  91. package/src/serialize/methods/text.js +43 -0
  92. package/src/serialize/output/buffer.js +48 -0
  93. package/src/serialize/output/encoding.js +146 -0
  94. package/src/serialize/output/expander.js +204 -0
  95. package/src/serialize/params/fromElement.js +141 -0
  96. package/src/serialize/params/fromMap.js +128 -0
  97. package/src/serialize/params/names.js +69 -0
  98. package/src/serialize/params/settings.js +153 -0
  99. package/src/serialize/params/values.js +163 -0
  100. package/src/serialize/sequence.js +111 -0
  101. package/src/xdm/arithmetic.js +155 -0
  102. package/src/xdm/atomic.js +142 -0
  103. package/src/xdm/binary.js +92 -0
  104. package/src/xdm/calendar.js +71 -0
  105. package/src/xdm/cast.js +176 -0
  106. package/src/xdm/compare.js +192 -0
  107. package/src/xdm/datetime.js +206 -0
  108. package/src/xdm/decimal.js +200 -0
  109. package/src/xdm/duration.js +128 -0
  110. package/src/xdm/generalCompare.js +152 -0
  111. package/src/xdm/index.js +47 -0
  112. package/src/xdm/lexical.js +126 -0
  113. package/src/xdm/nodes.js +170 -0
  114. package/src/xdm/numeric.js +122 -0
  115. package/src/xdm/qname.js +65 -0
  116. package/src/xdm/strings.js +52 -0
  117. package/src/xdm/temporalArithmetic.js +180 -0
  118. package/src/xdm/timeline.js +68 -0
  119. package/src/xdm/types.js +178 -0
  120. package/src/xpath/eval/atomics.js +45 -0
  121. package/src/xpath/eval/axes.js +212 -0
  122. package/src/xpath/eval/calls.js +168 -0
  123. package/src/xpath/eval/coercion.js +146 -0
  124. package/src/xpath/eval/compiler.js +71 -0
  125. package/src/xpath/eval/constructors.js +129 -0
  126. package/src/xpath/eval/decimalFormats.js +93 -0
  127. package/src/xpath/eval/descendantMemo.js +77 -0
  128. package/src/xpath/eval/documentOrder.js +142 -0
  129. package/src/xpath/eval/domNodes.js +174 -0
  130. package/src/xpath/eval/dynamicCall.js +27 -0
  131. package/src/xpath/eval/dynamicContext.js +161 -0
  132. package/src/xpath/eval/flow.js +115 -0
  133. package/src/xpath/eval/functionItems.js +194 -0
  134. package/src/xpath/eval/groupedChildren.js +88 -0
  135. package/src/xpath/eval/inline.js +71 -0
  136. package/src/xpath/eval/namespaceNodes.js +90 -0
  137. package/src/xpath/eval/nodeTests.js +164 -0
  138. package/src/xpath/eval/operators.js +190 -0
  139. package/src/xpath/eval/paths.js +163 -0
  140. package/src/xpath/eval/positional.js +81 -0
  141. package/src/xpath/eval/predicates.js +56 -0
  142. package/src/xpath/eval/primary.js +99 -0
  143. package/src/xpath/eval/resources.js +149 -0
  144. package/src/xpath/eval/scope.js +108 -0
  145. package/src/xpath/eval/sequenceType.js +193 -0
  146. package/src/xpath/eval/staticContext.js +142 -0
  147. package/src/xpath/eval/steps.js +98 -0
  148. package/src/xpath/eval/subtype.js +154 -0
  149. package/src/xpath/eval/typeExprs.js +147 -0
  150. package/src/xpath/eval/typeNames.js +101 -0
  151. package/src/xpath/eval/uris.js +21 -0
  152. package/src/xpath/eval/values.js +64 -0
  153. package/src/xpath/index.js +217 -0
  154. package/src/xpath/syntax/ast.js +130 -0
  155. package/src/xpath/syntax/chars.js +58 -0
  156. package/src/xpath/syntax/constructors.js +144 -0
  157. package/src/xpath/syntax/expressions.js +107 -0
  158. package/src/xpath/syntax/index.js +44 -0
  159. package/src/xpath/syntax/kindTests.js +134 -0
  160. package/src/xpath/syntax/lexer.js +220 -0
  161. package/src/xpath/syntax/operators.js +111 -0
  162. package/src/xpath/syntax/paths.js +102 -0
  163. package/src/xpath/syntax/postfix.js +91 -0
  164. package/src/xpath/syntax/primaries.js +149 -0
  165. package/src/xpath/syntax/steps.js +146 -0
  166. package/src/xpath/syntax/syntaxError.js +31 -0
  167. package/src/xpath/syntax/tokenStream.js +163 -0
  168. package/src/xpath/syntax/typeAst.js +76 -0
  169. package/src/xpath/syntax/typeOperators.js +127 -0
  170. package/src/xpath/syntax/types.js +158 -0
  171. package/src/xslt/api.js +153 -0
  172. package/src/xslt/compiler/acceptance.js +178 -0
  173. package/src/xslt/compiler/accumulatorDecl.js +146 -0
  174. package/src/xslt/compiler/attributeValues.js +174 -0
  175. package/src/xslt/compiler/attributes.js +211 -0
  176. package/src/xslt/compiler/avt.js +103 -0
  177. package/src/xslt/compiler/body.js +154 -0
  178. package/src/xslt/compiler/children.js +105 -0
  179. package/src/xslt/compiler/compatibility.js +130 -0
  180. package/src/xslt/compiler/components.js +179 -0
  181. package/src/xslt/compiler/contextItem.js +95 -0
  182. package/src/xslt/compiler/decimalFormats.js +132 -0
  183. package/src/xslt/compiler/declarationKinds.js +82 -0
  184. package/src/xslt/compiler/elementInfo.js +246 -0
  185. package/src/xslt/compiler/expressions.js +175 -0
  186. package/src/xslt/compiler/globalContextItem.js +92 -0
  187. package/src/xslt/compiler/globals.js +209 -0
  188. package/src/xslt/compiler/linker.js +134 -0
  189. package/src/xslt/compiler/miscDecl.js +171 -0
  190. package/src/xslt/compiler/modeDecl.js +88 -0
  191. package/src/xslt/compiler/moduleLoading.js +111 -0
  192. package/src/xslt/compiler/modules.js +158 -0
  193. package/src/xslt/compiler/outputDecl.js +209 -0
  194. package/src/xslt/compiler/overrideLinks.js +141 -0
  195. package/src/xslt/compiler/packageChecks.js +84 -0
  196. package/src/xslt/compiler/packageRegistry.js +95 -0
  197. package/src/xslt/compiler/packageTable.js +181 -0
  198. package/src/xslt/compiler/packageVersions.js +143 -0
  199. package/src/xslt/compiler/packages.js +138 -0
  200. package/src/xslt/compiler/registry.js +234 -0
  201. package/src/xslt/compiler/staticStage.js +197 -0
  202. package/src/xslt/compiler/staticVariables.js +93 -0
  203. package/src/xslt/compiler/stylesheet.js +205 -0
  204. package/src/xslt/compiler/tailPosition.js +74 -0
  205. package/src/xslt/compiler/templatesDecl.js +180 -0
  206. package/src/xslt/compiler/useAccumulators.js +76 -0
  207. package/src/xslt/compiler/useWhen.js +94 -0
  208. package/src/xslt/compiler/visibility.js +160 -0
  209. package/src/xslt/index.js +11 -0
  210. package/src/xslt/instructions/analyzeString.js +137 -0
  211. package/src/xslt/instructions/assert.js +72 -0
  212. package/src/xslt/instructions/attributeSets.js +115 -0
  213. package/src/xslt/instructions/conditional.js +99 -0
  214. package/src/xslt/instructions/control.js +123 -0
  215. package/src/xslt/instructions/copy.js +151 -0
  216. package/src/xslt/instructions/elements.js +161 -0
  217. package/src/xslt/instructions/evaluate.js +189 -0
  218. package/src/xslt/instructions/fork.js +35 -0
  219. package/src/xslt/instructions/grouping.js +106 -0
  220. package/src/xslt/instructions/index.js +94 -0
  221. package/src/xslt/instructions/iterate.js +182 -0
  222. package/src/xslt/instructions/literal.js +142 -0
  223. package/src/xslt/instructions/maps.js +72 -0
  224. package/src/xslt/instructions/merge.js +190 -0
  225. package/src/xslt/instructions/number.js +205 -0
  226. package/src/xslt/instructions/output.js +102 -0
  227. package/src/xslt/instructions/params.js +110 -0
  228. package/src/xslt/instructions/resultDocument.js +143 -0
  229. package/src/xslt/instructions/sort.js +114 -0
  230. package/src/xslt/instructions/sourceDocument.js +73 -0
  231. package/src/xslt/instructions/templates.js +153 -0
  232. package/src/xslt/instructions/text.js +151 -0
  233. package/src/xslt/instructions/try.js +186 -0
  234. package/src/xslt/instructions/variables.js +119 -0
  235. package/src/xslt/names.js +204 -0
  236. package/src/xslt/patterns/axisOrigins.js +121 -0
  237. package/src/xslt/patterns/compile.js +151 -0
  238. package/src/xslt/patterns/general.js +132 -0
  239. package/src/xslt/patterns/grammar.js +190 -0
  240. package/src/xslt/patterns/modes.js +153 -0
  241. package/src/xslt/patterns/paths.js +130 -0
  242. package/src/xslt/patterns/patternChecks.js +29 -0
  243. package/src/xslt/patterns/steps.js +162 -0
  244. package/src/xslt/processor.js +178 -0
  245. package/src/xslt/runtime/accumulatorFunctions.js +48 -0
  246. package/src/xslt/runtime/accumulators.js +174 -0
  247. package/src/xslt/runtime/apply.js +153 -0
  248. package/src/xslt/runtime/atomicKeys.js +36 -0
  249. package/src/xslt/runtime/availability.js +162 -0
  250. package/src/xslt/runtime/baseUri.js +64 -0
  251. package/src/xslt/runtime/compilerHook.js +27 -0
  252. package/src/xslt/runtime/conditional.js +110 -0
  253. package/src/xslt/runtime/context.js +106 -0
  254. package/src/xslt/runtime/copy.js +123 -0
  255. package/src/xslt/runtime/copyFunctions.js +71 -0
  256. package/src/xslt/runtime/copyOrigins.js +60 -0
  257. package/src/xslt/runtime/defaultMethod.js +42 -0
  258. package/src/xslt/runtime/documentContent.js +93 -0
  259. package/src/xslt/runtime/dynamicXPath.js +90 -0
  260. package/src/xslt/runtime/functionCache.js +65 -0
  261. package/src/xslt/runtime/functionCall.js +57 -0
  262. package/src/xslt/runtime/functions.js +191 -0
  263. package/src/xslt/runtime/groups.js +108 -0
  264. package/src/xslt/runtime/invocation.js +156 -0
  265. package/src/xslt/runtime/iteration.js +107 -0
  266. package/src/xslt/runtime/keys.js +141 -0
  267. package/src/xslt/runtime/lazy.js +97 -0
  268. package/src/xslt/runtime/locations.js +86 -0
  269. package/src/xslt/runtime/machine.js +137 -0
  270. package/src/xslt/runtime/merging.js +167 -0
  271. package/src/xslt/runtime/namespaceScope.js +141 -0
  272. package/src/xslt/runtime/numberFormat.js +89 -0
  273. package/src/xslt/runtime/numbering.js +168 -0
  274. package/src/xslt/runtime/params.js +59 -0
  275. package/src/xslt/runtime/properties.js +103 -0
  276. package/src/xslt/runtime/rawResults.js +82 -0
  277. package/src/xslt/runtime/sequenceReceiver.js +102 -0
  278. package/src/xslt/runtime/snapshot.js +66 -0
  279. package/src/xslt/runtime/sortKeys.js +119 -0
  280. package/src/xslt/runtime/streamAvailable.js +60 -0
  281. package/src/xslt/runtime/strip.js +102 -0
  282. package/src/xslt/runtime/transformFunction.js +169 -0
  283. package/src/xslt/runtime/transformation.js +168 -0
  284. package/src/xslt/runtime/transformationContext.js +85 -0
  285. package/src/xslt/runtime/treeReceiver.js +194 -0
  286. package/src/xslt/runtime/values.js +92 -0
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Picture strings of format-dateTime, format-date and format-time (F&O
3
+ * 3.1 sections 9.8.4.1 and 9.8.4.2): literal text, variable markers with
4
+ * their component, presentation modifiers and width modifier.
5
+ *
6
+ * @module @tradik/xslt3/functions/format/datePicture
7
+ */
8
+
9
+ import { XPathError } from "../../errors.js";
10
+
11
+ /**
12
+ * @typedef {object} Marker
13
+ * @property {string} component - One of YMDdFWwHhPmsfZzCE
14
+ * @property {string|null} first - First presentation modifier, null for
15
+ * the component's default
16
+ * @property {string|null} second - "a", "t", "c" or "o"
17
+ * @property {number|null} minWidth - null when absent or "*"
18
+ * @property {number|null} maxWidth - null when absent or "*"
19
+ */
20
+
21
+ /**
22
+ * @param {string} message
23
+ * @returns {never}
24
+ * @throws {XPathError} FOFD1340
25
+ */
26
+ export function dateFormatError(message) {
27
+ throw new XPathError("FOFD1340", `Invalid date/time picture: ${message}`);
28
+ }
29
+
30
+ const COMPONENTS = "YMDdFWwHhPmsfZzCE";
31
+ const widthPattern = /^(\*|\d+)(?:-(\*|\d+))?$/;
32
+
33
+ /**
34
+ * Parses the width modifier ("min" or "min-max", "*" for no limit).
35
+ * @param {string} text
36
+ * @returns {{minWidth: number|null, maxWidth: number|null}}
37
+ */
38
+ function parseWidth(text) {
39
+ const match = widthPattern.exec(text);
40
+ if (!match) dateFormatError(`width modifier ,${text}`);
41
+ const [, min, max = "*"] = match;
42
+ const minWidth = min === "*" ? null : Number(min);
43
+ const maxWidth = max === "*" ? null : Number(max);
44
+ if (
45
+ minWidth === 0 ||
46
+ maxWidth === 0 ||
47
+ (maxWidth !== null && maxWidth < (minWidth ?? 0))
48
+ ) {
49
+ dateFormatError(`width modifier ,${text}`);
50
+ }
51
+ return { minWidth, maxWidth };
52
+ }
53
+
54
+ /**
55
+ * Parses the inside of a variable marker.
56
+ * @param {string} text - Between the brackets
57
+ * @returns {Marker}
58
+ */
59
+ export function parseMarker(text) {
60
+ const marker = text.replace(/[ \t\n\r]+/g, "");
61
+ const component = marker[0];
62
+ if (component === undefined || !COMPONENTS.includes(component)) {
63
+ dateFormatError(`unknown component in [${text}]`);
64
+ }
65
+ const comma = marker.lastIndexOf(",");
66
+ const modifiers = comma < 0 ? marker.slice(1) : marker.slice(1, comma);
67
+ const width =
68
+ comma < 0
69
+ ? { minWidth: null, maxWidth: null }
70
+ : parseWidth(marker.slice(comma + 1));
71
+ const chars = Array.from(modifiers);
72
+ const hasSecond = chars.length > 1 && "atco".includes(chars.at(-1));
73
+ return {
74
+ component,
75
+ first:
76
+ modifiers === ""
77
+ ? null
78
+ : hasSecond
79
+ ? chars.slice(0, -1).join("")
80
+ : modifiers,
81
+ second: hasSecond ? chars.at(-1) : null,
82
+ ...width,
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Splits a picture into literal strings and markers.
88
+ * @param {string} picture
89
+ * @returns {Array<string|Marker>}
90
+ * @throws {XPathError} FOFD1340 for unbalanced brackets or invalid markers
91
+ */
92
+ export function parseDatePicture(picture) {
93
+ const parts = [];
94
+ let literal = "";
95
+ for (let i = 0; i < picture.length; i++) {
96
+ const c = picture[i];
97
+ if ((c === "[" || c === "]") && picture[i + 1] === c) {
98
+ literal += c;
99
+ i++;
100
+ } else if (c === "[") {
101
+ const end = picture.indexOf("]", i);
102
+ if (end < 0) dateFormatError("unterminated [");
103
+ if (literal) parts.push(literal);
104
+ literal = "";
105
+ parts.push(parseMarker(picture.slice(i + 1, end)));
106
+ i = end;
107
+ } else if (c === "]") {
108
+ dateFormatError("unescaped ]");
109
+ } else {
110
+ literal += c;
111
+ }
112
+ }
113
+ if (literal) parts.push(literal);
114
+ return parts;
115
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Decimal formats of fn:format-number (XPath 3.1 static context, XSLT
3
+ * `xsl:decimal-format`).
4
+ *
5
+ * The evaluator supplies the formats through the dynamic context hook
6
+ * `context.decimalFormats`, an object with `get(name)`: it receives the
7
+ * format name as written in the third argument with surrounding whitespace
8
+ * removed (a lexical QName or `Q{uri}local`, so the hook resolves the
9
+ * prefix), or "" for the unnamed format, and returns a partial
10
+ * {@link DecimalFormat} (missing properties take the defaults) or
11
+ * undefined when no such format is declared.
12
+ *
13
+ * @module @tradik/xslt3/functions/format/decimalFormat
14
+ */
15
+
16
+ import { XPathError } from "../../errors.js";
17
+
18
+ /**
19
+ * Properties of a decimal format; all but `infinity` and `nan` are single
20
+ * characters.
21
+ * @typedef {object} DecimalFormat
22
+ * @property {string} decimalSeparator
23
+ * @property {string} groupingSeparator
24
+ * @property {string} exponentSeparator
25
+ * @property {string} infinity
26
+ * @property {string} minusSign
27
+ * @property {string} nan
28
+ * @property {string} percent
29
+ * @property {string} perMille
30
+ * @property {string} zeroDigit - First digit of the decimal digit family
31
+ * @property {string} digit - The optional digit sign
32
+ * @property {string} patternSeparator
33
+ */
34
+
35
+ /** @type {Readonly<DecimalFormat>} the default (unnamed) decimal format */
36
+ export const DEFAULT_DECIMAL_FORMAT = Object.freeze({
37
+ decimalSeparator: ".",
38
+ groupingSeparator: ",",
39
+ exponentSeparator: "e",
40
+ infinity: "Infinity",
41
+ minusSign: "-",
42
+ nan: "NaN",
43
+ percent: "%",
44
+ perMille: "‰",
45
+ zeroDigit: "0",
46
+ digit: "#",
47
+ patternSeparator: ";",
48
+ });
49
+
50
+ /**
51
+ * Looks up a decimal format.
52
+ * @param {string|undefined} name - The third argument of format-number,
53
+ * undefined when absent or empty
54
+ * @param {{decimalFormats?: {get: (name: string) => (Partial<DecimalFormat>|undefined)}}} context
55
+ * @returns {DecimalFormat}
56
+ * @throws {XPathError} FODF1280 for an unknown format name
57
+ */
58
+ export function getDecimalFormat(name, context) {
59
+ const key = name?.trim() ?? "";
60
+ const custom = context.decimalFormats?.get(key);
61
+ if (custom === undefined && key !== "") {
62
+ throw new XPathError("FODF1280", `Unknown decimal format ${key}`);
63
+ }
64
+ return { ...DEFAULT_DECIMAL_FORMAT, ...custom };
65
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Decimal digit patterns of format-integer and format-date/time pictures
3
+ * (F&O 3.1 section 4.6.1): mandatory digits of one digit family, optional
4
+ * "#" digits and grouping separators, regular or not.
5
+ *
6
+ * @module @tradik/xslt3/functions/format/digitPattern
7
+ */
8
+
9
+ import { XPathError } from "../../errors.js";
10
+
11
+ const isDigitChar = (c) => /^\p{Nd}$/u.test(c);
12
+ const isAlphanumeric = (c) => /^[\p{L}\p{N}]$/u.test(c);
13
+
14
+ /**
15
+ * @param {string} message
16
+ * @returns {never}
17
+ * @throws {XPathError} FODF1310
18
+ */
19
+ export function pictureError(message) {
20
+ throw new XPathError("FODF1310", `Invalid picture string: ${message}`);
21
+ }
22
+
23
+ /**
24
+ * The zero digit of the family of a decimal digit: Unicode digit runs are
25
+ * contiguous blocks of ten starting at their zero.
26
+ * @param {string} digit - A character of category Nd
27
+ * @returns {number} codepoint of the zero
28
+ */
29
+ export function zeroOf(digit) {
30
+ const cp = digit.codePointAt(0);
31
+ let start = cp;
32
+ while (isDigitChar(String.fromCodePoint(start - 1))) start--;
33
+ return cp - ((cp - start) % 10);
34
+ }
35
+
36
+ /**
37
+ * @typedef {object} DigitPattern
38
+ * @property {number} zero - Codepoint of the zero digit
39
+ * @property {number} mandatory - Number of mandatory digits
40
+ * @property {number} digits - Number of digit signs, "#" included
41
+ * @property {Map<number, string>} separators - Separator by number of
42
+ * digit signs to its right
43
+ * @property {number} grouping - Regular grouping size, 0 when irregular
44
+ */
45
+
46
+ /**
47
+ * Whether a format token is a decimal digit pattern: it has a decimal
48
+ * digit ("#" alone falls back to the default format, see W3C bug 19004).
49
+ * @param {string} token
50
+ * @returns {boolean}
51
+ */
52
+ export const isDigitPattern = (token) => Array.from(token).some(isDigitChar);
53
+
54
+ /**
55
+ * Parses a decimal digit pattern.
56
+ * @param {string} token
57
+ * @returns {DigitPattern}
58
+ * @throws {XPathError} FODF1310 when invalid
59
+ */
60
+ export function parseDigitPattern(token) {
61
+ const chars = Array.from(token);
62
+ let zero = null;
63
+ let mandatory = 0;
64
+ const marks = [];
65
+ chars.forEach((c, i) => {
66
+ if (c === "#") {
67
+ if (mandatory > 0) pictureError(`"#" after a digit in ${token}`);
68
+ marks.push(null);
69
+ } else if (isDigitChar(c)) {
70
+ if (zero !== null && zeroOf(c) !== zero) {
71
+ pictureError(`digits of different families in ${token}`);
72
+ }
73
+ zero = zeroOf(c);
74
+ mandatory++;
75
+ marks.push(null);
76
+ } else if (isAlphanumeric(c)) {
77
+ pictureError(`letter ${c} in ${token}`);
78
+ } else {
79
+ if (i === 0 || i === chars.length - 1 || marks.at(-1) !== null) {
80
+ pictureError(`misplaced grouping separator in ${token}`);
81
+ }
82
+ marks.push(c);
83
+ }
84
+ });
85
+ if (zero === null) pictureError(`no mandatory digit in ${token}`);
86
+ const digits = marks.filter((m) => m === null).length;
87
+ const separators = new Map();
88
+ let right = 0;
89
+ for (let i = marks.length - 1; i >= 0; i--) {
90
+ if (marks[i] === null) right++;
91
+ else separators.set(right, marks[i]);
92
+ }
93
+ return {
94
+ zero,
95
+ mandatory,
96
+ digits,
97
+ separators,
98
+ grouping: regularGrouping(separators, digits),
99
+ };
100
+ }
101
+
102
+ /**
103
+ * The grouping size when the separators are regular (one character, at
104
+ * every multiple of the first position), else 0.
105
+ * @param {Map<number, string>} separators
106
+ * @param {number} digits
107
+ * @returns {number}
108
+ */
109
+ export function regularGrouping(separators, digits) {
110
+ if (separators.size === 0) return 0;
111
+ const size = Math.min(...separators.keys());
112
+ const chars = new Set(separators.values());
113
+ for (const position of separators.keys()) {
114
+ if (position % size !== 0) return 0;
115
+ }
116
+ for (let position = size; position < digits; position += size) {
117
+ if (!separators.has(position)) return 0;
118
+ }
119
+ return chars.size === 1 ? size : 0;
120
+ }
121
+
122
+ /**
123
+ * Formats a non-negative integer with a digit pattern.
124
+ * @param {bigint|string} value - Non-negative integer, or its ASCII digits
125
+ * @param {DigitPattern} pattern
126
+ * @param {number} [minDigits] - Overrides the mandatory digit count
127
+ * @returns {string}
128
+ */
129
+ export function formatDigits(value, pattern, minDigits = pattern.mandatory) {
130
+ const ascii = String(value).padStart(minDigits, "0");
131
+ const { zero, separators, grouping } = pattern;
132
+ const separator = grouping ? separators.get(grouping) : null;
133
+ let result = "";
134
+ for (let i = ascii.length - 1, count = 0; i >= 0; i--) {
135
+ if (count > 0) {
136
+ const mark = grouping
137
+ ? count % grouping === 0 && separator
138
+ : separators.get(count);
139
+ if (mark) result = mark + result;
140
+ }
141
+ result = String.fromCodePoint(zero + Number(ascii[i])) + result;
142
+ count++;
143
+ }
144
+ return result;
145
+ }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * fn:format-dateTime, fn:format-date and fn:format-time (F&O 3.1 section
3
+ * 9.8). Output is English with the Gregorian ("AD") or ISO calendar;
4
+ * other languages produce English prefixed with "[Language: en]", other
5
+ * calendars the Gregorian form prefixed with "[Calendar: AD]". A place
6
+ * that is an IANA timezone name adjusts the value (see place.js).
7
+ *
8
+ * @module @tradik/xslt3/functions/format/formatDateTime
9
+ */
10
+
11
+ import { define, stringItem } from "../support.js";
12
+ import {
13
+ componentName,
14
+ componentValue,
15
+ isAvailable,
16
+ presentName,
17
+ } from "./dateComponents.js";
18
+ import { dateFormatError, parseDatePicture } from "./datePicture.js";
19
+ import {
20
+ formatDigits,
21
+ isDigitPattern,
22
+ parseDigitPattern,
23
+ } from "./digitPattern.js";
24
+ import { formatToken } from "./formatInteger.js";
25
+ import { formatFraction } from "./fractionFormat.js";
26
+ import { applyPlace } from "./place.js";
27
+ import { formatTimezone } from "./timezoneFormat.js";
28
+ import { XPathError } from "../../errors.js";
29
+
30
+ const NAMED = new Set(["n", "N", "Nn"]);
31
+ /** Default presentation modifiers other than "1". */
32
+ /* calendar and era names keep their case ("AD") by default */
33
+ const DEFAULT_TOKENS = { m: "01", s: "01", F: "n", P: "n", C: "Nn", E: "Nn" };
34
+ const NUMBER_TOKENS = /^(a|A|i|I|w|W|Ww)$/;
35
+ const CALENDARS = new Set(
36
+ (
37
+ "AD AH AME AM AP AS BE CB CE CL CS EE FE ISO JE KE KY ME MS NS OS RS " +
38
+ "SE SH SS TE VE VS"
39
+ ).split(" "),
40
+ );
41
+
42
+ /**
43
+ * Formats a numeric component with a digit pattern and width.
44
+ * @param {number} n - Non-negative
45
+ * @param {import("./datePicture.js").Marker} marker
46
+ * @param {string} token - A decimal digit pattern
47
+ * @returns {string}
48
+ */
49
+ function formatNumeric(n, marker, token) {
50
+ const pattern = parseDigitPattern(token);
51
+ let value = BigInt(n);
52
+ if (marker.component === "Y") {
53
+ const width = marker.maxWidth ?? (pattern.digits >= 2 ? pattern.digits : 0);
54
+ if (width) value %= 10n ** BigInt(width);
55
+ }
56
+ const digits = formatDigits(
57
+ value,
58
+ pattern,
59
+ Math.max(pattern.mandatory, marker.minWidth ?? 0),
60
+ );
61
+ return marker.second === "o" ? formatToken(value, token, true) : digits;
62
+ }
63
+
64
+ /**
65
+ * Formats one variable marker.
66
+ * @param {import("./datePicture.js").Marker} marker
67
+ * @param {import("../../xdm/datetime.js").DateTimeValue} value
68
+ * @param {string} calendar
69
+ * @param {string|null} zone - Timezone name from the place argument
70
+ * @returns {string}
71
+ */
72
+ function formatMarker(marker, value, calendar, zone) {
73
+ const { component, minWidth, maxWidth } = marker;
74
+ if (component === "Z" || component === "z") {
75
+ return formatTimezone(value.timezone, marker, zone);
76
+ }
77
+ if (component === "f") return formatFraction(value.second, marker);
78
+ const name = componentName(value, component, calendar);
79
+ const fallback = DEFAULT_TOKENS[component] ?? "1";
80
+ let token = marker.first ?? fallback;
81
+ if (NAMED.has(token) && name === null) token = fallback;
82
+ if (NAMED.has(token)) return presentName(name, token, minWidth, maxWidth);
83
+ if ("PCE".includes(component)) {
84
+ return presentName(name, fallback, minWidth, maxWidth);
85
+ }
86
+ const n = componentValue(value, component);
87
+ if (isDigitPattern(token)) return formatNumeric(n, marker, token);
88
+ if (NUMBER_TOKENS.test(token)) {
89
+ // the year is shown modulo 10^max-width (F&O 3.1 section 9.8.4.4)
90
+ const shown = component === "Y" && maxWidth ? n % 10 ** maxWidth : n;
91
+ const text = formatToken(BigInt(shown), token, marker.second === "o");
92
+ return text.padEnd(minWidth ?? 0, " ");
93
+ }
94
+ return fallback === "n"
95
+ ? presentName(name, "n", minWidth, maxWidth)
96
+ : formatNumeric(n, marker, fallback);
97
+ }
98
+
99
+ const EQNAME =
100
+ /^(Q\{[^{}]*\}|[\p{L}_][\p{L}\p{N}._-]*:)[\p{L}_][\p{L}\p{N}._-]*$/u;
101
+
102
+ /**
103
+ * The calendar used for a calendar argument: AD, ISO or CE are supported;
104
+ * other known designators and calendars in a namespace fall back to AD
105
+ * with a "[Calendar: AD]" prefix.
106
+ * @param {string|null|undefined} calendar
107
+ * @returns {{used: string, prefix: string}}
108
+ * @throws {XPathError} FOFD1340 for an unknown calendar in no namespace
109
+ */
110
+ function resolveCalendar(calendar) {
111
+ const name = (calendar?.trim() ?? "").replace(/^Q\{\}/, "");
112
+ if (name === "" || name === "AD" || name === "ISO" || name === "CE") {
113
+ return { used: name || "AD", prefix: "" };
114
+ }
115
+ if (!CALENDARS.has(name) && !EQNAME.test(name)) {
116
+ dateFormatError(`unknown calendar ${calendar}`);
117
+ }
118
+ return { used: "AD", prefix: "[Calendar: AD]" };
119
+ }
120
+
121
+ /**
122
+ * Formats a date/time value with a picture string.
123
+ * @param {string} kind - "dateTime", "date" or "time"
124
+ * @param {import("../../xdm/datetime.js").DateTimeValue} value
125
+ * @param {string} picture
126
+ * @param {{language?: string|null, calendar?: string|null,
127
+ * place?: string|null}} [options]
128
+ * @returns {string}
129
+ * @throws {XPathError} FOFD1340 for an invalid picture or calendar,
130
+ * FOFD1350 for a component the value does not have
131
+ */
132
+ export function formatDateTime(kind, value, picture, options = {}) {
133
+ try {
134
+ return formatParts(kind, value, picture, options);
135
+ } catch (error) {
136
+ // invalid digit patterns inside markers are picture errors here
137
+ if (error.code !== "FODF1310") throw error;
138
+ return dateFormatError(error.message);
139
+ }
140
+ }
141
+
142
+ /**
143
+ * {@link formatDateTime} without the error code mapping.
144
+ * @param {string} kind
145
+ * @param {import("../../xdm/datetime.js").DateTimeValue} original
146
+ * @param {string} picture
147
+ * @param {object} options
148
+ * @returns {string}
149
+ */
150
+ function formatParts(kind, original, picture, options) {
151
+ const { value, zone } = applyPlace(kind, original, options.place);
152
+ const { used, prefix } = resolveCalendar(options.calendar);
153
+ const language = options.language?.trim();
154
+ const fallback =
155
+ language && !/^en(-|$)/i.test(language) ? "[Language: en]" : "";
156
+ const parts = parseDatePicture(picture).map((part) => {
157
+ if (typeof part === "string") return part;
158
+ if (!isAvailable(kind, part.component)) {
159
+ throw new XPathError(
160
+ "FOFD1350",
161
+ `[${part.component}] is not available in an xs:${kind}`,
162
+ );
163
+ }
164
+ return formatMarker(part, value, used, zone);
165
+ });
166
+ return prefix + fallback + parts.join("");
167
+ }
168
+
169
+ /**
170
+ * Declares the 2- and 5-argument forms of one function.
171
+ * @param {string} kind
172
+ * @returns {import("../support.js").FunctionDefinition[]}
173
+ */
174
+ function definitions(kind) {
175
+ const local = `format-${kind}`;
176
+ const param = `xs:${kind}?`;
177
+ const impl = ([value, [picture], language, calendar, place]) =>
178
+ value.length === 0
179
+ ? []
180
+ : [
181
+ stringItem(
182
+ formatDateTime(kind, value[0].value, picture.value, {
183
+ language: language?.[0]?.value,
184
+ calendar: calendar?.[0]?.value,
185
+ place: place?.[0]?.value,
186
+ }),
187
+ ),
188
+ ];
189
+ const S_OPT = "xs:string?";
190
+ return [
191
+ define(local, [param, "xs:string"], "xs:string?", impl),
192
+ define(
193
+ local,
194
+ [param, "xs:string", S_OPT, S_OPT, S_OPT],
195
+ "xs:string?",
196
+ impl,
197
+ ),
198
+ ];
199
+ }
200
+
201
+ /** @type {import("../support.js").FunctionDefinition[]} */
202
+ export const formatDateTimeFunctions = ["dateTime", "date", "time"].flatMap(
203
+ definitions,
204
+ );
@@ -0,0 +1,109 @@
1
+ /**
2
+ * fn:format-integer (F&O 3.1 section 4.6.1). Supported primary format
3
+ * tokens: decimal digit patterns in any Unicode digit family with
4
+ * grouping separators, "a", "A", "i", "I", "w", "W" and "Ww"; other tokens
5
+ * fall back to "1". The format modifier "o" gives ordinals ("1st",
6
+ * "first"). Words and ordinals follow the language argument where
7
+ * numberLanguages.js knows it (English and German), else English.
8
+ *
9
+ * @module @tradik/xslt3/functions/format/formatInteger
10
+ */
11
+
12
+ import { define, stringItem } from "../support.js";
13
+ import {
14
+ formatDigits,
15
+ isDigitPattern,
16
+ parseDigitPattern,
17
+ pictureError,
18
+ } from "./digitPattern.js";
19
+ import { numberLanguage } from "./numberLanguages.js";
20
+ import { alphabetic, caseWords, MAX_ROMAN, roman } from "./numberWords.js";
21
+
22
+ const DEFAULT_PATTERN = parseDigitPattern("1");
23
+
24
+ /**
25
+ * Formats a non-negative integer with a primary format token.
26
+ * @param {bigint} n - Non-negative
27
+ * @param {string} token - Primary format token
28
+ * @param {boolean|string} ordinal - Whether the "o" modifier is present,
29
+ * or its form (the text in parentheses, "" for none)
30
+ * @param {import("./numberLanguages.js").NumberLanguage} [language]
31
+ * @returns {string}
32
+ */
33
+ export function formatToken(n, token, ordinal, language = numberLanguage()) {
34
+ const form = ordinal === true ? "" : ordinal;
35
+ if (isDigitPattern(token)) {
36
+ const digits = formatDigits(n, parseDigitPattern(token));
37
+ return form === false ? digits : digits + language.suffix(n);
38
+ }
39
+ if ((token === "a" || token === "A") && n > 0n) return alphabetic(n, token);
40
+ if ((token === "i" || token === "I") && n > 0n && n <= MAX_ROMAN) {
41
+ const numerals = roman(n);
42
+ return token === "I" ? numerals.toUpperCase() : numerals;
43
+ }
44
+ if (token === "w" || token === "W" || token === "Ww") {
45
+ const words =
46
+ form === false ? language.cardinal(n) : language.ordinal(n, form);
47
+ return caseWords(words, token);
48
+ }
49
+ const digits = formatDigits(n, DEFAULT_PATTERN);
50
+ return form === false ? digits : digits + language.suffix(n);
51
+ }
52
+
53
+ /**
54
+ * Splits a picture into its primary format token and whether the format
55
+ * modifier asks for ordinal numbering.
56
+ * @param {string} picture
57
+ * @returns {{token: string, ordinal: boolean|string}} ordinal: false, or
58
+ * the text in parentheses after "o" ("" for none)
59
+ * @throws {XPathError} FODF1310 for an empty token or invalid modifier
60
+ */
61
+ export function parseIntegerPicture(picture) {
62
+ const semicolon = picture.lastIndexOf(";");
63
+ const token = semicolon < 0 ? picture : picture.slice(0, semicolon);
64
+ const modifier = semicolon < 0 ? "" : picture.slice(semicolon + 1);
65
+ if (token === "") pictureError("empty primary format token");
66
+ if (!/^([co](\(.+\))?)?[at]?$/su.test(modifier)) {
67
+ pictureError(`invalid format modifier ${modifier}`);
68
+ }
69
+ const form = /^o(?:\((.+)\))?/su.exec(modifier);
70
+ return { token, ordinal: form ? (form[1] ?? "") : false };
71
+ }
72
+
73
+ /**
74
+ * fn:format-integer on JS values.
75
+ * @param {bigint} value
76
+ * @param {string} picture
77
+ * @param {string|null} [lang] - Language tag
78
+ * @returns {string}
79
+ */
80
+ export function formatInteger(value, picture, lang = null) {
81
+ const { token, ordinal } = parseIntegerPicture(picture);
82
+ const negative = value < 0n;
83
+ const language = numberLanguage(lang);
84
+ const body = formatToken(negative ? -value : value, token, ordinal, language);
85
+ return negative ? `-${body}` : body;
86
+ }
87
+
88
+ /**
89
+ * @param {Array<*>[]} args - [$value, $picture, $lang?]
90
+ * @returns {Array<*>}
91
+ */
92
+ const impl = ([value, [picture], lang]) => [
93
+ stringItem(
94
+ value.length === 0
95
+ ? ""
96
+ : formatInteger(value[0].value, picture.value, lang?.[0]?.value),
97
+ ),
98
+ ];
99
+
100
+ /** @type {import("../support.js").FunctionDefinition[]} */
101
+ export const formatIntegerFunctions = [
102
+ define("format-integer", ["xs:integer?", "xs:string"], "xs:string", impl),
103
+ define(
104
+ "format-integer",
105
+ ["xs:integer?", "xs:string", "xs:string?"],
106
+ "xs:string",
107
+ impl,
108
+ ),
109
+ ];