@es-joy/jsoe 0.26.1 → 0.28.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 (158) hide show
  1. package/.gitignore +10 -0
  2. package/.npmignore +9 -0
  3. package/CHANGES.md +76 -0
  4. package/README.md +6 -4
  5. package/badges/coverage-badge.svg +1 -1
  6. package/badges/tests-badge.svg +1 -0
  7. package/dist/formats/schema.d.ts +11 -0
  8. package/dist/formats/schema.d.ts.map +1 -1
  9. package/dist/formats/structuredCloning.d.ts.map +1 -1
  10. package/dist/fundamentalTypes/arrayType.d.ts.map +1 -1
  11. package/dist/fundamentalTypes/stringType.d.ts.map +1 -1
  12. package/dist/index.js +2 -2
  13. package/dist/index.js.map +1 -1
  14. package/dist/typeChoices.d.ts +32 -2
  15. package/dist/typeChoices.d.ts.map +1 -1
  16. package/dist/types.d.ts +8 -2
  17. package/dist/types.d.ts.map +1 -1
  18. package/dist/utils/rawTypesonEditor.d.ts +110 -0
  19. package/dist/utils/rawTypesonEditor.d.ts.map +1 -0
  20. package/dist/vendor-imports.d.ts +4 -0
  21. package/docs/proposals/raw-typeson-edit-view.md +575 -0
  22. package/eslint.config.js +36 -27
  23. package/mmr.json +6 -0
  24. package/package.json +22 -12
  25. package/pnpm-workspace.yaml +4 -2
  26. package/src/formats/schema.js +38 -7
  27. package/src/formats/structuredCloning.js +24 -17
  28. package/src/fundamentalTypes/arrayType.js +135 -7
  29. package/src/fundamentalTypes/stringType.js +5 -0
  30. package/src/jsoe.css +12 -0
  31. package/src/typeChoices.js +39 -8
  32. package/src/types.js +20 -2
  33. package/src/utils/rawTypesonEditor.js +669 -0
  34. package/src/vendor-imports.js +12 -0
  35. package/tsconfig.json +1 -1
  36. package/typings/json-6.d.ts +12 -0
  37. package/vendor/@codemirror/autocomplete/dist/index.js +2125 -0
  38. package/vendor/@codemirror/commands/dist/index.js +1826 -0
  39. package/vendor/@codemirror/lang-javascript/dist/index.js +513 -0
  40. package/vendor/@codemirror/language/dist/index.js +2693 -0
  41. package/vendor/@codemirror/lint/dist/index.js +956 -0
  42. package/vendor/@codemirror/search/dist/index.js +1238 -0
  43. package/vendor/@codemirror/state/dist/index.js +3947 -0
  44. package/vendor/@codemirror/view/dist/index.js +11867 -0
  45. package/vendor/@lezer/common/dist/index.js +2202 -0
  46. package/vendor/@lezer/highlight/dist/index.js +927 -0
  47. package/vendor/@lezer/javascript/dist/index.js +192 -0
  48. package/vendor/@lezer/lr/dist/index.js +1889 -0
  49. package/vendor/@marijn/find-cluster-break/src/index.js +87 -0
  50. package/vendor/codemirror/dist/index.js +96 -0
  51. package/vendor/crelt/index.js +28 -0
  52. package/vendor/json-6/dist/index.mjs +1783 -0
  53. package/vendor/style-mod/src/style-mod.js +172 -0
  54. package/vendor/typeson-registry/dist/index.js +83 -10
  55. package/vendor/w3c-keyname/index.js +119 -0
  56. package/vendor/zod/classic/checks.d.ts +1 -1
  57. package/vendor/zod/classic/checks.js +1 -1
  58. package/vendor/zod/classic/external.d.ts +1 -1
  59. package/vendor/zod/classic/external.js +1 -1
  60. package/vendor/zod/classic/from-json-schema.js +245 -24
  61. package/vendor/zod/classic/schemas.d.ts +17 -1
  62. package/vendor/zod/classic/schemas.js +77 -29
  63. package/vendor/zod/core/api.d.ts +6 -4
  64. package/vendor/zod/core/api.js +17 -2
  65. package/vendor/zod/core/checks.d.ts +1 -1
  66. package/vendor/zod/core/checks.js +0 -96
  67. package/vendor/zod/core/compile.d.ts +18 -4
  68. package/vendor/zod/core/compile.js +146 -67
  69. package/vendor/zod/core/doc.js +7 -2
  70. package/vendor/zod/core/errors.js +2 -5
  71. package/vendor/zod/core/json-schema-generator.js +2 -2
  72. package/vendor/zod/core/json-schema-processors.d.ts +17 -0
  73. package/vendor/zod/core/json-schema-processors.js +195 -64
  74. package/vendor/zod/core/memoizer.js +72 -23
  75. package/vendor/zod/core/parse.js +37 -16
  76. package/vendor/zod/core/regexes.d.ts +2 -0
  77. package/vendor/zod/core/regexes.js +8 -4
  78. package/vendor/zod/core/schemas.d.ts +42 -7
  79. package/vendor/zod/core/schemas.js +301 -94
  80. package/vendor/zod/core/to-json-schema.d.ts +7 -3
  81. package/vendor/zod/core/to-json-schema.js +8 -7
  82. package/vendor/zod/core/util.d.ts +25 -4
  83. package/vendor/zod/core/util.js +160 -143
  84. package/vendor/zod/core/versions.d.ts +1 -1
  85. package/vendor/zod/core/versions.js +2 -2
  86. package/vendor/zod/core/visit.js +12 -0
  87. package/vendor/zod/locales/ar.js +1 -0
  88. package/vendor/zod/locales/az.js +1 -0
  89. package/vendor/zod/locales/be.js +1 -0
  90. package/vendor/zod/locales/bg.js +1 -0
  91. package/vendor/zod/locales/bn.js +1 -0
  92. package/vendor/zod/locales/ca.js +1 -0
  93. package/vendor/zod/locales/ckb.js +1 -0
  94. package/vendor/zod/locales/cs.js +1 -0
  95. package/vendor/zod/locales/da.js +1 -0
  96. package/vendor/zod/locales/de.js +1 -0
  97. package/vendor/zod/locales/el.js +1 -0
  98. package/vendor/zod/locales/en.js +1 -0
  99. package/vendor/zod/locales/eo.js +1 -0
  100. package/vendor/zod/locales/es.js +1 -0
  101. package/vendor/zod/locales/fa.js +1 -0
  102. package/vendor/zod/locales/fi.js +1 -0
  103. package/vendor/zod/locales/fr-CA.js +1 -0
  104. package/vendor/zod/locales/fr.js +1 -0
  105. package/vendor/zod/locales/gu.js +1 -0
  106. package/vendor/zod/locales/he.js +1 -0
  107. package/vendor/zod/locales/hi.js +1 -0
  108. package/vendor/zod/locales/hr.js +1 -0
  109. package/vendor/zod/locales/hu.js +1 -0
  110. package/vendor/zod/locales/hy.js +1 -0
  111. package/vendor/zod/locales/id.js +1 -0
  112. package/vendor/zod/locales/index.d.ts +1 -0
  113. package/vendor/zod/locales/index.js +1 -0
  114. package/vendor/zod/locales/is.js +1 -0
  115. package/vendor/zod/locales/it.js +1 -0
  116. package/vendor/zod/locales/ja.js +1 -0
  117. package/vendor/zod/locales/ka.js +1 -0
  118. package/vendor/zod/locales/km.js +1 -0
  119. package/vendor/zod/locales/kn.js +1 -0
  120. package/vendor/zod/locales/ko.js +1 -0
  121. package/vendor/zod/locales/lt.js +1 -0
  122. package/vendor/zod/locales/mk.js +1 -0
  123. package/vendor/zod/locales/ms.js +1 -0
  124. package/vendor/zod/locales/ne.js +1 -0
  125. package/vendor/zod/locales/nl.js +1 -0
  126. package/vendor/zod/locales/nn.js +1 -0
  127. package/vendor/zod/locales/no.js +1 -0
  128. package/vendor/zod/locales/ota.js +1 -0
  129. package/vendor/zod/locales/pl.js +1 -0
  130. package/vendor/zod/locales/ps.js +1 -0
  131. package/vendor/zod/locales/pt-BR.js +1 -0
  132. package/vendor/zod/locales/pt.js +1 -0
  133. package/vendor/zod/locales/ro.js +1 -0
  134. package/vendor/zod/locales/ru.js +1 -0
  135. package/vendor/zod/locales/sk.js +1 -0
  136. package/vendor/zod/locales/sl.js +1 -0
  137. package/vendor/zod/locales/sv.js +1 -0
  138. package/vendor/zod/locales/ta.js +1 -0
  139. package/vendor/zod/locales/tg.d.ts +4 -0
  140. package/vendor/zod/locales/tg.js +116 -0
  141. package/vendor/zod/locales/th.js +1 -0
  142. package/vendor/zod/locales/tk.js +1 -0
  143. package/vendor/zod/locales/tr.js +1 -0
  144. package/vendor/zod/locales/uk.js +1 -0
  145. package/vendor/zod/locales/ur.js +1 -0
  146. package/vendor/zod/locales/uz.js +1 -0
  147. package/vendor/zod/locales/vi.js +1 -0
  148. package/vendor/zod/locales/yo.js +1 -0
  149. package/vendor/zod/locales/zh-CN.js +1 -0
  150. package/vendor/zod/locales/zh-TW.js +1 -0
  151. package/vendor/zod/mini/checks.d.ts +1 -1
  152. package/vendor/zod/mini/checks.js +1 -1
  153. package/vendor/zod/mini/external.d.ts +1 -1
  154. package/vendor/zod/mini/external.js +1 -1
  155. package/vendor/zod/mini/schemas.d.ts +8 -0
  156. package/vendor/zod/mini/schemas.js +19 -2
  157. package/vendor/zodexy/dist/esm/index.js +34 -1
  158. package/vendor/zodexy/dist/schema.zodexy.json +18 -0
