@es-joy/jsoe 0.27.0 → 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 (49) hide show
  1. package/CHANGES.md +33 -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/index.js +2 -2
  10. package/dist/index.js.map +1 -1
  11. package/dist/typeChoices.d.ts +32 -2
  12. package/dist/typeChoices.d.ts.map +1 -1
  13. package/dist/types.d.ts +8 -2
  14. package/dist/types.d.ts.map +1 -1
  15. package/dist/utils/rawTypesonEditor.d.ts +110 -0
  16. package/dist/utils/rawTypesonEditor.d.ts.map +1 -0
  17. package/dist/vendor-imports.d.ts +4 -0
  18. package/docs/proposals/raw-typeson-edit-view.md +575 -0
  19. package/mmr.json +6 -0
  20. package/package.json +13 -4
  21. package/pnpm-workspace.yaml +1 -0
  22. package/src/formats/schema.js +3 -1
  23. package/src/formats/structuredCloning.js +24 -17
  24. package/src/fundamentalTypes/arrayType.js +105 -1
  25. package/src/jsoe.css +12 -0
  26. package/src/typeChoices.js +36 -7
  27. package/src/types.js +13 -2
  28. package/src/utils/rawTypesonEditor.js +669 -0
  29. package/src/vendor-imports.js +12 -0
  30. package/tsconfig.json +1 -1
  31. package/typings/json-6.d.ts +12 -0
  32. package/vendor/@codemirror/autocomplete/dist/index.js +2125 -0
  33. package/vendor/@codemirror/commands/dist/index.js +1826 -0
  34. package/vendor/@codemirror/lang-javascript/dist/index.js +513 -0
  35. package/vendor/@codemirror/language/dist/index.js +2693 -0
  36. package/vendor/@codemirror/lint/dist/index.js +956 -0
  37. package/vendor/@codemirror/search/dist/index.js +1238 -0
  38. package/vendor/@codemirror/state/dist/index.js +3947 -0
  39. package/vendor/@codemirror/view/dist/index.js +11867 -0
  40. package/vendor/@lezer/common/dist/index.js +2202 -0
  41. package/vendor/@lezer/highlight/dist/index.js +927 -0
  42. package/vendor/@lezer/javascript/dist/index.js +192 -0
  43. package/vendor/@lezer/lr/dist/index.js +1889 -0
  44. package/vendor/@marijn/find-cluster-break/src/index.js +87 -0
  45. package/vendor/codemirror/dist/index.js +96 -0
  46. package/vendor/crelt/index.js +28 -0
  47. package/vendor/json-6/dist/index.mjs +1783 -0
  48. package/vendor/style-mod/src/style-mod.js +172 -0
  49. package/vendor/w3c-keyname/index.js +119 -0
@@ -194,13 +194,15 @@ const dezerializerCodecs = {
194
194
  };
195
195
 
196
196
  /**
197
+ * Exported for reuse by `src/utils/rawTypesonEditor.js`, which must enforce
198
+ * the same schema conformance check before accepting a raw-edited value.
197
199
  * @param {InstanceType<typeof import('../types.js').default>} types
198
200
  * @param {ZodexSchema} schemaObject
199
201
  * @param {ZodexSchema} originalShape
200
202
  * @param {unknown} value
201
203
  * @returns {ReturnType<ReturnType<typeof dezerialize>['safeParse']>}
202
204
  */
