@es-joy/jsoe 0.27.0 → 0.28.1

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 (148) hide show
  1. package/CHANGES.md +37 -0
  2. package/README.md +3 -1
  3. package/badges/coverage-badge.svg +1 -1
  4. package/badges/tests-badge.svg +1 -0
  5. package/dist/formats/schema.d.ts +11 -0
  6. package/dist/formats/schema.d.ts.map +1 -1
  7. package/dist/formats/structuredCloning.d.ts.map +1 -1
  8. package/dist/fundamentalTypes/arrayType.d.ts.map +1 -1
  9. package/dist/fundamentalTypes/stringType.d.ts.map +1 -1
  10. package/dist/index.js +2 -2
  11. package/dist/index.js.map +1 -1
  12. package/dist/typeChoices.d.ts +32 -2
  13. package/dist/typeChoices.d.ts.map +1 -1
  14. package/dist/types.d.ts +8 -2
  15. package/dist/types.d.ts.map +1 -1
  16. package/dist/utils/rawTypesonEditor.d.ts +110 -0
  17. package/dist/utils/rawTypesonEditor.d.ts.map +1 -0
  18. package/dist/vendor-imports.d.ts +4 -0
  19. package/docs/proposals/raw-typeson-edit-view.md +575 -0
  20. package/mmr.json +6 -0
  21. package/package.json +34 -12
  22. package/pnpm-workspace.yaml +32 -30
  23. package/src/formats/schema.js +10 -15
  24. package/src/formats/structuredCloning.js +24 -17
  25. package/src/fundamentalTypes/arrayType.js +119 -23
  26. package/src/fundamentalTypes/blobType.js +16 -16
  27. package/src/fundamentalTypes/fileType.js +13 -13
  28. package/src/fundamentalTypes/regexpType.js +1 -1
  29. package/src/fundamentalTypes/stringType.js +5 -0
  30. package/src/jsoe.css +12 -0
  31. package/src/subTypes/blobHTMLType.js +1 -1
  32. package/src/superTypes/dommatrixType.js +1 -1
  33. package/src/superTypes/errorsSpecialType.js +1 -1
  34. package/src/typeChoices.js +37 -10
  35. package/src/types.js +13 -8
  36. package/src/utils/media.js +3 -3
  37. package/src/utils/rawTypesonEditor.js +669 -0
  38. package/src/vendor-imports.js +12 -0
  39. package/tsconfig.json +1 -1
  40. package/typings/json-6.d.ts +12 -0
  41. package/vendor/@codemirror/autocomplete/dist/index.js +2125 -0
  42. package/vendor/@codemirror/commands/dist/index.js +1826 -0
  43. package/vendor/@codemirror/lang-javascript/dist/index.js +513 -0
  44. package/vendor/@codemirror/language/dist/index.js +2693 -0
  45. package/vendor/@codemirror/lint/dist/index.js +956 -0
  46. package/vendor/@codemirror/search/dist/index.js +1238 -0
  47. package/vendor/@codemirror/state/dist/index.js +3947 -0
  48. package/vendor/@codemirror/view/dist/index.js +11867 -0
  49. package/vendor/@lezer/common/dist/index.js +2202 -0
  50. package/vendor/@lezer/highlight/dist/index.js +927 -0
  51. package/vendor/@lezer/javascript/dist/index.js +192 -0
  52. package/vendor/@lezer/lr/dist/index.js +1889 -0
  53. package/vendor/@marijn/find-cluster-break/src/index.js +87 -0
  54. package/vendor/codemirror/dist/index.js +96 -0
  55. package/vendor/crelt/index.js +28 -0
  56. package/vendor/jamilih/dist/jml.mjs +898 -25
  57. package/vendor/json-6/dist/index.mjs +1783 -0
  58. package/vendor/style-mod/src/style-mod.js +172 -0
  59. package/vendor/w3c-keyname/index.js +119 -0
  60. package/vendor/zod/classic/checks.d.ts +1 -1
  61. package/vendor/zod/classic/checks.js +1 -1
  62. package/vendor/zod/classic/schemas.d.ts +2 -6
  63. package/vendor/zod/classic/schemas.js +9 -13
  64. package/vendor/zod/core/api.d.ts +8 -4
  65. package/vendor/zod/core/api.js +11 -23
  66. package/vendor/zod/core/checks.d.ts +16 -0
  67. package/vendor/zod/core/checks.js +31 -0
  68. package/vendor/zod/core/compile.js +20 -24
  69. package/vendor/zod/core/core.js +1 -1
  70. package/vendor/zod/core/json-schema-processors.d.ts +0 -1
  71. package/vendor/zod/core/json-schema-processors.js +8 -43
  72. package/vendor/zod/core/memoizer.js +2 -5
  73. package/vendor/zod/core/regexes.d.ts +1 -0
  74. package/vendor/zod/core/regexes.js +2 -0
  75. package/vendor/zod/core/schemas.d.ts +8 -23
  76. package/vendor/zod/core/schemas.js +39 -80
  77. package/vendor/zod/core/to-json-schema.d.ts +0 -3
  78. package/vendor/zod/core/to-json-schema.js +1 -1
  79. package/vendor/zod/core/util.js +0 -4
  80. package/vendor/zod/core/versions.js +1 -1
  81. package/vendor/zod/core/visit.js +0 -12
  82. package/vendor/zod/locales/ar.js +1 -0
  83. package/vendor/zod/locales/az.js +1 -0
  84. package/vendor/zod/locales/be.js +1 -0
  85. package/vendor/zod/locales/bg.js +1 -0
  86. package/vendor/zod/locales/bn.js +1 -0
  87. package/vendor/zod/locales/ca.js +1 -0
  88. package/vendor/zod/locales/ckb.js +1 -0
  89. package/vendor/zod/locales/cs.js +1 -0
  90. package/vendor/zod/locales/da.js +1 -0
  91. package/vendor/zod/locales/de.js +1 -0
  92. package/vendor/zod/locales/el.js +1 -0
  93. package/vendor/zod/locales/en.js +1 -0
  94. package/vendor/zod/locales/eo.js +1 -0
  95. package/vendor/zod/locales/es.js +1 -0
  96. package/vendor/zod/locales/fa.js +1 -0
  97. package/vendor/zod/locales/fi.js +1 -0
  98. package/vendor/zod/locales/fr-CA.js +1 -0
  99. package/vendor/zod/locales/fr.js +1 -0
  100. package/vendor/zod/locales/gu.js +1 -0
  101. package/vendor/zod/locales/he.js +1 -0
  102. package/vendor/zod/locales/hi.js +1 -0
  103. package/vendor/zod/locales/hr.js +1 -0
  104. package/vendor/zod/locales/hu.js +1 -0
  105. package/vendor/zod/locales/hy.js +1 -0
  106. package/vendor/zod/locales/id.js +1 -0
  107. package/vendor/zod/locales/is.js +1 -0
  108. package/vendor/zod/locales/it.js +1 -0
  109. package/vendor/zod/locales/ja.js +1 -0
  110. package/vendor/zod/locales/ka.js +1 -0
  111. package/vendor/zod/locales/km.js +1 -0
  112. package/vendor/zod/locales/kn.js +1 -0
  113. package/vendor/zod/locales/ko.js +1 -0
  114. package/vendor/zod/locales/lt.js +1 -0
  115. package/vendor/zod/locales/mk.js +1 -0
  116. package/vendor/zod/locales/ms.js +1 -0
  117. package/vendor/zod/locales/ne.js +1 -0
  118. package/vendor/zod/locales/nl.js +1 -0
  119. package/vendor/zod/locales/nn.js +1 -0
  120. package/vendor/zod/locales/no.js +1 -0
  121. package/vendor/zod/locales/ota.js +1 -0
  122. package/vendor/zod/locales/pl.js +1 -0
  123. package/vendor/zod/locales/ps.js +1 -0
  124. package/vendor/zod/locales/pt-BR.js +1 -0
  125. package/vendor/zod/locales/pt.js +1 -0
  126. package/vendor/zod/locales/ro.js +1 -0
  127. package/vendor/zod/locales/ru.js +1 -0
  128. package/vendor/zod/locales/sk.js +1 -0
  129. package/vendor/zod/locales/sl.js +1 -0
  130. package/vendor/zod/locales/sv.js +1 -0
  131. package/vendor/zod/locales/ta.js +1 -0
  132. package/vendor/zod/locales/tg.js +1 -0
  133. package/vendor/zod/locales/th.js +1 -0
  134. package/vendor/zod/locales/tk.js +1 -0
  135. package/vendor/zod/locales/tr.js +1 -0
  136. package/vendor/zod/locales/uk.js +1 -0
  137. package/vendor/zod/locales/ur.js +1 -0
  138. package/vendor/zod/locales/uz.js +1 -0
  139. package/vendor/zod/locales/vi.js +1 -0
  140. package/vendor/zod/locales/yo.js +1 -0
  141. package/vendor/zod/locales/zh-CN.js +1 -0
  142. package/vendor/zod/locales/zh-TW.js +1 -0
  143. package/vendor/zod/mini/checks.d.ts +1 -1
  144. package/vendor/zod/mini/checks.js +1 -1
  145. package/vendor/zod/mini/schemas.d.ts +2 -5
  146. package/vendor/zod/mini/schemas.js +9 -11
  147. package/vendor/zodexy/dist/esm/index.js +4 -33
  148. package/vendor/zodexy/dist/schema.zodexy.json +1 -17
