@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,203 @@
1
+ /**
2
+ * A strict JSON parser (RFC 7159) for fn:parse-json and fn:json-to-xml
3
+ * (F&O 3.1 section 17.5). Each value is handed to a builder as soon as it
4
+ * is read, so that each function makes its own result (DOM nodes, XDM
5
+ * items) without an intermediate tree; {@link TREE_BUILDER} makes a plain
6
+ * tree. Strings keep track of which characters were written as escape
7
+ * sequences, because the `escape` and `fallback` options treat them by
8
+ * their original form: a string without escape sequences is the slice of
9
+ * the text it is, the others a list of characters. The `liberal` option
10
+ * accepts nothing more (allowed: what a liberal parser accepts is
11
+ * implementation-defined).
12
+ *
13
+ * @module @tradik/xslt3/functions/json/jsonParser
14
+ */
15
+
16
+ import { XPathError } from "../../errors.js";
17
+ import { scanChars } from "./jsonChars.js";
18
+
19
+ /** @typedef {import("./jsonChars.js").JsonChar} JsonChar */
20
+
21
+ /**
22
+ * The characters of a JSON string: the string itself when it was written
23
+ * without escape sequences, else its characters.
24
+ * @typedef {string|JsonChar[]} JsonString
25
+ */
26
+
27
+ /**
28
+ * @typedef {{kind: "object", entries: Array<[JsonString, JsonValue]>}
29
+ * | {kind: "array", members: JsonValue[]}
30
+ * | {kind: "string", chars: JsonString}
31
+ * | {kind: "number", text: string}
32
+ * | {kind: "boolean", value: boolean}
33
+ * | {kind: "null"}} JsonValue
34
+ */
35
+
36
+ /**
37
+ * Makes the result of each JSON value from the results of its members.
38
+ * @template T
39
+ * @typedef {object} JsonBuilder
40
+ * @property {(entries: Array<[JsonString, T]>) => T} object
41
+ * @property {(members: T[]) => T} array
42
+ * @property {(chars: JsonString) => T} string
43
+ * @property {(text: string) => T} number - The number as written
44
+ * @property {(value: boolean) => T} boolean
45
+ * @property {() => T} null
46
+ */
47
+
48
+ /** @type {JsonBuilder<JsonValue>} The builder of a plain tree. */
49
+ export const TREE_BUILDER = Object.freeze({
50
+ object: (entries) => ({ kind: "object", entries }),
51
+ array: (members) => ({ kind: "array", members }),
52
+ string: (chars) => ({ kind: "string", chars }),
53
+ number: (text) => ({ kind: "number", text }),
54
+ boolean: (value) => ({ kind: "boolean", value }),
55
+ null: () => ({ kind: "null" }),
56
+ });
57
+
58
+ const LITERALS = new Set(["true", "false", "null"]);
59
+ const NUMBER = /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/y;
60
+ const WHITESPACE = /[ \t\n\r]*/y;
61
+ const WORD = /[a-z]+/y;
62
+ /** The body of a string up to a quote, a backslash or a control. */
63
+ // eslint-disable-next-line no-control-regex -- JSON strings exclude them
64
+ const PLAIN = /[^"\\\u0000-\u001f]*/y;
65
+
66
+ /**
67
+ * @param {string} message
68
+ * @returns {never}
69
+ * @throws {XPathError} FOJS0001
70
+ */
71
+ const syntaxError = (message) => {
72
+ throw new XPathError("FOJS0001", `Invalid JSON: ${message}`);
73
+ };
74
+
75
+ /** A recursive descent parser over one JSON text. */
76
+ class Parser {
77
+ /**
78
+ * @param {string} text
79
+ * @param {JsonBuilder<*>} builder
80
+ */
81
+ constructor(text, builder) {
82
+ this.text = text;
83
+ this.pos = 0;
84
+ this.builder = builder;
85
+ }
86
+
87
+ /** Skips JSON whitespace. */
88
+ skip() {
89
+ WHITESPACE.lastIndex = this.pos;
90
+ WHITESPACE.exec(this.text);
91
+ this.pos = WHITESPACE.lastIndex;
92
+ }
93
+
94
+ /**
95
+ * @param {string} char - Expected character, after whitespace
96
+ */
97
+ expect(char) {
98
+ this.skip();
99
+ if (this.text[this.pos] !== char) {
100
+ syntaxError(`expected "${char}" at offset ${this.pos}`);
101
+ }
102
+ this.pos++;
103
+ }
104
+
105
+ /** @returns {*} what the builder makes of the value */
106
+ value() {
107
+ this.skip();
108
+ const char = this.text[this.pos];
109
+ if (char === "{") return this.object();
110
+ if (char === "[") return this.array();
111
+ if (char === '"') return this.builder.string(this.string());
112
+ WORD.lastIndex = this.pos;
113
+ const word = WORD.exec(this.text)?.[0];
114
+ if (LITERALS.has(word)) {
115
+ this.pos += word.length;
116
+ return word === "null"
117
+ ? this.builder.null()
118
+ : this.builder.boolean(word === "true");
119
+ }
120
+ NUMBER.lastIndex = this.pos;
121
+ const number = NUMBER.exec(this.text)?.[0];
122
+ if (number === undefined) syntaxError(`unexpected input at ${this.pos}`);
123
+ this.pos += number.length;
124
+ if (/^[\w.+-]/.test(this.text[this.pos] ?? "")) {
125
+ syntaxError(`invalid number at ${this.pos}`);
126
+ }
127
+ return this.builder.number(number);
128
+ }
129
+
130
+ /**
131
+ * Members of an object or array up to the closing bracket.
132
+ * @param {string} close
133
+ * @param {() => *} member
134
+ * @returns {Array}
135
+ */
136
+ list(close, member) {
137
+ this.pos++;
138
+ this.skip();
139
+ const items = [];
140
+ if (this.text[this.pos] === close) {
141
+ this.pos++;
142
+ return items;
143
+ }
144
+ for (;;) {
145
+ items.push(member());
146
+ this.skip();
147
+ const char = this.text[this.pos++];
148
+ if (char === close) return items;
149
+ if (char !== ",") syntaxError(`expected "," or "${close}"`);
150
+ }
151
+ }
152
+
153
+ /** @returns {*} */
154
+ object() {
155
+ const entries = this.list("}", () => {
156
+ this.skip();
157
+ if (this.text[this.pos] !== '"') syntaxError("expected a string key");
158
+ const key = this.string();
159
+ this.expect(":");
160
+ return [key, this.value()];
161
+ });
162
+ return this.builder.object(entries);
163
+ }
164
+
165
+ /** @returns {*} */
166
+ array() {
167
+ return this.builder.array(this.list("]", () => this.value()));
168
+ }
169
+
170
+ /** @returns {JsonString} the characters of a string literal */
171
+ string() {
172
+ const start = this.pos + 1;
173
+ PLAIN.lastIndex = start;
174
+ PLAIN.exec(this.text);
175
+ const plainEnd = PLAIN.lastIndex;
176
+ if (this.text[plainEnd] === '"') {
177
+ this.pos = plainEnd + 1;
178
+ return this.text.slice(start, plainEnd);
179
+ }
180
+ const { chars, end } = scanChars(this.text, start, syntaxError);
181
+ this.pos = end + 1;
182
+ return chars;
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Parses a JSON text; a leading byte order mark is ignored.
188
+ * @template T
189
+ * @param {string} text
190
+ * @param {JsonBuilder<T>} [builder] - Makes the results (default: a plain
191
+ * tree of {@link JsonValue})
192
+ * @returns {T} what the builder makes of the whole text
193
+ * @throws {XPathError} FOJS0001 when the text is not JSON
194
+ */
195
+ export function parseJsonText(text, builder = TREE_BUILDER) {
196
+ const parser = new Parser(text.replace(/^\uFEFF/, ""), builder);
197
+ const value = parser.value();
198
+ parser.skip();
199
+ if (parser.pos < parser.text.length) {
200
+ syntaxError(`unexpected input after the value at ${parser.pos}`);
201
+ }
202
+ return value;
203
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Strings between JSON and XDM (F&O 3.1 section 17.5): the `escape` and
3
+ * `fallback` options of fn:parse-json and fn:json-to-xml, and the escaping
4
+ * of strings written by fn:xml-to-json.
5
+ *
6
+ * @module @tradik/xslt3/functions/json/jsonStrings
7
+ */
8
+
9
+ import { isXmlChar, stringItem } from "../support.js";
10
+
11
+ const SHORT_ESCAPES = new Map([
12
+ [0x08, "\\b"],
13
+ [0x09, "\\t"],
14
+ [0x0a, "\\n"],
15
+ [0x0c, "\\f"],
16
+ [0x0d, "\\r"],
17
+ [0x22, '\\"'],
18
+ [0x5c, "\\\\"],
19
+ [0x2f, "\\/"],
20
+ ]);
21
+
22
+ /**
23
+ * The escape sequence of a character: a two-character one where JSON has
24
+ * it, else \uXXXX (upper-case hex, a surrogate pair beyond the BMP).
25
+ * @param {number} cp
26
+ * @returns {string}
27
+ */
28
+ export function escapeChar(cp) {
29
+ const short = SHORT_ESCAPES.get(cp);
30
+ if (short) return short;
31
+ const hex = (unit) =>
32
+ `\\u${unit.toString(16).toUpperCase().padStart(4, "0")}`;
33
+ if (cp < 0x10000) return hex(cp);
34
+ const offset = cp - 0x10000;
35
+ return hex(0xd800 + (offset >> 10)) + hex(0xdc00 + (offset & 0x3ff));
36
+ }
37
+
38
+ /**
39
+ * Whether a character is "special" for the escape option: a control
40
+ * character (x00-x1F, x7F-x9F), a codepoint that is not an XML character
41
+ * (lone surrogates included) or the backslash.
42
+ * @param {number} cp
43
+ * @returns {boolean}
44
+ */
45
+ export const isSpecial = (cp) =>
46
+ cp <= 0x1f || (cp >= 0x7f && cp <= 0x9f) || cp === 0x5c || !isXmlChar(cp);
47
+
48
+ /**
49
+ * Characters that may need the escape or fallback option: controls, the
50
+ * C1 range, surrogates and the non-characters xFFFE and xFFFF (a superset
51
+ * of the special and non-XML characters of the BMP).
52
+ */
53
+ // eslint-disable-next-line no-control-regex -- controls are escaped
54
+ const MAY_CHANGE = /[\u0000-\u001f\u007f-\u009f\ud800-\udfff\uFFFE\uFFFF]/;
55
+
56
+ /**
57
+ * The characters of a string written without escape sequences.
58
+ * @param {string} text
59
+ * @returns {import("./jsonChars.js").JsonChar[]}
60
+ */
61
+ const charsOf = (text) =>
62
+ Array.from(text, (char) => ({ cp: char.codePointAt(0) }));
63
+
64
+ /**
65
+ * The function that turns parsed JSON characters into a string under the
66
+ * escape and fallback options.
67
+ * @param {{escape: boolean, fallback?: import("../../items/function.js").FunctionItem}} options
68
+ * @returns {(chars: import("./jsonParser.js").JsonString) => string}
69
+ */
70
+ export function stringDecoder({ escape, fallback }) {
71
+ const replace = fallback
72
+ ? (sequence) => fallback.invoke([[stringItem(sequence)]])[0].value
73
+ : () => "\uFFFD";
74
+ return (json) => {
75
+ // a string without escape sequences is mostly kept as it is
76
+ if (typeof json === "string" && !MAY_CHANGE.test(json)) return json;
77
+ const chars = typeof json === "string" ? charsOf(json) : json;
78
+ let result = "";
79
+ for (const { cp, escape: written } of chars) {
80
+ if (escape) {
81
+ result += isSpecial(cp) ? escapeChar(cp) : String.fromCodePoint(cp);
82
+ } else if (isXmlChar(cp)) {
83
+ result += String.fromCodePoint(cp);
84
+ } else {
85
+ result += replace(written ?? escapeChar(cp));
86
+ }
87
+ }
88
+ return result;
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Escapes a string for JSON output (fn:xml-to-json): quotes, backslashes,
94
+ * solidus and control characters (x00-x1F, x7F-x9F).
95
+ * @param {string} text
96
+ * @returns {string}
97
+ */
98
+ export function escapeJsonString(text) {
99
+ let result = "";
100
+ for (const char of text) {
101
+ const cp = char.codePointAt(0);
102
+ result += needsEscape(cp) ? escapeChar(cp) : char;
103
+ }
104
+ return result;
105
+ }
106
+
107
+ /**
108
+ * @param {number} cp
109
+ * @returns {boolean} whether xml-to-json writes the character escaped
110
+ */
111
+ export const needsEscape = (cp) =>
112
+ cp <= 0x1f ||
113
+ (cp >= 0x7f && cp <= 0x9f) ||
114
+ cp === 0x22 ||
115
+ cp === 0x5c ||
116
+ cp === 0x2f;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * fn:json-to-xml (F&O 3.1 section 17.5.3): JSON to the XML representation
3
+ * of JSON, elements map, array, string, number, boolean and null in the
4
+ * fn namespace, built in a new document (dynamic context hook
5
+ * `createDocument`). A basic processor does not validate (validate=true
6
+ * raises FOJS0004).
7
+ *
8
+ * @module @tradik/xslt3/functions/json/jsonToXml
9
+ */
10
+
11
+ import { XPathError } from "../../errors.js";
12
+ import { setBaseUri } from "../baseUris.js";
13
+ import { define, FN_NAMESPACE } from "../support.js";
14
+ import { parseJsonText } from "./jsonParser.js";
15
+ import { stringDecoder } from "./jsonStrings.js";
16
+ import { jsonOptions } from "./parseJson.js";
17
+
18
+ const SPECS = {
19
+ duplicates: {
20
+ type: "xs:string",
21
+ default: "retain",
22
+ values: ["reject", "use-first", "retain"],
23
+ },
24
+ validate: { type: "xs:boolean", default: false },
25
+ };
26
+
27
+ /**
28
+ * The JSON builder (see jsonParser.js) that makes the elements of the XML
29
+ * representation while the text is parsed, with no intermediate tree.
30
+ * @param {Document} document
31
+ * @param {object} options - escape, duplicates and the string decoder
32
+ * @returns {import("./jsonParser.js").JsonBuilder<Element>}
33
+ */
34
+ function elementBuilder(document, options) {
35
+ const element = (name, content) => {
36
+ const result = document.createElementNS(FN_NAMESPACE, name);
37
+ if (content) result.appendChild(document.createTextNode(content));
38
+ return result;
39
+ };
40
+ return {
41
+ object(entries) {
42
+ const map = element("map");
43
+ const seen = new Set();
44
+ for (const [keyChars, child] of entries) {
45
+ const key = options.decode(keyChars);
46
+ if (seen.has(key) && options.duplicates !== "retain") {
47
+ if (options.duplicates === "reject") {
48
+ throw new XPathError("FOJS0003", `Duplicate key "${key}"`);
49
+ }
50
+ continue;
51
+ }
52
+ seen.add(key);
53
+ child.setAttribute("key", key);
54
+ if (options.escape && key.includes("\\")) {
55
+ child.setAttribute("escaped-key", "true");
56
+ }
57
+ map.appendChild(child);
58
+ }
59
+ return map;
60
+ },
61
+ array(members) {
62
+ const array = element("array");
63
+ for (const member of members) array.appendChild(member);
64
+ return array;
65
+ },
66
+ string(chars) {
67
+ const content = options.decode(chars);
68
+ const result = element("string", content);
69
+ if (options.escape && content.includes("\\")) {
70
+ result.setAttribute("escaped", "true");
71
+ }
72
+ return result;
73
+ },
74
+ number: (text) => element("number", text),
75
+ boolean: (value) => element("boolean", String(value)),
76
+ null: () => element("null"),
77
+ };
78
+ }
79
+
80
+ /**
81
+ * fn:json-to-xml of a string.
82
+ * @param {string} json
83
+ * @param {Array|undefined} optionsArg
84
+ * @param {{createDocument: () => Document, staticBaseUri?: string}} context
85
+ * @returns {Array} the document node, whose base URI is the static base URI
86
+ */
87
+ function jsonToXml(json, optionsArg, context) {
88
+ const options = jsonOptions(optionsArg, SPECS);
89
+ if (options.validate) {
90
+ throw new XPathError(
91
+ "FOJS0004",
92
+ "validate=true needs a schema-aware processor",
93
+ );
94
+ }
95
+ const document = context.createDocument();
96
+ const builder = elementBuilder(document, {
97
+ ...options,
98
+ decode: stringDecoder(options),
99
+ });
100
+ document.appendChild(parseJsonText(json, builder));
101
+ return [setBaseUri(document, context.staticBaseUri)];
102
+ }
103
+
104
+ /** Function definitions. */
105
+ export const jsonToXmlFunctions = [
106
+ define("json-to-xml", ["xs:string?"], "document-node()?", ([json], c) =>
107
+ json.length ? jsonToXml(json[0].value, undefined, c) : [],
108
+ ),
109
+ define(
110
+ "json-to-xml",
111
+ ["xs:string?", "map(*)"],
112
+ "document-node()?",
113
+ ([json, options], c) =>
114
+ json.length ? jsonToXml(json[0].value, options, c) : [],
115
+ ),
116
+ ];
@@ -0,0 +1,119 @@
1
+ /**
2
+ * fn:parse-json and fn:json-doc (F&O 3.1 sections 17.5.1 and 17.5.2):
3
+ * JSON to maps, arrays, xs:string, xs:double and xs:boolean values (null
4
+ * is the empty sequence).
5
+ *
6
+ * @module @tradik/xslt3/functions/json/parseJson
7
+ */
8
+
9
+ import { XPathError } from "../../errors.js";
10
+ import { XdmArray } from "../../items/array.js";
11
+ import { XdmMap } from "../../items/map.js";
12
+ import { mapKey } from "../../items/mapKey.js";
13
+ import { readOptions } from "../options.js";
14
+ import { booleanItem, define, doubleItem, stringItem } from "../support.js";
15
+ import { parseJsonText } from "./jsonParser.js";
16
+ import { stringDecoder } from "./jsonStrings.js";
17
+
18
+ /** Option declarations shared by parse-json and json-to-xml. */
19
+ export const JSON_OPTIONS = {
20
+ liberal: { type: "xs:boolean", default: false },
21
+ escape: { type: "xs:boolean", default: false },
22
+ fallback: { type: "function(xs:string) as xs:string" },
23
+ };
24
+
25
+ /**
26
+ * Reads the options of a JSON parsing function.
27
+ * @param {Array} options - The options argument (a map), or undefined
28
+ * @param {Record<string, object>} specs - Declarations of the options
29
+ * that differ between the functions (duplicates, validate)
30
+ * @returns {Record<string, *>}
31
+ * @throws {XPathError} FOJS0005 for invalid values, among which a fallback
32
+ * function together with escape=true
33
+ */
34
+ export function jsonOptions(options, specs) {
35
+ const result = readOptions(
36
+ options?.[0],
37
+ { ...JSON_OPTIONS, ...specs },
38
+ "FOJS0005",
39
+ );
40
+ if (result.escape && result.fallback) {
41
+ throw new XPathError(
42
+ "FOJS0005",
43
+ "The fallback option is not allowed with escape=true",
44
+ );
45
+ }
46
+ return result;
47
+ }
48
+
49
+ /**
50
+ * The JSON builder (see jsonParser.js) that makes XDM values while the
51
+ * text is parsed: each value is a sequence of at most one item.
52
+ * @param {(chars: import("./jsonParser.js").JsonString) => string} decode
53
+ * - String decoder
54
+ * @param {string} duplicates - "reject", "use-first" or "use-last"
55
+ * @returns {import("./jsonParser.js").JsonBuilder<Array>}
56
+ */
57
+ function xdmBuilder(decode, duplicates) {
58
+ return {
59
+ object(members) {
60
+ const entries = new Map();
61
+ for (const [keyChars, value] of members) {
62
+ const key = stringItem(decode(keyChars));
63
+ const k = mapKey(key);
64
+ if (entries.has(k)) {
65
+ if (duplicates === "reject") {
66
+ throw new XPathError("FOJS0003", `Duplicate key "${key.value}"`);
67
+ }
68
+ if (duplicates === "use-first") continue;
69
+ }
70
+ entries.set(k, { key, value });
71
+ }
72
+ return [new XdmMap(entries)];
73
+ },
74
+ array: (members) => [new XdmArray(members)],
75
+ string: (chars) => [stringItem(decode(chars))],
76
+ number: (text) => [doubleItem(Number(text))],
77
+ boolean: (value) => [booleanItem(value)],
78
+ null: () => [],
79
+ };
80
+ }
81
+
82
+ /**
83
+ * fn:parse-json of a string.
84
+ * @param {string} text
85
+ * @param {Array} [options] - The options argument
86
+ * @returns {Array}
87
+ */
88
+ export function parseJson(text, options) {
89
+ const { duplicates, ...rest } = jsonOptions(options, {
90
+ duplicates: {
91
+ type: "xs:string",
92
+ default: "use-first",
93
+ values: ["reject", "use-first", "use-last"],
94
+ },
95
+ });
96
+ return parseJsonText(text, xdmBuilder(stringDecoder(rest), duplicates));
97
+ }
98
+
99
+ const OPTIONS = "map(*)";
100
+
101
+ /** Function definitions. */
102
+ export const parseJsonFunctions = [
103
+ define("parse-json", ["xs:string?"], "item()?", ([json]) =>
104
+ json.length ? parseJson(json[0].value) : [],
105
+ ),
106
+ define("parse-json", ["xs:string?", OPTIONS], "item()?", ([json, options]) =>
107
+ json.length ? parseJson(json[0].value, options) : [],
108
+ ),
109
+ define("json-doc", ["xs:string?"], "item()?", ([href], context) =>
110
+ href.length ? parseJson(context.loadText(href[0].value)) : [],
111
+ ),
112
+ define(
113
+ "json-doc",
114
+ ["xs:string?", OPTIONS],
115
+ "item()?",
116
+ ([href, options], context) =>
117
+ href.length ? parseJson(context.loadText(href[0].value), options) : [],
118
+ ),
119
+ ];
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Checks of the XML representation of JSON read by fn:xml-to-json (F&O
3
+ * 3.1 section 17.5.4): boolean attributes, allowed attributes, the text
4
+ * of simple elements and the members of maps and arrays (FOJS0006).
5
+ *
6
+ * @module @tradik/xslt3/functions/json/xmlRepresentation
7
+ */
8
+
9
+ import { XPathError } from "../../errors.js";
10
+ import { normalizeWhitespace, parseBoolean } from "../../xdm/strings.js";
11
+ import { attributesOf } from "../../xpath/eval/domNodes.js";
12
+ import { FN_NAMESPACE } from "../support.js";
13
+
14
+ /**
15
+ * @param {string} message
16
+ * @returns {never}
17
+ * @throws {XPathError} FOJS0006
18
+ */
19
+ export const invalid = (message) => {
20
+ throw new XPathError(
21
+ "FOJS0006",
22
+ `Invalid XML representation of JSON: ${message}`,
23
+ );
24
+ };
25
+
26
+ const ATTRIBUTES = new Set(["key", "escaped", "escaped-key"]);
27
+
28
+ /**
29
+ * A boolean attribute of the representation.
30
+ * @param {Element} element
31
+ * @param {string} name
32
+ * @returns {boolean}
33
+ */
34
+ export function flag(element, name) {
35
+ if (!element.hasAttribute(name)) return false;
36
+ const value = parseBoolean(
37
+ normalizeWhitespace(element.getAttribute(name), "collapse"),
38
+ );
39
+ if (value === null) invalid(`${name}="${element.getAttribute(name)}"`);
40
+ return value;
41
+ }
42
+
43
+ /**
44
+ * The children of an element that matter: elements and text, without
45
+ * comments and processing instructions.
46
+ * @param {Element} element
47
+ * @returns {Node[]}
48
+ */
49
+ const contentOf = (element) =>
50
+ [...element.childNodes].filter((n) => n.nodeType !== 7 && n.nodeType !== 8);
51
+
52
+ /** @param {Node} node @returns {boolean} whether it is a text node */
53
+ const isText = (node) => node.nodeType === 3 || node.nodeType === 4;
54
+
55
+ /**
56
+ * Text content of a simple element (string, number, boolean, null).
57
+ * @param {Element} element
58
+ * @returns {string}
59
+ */
60
+ export function textOf(element) {
61
+ let text = "";
62
+ for (const node of contentOf(element)) {
63
+ if (node.nodeType === 1) invalid(`element in <${element.localName}>`);
64
+ if (isText(node)) text += node.nodeValue;
65
+ }
66
+ return text;
67
+ }
68
+
69
+ /**
70
+ * The element children of a map or array.
71
+ * @param {Element} element
72
+ * @returns {Element[]}
73
+ */
74
+ export function members(element) {
75
+ const result = [];
76
+ for (const node of contentOf(element)) {
77
+ if (node.nodeType === 1) result.push(node);
78
+ else if (isText(node) && /\S/.test(node.nodeValue)) {
79
+ invalid(`text in <${element.localName}>`);
80
+ }
81
+ }
82
+ return result;
83
+ }
84
+
85
+ /**
86
+ * Checks the attributes of an element: key and escaped-key, escaped on
87
+ * strings, and attributes in other namespaces than fn.
88
+ * @param {Element} element
89
+ * @throws {XPathError} FOJS0006 for another attribute
90
+ */
91
+ export function checkAttributes(element) {
92
+ for (const attribute of attributesOf(element)) {
93
+ const uri = attribute.namespaceURI ?? "";
94
+ const allowed =
95
+ uri === ""
96
+ ? ATTRIBUTES.has(attribute.localName) &&
97
+ (attribute.localName !== "escaped" || element.localName === "string")
98
+ : uri !== FN_NAMESPACE;
99
+ if (!allowed) {
100
+ invalid(`attribute ${attribute.name} on <${element.localName}>`);
101
+ }
102
+ }
103
+ }