@@ -0,0 +1,669 @@
1
+ import dialogs from './dialogs.js';
2
+ import {$e, DOM} from './templateUtils.js';
3
+ import {
4
+ Typeson, JSON6, EditorView, basicSetup, EditorState,
5
+ javascript, toStringTag
6
+ } from '../vendor-imports.js';
7
+ import {structuredCloningJsoe} from '../formats/structuredCloning.js';
8
+ import {parseValue} from '../formats/schema.js';
9
+
10
+ /**
11
+ * @returns {import('typeson').Typeson}
12
+ */
13
+ const buildTypeson = () => {
14
+ return new Typeson().register(structuredCloningJsoe);
15
+ };
16
+
17
+ const identifierKeyRegex = (/^[A-Za-z_$][\w$]*$/u);
18
+
19
+ /** @type {ReadonlySet<string>} */
20
+ const typedArrayTagNames = new Set([
21
+ 'Int8Array', 'Uint8Array', 'Uint8ClampedArray', 'Int16Array',
22
+ 'Uint16Array', 'Int32Array', 'Uint32Array', 'Float16Array',
23
+ 'Float32Array', 'Float64Array', 'BigInt64Array', 'BigUint64Array'
24
+ ]);
25
+
26
+ /** @type {ReadonlySet<string>} */
27
+ const errorTagNames = new Set([
28
+ 'Error', 'TypeError', 'RangeError', 'SyntaxError', 'ReferenceError',
29
+ 'EvalError', 'URIError', 'AggregateError'
30
+ ]);
31
+
32
+ /**
33
+ * Base64-encodes raw bytes, chunked so a large buffer doesn't blow the
34
+ * argument-count limit `String.fromCodePoint(...bytes)` would hit spread
35
+ * over the whole thing at once.
36
+ * @param {Uint8Array} bytes
37
+ * @returns {string}
38
+ */
39
+ const bytesToBase64 = (bytes) => {
40
+ const chunkSize = 0x8000;
41
+ let binary = '';
42
+ for (let i = 0; i < bytes.length; i += chunkSize) {
43
+ binary += String.fromCodePoint(...bytes.subarray(i, i + chunkSize));
44
+ }
45
+ return btoa(binary);
46
+ };
47
+
48
+ /**
49
+ * A minimal JSON6-*subset* stringifier: unquotes object keys that are valid
50
+ * identifiers (for the "simplified" display the JSON6 mode is meant to
51
+ * give), while always fully quoting string *values* via `JSON.stringify`.
52
+ *
53
+ * `JSON6.stringify` (from the `json-6` package) was tried first but has a
54
+ * real bug: it runs the same identifier-unquoting logic it uses for keys
55
+ * on string *values* too, so e.g. the value `"hello"` is emitted as the
56
+ * bare word `hello` — not valid JS/JSON6 syntax as a value, and not even
57
+ * re-parseable by the package's own `JSON6.parse`. Hand-rolling this
58
+ * avoids depending on that broken path while still using `JSON6.parse`
59
+ * (which is correct) to read the text back after editing.
60
+ * @param {unknown} value
61
+ * @param {string} indent
62
+ * @param {string} curIndent
63
+ * @returns {string}
64
+ */
65
+ const stringifyJSON6 = (value, indent, curIndent) => {
66
+ if (value === null || typeof value !== 'object') {
67
+ return JSON.stringify(value);
68
+ }
69
+ const nextIndent = curIndent + indent;
70
+ if (Array.isArray(value)) {
71
+ if (!value.length) {
72
+ return '[]';
73
+ }
74
+ const items = value.map((item) => {
75
+ return `${nextIndent}${stringifyJSON6(item, indent, nextIndent)}`;
76
+ });
77
+ return `[\n${items.join(',\n')}\n${curIndent}]`;
78
+ }
79
+ const keys = Object.keys(value);
80
+ if (!keys.length) {
81
+ return '{}';
82
+ }
83
+ const items = keys.map((key) => {
84
+ const keyText = identifierKeyRegex.test(key)
85
+ ? key
86
+ : JSON.stringify(key);
87
+ return `${nextIndent}${keyText}: ${
88
+ stringifyJSON6(
89
+ /** @type {{[key: string]: unknown}} */ (value)[key], indent,
90
+ nextIndent
91
+ )
92
+ }`;
93
+ });
94
+ return `{\n${items.join(',\n')}\n${curIndent}}`;
95
+ };
96
+
97
+ /**
98
+ * Builds the Typeson/JSON6 text shown by both the view-mode "View raw"
99
+ * button and, seeded, the edit-mode "Edit raw" button's Typeson mode.
100
+ * @param {unknown} value
101
+ * @returns {Promise<string>}
102
+ */
103
+ export async function getTypesonTextForValue (value) {
104
+ const typeson = buildTypeson();
105
+ // `throwOnBadSyncType: false` matches `../formats/structuredCloning.js`'s
106
+ // own `encapsulateAsync` call: without it, Typeson throws when a value
107
+ // has no actually-async parts (no Blob/File/Promise) and so resolves
108
+ // synchronously despite the "Async" method having been called.
109
+ const encapsulated = await typeson.encapsulateAsync(value, null, {
110
+ throwOnBadSyncType: false
111
+ });
112
+ return stringifyJSON6(encapsulated, ' ', '');
113
+ }
114
+
115
+ /**
116
+ * Reverses `getTypesonTextForValue`: parses JSON6 (superset) syntax and
117
+ * revives any Typeson-tagged values back into real JS values.
118
+ * @param {string} text
119
+ * @returns {unknown}
120
+ */
121
+ export function getValueForTypesonText (text) {
122
+ const typeson = buildTypeson();
123
+ const parsed = JSON6.parse(text);
124
+ return typeson.revive(parsed);
125
+ }
126
+
127
+ /**
128
+ * Only ever called when `types.allowUnsafeEval` is true. The whole point of
129
+ * eval mode is that the user's text is a plain JS expression (e.g.
130
+ * `{a: new Date()}`) that already evaluates to the real value directly;
131
+ * this never touches Typeson, unlike `getValueForTypesonText`.
132
+ * @param {string} text
133
+ * @returns {unknown}
134
+ */
135
+ export function getValueForEvalText (text) {
136
+ // User explicitly opted in via the `allowUnsafeEval` constructor option;
137
+ // same rationale as `functionSpec.revive` in
138
+ // `../formats/structuredCloning.js`.
139
+ // eslint-disable-next-line no-eval -- User opted in
140
+ return (0, eval)('(' + text + ')');
141
+ }
142
+
143
+ /**
144
+ * Base64-encodes a `Blob`/`File`'s content — the one genuinely asynchronous
145
+ * part of eval-mode seeding, since reading a `Blob`'s bytes always returns
146
+ * a promise (unlike `ArrayBuffer`/typed-array bytes, already in memory).
147
+ * @param {Blob} blob
148
+ * @returns {Promise<string>}
149
+ */
150
+ const blobToBase64 = async (blob) => {
151
+ return bytesToBase64(new Uint8Array(await blob.arrayBuffer()));
152
+ };
153
+
154
+ /**
155
+ * A dedicated serializer for seeding the editor's text when switching into
156
+ * eval mode. Deliberately does **not** reuse `getTypesonTextForValue`'s
157
+ * output: that form tags special values (e.g. a `Date` becomes an ISO
158
+ * string plus a `$types` entry), and evaluating that tagged text verbatim
159
+ * would hand back the plain tagged object, not a real `Date` — eval mode
160
+ * has no revive step to undo the tagging. This instead emits genuine JS
161
+ * constructor source for the same value.
162
+ *
163
+ * Async because a `Blob`/`File`'s content can only be read
164
+ * asynchronously (`Blob#arrayBuffer()`); every other branch is
165
+ * synchronous work wrapped in a resolved promise.
166
+ * @param {unknown} value
167
+ * @param {Set<unknown>} [seen] Tracks objects already being serialized, to
168
+ * fail clearly on a cyclic value rather than recursing forever.
169
+ * @returns {Promise<string>}
170
+ */
171
+ export async function getEvalSeedTextForValue (value, seen = new Set()) {
172
+ if (value === null) {
173
+ return 'null';
174
+ }
175
+ if (value === undefined) {
176
+ return 'undefined';
177
+ }
178
+ switch (typeof value) {
179
+ case 'string':
180
+ return JSON.stringify(value);
181
+ case 'number':
182
+ // `String(-0)` is `"0"`, silently losing the sign; matches the
183
+ // convention already used by `../superTypes/SpecialNumberType.js`.
184
+ return Object.is(value, -0) ? '-0' : String(value);
185
+ case 'boolean':
186
+ return String(value);
187
+ case 'bigint':
188
+ return `${value}n`;
189
+ case 'function':
190
+ // Mirrors `functionSpec.replace` in `../formats/structuredCloning.js`.
191
+ return value.toString();
192
+ default:
193
+ break;
194
+ }
195
+ const tag = toStringTag(value);
196
+ if (tag === 'Symbol') {
197
+ // Mirrors `src/fundamentalTypes/symbolType.js`'s own convention
198
+ // (`String(value).slice(7, -1)` rather than `.description`, which
199
+ // would be `undefined` for `Symbol()` with no description).
200
+ const sym = /** @type {symbol} */ (value);
201
+ const key = Symbol.keyFor(sym);
202
+ return key === undefined
203
+ ? `Symbol(${JSON.stringify(String(sym).slice(7, -1))})`
204
+ : `Symbol.for(${JSON.stringify(key)})`;
205
+ }
206
+ if (tag === 'Date') {
207
+ return `new Date(${
208
+ JSON.stringify(/** @type {Date} */ (value).toISOString())
209
+ })`;
210
+ }
211
+ if (tag === 'RegExp') {
212
+ return value.toString();
213
+ }
214
+ // Boxed primitives (`new String(...)`/`new Number(...)`/`new Boolean(...)`
215
+ // — distinct from the plain-primitive `string`/`number`/`boolean` cases
216
+ // in the `switch` above, which never reach here).
217
+ if (tag === 'String') {
218
+ const boxed = /** @type {{valueOf: () => string}} */ (value);
219
+ return `new String(${JSON.stringify(boxed.valueOf())})`;
220
+ }
221
+ if (tag === 'Number') {
222
+ const boxed = /** @type {{valueOf: () => number}} */ (value);
223
+ return `new Number(${boxed.valueOf()})`;
224
+ }
225
+ if (tag === 'Boolean') {
226
+ const boxed = /** @type {{valueOf: () => boolean}} */ (value);
227
+ return `new Boolean(${boxed.valueOf()})`;
228
+ }
229
+ if (tag === 'BigInt') {
230
+ // Matches typeson-registry's own `bigintObject` revive:
231
+ // `new Object(BigInt(e))`.
232
+ const boxed = /** @type {{valueOf: () => bigint}} */ (value);
233
+ return `Object(${boxed.valueOf()}n)`;
234
+ }
235
+ if (tag === 'DOMException') {
236
+ const exc = /** @type {DOMException} */ (value);
237
+ return `new DOMException(${JSON.stringify(exc.message)}, ${
238
+ JSON.stringify(exc.name)
239
+ })`;
240
+ }
241
+ // Matches `src/superTypes/domrectType.js`/`dompointType.js`, which support
242
+ // both the read-write and read-only variant under the same base type.
243
+ if (tag === 'DOMRect' || tag === 'DOMRectReadOnly') {
244
+ const r = /** @type {DOMRect} */ (value);
245
+ return `new ${tag}(${r.x}, ${r.y}, ${r.width}, ${r.height})`;
246
+ }
247
+ if (tag === 'DOMPoint' || tag === 'DOMPointReadOnly') {
248
+ const p = /** @type {DOMPoint} */ (value);
249
+ return `new ${tag}(${p.x}, ${p.y}, ${p.z}, ${p.w})`;
250
+ }
251
+ if (tag === 'DOMMatrix' || tag === 'DOMMatrixReadOnly') {
252
+ // Matches `src/superTypes/dommatrixType.js`'s own two constructor forms.
253
+ const m = /** @type {DOMMatrix} */ (value);
254
+ const nums = m.is2D
255
+ ? [m.a, m.b, m.c, m.d, m.e, m.f]
256
+ : [
257
+ m.m11, m.m12, m.m13, m.m14,
258
+ m.m21, m.m22, m.m23, m.m24,
259
+ m.m31, m.m32, m.m33, m.m34,
260
+ m.m41, m.m42, m.m43, m.m44
261
+ ];
262
+ return `new ${tag}([${nums.join(', ')}])`;
263
+ }
264
+ if (tag === 'ArrayBuffer' || tag === 'DataView' ||
265
+ typedArrayTagNames.has(tag)
266
+ ) {
267
+ // Unlike `Blob`/`File`, a buffer's bytes are already in memory — no
268
+ // async read needed, just the same base64 wrapping.
269
+ const bytes = tag === 'ArrayBuffer'
270
+ ? new Uint8Array(/** @type {ArrayBuffer} */ (value))
271
+ : new Uint8Array(
272
+ /** @type {{buffer: ArrayBuffer, byteOffset: number, byteLength: number}} */
273
+ (value).buffer,
274
+ /** @type {{byteOffset: number}} */ (value).byteOffset,
275
+ /** @type {{byteLength: number}} */ (value).byteLength
276
+ );
277
+ const bufferExpr = `Uint8Array.from(atob(${
278
+ JSON.stringify(bytesToBase64(bytes))
279
+ }), (c) => c.charCodeAt(0)).buffer`;
280
+ if (tag === 'ArrayBuffer') {
281
+ return bufferExpr;
282
+ }
283
+ return `new ${tag}(${bufferExpr})`;
284
+ }
285
+ if (tag === 'Blob' || tag === 'File') {
286
+ const blob = /** @type {Blob} */ (value);
287
+ const base64 = await blobToBase64(blob);
288
+ const bytesExpr = `Uint8Array.from(atob(${
289
+ JSON.stringify(base64)
290
+ }), (c) => c.charCodeAt(0))`;
291
+ if (tag === 'File') {
292
+ const file = /** @type {File} */ (value);
293
+ return `new File([${bytesExpr}], ${JSON.stringify(file.name)}, ${
294
+ JSON.stringify({type: file.type, lastModified: file.lastModified})
295
+ })`;
296
+ }
297
+ return `new Blob([${bytesExpr}], ${JSON.stringify({type: blob.type})})`;
298
+ }
299
+ if (tag === 'Promise') {
300
+ // A pending/rejected/already-settled `Promise` has no meaningful static
301
+ // literal form; unlike the lossy fallback below, this is a value shape
302
+ // we recognize but have deliberately chosen not to support.
303
+ throw new TypeError(
304
+ 'Eval mode cannot represent a Promise as JS source; use the Typeson ' +
305
+ '(JSON6) mode instead.'
306
+ );
307
+ }
308
+ if (seen.has(value)) {
309
+ throw new Error(
310
+ 'Eval mode cannot represent a cyclic/repeated-reference value as ' +
311
+ 'JS source; use the Typeson (JSON6) mode instead.'
312
+ );
313
+ }
314
+ seen.add(value);
315
+ try {
316
+ if (tag === 'FileList' || Array.isArray(value)) {
317
+ // jsoe's own `filelist` control never actually requires a genuine
318
+ // `instanceof FileList` for its value model (see
319
+ // `docs/proposals/raw-typeson-edit-view.md` §1) — a plain array of
320
+ // `File`s round-trips the same way, so no special construct needed.
321
+ let arr;
322
+ if (tag === 'FileList') {
323
+ const fileList = /** @type {Iterable<unknown>} */ (value);
324
+ arr = [...fileList];
325
+ } else {
326
+ arr = /** @type {unknown[]} */ (value);
327
+ }
328
+ const items = await Promise.all(
329
+ arr.map((item) => getEvalSeedTextForValue(item, seen))
330
+ );
331
+ return `[${items.join(', ')}]`;
332
+ }
333
+ if (tag === 'Map') {
334
+ const map = /** @type {Map<unknown, unknown>} */ (value);
335
+ // Not using `Iterator#toArray()`: not yet baseline-widely-available
336
+ // eslint-disable-next-line unicorn/prefer-iterator-to-array -- Above
337
+ const entries = await Promise.all([...map.entries()].map(
338
+ async ([k, v]) => {
339
+ return `[${await getEvalSeedTextForValue(k, seen)}, ${
340
+ await getEvalSeedTextForValue(v, seen)
341
+ }]`;
342
+ }
343
+ ));
344
+ return `new Map([${entries.join(', ')}])`;
345
+ }
346
+ if (tag === 'Set') {
347
+ const set = /** @type {Set<unknown>} */ (value);
348
+ const items = await Promise.all(
349
+ [...set].map((item) => getEvalSeedTextForValue(item, seen))
350
+ );
351
+ return `new Set([${items.join(', ')}])`;
352
+ }
353
+ if (tag === 'Error') {
354
+ // `Object.prototype.toString` reports every `Error` subclass as
355
+ // `[object Error]` (an `[[ErrorData]]`-slot check, not a
356
+ // `Symbol.toStringTag` lookup), so `tag` alone can't tell a
357
+ // `TypeError` from a plain `Error` here — recover the real
358
+ // constructor name, falling back to `Error` for a subclass eval
359
+ // mode doesn't know how to name-construct (e.g. a user-defined
360
+ // `class MyError extends Error {}`).
361
+ const err = /**
362
+ * @type {Error & {
363
+ * cause?: unknown, fileName?: string, lineNumber?: number,
364
+ * columnNumber?: number
365
+ * }}
366
+ */ (value);
367
+ const ctorName = errorTagNames.has(err.constructor?.name)
368
+ ? err.constructor.name
369
+ : 'Error';
370
+ const messageExpr = JSON.stringify(err.message);
371
+ const optionsExpr = err.cause === undefined
372
+ ? ''
373
+ : `, {cause: ${await getEvalSeedTextForValue(err.cause, seen)}}`;
374
+ const ctorExpr = ctorName === 'AggregateError'
375
+ ? `new AggregateError([${
376
+ (await Promise.all(
377
+ /** @type {AggregateError} */ (err).errors.map(
378
+ (e) => getEvalSeedTextForValue(e, seen)
379
+ )
380
+ )).join(', ')
381
+ }], ${messageExpr}${optionsExpr})`
382
+ : `new ${ctorName}(${messageExpr}${optionsExpr})`;
383
+ // `name`/`fileName`/`lineNumber`/`columnNumber`/`stack` aren't
384
+ // constructor arguments — set via `Object.assign`, which (unlike a
385
+ // sequence of statements) still keeps this a single expression.
386
+ const extraProps = [];
387
+ if (typeof err.name === 'string') {
388
+ extraProps.push(`name: ${JSON.stringify(err.name)}`);
389
+ }
390
+ if (typeof err.fileName === 'string') {
391
+ extraProps.push(`fileName: ${JSON.stringify(err.fileName)}`);
392
+ }
393
+ if (typeof err.lineNumber === 'number') {
394
+ extraProps.push(`lineNumber: ${err.lineNumber}`);
395
+ }
396
+ if (typeof err.columnNumber === 'number') {
397
+ extraProps.push(`columnNumber: ${err.columnNumber}`);
398
+ }
399
+ if (typeof err.stack === 'string') {
400
+ extraProps.push(`stack: ${JSON.stringify(err.stack)}`);
401
+ }
402
+ return extraProps.length
403
+ ? `Object.assign(${ctorExpr}, {${extraProps.join(', ')}})`
404
+ : ctorExpr;
405
+ }
406
+ if (typeof value === 'object') {
407
+ // Anything reaching here that isn't a plain object literal (a class
408
+ // instance with a non-`Object.prototype` prototype) can't be
409
+ // faithfully reconstructed by the `{...}` literal below — better to
410
+ // fail clearly than silently emit a same-shaped-but-wrong plain
411
+ // object (losing the value's real identity/behavior entirely).
412
+ const proto = Object.getPrototypeOf(value);
413
+ if (proto !== null && proto !== Object.prototype) {
414
+ // `toStringTag` reports a generic `Object` for most user-defined
415
+ // classes (no `Symbol.toStringTag` of their own), so name the
416
+ // error after the actual constructor instead where possible.
417
+ const ctor = /** @type {{constructor?: {name?: string}}} */ (
418
+ value
419
+ ).constructor;
420
+ throw new TypeError(
421
+ `Eval mode cannot represent a ${
422
+ ctor?.name || tag || 'non-plain-object'
423
+ } value as JS source; use the Typeson (JSON6) mode instead.`
424
+ );
425
+ }
426
+ const entries = await Promise.all(
427
+ Object.entries(value).map(async ([k, v]) => {
428
+ const key = (/^[A-Za-z_$][\w$]*$/u).test(k) ? k : JSON.stringify(k);
429
+ return `${key}: ${await getEvalSeedTextForValue(v, seen)}`;
430
+ })
431
+ );
432
+ return `{${entries.join(', ')}}`;
433
+ }
434
+ /* istanbul ignore next -- No other `typeof` reaches here */
435
+ return String(value);
436
+ } finally {
437
+ seen.delete(value);
438
+ }
439
+ }
440
+
441
+ /**
442
+ * Repopulates an existing object/array/set/map/filelist control's children
443
+ * in place after a raw-value edit. Object/array-family type objects have
444
+ * no `setValue` of their own (`types.setValue` is a no-op for them, as
445
+ * only leaf types implement it) — their children are always attached one
446
+ * at a time via `$addAndSetArrayElement`, driven by walking a Typeson
447
+ * `encapsulateAsync` pass. This reuses that exact mechanism, scoped to
448
+ * just this container, by pre-seeding `stateObj.rootUI` with the existing
449
+ * `root` element (see the corresponding change in
450
+ * `../formats/structuredCloning.js`'s `encapsulateObserver`, which reuses
451
+ * a pre-seeded root instead of building a new one).
452
+ *
453
+ * The control's own root element is kept (not replaced), since other code
454
+ * may already hold a reference to it (most importantly, the root control
455
+ * of the whole form). The container's own `itemIndex`/legend-numbering
456
+ * counter, closed over since the control was first built, is reset via
457
+ * `root.$resetItemIndex()` right after its old children are cleared and
458
+ * before the new ones are attached, back to the same baseline the
459
+ * container started at when first built — so the repopulated items (and
460
+ * any later manually-added one, via "+ Item") end up numbered exactly as
461
+ * a fresh container holding the same new items would be, rather than
462
+ * continuing from wherever the replaced content had left off.
463
+ * @param {object} cfg
464
+ * @param {InstanceType<typeof import('../types.js').default>} cfg.types
465
+ * @param {import('../formats.js').AvailableFormat} cfg.format
466
+ * @param {import('../types.js').AvailableArbitraryType} cfg.type
467
+ * @param {HTMLDivElement} cfg.root
468
+ * @param {HTMLDivElement} cfg.topRoot
469
+ * @param {string} [cfg.typeNamespace]
470
+ * @param {import('zodexy').SzType} [cfg.specificSchemaObject]
471
+ * @param {unknown} cfg.value
472
+ * @returns {Promise<void>}
473
+ */
474
+ export async function commitValueToContainer ({
475
+ types, format, type, root, topRoot, typeNamespace, specificSchemaObject,
476
+ value
477
+ }) {
478
+ const {formats} = types;
479
+ const rootWithCustomMethods = /**
480
+ * @type {{
481
+ * $getArrayItems: () => HTMLElement,
482
+ * $resetItemIndex: () => void
483
+ * }}
484
+ */ (/** @type {unknown} */ (root));
485
+ DOM.removeChildren(rootWithCustomMethods.$getArrayItems());
486
+ rootWithCustomMethods.$resetItemIndex();
487
+ const stateObj = /** @type {import('../types.js').StateObject} */ ({
488
+ types,
489
+ formats,
490
+ format,
491
+ typeNamespace,
492
+ readonly: false,
493
+ // This container's own schema stands in as the "whole record" schema
494
+ // for this scoped re-population, mirroring how the overall document
495
+ // schema drives the very first `getControlsForFormatAndValue` call.
496
+ schemaContent: specificSchemaObject,
497
+ rootUI: root
498
+ });
499
+ await formats.getAvailableFormat(format).iterate(value, stateObj);
500
+ await stateObj.whenBuilt;
501
+ if (stateObj.error) {
502
+ throw stateObj.error;
503
+ }
504
+ types.validate({type, root, topRoot, avoidReport: false});
505
+ }
506
+
507
+ /**
508
+ * Builds and shows the modal for the "View raw"/"Edit raw" buttons added to
509
+ * every eligible object/array-family control in
510
+ * `../fundamentalTypes/arrayType.js`.
511
+ * @param {object} cfg
512
+ * @param {InstanceType<typeof import('../types.js').default>} cfg.types
513
+ * @param {import('../formats.js').AvailableFormat} cfg.format
514
+ * @param {import('../types.js').AvailableArbitraryType} cfg.type
515
+ * @param {HTMLDivElement} cfg.root
516
+ * @param {HTMLDivElement} cfg.topRoot
517
+ * @param {string} [cfg.typeNamespace]
518
+ * @param {import('zodexy').SzType} [cfg.specificSchemaObject]
519
+ * @param {boolean} cfg.readonly
520
+ * @returns {Promise<void>}
521
+ */
522
+ export async function openRawEditorDialog ({
523
+ types, format, type, root, topRoot, typeNamespace, specificSchemaObject,
524
+ readonly
525
+ }) {
526
+ const {formats} = types;
527
+ const value = types.getValueForRoot(
528
+ /** @type {any} */ (root),
529
+ /** @type {import('../types.js').StateObject} */ ({
530
+ typeNamespace, formats, format, types
531
+ }),
532
+ ''
533
+ );
534
+
535
+ /** @type {'typeson'|'eval'} */
536
+ let mode = 'typeson';
537
+
538
+ // Declared before assignment (rather than `const`) since it is referenced
539
+ // by closures defined further up, before the dialog (and so the mount
540
+ // point `view` is assigned into) exists.
541
+ /** @type {import('codemirror').EditorView} */
542
+ // eslint-disable-next-line prefer-const -- See comment above
543
+ let view;
544
+
545
+ const errorClass = 'jsoe-raw-editor-error';
546
+ const editorClass = 'jsoe-raw-editor';
547
+
548
+ /**
549
+ * @param {HTMLElement} dialog
550
+ * @param {string} message
551
+ * @returns {void}
552
+ */
553
+ const showError = (dialog, message) => {
554
+ const errorEl = /** @type {HTMLElement} */ ($e(dialog, `.${errorClass}`));
555
+ errorEl.textContent = message;
556
+ };
557
+
558
+ /**
559
+ * @param {string} text
560
+ * @returns {void}
561
+ */
562
+ const setText = (text) => {
563
+ view.dispatch({
564
+ changes: {from: 0, to: view.state.doc.length, insert: text}
565
+ });
566
+ };
567
+
568
+ const seedTextForMode = async () => {
569
+ return mode === 'eval'
570
+ ? await getEvalSeedTextForValue(value)
571
+ : await getTypesonTextForValue(value);
572
+ };
573
+
574
+ const initialText = readonly
575
+ ? await getTypesonTextForValue(value)
576
+ : await seedTextForMode();
577
+
578
+ /** @type {import('jamilih').JamilihArray[]} */
579
+ const children = [
580
+ ['div', {class: errorClass, role: 'alert'}],
581
+ ...(!readonly && types.allowUnsafeEval
582
+ ? /** @type {import('jamilih').JamilihArray[]} */ ([
583
+ ['select', {
584
+ class: 'jsoe-raw-editor-mode',
585
+ $on: {
586
+ async change (/** @type {Event} */ e) {
587
+ mode = /** @type {HTMLSelectElement} */ (
588
+ e.target
589
+ ).value === 'eval'
590
+ ? 'eval'
591
+ : 'typeson';
592
+ try {
593
+ setText(await seedTextForMode());
594
+ } catch (error) {
595
+ setText('');
596
+ showError(
597
+ /** @type {HTMLElement} */ (
598
+ /** @type {HTMLSelectElement} */ (e.target).closest(
599
+ 'dialog'
600
+ )
601
+ ),
602
+ /** @type {Error} */ (error).message
603
+ );
604
+ }
605
+ }
606
+ }
607
+ }, [
608
+ ['option', {value: 'typeson'}, ['Typeson (JSON6)']],
609
+ ['option', {value: 'eval'}, ['JS (eval)']]
610
+ ]]
611
+ ])
612
+ : []),
613
+ ['div', {class: editorClass}]
614
+ ];
615
+
616
+ const dialog = readonly
617
+ // @ts-expect-error TS bug
618
+ ? dialogs.makeCancelDialog({children})
619
+ : dialogs.makeSubmitDialog({
620
+ submitText: 'Save',
621
+ async submit ({dialog: dlg}) {
622
+ const text = view.state.doc.toString();
623
+ showError(dlg, '');
624
+ let newValue;
625
+ try {
626
+ newValue = mode === 'eval'
627
+ ? getValueForEvalText(text)
628
+ : await getValueForTypesonText(text);
629
+ } catch (error) {
630
+ showError(dlg, /** @type {Error} */ (error).message);
631
+ return;
632
+ }
633
+ if (specificSchemaObject) {
634
+ const result = parseValue(
635
+ types, specificSchemaObject, specificSchemaObject, newValue
636
+ );
637
+ if (!result.success) {
638
+ showError(
639
+ dlg, 'That value does not satisfy this field’s schema.'
640
+ );
641
+ return;
642
+ }
643
+ }
644
+ try {
645
+ await commitValueToContainer({
646
+ types, format, type, root, topRoot, typeNamespace,
647
+ specificSchemaObject, value: newValue
648
+ });
649
+ } catch (error) {
650
+ showError(dlg, /** @type {Error} */ (error).message);
651
+ return;
652
+ }
653
+ dlg.close();
654
+ },
655
+ // @ts-expect-error TS bug
656
+ children
657
+ });
658
+
659
+ const editorHost = /** @type {HTMLElement} */ ($e(dialog, `.${editorClass}`));
660
+ view = new EditorView({
661
+ doc: initialText,
662
+ extensions: [
663
+ basicSetup,
664
+ javascript(),
665
+ ...(readonly ? [EditorState.readOnly.of(true)] : [])
666
+ ],
667
+ parent: editorHost
668
+ });
669
+ }
@@ -19,3 +19,15 @@ export {default as mimeStandardTypes} from 'mime/types/standard.js';
19
19
  export {default as mimeOtherTypes} from 'mime/types/other.js';
20
20
  // } from '../node_modules/mime/dist/types/standard.js';
21
21
  // } from '../node_modules/mime/dist/types/other.js';
22
+
23
+ export {default as JSON6} from 'json-6';
24
+ // } from '../node_modules/json-6/dist/index.mjs';
25
+
26
+ export {EditorView, basicSetup} from 'codemirror';
27
+ // } from '../node_modules/codemirror/dist/index.js';
28
+
29
+ export {EditorState} from '@codemirror/state';
30
+ // } from '../node_modules/@codemirror/state/dist/index.js';
31
+
32
+ export {javascript} from '@codemirror/lang-javascript';
33
+ // } from '../node_modules/@codemirror/lang-javascript/dist/index.js';
package/tsconfig.json CHANGED
@@ -15,6 +15,6 @@
15
15
  "rootDir": "src",
16
16
  "outDir": "dist"
17
17
  },
18
- "include": ["src/**/*.js", "typings/sceditor.d.ts"],
18
+ "include": ["src/**/*.js", "typings/sceditor.d.ts", "typings/json-6.d.ts"],
19
19
  "exclude": ["node_modules", "src/deepEqual.js", "src/formats/schema.js", "./src/index.ts", "./dist/**/*.js"]
20
20
  }