@@ -0,0 +1,575 @@
1
+ # Proposal: raw Typeson/JSON6 edit & view buttons on object/array controls
2
+
3
+ Status: implemented (see "Implementation notes" at the end for corrections
4
+ found while building it)
5
+
6
+ ## Goal
7
+
8
+ jsoe's README already carries an aspirational to-do: "Ability to replace
9
+ content with raw JSON6/Typeson/Safe Eval ... if it validates"
10
+ (`README.md:124`). This proposal implements it for every object- and
11
+ array-based control: a small "Edit raw" button in edit mode that opens a
12
+ syntax-highlighted textual view of the control's current value
13
+ (Typeson-encoded, written in JSON6 syntax) which can be hand-edited and saved
14
+ back in; and, only when a new opt-in "unsafe eval" privilege is granted, a
15
+ second mode in that same editor that instead evaluates the text as a literal
16
+ JS expression (mirroring the existing `eval(o)` pattern already used for
17
+ `arbitraryJS` functions in `src/formats/structuredCloning.js:23-27`). A
18
+ parallel read-only "View raw" button is added to view-mode (`viewUI`)
19
+ controls, showing the same highlighted Typeson/JSON6 text with no edit/eval
20
+ capability.
21
+
22
+ Because `objectType.js` and `arrayType.js` share one implementation
23
+ (`src/fundamentalTypes/arrayType.js`), and the root control renders through
24
+ that same code path (the `topRoot` pattern), adding the buttons there
25
+ automatically covers every object/array control in the tree, including root,
26
+ with no special-casing needed.
27
+
28
+ ## 1. Scope: which controls get the buttons
29
+
30
+ The button applies to `type === 'object'` and `type === 'array'`, including
31
+ their schema-refined forms (`record`/`tuple`, detected the same way the file
32
+ already does via `recordMode`/`tupleMode`) and to `set`/`map` (also real
33
+ object/array data, fully representable in Typeson).
34
+
35
+ `filelist` is **included** too, on closer look: browsers *can* construct a
36
+ real `File` via `new File(...)`, and — checked directly in
37
+ `src/fundamentalTypes/filelistType.js` and `arrayType.js`'s `getValue` —
38
+ jsoe's own filelist control never actually requires a genuine
39
+ `instanceof FileList` for its internal value model; it already treats a
40
+ filelist's value as a plain array-like of `File` objects (built the same way
41
+ the `<input type="file" multiple>` change handler at `arrayType.js:2652-2684`
42
+ does, one `File` at a time via `$addAndSetArrayElement`). So no `FileList`
43
+ polyfill or extra dependency is needed: Typeson's existing `file` type-spec
44
+ (already in `structuredCloningJsoe`'s type list) round-trips `File` content
45
+ through the same encapsulate/revive path as everything else, and
46
+ `types.setValue({type: 'filelist', root, value})` accepts a plain array of
47
+ revived `File`s exactly as it does today. One caveat worth flagging in UI
48
+ copy or docs: `File` content typeson-encodes as base64 in the JSON6 text, so
49
+ large files will make for a very large edit/view buffer — acceptable for a
50
+ first pass, not a blocker.
51
+
52
+ ```js
53
+ const isRawEditable = types.showRawTypesonControls &&
54
+ ((type === 'object' || type === 'array' ||
55
+ type === 'set' || type === 'map' || type === 'filelist') ||
56
+ recordMode || tupleMode);
57
+ ```
58
+
59
+ ## 2. New dependencies
60
+
61
+ - **`json-6`** (not the unrelated, unmaintained `json6` package — confirmed
62
+ by inspecting both on the registry: `json6` is a dead 2015 prototype with no
63
+ `module`/ESM build; `json-6` is d3x0r's actual JSON6 implementation, matches
64
+ the GitHub project already linked from `README.md:124`, and ships an ESM
65
+ build at `dist/index.mjs`). API used: both `JSON6.parse(text)` and
66
+ `JSON6.stringify(value, replacer, space)` (default export). The README's
67
+ one-line description of `stringify` ("uses JSON stringify, so don't have to
68
+ replace") is misleading — reading `lib/json6.js:1195-1401` shows
69
+ `JSON6.stringify` is reassigned to a real, independent stringifier (not the
70
+ earlier `JSON.stringify` alias, which is just a bootstrapping placeholder
71
+ overwritten further down the file). It emits unquoted keys wherever a key
72
+ doesn't need quoting (`getIdentifier`, only quoting keywords, keys starting
73
+ with a digit/`-`, or keys containing whitespace/special characters), giving
74
+ genuinely simplified, more JS-object-literal-like output than
75
+ `JSON.stringify` would — a better fit for "syntax-highlighted typeson in
76
+ json6 format" as the *initial* text shown to the user. One behavioral quirk
77
+ to note: it also re-sorts object keys alphabetically rather than preserving
78
+ insertion order, which is harmless for round-tripping (the parser doesn't
79
+ care about key order) but worth being aware of if the display order looks
80
+ different from the form's own field order. `JSON6.stringify` produces the
81
+ text shown when the editor/viewer opens; `JSON6.parse` reads back whatever
82
+ superset syntax the user then edits it into (comments, unquoted keys,
83
+ trailing commas, etc.). Have asked the `json-6` author whether he may be
84
+ willing to accept a PR to stop the field sorting. If that isn't resolved
85
+ upstream in time, fall back to reordering `JSON6.stringify`'s output
86
+ ourselves post-hoc (walk the encapsulated value's own key order and reorder
87
+ the top level of each emitted object to match, a small local fixup) rather
88
+ than blocking this feature on the upstream change landing.
89
+ - **CodeMirror 6**: `codemirror` (meta-package bundling `basicSetup` +
90
+ `EditorView`/`EditorState`) and `@codemirror/lang-javascript` — used as the
91
+ language extension for *both* modes, not just eval. `@codemirror/lang-json`
92
+ is deliberately not used: it's a strict-JSON grammar and would mishighlight
93
+ (or just fail to recognize) JSON6's extensions — unquoted keys, comments,
94
+ trailing commas — that the safe/Typeson mode's text legitimately contains.
95
+ Since JSON6's syntax is (mostly) a subset of JS object-literal syntax, the
96
+ JS language extension already highlights it reasonably; this gets more
97
+ reliably true once the upstream `json-6` PR mentioned above (stop
98
+ alphabetical key sorting) — and ideally a second guarantee that its
99
+ `stringify` output is always valid ES/JS syntax, not just valid JSON6 — is
100
+ accepted, since JSON6 as a spec is intentionally a bit looser than JS in a
101
+ few corners.
102
+ - Both need the same vendoring treatment already established for
103
+ jamilih/typeson-registry/zod/zodexy/acorn/mime (`package.json:39-46`,
104
+ `src/vendor-imports.js`, demo `<script type="importmap">` blocks), so the
105
+ repo's own no-bundler demo pages keep working: add `copy-json6` and
106
+ `copy-codemirror` scripts copying the relevant `node_modules/**/dist` (and
107
+ CodeMirror's transitive `@lezer/*`, `style-mod`, `w3c-keyname`, `crelt`)
108
+ into `vendor/`, add matching bare-specifier re-exports in
109
+ `src/vendor-imports.js`, and add entries to each demo HTML's import map.
110
+ (npm consumers using their own bundler are unaffected either way, since
111
+ `exports`/`main` in `package.json` already point at unbundled `src/`.)
112
+
113
+ ## 3. New constructor options: `allowUnsafeEval` and `showRawTypesonControls`
114
+
115
+ In `src/types.js`, add two new options alongside the existing
116
+ `useZodexyErrorMessages`/`useZodexyErrorMessagesInTypes` options
117
+ (`src/types.js:437-451`), assigning both onto `this`:
118
+
119
+ - `allowUnsafeEval = false` — gates the eval-mode sub-feature (see §4).
120
+ - `showRawTypesonControls = true` — a master on/off switch for the whole
121
+ feature. When `false`, neither the "Edit raw" nor "View raw" button is
122
+ rendered at all on any object/array control (including root) — the feature
123
+ is fully hidden, not merely disabled/greyed-out, for integrators who don't
124
+ want end users to see it. `allowUnsafeEval` has no effect when this is
125
+ `false`.
126
+
127
+ No further threading is required: `getUIForModeAndType` already passes
128
+ `types: this` into every `editUI`/`viewUI` call (`src/types.js:781,789`), and
129
+ `arrayType.js` already destructures `types` in both functions, so
130
+ `types.allowUnsafeEval`/`types.showRawTypesonControls` are immediately
131
+ readable where the buttons are built.
132
+
133
+ ## 4. New shared module: `src/utils/rawTypesonEditor.js`
134
+
135
+ Extracting this out of `arrayType.js` (already 2771 lines) keeps the type
136
+ file's diff small. Exports:
137
+
138
+ - `getTypesonTextForValue(value, {types, format})` — builds a fresh
139
+ `new Typeson().register(structuredCloningJsoe)` (import
140
+ `structuredCloningJsoe` from `src/formats/structuredCloning.js`, the same
141
+ curated type-spec set the real format iteration uses — deliberately
142
+ *without* `functionSpec`, so the safe/Typeson text can never itself carry an
143
+ executable function tag), calls `await typeson.encapsulateAsync(value)`,
144
+ and returns `JSON6.stringify(encapsulated, null, 2)` for the simplified,
145
+ unquoted-key initial display text (see §2).
146
+ - `getValueForTypesonText(text, {types, format})` — `JSON6.parse(text)` then
147
+ `typeson.revive(parsed)` using the same registered instance/type-spec set.
148
+ Parse/revive errors propagate to the caller to show inline.
149
+ - `getValueForEvalText(text)` — only ever called when `allowUnsafeEval` is
150
+ true; `return (0, eval)('(' + text + ')');` (indirect eval, same "user
151
+ opted in" rationale as `structuredCloning.js:23-27`). This never touches
152
+ Typeson: the whole point of eval mode is that the user's text is a plain JS
153
+ expression like `{a: new Date()}` that already evaluates to the real value
154
+ directly, with no `$types` tagging or revive step involved.
155
+ - `getEvalSeedTextForValue(value)` — a **separate, dedicated** serializer used
156
+ only to seed the editor's text *when switching into eval mode*; it must not
157
+ reuse `getTypesonTextForValue`'s output. Typeson's encapsulated form tags
158
+ special values (e.g. a `Date` becomes an ISO-string field plus a `$types`
159
+ entry pointing at it); evaluating that tagged JSON6 text verbatim in eval
160
+ mode would hand back the plain tagged object, not a real `Date` — eval mode
161
+ has no revive step to undo the tagging. So this instead recursively emits
162
+ genuine JS constructor source for the same value — `new Date(<ISO string>)`,
163
+ `new Map([...])`, `new Set([...])`, a `RegExp`'s own literal form
164
+ (`/pattern/flags`), a function's own `toString()` (mirroring
165
+ `functionSpec.replace` in `structuredCloning.js:20-22`, which already does
166
+ exactly this for functions) — recursing into plain objects/arrays
167
+ structurally. A `Blob`/`File` gets the same treatment: its content is read
168
+ via `Blob#arrayBuffer()`, base64-encoded (chunked to avoid blowing the
169
+ argument-count limit `String.fromCodePoint(...bytes)` would hit spread over
170
+ a whole large file at once), and emitted as
171
+ `new Blob([Uint8Array.from(atob("..."), (c) => c.charCodeAt(0))], {type})`
172
+ (`new File([...], name, {type, lastModified})` for a `File`) — real,
173
+ independently-evaluable JS source, not Typeson's tagged form. This is the
174
+ one branch that's genuinely asynchronous (reading a `Blob`'s bytes always
175
+ returns a promise), which is why the whole function is `async` despite
176
+ every other branch being synchronous work. Scope the rest to the same
177
+ value shapes `structuredCloningJsoe` already covers; it's new, hand-rolled
178
+ code (no extra dependency needed — each case is a one-line template), kept
179
+ in this module specifically so it never gets confused with the
180
+ Typeson-tagged serializer.
181
+ - `openRawEditorDialog({types, format, root, type, topRoot, readonly})` —
182
+ builds the modal using the existing `src/utils/dialogs.js` helpers
183
+ (`makeSubmitDialog` for edit mode, `makeDialog`/`alert`-style for read-only
184
+ view mode, following the `fileType.js:201-241` pattern for structure).
185
+ Internals:
186
+ - Reads the *live* current value via
187
+ `types.getValueForRoot(root, {typeNamespace, formats: types.formats, format, types}, '')`
188
+ — using the control's own root element at click-time (not a stale
189
+ construction-time closure variable), since nested fields may have been
190
+ edited since the control was built.
191
+ - Mounts a CodeMirror `EditorView` (readonly in view mode via
192
+ `EditorState.readOnly.of(true)`/`EditorView.editable.of(false)`) inside
193
+ the dialog, using the `javascript()` language extension from
194
+ `@codemirror/lang-javascript` for both modes (Typeson/JSON6 and eval) —
195
+ see §2 on why `@codemirror/lang-json` is not used.
196
+ - In edit mode, if `types.allowUnsafeEval`, renders a small mode toggle
197
+ (radio/select) between "Typeson (JSON6)" and "JS (eval)". Switching modes
198
+ re-seeds the editor from the *original live value*, each mode using its
199
+ own serializer — `getTypesonTextForValue` for Typeson/JSON6,
200
+ `getEvalSeedTextForValue` for eval — rather than trying to textually
201
+ convert one mode's already-produced text into the other's syntax. This
202
+ means anything the user has typed but not yet saved is discarded on a
203
+ mode switch; that's an acceptable, clearly-scoped trade-off (the toggle is
204
+ for picking how to *start* editing, not for round-tripping in-progress
205
+ edits between the two syntaxes).
206
+ - On Save: parse via whichever mode is active
207
+ (`getValueForTypesonText`/`getValueForEvalText`) *before* touching the
208
+ underlying form tree at all. A parse failure — invalid JSON6 syntax from
209
+ `JSON6.parse`, a Typeson `revive` error, or a thrown/syntax-error `eval`
210
+ in unsafe mode — is caught and shown inline in the dialog; the dialog
211
+ stays open with the user's text untouched and **no existing DOM/control
212
+ is replaced or modified in any way**.
213
+
214
+ When the control has a `specificSchemaObject` (i.e. a schema is actually
215
+ driving this node), a successful parse is not sufficient on its own: the
216
+ resulting value must also pass that schema before Save is allowed to
217
+ proceed — reusing the existing schema-conformance check already used for
218
+ the `'schema'` format, `parseValue(types, schema, originalShape, value)`
219
+ in `src/formats/schema.js:203-210` (currently module-private; export it
220
+ for reuse here). A schema failure is reported inline the same way a parse
221
+ failure is — dialog stays open, nothing is written to the form. This is a
222
+ deliberately stricter gate than ordinary field-by-field editing (which
223
+ the rest of the app allows to sit in a transiently invalid state while
224
+ typing): a raw-text bulk replacement is a single commit action, not
225
+ incremental typing, so it's the right point to enforce the schema up
226
+ front rather than accept a bad bulk replacement and only flag it
227
+ afterward.
228
+
229
+ Only once parsing (and, where applicable, schema validation) succeeds
230
+ does the code proceed to call `types.setValue({type, root, value})` then
231
+ `types.validate({type, root, topRoot, avoidReport: false})` — per the
232
+ project rule that validity must always be recomputed and shown, never
233
+ suppressed, after a bulk value replacement — and close the dialog. This
234
+ means the existing object/array subtree in the form is swapped out only
235
+ on a successful parse; a bad edit never leaves the form in a
236
+ half-replaced or blanked state.
237
+
238
+ ## 5. Changes to `src/fundamentalTypes/arrayType.js`
239
+
240
+ The eligibility check (§1) is computed identically in both functions, mirroring
241
+ the existing `recordMode`/`tupleMode` derivation already present in each.
242
+
243
+ - **`viewUI`** (~`arrayType.js:416-701`): when `isRawEditable`, add a plain
244
+ text button right after the existing collapse toggle (`arrayType.js:650-662`),
245
+ e.g. `['button', {class: 'viewRawTypeson', $on: {click () {...}}}, ['View raw']]`,
246
+ calling `openRawEditorDialog({..., readonly: true})`. Inserted *after* the
247
+ collapse button (not before) so `getInput`'s `$e(root, 'button')`
248
+ (`arrayType.js:702-705`, which grabs the first button for focus purposes)
249
+ keeps selecting the same element as today.
250
+ - **`editUI`** (~`arrayType.js:709-2767`): when `isRawEditable`, add the
251
+ analogous `['button', {class: 'editRawTypeson', ...}]` next to `minusButton`
252
+ at the `[..., minusButton, arrayContents]` assembly (`arrayType.js:2686-2687`),
253
+ calling `openRawEditorDialog({..., readonly: false})`.
254
+ - Both call sites pass `root: div` (the control's own `div.arrayHolder` /
255
+ `div[data-type]` element already in scope in each function), `type`,
256
+ `topRoot`, `types`, and `format`.
257
+
258
+ ## 6. CSS
259
+
260
+ No new styling is required to match existing conventions — buttons in this
261
+ file are plain unstyled `<button>` elements with class names used only as
262
+ JS/test hooks (`arrayType.js:2139-2182` `addArrayElement`, confirmed no
263
+ matching CSS rule in `src/jsoe.css`). Add one small `.jsoe-raw-editor` rule to
264
+ `src/jsoe.css` sizing the CodeMirror host element, following the existing
265
+ `.view-binary, .view-text { width:300px; height:300px; }` convention
266
+ (`src/jsoe.css:137-140`).
267
+
268
+ ## 7. Out of scope
269
+
270
+ - A real bidirectional syntax converter between the Typeson/JSON6 mode and
271
+ the eval mode's text — switching modes re-seeds from the original value
272
+ instead (§4).
273
+ - A `FileList` polyfill/construction library — not needed (§1).
274
+ - Constructing real, upstream-standard-conformant `FileList` instances at
275
+ all; jsoe's own filelist value model already tolerates a plain array-like
276
+ of `File`s, so this proposal doesn't change that.
277
+
278
+ ## 8. Tests
279
+
280
+ - Unit tests for `getTypesonTextForValue`/`getValueForTypesonText`/
281
+ `getValueForEvalText`/`getEvalSeedTextForValue` round-tripping
282
+ representative values (plain object, array, nested Date/RegExp/Map/Set, and
283
+ — only under `allowUnsafeEval` — a function).
284
+ - Cypress specs exercising: the view-mode button opens a read-only highlighted
285
+ panel at root and at a nested object/array; the edit-mode button opens,
286
+ editing valid JSON6 text and saving updates the real form fields; invalid
287
+ syntax on Save shows an inline error and does not close the dialog; a
288
+ schema-driven control rejects a syntactically valid but schema-invalid
289
+ value the same way; the eval-mode option is absent unless `allowUnsafeEval`
290
+ is passed to `Types`, and, when present, evaluated text populates the
291
+ control; `showRawTypesonControls: false` hides both buttons everywhere.
292
+ Keep this work free of stray `console.log(obj)` lines — several specs stub
293
+ `window.console.log`.
294
+
295
+ ## 9. Verification
296
+
297
+ 1. `npm test` (unit) and the Cypress suite to confirm no regressions in
298
+ existing object/array editing flows.
299
+ 2. Manually run the demo (`demo/index.html` or `demo/index-schema.html`),
300
+ exercise "View raw"/"Edit raw" at root and on a nested object/array, and
301
+ confirm the eval mode is absent (these pages construct a plain
302
+ `new Types()`). `demo/index-unsafe-eval.html` (+ its `-instrumented`
303
+ pair) is a dedicated demo for the opposite case: it constructs
304
+ `new Types({allowUnsafeEval: true})`, and its "Initialize with an object
305
+ containing a Date" button seeds a value so opening that object's
306
+ "Edit raw" button immediately shows the "Typeson (JSON6)"/"JS (eval)"
307
+ mode selector, letting you toggle between them and see the Date rendered
308
+ as Typeson-tagged JSON vs. real `new Date(...)` source.
309
+ 3. Confirm `new Types({showRawTypesonControls: false})` renders no "Edit raw"/
310
+ "View raw" buttons anywhere in the tree (root or nested), and that the
311
+ default (option omitted) still shows them.
312
+
313
+ ## Summary of the change surface
314
+
315
+ One new dependency pair (`json-6`, CodeMirror 6) plus their vendoring, two new
316
+ `Types` constructor options, one new util
317
+ (`src/utils/rawTypesonEditor.js`), an export added to
318
+ `src/formats/schema.js` (`parseValue`), and a button + eligibility check added
319
+ to both `viewUI` and `editUI` in `src/fundamentalTypes/arrayType.js`. No
320
+ existing behavior changes when the buttons are unused; `showRawTypesonControls`
321
+ lets integrators opt out entirely.
322
+
323
+ ## Implementation notes (corrections found while building)
324
+
325
+ Three things in this proposal turned out to be wrong or incomplete once
326
+ actually implemented and exercised against real Typeson output and a real
327
+ browser:
328
+
329
+ 1. **Object/array type objects have no `setValue`.** `types.setValue({type,
330
+ root, value})` (§4's original plan for "Save") is a silent no-op for
331
+ `object`/`array`/`set`/`map`/`filelist` — only leaf types implement
332
+ `setValue`; a container's children are always attached one at a time via
333
+ `$addAndSetArrayElement`, driven by walking a Typeson `encapsulateAsync`
334
+ pass (`src/formats/structuredCloning.js`'s `encapsulateObserver`/
335
+ `iterate`). "Save" therefore needed a real `commitValueToContainer`
336
+ helper that reuses that exact walk, scoped to just the edited container,
337
+ rather than a single `setValue` call.
338
+
339
+ Reusing it required one small, behavior-preserving change to
340
+ `encapsulateObserver` in `src/formats/structuredCloning.js`: the branch
341
+ that registers a walked value's root as an attachment point
342
+ (`parents['']`) previously only ran `if (!stateObj.rootUI)` — i.e. only
343
+ when building a brand-new root. Changed to `if (!parents[''])`, with the
344
+ `getUIForModeAndType` call to build a *new* root now nested inside and
345
+ conditioned on `!stateObj.rootUI` — so a caller can pre-seed
346
+ `stateObj.rootUI` with an *existing* element (our control's own root) and
347
+ still have it correctly registered, letting the same walk populate an
348
+ existing container's children instead of only ever building a fresh one.
349
+ This matters because the edited container's root element must keep its
350
+ identity — other code (crucially, the root of the whole form) may already
351
+ hold a reference to it.
352
+
353
+ That in turn surfaced a second gap: the container's own
354
+ `itemIndex`/legend-numbering counter, closed over since it was first
355
+ built, wasn't reset by any of this, so manually adding an item afterward
356
+ (the "+ Item" button) could get a legend number continuing from the
357
+ *replaced* content instead of the new content (data was never affected,
358
+ only the numbering shown for a subsequently-added item). Fixed by adding
359
+ `$resetItemIndex()` — a new `$custom` method on the container alongside
360
+ `$addAndSetArrayElement`/`$getArrayItems` in `arrayType.js` —
361
+ `commitValueToContainer` calls it right after clearing the old children
362
+ and before repopulating. (One subtlety found while writing this:
363
+ `arrayNonindexKeys`, being sparse, already self-heals `itemIndex` by
364
+ recomputing it from the live DOM on every "+ Item" click, so it doesn't
365
+ exercise this bug — plain `object`/`array`, which just do `itemIndex++`
366
+ obliviously, do.)
367
+
368
+ The first version of this fix reset to `-1` unconditionally, copying the
369
+ existing "x All" button's hardcoded reset value — which is only correct
370
+ for arrays (whose baseline really is `-1`); an *object*'s baseline is `0`
371
+ (`itemAdjust - 1`, the exact value `itemIndex` is initialized to before
372
+ any item is ever added). Manual testing on `demo/index-unsafe-eval.html`
373
+ caught the resulting regression directly: adding a single property (shown
374
+ as "1", correctly) and then changing only its *value* via a raw eval edit
375
+ — never touching its name or the property count — renumbered it to "0".
376
+ `$resetItemIndex()` now resets to `itemAdjust - 1` (matching the
377
+ container's own true starting baseline) rather than the array-specific
378
+ `-1`, so repopulating N unchanged properties leaves them numbered exactly
379
+ as a fresh container holding those same N properties would be — which,
380
+ for an edit that doesn't change the property count, is identical to what
381
+ they were numbered before the edit.
382
+
383
+ 2. **`json-6`'s `stringify` has a real bug beyond the key-sorting one**,
384
+ found by actually round-tripping values: it runs the same
385
+ identifier-unquoting logic it correctly uses for object *keys* on string
386
+ *values* too, so `{b: "hello"}` stringified to `{b: hello}` — a bareword
387
+ that parses back as an identifier reference, not the string, and wasn't
388
+ even re-parseable by the package's own `JSON6.parse`. `getTypesonTextForValue`
389
+ therefore does not call `JSON6.stringify` at all; it uses a small
390
+ hand-rolled `stringifyJSON6` (in `src/utils/rawTypesonEditor.js`) that
391
+ unquotes identifier-shaped keys but always fully quotes string values via
392
+ `JSON.stringify`. `JSON6.parse` (used to read the text back) is unaffected
393
+ and works correctly. A fix for the value-quoting bug (not the key-sorting
394
+ one, which stays a separate future PR per the maintainer's fork owner) was
395
+ prepared as a commit on the `fix/stringify-value-quoting` branch in the
396
+ local `json-6` fork checkout, with accompanying test cases, ready to
397
+ submit upstream — jsoe's own code keeps its local workaround regardless of
398
+ whether/when that lands, since a published jsoe cannot assume a consumer's
399
+ copy of `json-6` is patched.
400
+
401
+ 3. **`typeson.encapsulateAsync(value)` throws `TypeError: Async method
402
+ requested but sync result obtained`** for a value with no actually-async
403
+ parts (no `Blob`/`File`/`Promise`) unless called with
404
+ `{throwOnBadSyncType: false}` — exactly the option
405
+ `src/formats/structuredCloning.js`'s own `iterate()` already passes.
406
+ `getTypesonTextForValue` now passes the same option.
407
+
408
+ 4. **`npm run tsc-cypress` had four pre-existing failures**, unrelated to this
409
+ feature but fixed at the source rather than left as "pre-existing" once
410
+ noticed, since they were genuine gaps rather than acceptable noise:
411
+ - A test's `/** @type {import('zodexy').SzUnion} */` cast on a 2-option
412
+ union literal silently relied on `SzUnion`'s default generic, which is a
413
+ 1-element tuple (`SzUnion<[SzType]> `); parametrized it explicitly as
414
+ `SzUnion<[SzType, SzType]>` to match the actual literal.
415
+ - `src/typeChoices.js`'s `TypeChoicesElementAPI` typedef was missing three
416
+ methods (`$setType`, `$getValue`, `$addTypeAndEditUI`) that the
417
+ `$custom` object backing every type-choices `<select>` genuinely
418
+ implements — a real gap in the type, not a test-only issue. Added them.
419
+ - That in turn exposed a second, more interesting gap: `domArray`'s
420
+ `select` slot (and `buildXorTypeChoices`'s return) were typed as
421
+ `TypeChoicesElementAPI`, i.e. `HTMLSelectElement & {...methods}` — but
422
+ `buildXorTypeChoices` actually returns a real `<fieldset>` with
423
+ `value`/`selectedIndex`/`selectedOptions` shimmed on via
424
+ `Object.defineProperties` and the `$`-methods copied over by reference,
425
+ standing in for a `<select>` for the `xor` (exclusive union) case —
426
+ never an actual `HTMLSelectElement`. Asserting `HTMLSelectElement` there
427
+ was a false claim that merely happened not to be exercised strictly
428
+ enough to fail until the missing-methods gap above was fixed and
429
+ `tsc-cypress` type-checked deeper. Fixed by splitting the `$`-methods
430
+ out into their own `TypeChoicesAPIMethods` type, keeping
431
+ `TypeChoicesElementAPI` (`HTMLSelectElement & TypeChoicesAPIMethods`)
432
+ for the real `<select>`, and adding `TypeChoicesControl`
433
+ (`HTMLElement & TypeChoicesAPIMethods & {value, selectedIndex,
434
+ selectedOptions}`) — accurately describing only the shimmed surface
435
+ downstream code actually relies on — for anywhere a `xor` fieldset can
436
+ stand in for the select (`domArray`, `buildXorTypeChoices`'s
437
+ parameter/return).
438
+ - A `getUIForModeAndType` test call was missing the required `value`/
439
+ `hasValue` properties; added `value: undefined, hasValue: false`,
440
+ matching how another test in the same file already builds an
441
+ unseeded control.
442
+ - Separately, `cypress/e2e/util-unit-tests/rawTypesonEditor.cy.js`
443
+ originally imported `Types` via `#jsoe/index.js`'s re-export, which
444
+ `tsc-cypress` treated as a nominally different `Types` declaration than
445
+ the one `commitValueToContainer`'s own JSDoc references via a relative
446
+ `'../types.js'` path — surfacing as a confusing
447
+ "Type 'Types' is missing ... from type 'Types'" error. Importing
448
+ `Types` directly from `#jsoe/types.js` instead (matching the existing
449
+ convention in `cypress/e2e/src/types.cy.js`) resolved it.
450
+
451
+ `npm run tsc-cypress` is now fully clean, with no remaining pre-existing
452
+ errors.
453
+
454
+ 5. **`getEvalSeedTextForValue` originally fell through to an unquoted
455
+ `String(value)` or a same-shaped-but-wrong `{}` literal for several value
456
+ shapes it didn't explicitly recognize**, rather than either reconstructing
457
+ them correctly or failing clearly. A follow-up pass ("low-hanging fruit")
458
+ added explicit handling for the easy/mechanical cases:
459
+ - `Symbol` — previously emitted a bare, unquoted, unparseable
460
+ `Symbol(desc)`-as-identifier-reference; now emits real
461
+ `Symbol("desc")`/`Symbol.for("key")` source, using the same
462
+ `String(sym).slice(7, -1)` convention `src/fundamentalTypes/symbolType.js`
463
+ itself uses (not `.description`, which is `undefined` for a
464
+ no-description `Symbol()`).
465
+ - Boxed primitives (`new String(...)`/`new Number(...)`/`new Boolean(...)`/
466
+ boxed `BigInt`) — previously treated identically to their primitive
467
+ forms (losing the boxing); now emit the equivalent `new` expression
468
+ (`Object(5n)` for boxed `BigInt`, matching typeson-registry's own
469
+ `bigintObject` revive convention).
470
+ - `DOMException`, `DOMRect`/`DOMRectReadOnly`, `DOMPoint`/
471
+ `DOMPointReadOnly`, `DOMMatrix`/`DOMMatrixReadOnly` — each now emits its
472
+ real constructor call with its own field values (a `DOMMatrix` branches
473
+ on `m.is2D` to emit either the 6-number or 16-number constructor form,
474
+ matching `src/superTypes/dommatrixType.js`).
475
+ - `ArrayBuffer`, `DataView`, and all typed array types (including
476
+ `Float16Array`, added after being noticed missing from the initial
477
+ `typedArrayTagNames` list) — bytes are copied synchronously (no async
478
+ read needed, unlike `Blob`/`File`) and base64-encoded via a new shared
479
+ `bytesToBase64` helper, emitted as `Uint8Array.from(atob("..."), (c) =>
480
+ c.charCodeAt(0)).buffer` (bare for `ArrayBuffer`) or wrapped in `new
481
+ DataView(...)`/`new Int8Array(...)`/etc.
482
+ - `FileList` — treated as a plain array of `File`s for eval-source
483
+ purposes (spread into a real array first), consistent with §1's finding
484
+ that jsoe's own filelist control never requires a genuine
485
+ `instanceof FileList`.
486
+ - `Promise` and any value whose prototype isn't `Object.prototype`/`null`
487
+ (an unrecognized class instance) now throw a clear, named `TypeError`
488
+ instead of silently falling through to a same-shaped-but-wrong `{}` or
489
+ unquoted `String(value)` — eval mode has no way to reconstruct a
490
+ pending/settled `Promise`'s state or an arbitrary class's behavior, and a
491
+ loud failure pointing at the Typeson/JSON6 mode instead is far better
492
+ than a value that looks right but silently isn't.
493
+
494
+ Two of these surfaced their own bugs only once actually run (type-checking
495
+ alone didn't catch either, since both are about which *string* ends up in
496
+ the output, not its type):
497
+ - `Object.prototype.toString` (and thus this codebase's own `toStringTag`
498
+ helper) reports **every** `Error` subclass — `TypeError`, `RangeError`,
499
+ `AggregateError`, etc. — as `[object Error]`; this is a spec'd
500
+ `[[ErrorData]]`-internal-slot check, not a `Symbol.toStringTag` lookup,
501
+ so it can't be worked around by reading a tag property. A `new
502
+ TypeError('bad type')` was therefore being re-emitted as `new
503
+ Error('bad type')`, silently downgrading the reconstructed error's own
504
+ type. Fixed by recovering the real constructor name from
505
+ `err.constructor.name` instead of `tag`, falling back to plain `Error`
506
+ only when that name isn't one eval mode actually knows how to
507
+ name-construct (guards against a user-defined `class MyError extends
508
+ Error {}`, which isn't globally nameable this way).
509
+ - The same tag-collapsing issue affected the unrecognized-class-instance
510
+ error message: `toStringTag` reports a generic `Object` for most
511
+ user-defined classes (they don't set their own `Symbol.toStringTag`), so
512
+ the thrown message read "...cannot represent a Object value..." instead
513
+ of naming the actual class. Fixed by preferring
514
+ `value.constructor?.name` over `tag` when building that message.
515
+
516
+ Both were caught by running the corresponding new Cypress tests (not by
517
+ type-checking, which was clean throughout) — `expected [TypeError: bad
518
+ type] to be an instance of TypeError` and a message-content assertion
519
+ expecting `/Whatever/u` that got the generic `Object` text instead.
520
+
521
+ Validated: `npx eslint .`, `npx tsc`, `npm run tsc:ts7` (TypeScript 7 nightly),
522
+ and `npm run tsc-cypress` are all clean. `cypress/e2e/util-unit-tests/rawTypesonEditor.cy.js`
523
+ (22 tests, run against a real Chrome via Cypress, all passing) covers:
524
+ - Typeson/JSON6 and eval-mode round-tripping, including Date/RegExp/Map/Set,
525
+ quoted-vs-unquoted keys, hand-edited JSON6 syntax (comments) being accepted
526
+ back, a `File` and a plain `Blob` each round-tripping through eval with
527
+ their real binary content (name/type/`lastModified` and bytes all
528
+ verified, via `evaled.text()`), and a cyclic value failing clearly in eval
529
+ mode;
530
+ - The low-hanging-fruit eval-mode fixes above: a plain and a registered
531
+ `Symbol` round-tripping to the same symbol (via `Symbol.for`); boxed
532
+ `String`/`Number`/`Boolean`/`BigInt` round-tripping as real boxed objects;
533
+ a `TypeError` and an `AggregateError` (with `cause`, and with its nested
534
+ `errors` array) round-tripping as their real, specific constructors, not a
535
+ generic `Error`; `DOMException`/`DOMRect`/`DOMPoint`/`DOMMatrix`
536
+ round-tripping their fields and `instanceof`; `ArrayBuffer`/`DataView`/a
537
+ typed array round-tripping their real bytes; a `FileList` (built via a real
538
+ `DataTransfer`) round-tripping as a plain array of `File`s; and a `Promise`
539
+ and a user-defined class instance each throwing a clear, correctly-named
540
+ error instead of silently producing a wrong value;
541
+ - `commitValueToContainer`'s `$resetItemIndex` fix, two ways: (1) builds a
542
+ real 3-property object control, replaces it via `commitValueToContainer`
543
+ with a single property, then clicks the real "+ Item" button and asserts
544
+ both the repopulated property's own legend number and the new manually-
545
+ added property's legend number match what a fresh container with that
546
+ many properties would show; (2) the exact regression manual testing
547
+ found — a single manually-added property (legend "1") whose *value* alone
548
+ is changed via `commitValueToContainer` (reading the live value via
549
+ `types.getValueForRoot`, exactly as `openRawEditorDialog` does, and
550
+ writing back the same key with a new value) keeps showing "1" afterward;
551
+ - `allowUnsafeEval` gating, driven through the real "Edit raw" button and
552
+ dialog: the eval-mode `<select>` is absent by default, present when
553
+ `allowUnsafeEval: true`, and switching to it reseeds the editor with real
554
+ JS constructor source (`new Date(`) instead of Typeson-tagged data
555
+ (`$types`).
556
+
557
+ The existing 59-test `cypress/e2e/fundamentalTypes/array.cy.js` suite still
558
+ passes unchanged throughout, confirming no regression to existing
559
+ object/array/tuple/record behavior.
560
+
561
+ One environment quirk worth noting for future tests in this file: none of
562
+ these specs call `cy.visit()` (matching this directory's existing style,
563
+ e.g. `dialogs.cy.js`), which means there is no application document for
564
+ `cy.get(cssSelector)` to search — elements appended via a plain
565
+ `document.body.append(...)` are invisible to it and any `cy.get()` call
566
+ against a selector will simply time out. The fix used throughout is to hold
567
+ the actual element reference (via `.as('alias')`) and query relative to it
568
+ with `cy.wrap(...)`/`cy.get('@alias').find(...)`, which operate on that
569
+ specific element's own subtree directly rather than searching a page. A
570
+ related wrinkle: action commands like `.click()`/`.select()` refuse to act on
571
+ elements outside cypress's (here, blank/unused) viewport; `{force: true}` is
572
+ disallowed by this repo's lint config (`sonarjs/no-forced-browser-interaction`),
573
+ so these tests instead call `.invoke('click')` or manually set `.value` and
574
+ dispatch a `change` event, matching the plain-DOM-interaction style
575
+ `dialogs.cy.js` already uses for its own dialogs.
package/mmr.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "mochaBadgeGeneratorReporterOptions": {
3
+ "badge_output": "badges/tests-badge.svg"
4
+ },
5
+ "reporterEnabled": "spec, mocha-badge-generator"
6
+ }