203
- function parseValue (types, schemaObject, originalShape, value) {
205
+ export function parseValue (types, schemaObject, originalShape, value) {
204
206
  return dezerialize(schemaObject, {
205
207
  checks: getChecks(types),
206
208
  codecs: dezerializerCodecs,
@@ -274,23 +274,30 @@ const encapsulateObserver = (stateObj) => {
274
274
  // console.log('but could set to', schema);
275
275
  // console.log('arrayOrObjectPropertyName', arrayOrObjectPropertyName);
276
276
 
277
- if (!stateObj.rootUI) {
278
- // console.log('vvvv0', newType, newValue);
279
- stateObj.rootUI = types?.getUIForModeAndType({
280
- readonly,
281
- typeNamespace,
282
- type: newType,
283
- bringIntoFocus: false,
284
- buildTypeChoices,
285
- format,
286
- specificSchemaObject: schema,
287
- schemaContent,
288
- value: newValue,
289
- hasValue: true,
290
- // Not currently in use but may be convenient for a
291
- // type wanting the serialized data
292
- replaced
293
- });
277
+ if (!parents['']) {
278
+ // A caller repopulating an *existing* container in place (e.g. the raw
279
+ // Typeson/JSON6 editor replacing one control's children after a
280
+ // bulk edit, `src/utils/rawTypesonEditor.js`) pre-seeds
281
+ // `stateObj.rootUI` with that container so it is reused here instead
282
+ // of a fresh one being built.
283
+ if (!stateObj.rootUI) {
284
+ // console.log('vvvv0', newType, newValue);
285
+ stateObj.rootUI = types?.getUIForModeAndType({
286
+ readonly,
287
+ typeNamespace,
288
+ type: newType,
289
+ bringIntoFocus: false,
290
+ buildTypeChoices,
291
+ format,
292
+ specificSchemaObject: schema,
293
+ schemaContent,
294
+ value: newValue,
295
+ hasValue: true,
296
+ // Not currently in use but may be convenient for a
297
+ // type wanting the serialized data
298
+ replaced
299
+ });
300
+ }
294
301
  parents[''] = /**
295
302
  * @type {HTMLElement &
296
303
  * {$addAndSetArrayElement: AddAndSetArrayElement}}
@@ -7,6 +7,7 @@ import {
7
7
  schemaLabel, lastTypeChild, deprecatedClassSuffix
8
8
  } from '../utils/schemaMeta.js';
9
9
  import dialogs from '../utils/dialogs.js';
10
+ import {openRawEditorDialog} from '../utils/rawTypesonEditor.js';
10
11
  import {
11
12
  resolveJSONPointer, getJSONPointerParts, reduceJSONPointerParts
12
13
  } from '../utils/jsonPointer.js';
@@ -428,6 +429,13 @@ const arrayType = {
428
429
  specificSchemaObject?.type === 'looseRecord';
429
430
  const tupleMode = specificSchemaObject?.type === 'tuple';
430
431
 
432
+ // See `docs/proposals/raw-typeson-edit-view.md` §1 for the scope
433
+ // rationale (why `filelist` is included, why record/tuple count).
434
+ const isRawEditable = types.showRawTypesonControls &&
435
+ ((type === 'object' || type === 'array' ||
436
+ type === 'set' || type === 'map' || type === 'filelist') ||
437
+ recordMode || tupleMode);
438
+
431
439
  /**
432
440
  * @param {{
433
441
  * itemIndex: number,
@@ -660,6 +668,31 @@ const arrayType = {
660
668
  target
661
669
  ).textContent = arrayContents.hidden ? '+' : '-';
662
670
  }}}, ['-']],
671
+ ...(isRawEditable
672
+ ? /** @type {import('jamilih').JamilihArray[]} */ ([
673
+ nbsp.repeat(2),
674
+ ['button', {
675
+ class: 'viewRawTypeson',
676
+ $on: {click (/** @type {Event} */ e) {
677
+ e.preventDefault();
678
+ openRawEditorDialog({
679
+ types,
680
+ format: /** @type {import('../formats.js').AvailableFormat} */ (
681
+ format
682
+ ),
683
+ type: /** @type {import('../types.js').AvailableArbitraryType} */ (
684
+ type
685
+ ),
686
+ root: /** @type {HTMLDivElement} */ (div),
687
+ topRoot: /** @type {HTMLDivElement} */ (topRoot),
688
+ typeNamespace,
689
+ specificSchemaObject,
690
+ readonly: true
691
+ });
692
+ }}
693
+ }, ['View raw']]
694
+ ])
695
+ : []),
663
696
  ['div', {class: 'arrayContents'}, [
664
697
  !recordMode && this.array
665
698
  ? ['div', {
@@ -724,6 +757,13 @@ const arrayType = {
724
757
  specificSchemaObject?.type === 'looseRecord';
725
758
  const tupleMode = specificSchemaObject?.type === 'tuple';
726
759
 
760
+ // See `docs/proposals/raw-typeson-edit-view.md` §1 for the scope
761
+ // rationale (why `filelist` is included, why record/tuple count).
762
+ const isRawEditable = types.showRawTypesonControls &&
763
+ ((type === 'object' || type === 'array' ||
764
+ type === 'set' || type === 'map' || type === 'filelist') ||
765
+ recordMode || tupleMode);
766
+
727
767
  // A `record` refines `object` but, for legend numbering/reordering, behaves
728
768
  // like the array-based types did when it was its own (`array: true`) type.
729
769
  const parentIsArrayLike = parentTypeObject.array || recordMode;
@@ -2358,6 +2398,34 @@ const arrayType = {
2358
2398
  ).textContent = arrayContents.hidden ? '+' : '-';
2359
2399
  }}}, ['-']];
2360
2400
 
2401
+ const rawEditButton = isRawEditable
2402
+ ? [
2403
+ nbsp.repeat(2),
2404
+ ['button', {
2405
+ class: 'editRawTypeson',
2406
+ $on: {click (/** @type {Event} */ e) {
2407
+ e.preventDefault();
2408
+ openRawEditorDialog({
2409
+ types,
2410
+ format: /** @type {import('../formats.js').AvailableFormat} */ (
2411
+ format
2412
+ ),
2413
+ type: /** @type {import('../types.js').AvailableArbitraryType} */ (
2414
+ type
2415
+ ),
2416
+ root: /** @type {HTMLDivElement} */ (div),
2417
+ // By click time `topRoot ||= div` below has already run if
2418
+ // this control is itself the root.
2419
+ topRoot: /** @type {HTMLDivElement} */ (topRoot),
2420
+ typeNamespace,
2421
+ specificSchemaObject,
2422
+ readonly: false
2423
+ });
2424
+ }}
2425
+ }, ['Edit raw']]
2426
+ ]
2427
+ : [];
2428
+
2361
2429
  const arrayContents = /** @type {import('jamilih').JamilihArray} */ (
2362
2430
  ['div', {class: 'arrayContents'}, [
2363
2431
  arrayContentsFirstChild,
@@ -2433,7 +2501,8 @@ const arrayType = {
2433
2501
  * $getTypeChoices: () => HTMLSelectElement & {
2434
2502
  * $setType: import('../typeChoices.js').SetType,
2435
2503
  * $getTypeRoot: import('../formatAndTypeChoices.js').TypeRootGetter
2436
- * }
2504
+ * },
2505
+ * $resetItemIndex: () => void
2437
2506
  * }} DivArrayOrObjectHolder
2438
2507
  */
2439
2508
 
@@ -2585,6 +2654,40 @@ const arrayType = {
2585
2654
  this.$getAddArrayElement().previousElementSibling
2586
2655
  );
2587
2656
  },
2657
+ /**
2658
+ * Resets the `itemIndex`/legend-numbering counter back to its
2659
+ * empty-container baseline (`itemAdjust - 1`, the same value
2660
+ * it's initialized to before any item is ever added — *not*
2661
+ * the `-1` the "x All" button below hardcodes, which is only
2662
+ * correct for arrays; an object's baseline is `0`). Exposed so
2663
+ * a caller that repopulates this container's children from
2664
+ * outside this closure (bypassing the normal per-item
2665
+ * add/remove handlers that keep `itemIndex` in sync as they
2666
+ * go) — currently only
2667
+ * `src/utils/rawTypesonEditor.js`'s `commitValueToContainer`,
2668
+ * after a raw-value edit replaces every child — ends up with
2669
+ * exactly the same numbering a fresh container holding the
2670
+ * same new items would have, rather than continuing from
2671
+ * wherever the replaced content had left off (or, using the
2672
+ * wrong baseline, starting one lower than a fresh container
2673
+ * would: a single-property object's only property would be
2674
+ * renumbered from "1" to "0" by a bulk edit that didn't even
2675
+ * change its key, for example).
2676
+ * @this {DivArrayOrObjectHolder}
2677
+ * @returns {void}
2678
+ */
2679
+ $resetItemIndex () {
2680
+ if (sparse) {
2681
+ decrementItemIndex(
2682
+ /**
2683
+ * @type {HTMLDivElement & {
2684
+ * $getPropertyInputs: GetPropertyInputs
2685
+ }} */ (this.$getArrayItems())
2686
+ );
2687
+ } else {
2688
+ itemIndex = itemAdjust - 1;
2689
+ }
2690
+ },
2588
2691
  /**
2589
2692
  * @this {DivArrayOrObjectHolder}
2590
2693
  * @returns {HTMLSelectElement & {
@@ -2684,6 +2787,7 @@ const arrayType = {
2684
2787
  }]
2685
2788
  : '',
2686
2789
  minusButton,
2790
+ ...rawEditButton,
2687
2791
  arrayContents
2688
2792
  ]))
2689
2793
  );
package/src/jsoe.css CHANGED
@@ -139,6 +139,18 @@ video.recordedMedia {
139
139
  height: 300px;
140
140
  }
141
141
 
142
+ .jsoe-raw-editor {
143
+ width: 500px;
144
+ max-width: 100%;
145
+ height: 300px;
146
+ overflow: auto;
147
+ border: 1px solid;
148
+ }
149
+
150
+ .jsoe-raw-editor-error:not(:empty) {
151
+ color: #b00;
152
+ }
153
+
142
154
  .visualizer {
143
155
  height: 60px;
144
156
  }
@@ -66,17 +66,44 @@ import deepEqual from 'fast-deep-equal/es6/index.js';
66
66
  */
67
67
 
68
68
  /**
69
- * @typedef {HTMLSelectElement & {
69
+ * The `$`-methods every type-choices control exposes, whether it's a real
70
+ * `<select>` (`TypeChoicesElementAPI`) or the `xor` radio group's
71
+ * `<fieldset>` standing in for one (`TypeChoicesControl` — see its comment
72
+ * for why that one is *not* typed as `HTMLSelectElement`).
73
+ * @typedef {{
70
74
  * $addAndValidateEditUI: AddAndValidateEditUI,
75
+ * $addTypeAndEditUI: AddTypeAndEditUI,
71
76
  * $setStyles: SetStyles,
72
77
  * $getTypeRoot: GetTypeRoot,
73
78
  * $getContainer: () => HTMLElement,
74
79
  * $getTopRoot: () => HTMLDivElement,
75
80
  * $addEditUI: AddEditUI,
76
81
  * $validate: Validate,
82
+ * $setType: SetType,
77
83
  * $setTypeNoEditUI: SetTypeNoEditUI,
84
+ * $getValue: GetValue,
78
85
  * $whenReady: WhenReady
79
- * }} TypeChoicesElementAPI
86
+ * }} TypeChoicesAPIMethods
87
+ */
88
+
89
+ /**
90
+ * @typedef {HTMLSelectElement & TypeChoicesAPIMethods} TypeChoicesElementAPI
91
+ */
92
+
93
+ /**
94
+ * The type-choices control surface downstream code actually relies on:
95
+ * the `$`-methods, plus the `value`/`selectedIndex`/`selectedOptions`
96
+ * surface a real `<select>` provides natively. Deliberately *not* typed as
97
+ * `HTMLSelectElement`, because `buildXorTypeChoices` below returns a real
98
+ * `<fieldset>` with that surface shimmed on via `Object.defineProperties`
99
+ * (and the `$`-methods copied by reference from the parallel `<select>`)
100
+ * rather than an actual `<select>` — asserting `HTMLSelectElement` there
101
+ * would be a false claim, not just a loose one.
102
+ * @typedef {HTMLElement & TypeChoicesAPIMethods & {
103
+ * value: string,
104
+ * selectedIndex: number,
105
+ * selectedOptions: ArrayLike<{value: string, dataset?: {idx?: string}}>
106
+ * }} TypeChoicesControl
80
107
  */
81
108
 
82
109
  /**
@@ -126,7 +153,7 @@ import deepEqual from 'fast-deep-equal/es6/index.js';
126
153
  * schemaContent?: import('./formatAndTypeChoices.js').ZodexSchema,
127
154
  * }} cfg
128
155
  * @returns {{
129
- * domArray: [select: HTMLElement, typeContainer: HTMLElement],
156
+ * domArray: [select: TypeChoicesControl, typeContainer: HTMLElement],
130
157
  * getValue: GetValue,
131
158
  * getType: GetType,
132
159
  * validValuesSet: ValidValuesSet,
@@ -200,9 +227,9 @@ function deriveXorBranchLabel (schemaObj, optText, idx) {
200
227
  * types: InstanceType<typeof import('./types.js').default>,
201
228
  * typeContainer: HTMLElement,
202
229
  * xorSchema: import('zodexy').SzUnion|undefined,
203
- * selectEl: HTMLSelectElement
230
+ * selectEl: TypeChoicesElementAPI
204
231
  * }} cfg
205
- * @returns {HTMLSelectElement}
232
+ * @returns {TypeChoicesControl}
206
233
  */
207
234
  function buildXorTypeChoices ({
208
235
  typeNamespace, keySelectClass, typeOptions, schemaObjs, types,
@@ -359,7 +386,9 @@ function buildXorTypeChoices ({
359
386
  }
360
387
  });
361
388
 
362
- return /** @type {HTMLSelectElement} */ (/** @type {unknown} */ (fieldset));
389
+ return /** @type {TypeChoicesControl} */ (
390
+ /** @type {unknown} */ (fieldset)
391
+ );
363
392
  }
364
393
 
365
394
  /**
@@ -437,7 +466,7 @@ export const buildTypeChoices = ({
437
466
  // match indicator to value changes inside it
438
467
  const typeContainer = jml('div', {class: 'typeContainer'});
439
468
 
440
- const selectEl = /** @type {HTMLSelectElement} */ (jml('select', {
469
+ const selectEl = /** @type {TypeChoicesElementAPI} */ (jml('select', {
441
470
  hidden: requireObject || typeOptions.length === 1,
442
471
  class: `typeChoices-${typeNamespace}${keySelectClass
443
472
  ? ' ' + keySelectClass
package/src/types.js CHANGED
@@ -438,17 +438,28 @@ class Types {
438
438
  /**
439
439
  * @param {{
440
440
  * useZodexyErrorMessages?: boolean,
441
- * useZodexyErrorMessagesInTypes?: boolean
441
+ * useZodexyErrorMessagesInTypes?: boolean,
442
+ * allowUnsafeEval?: boolean,
443
+ * showRawTypesonControls?: boolean
442
444
  * }} [cfg]
443
445
  */
444
446
  constructor ({
445
447
  useZodexyErrorMessages = false,
446
- useZodexyErrorMessagesInTypes = false
448
+ useZodexyErrorMessagesInTypes = false,
449
+ allowUnsafeEval = false,
450
+ showRawTypesonControls = true
447
451
  } = {}) {
448
452
  this.formats = new Formats(); // Todo: Make customizable and test
449
453
 
450
454
  this.useZodexyErrorMessages = useZodexyErrorMessages;
451
455
  this.useZodexyErrorMessagesInTypes = useZodexyErrorMessagesInTypes;
456
+ // Gates the eval-mode sub-feature of the raw Typeson edit/view buttons
457
+ // (`src/utils/rawTypesonEditor.js`); disabled by default since eval'ing
458
+ // arbitrary user-supplied text is inherently unsafe.
459
+ this.allowUnsafeEval = allowUnsafeEval;
460
+ // Master on/off switch for the raw Typeson edit/view buttons themselves;
461
+ // when `false` neither button is rendered anywhere in the tree.
462
+ this.showRawTypesonControls = showRawTypesonControls;
452
463
  /** @type {WeakMap<RootElement, import('./formats/schema.js').ZodexSchema>} */
453
464
  this.schemasForRoots = new WeakMap();
454
465