@portabletext/editor 2.21.3 → 3.0.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 (95) hide show
  1. package/lib/_chunks-dts/index.d.ts +49 -209
  2. package/lib/_chunks-es/selector.is-at-the-start-of-block.js +103 -20
  3. package/lib/_chunks-es/selector.is-at-the-start-of-block.js.map +1 -1
  4. package/lib/_chunks-es/{util.get-text-block-text.js → util.slice-blocks.js} +73 -24
  5. package/lib/_chunks-es/util.slice-blocks.js.map +1 -0
  6. package/lib/_chunks-es/util.slice-text-block.js +13 -2
  7. package/lib/_chunks-es/util.slice-text-block.js.map +1 -1
  8. package/lib/behaviors/index.d.ts +1 -1
  9. package/lib/index.d.ts +2 -2
  10. package/lib/index.js +339 -341
  11. package/lib/index.js.map +1 -1
  12. package/lib/plugins/index.d.ts +2 -133
  13. package/lib/plugins/index.js +2 -796
  14. package/lib/plugins/index.js.map +1 -1
  15. package/lib/selectors/index.d.ts +2 -24
  16. package/lib/selectors/index.js +28 -130
  17. package/lib/selectors/index.js.map +1 -1
  18. package/lib/utils/index.d.ts +6 -4
  19. package/lib/utils/index.js +98 -9
  20. package/lib/utils/index.js.map +1 -1
  21. package/package.json +1 -3
  22. package/src/behaviors/behavior.abstract.split.ts +1 -0
  23. package/src/behaviors/behavior.perform-event.ts +7 -7
  24. package/src/converters/converter.portable-text.ts +1 -0
  25. package/src/converters/converter.text-html.ts +1 -0
  26. package/src/converters/converter.text-plain.ts +1 -0
  27. package/src/editor/Editable.tsx +1 -0
  28. package/src/editor/PortableTextEditor.tsx +0 -19
  29. package/src/editor/create-editor.ts +0 -3
  30. package/src/editor/editor-machine.ts +0 -10
  31. package/src/editor/event-to-change.tsx +5 -1
  32. package/src/editor/plugins/create-with-event-listeners.ts +30 -6
  33. package/src/editor/plugins/createWithObjectKeys.ts +2 -1
  34. package/src/editor/plugins/createWithPatches.ts +3 -3
  35. package/src/editor/plugins/createWithPlaceholderBlock.ts +2 -1
  36. package/src/editor/plugins/createWithPortableTextMarkModel.ts +2 -1
  37. package/src/editor/plugins/with-plugins.ts +10 -14
  38. package/src/editor/relay-machine.ts +0 -4
  39. package/src/editor/sync-machine.ts +2 -2
  40. package/src/editor.ts +0 -4
  41. package/src/history/behavior.operation.history.redo.ts +67 -0
  42. package/src/history/behavior.operation.history.undo.ts +71 -0
  43. package/src/history/event.history.undo.test.tsx +672 -0
  44. package/src/history/history.preserving-keys.test.tsx +112 -0
  45. package/src/history/remote-patches.ts +20 -0
  46. package/src/history/slate-plugin.history.ts +146 -0
  47. package/src/history/slate-plugin.redoing.ts +21 -0
  48. package/src/history/slate-plugin.undoing.ts +21 -0
  49. package/src/history/slate-plugin.without-history.ts +23 -0
  50. package/src/history/transform-operation.ts +245 -0
  51. package/src/history/undo-redo-collaboration.test.tsx +541 -0
  52. package/src/history/undo-redo.feature +125 -0
  53. package/src/history/undo-redo.test.tsx +195 -0
  54. package/src/history/undo-step.ts +148 -0
  55. package/src/index.ts +0 -1
  56. package/src/internal-utils/operation-to-patches.test.ts +23 -25
  57. package/src/internal-utils/operation-to-patches.ts +31 -22
  58. package/src/internal-utils/selection-text.test.ts +3 -0
  59. package/src/internal-utils/selection-text.ts +5 -2
  60. package/src/internal-utils/values.ts +23 -11
  61. package/src/operations/behavior.operation.block.set.ts +1 -0
  62. package/src/operations/behavior.operation.block.unset.ts +2 -0
  63. package/src/operations/behavior.operation.insert.block.ts +1 -0
  64. package/src/operations/behavior.operations.ts +2 -4
  65. package/src/plugins/index.ts +0 -3
  66. package/src/selectors/index.ts +0 -3
  67. package/src/test/vitest/step-definitions.tsx +57 -0
  68. package/src/test/vitest/test-editor.tsx +1 -1
  69. package/src/utils/parse-blocks.test.ts +296 -16
  70. package/src/utils/parse-blocks.ts +81 -22
  71. package/src/utils/util.merge-text-blocks.ts +5 -1
  72. package/src/utils/util.slice-blocks.ts +24 -10
  73. package/lib/_chunks-es/selector.get-selection-text.js +0 -92
  74. package/lib/_chunks-es/selector.get-selection-text.js.map +0 -1
  75. package/lib/_chunks-es/selector.get-text-before.js +0 -36
  76. package/lib/_chunks-es/selector.get-text-before.js.map +0 -1
  77. package/lib/_chunks-es/util.get-text-block-text.js.map +0 -1
  78. package/lib/_chunks-es/util.is-empty-text-block.js +0 -40
  79. package/lib/_chunks-es/util.is-empty-text-block.js.map +0 -1
  80. package/lib/_chunks-es/util.merge-text-blocks.js +0 -101
  81. package/lib/_chunks-es/util.merge-text-blocks.js.map +0 -1
  82. package/src/editor/plugins/createWithMaxBlocks.ts +0 -53
  83. package/src/editor/plugins/createWithUndoRedo.ts +0 -628
  84. package/src/editor/with-undo-step.ts +0 -37
  85. package/src/editor/withUndoRedo.ts +0 -34
  86. package/src/editor-event-listener.tsx +0 -28
  87. package/src/plugins/plugin.decorator-shortcut.ts +0 -238
  88. package/src/plugins/plugin.markdown.test.tsx +0 -42
  89. package/src/plugins/plugin.markdown.tsx +0 -131
  90. package/src/plugins/plugin.one-line.tsx +0 -123
  91. package/src/selectors/selector.get-list-state.test.ts +0 -189
  92. package/src/selectors/selector.get-list-state.ts +0 -96
  93. package/src/selectors/selector.get-selected-slice.ts +0 -13
  94. package/src/selectors/selector.get-trimmed-selection.test.ts +0 -657
  95. package/src/selectors/selector.get-trimmed-selection.ts +0 -189
@@ -114,6 +114,7 @@ export const stepDefinitions = [
114
114
  },
115
115
  blocks: JSON.parse(blocks),
116
116
  options: {
117
+ normalize: false,
117
118
  removeUnusedMarkDefs: false,
118
119
  validateFields: true,
119
120
  },
@@ -235,6 +236,21 @@ export const stepDefinitions = [
235
236
  })
236
237
  },
237
238
  ),
239
+ When(
240
+ '{button} is pressed in Editor B',
241
+ async (context: Context, button: Parameter['button']) => {
242
+ const previousSelection = context.editorB.getSnapshot().context.selection
243
+ await userEvent.keyboard(button)
244
+
245
+ await vi.waitFor(() => {
246
+ const currentSelection = context.editorB.getSnapshot().context.selection
247
+
248
+ if (currentSelection) {
249
+ expect(currentSelection).not.toBe(previousSelection)
250
+ }
251
+ })
252
+ },
253
+ ),
238
254
  When(
239
255
  '{button} is pressed {int} times',
240
256
  async (context: Context, button: Parameter['button'], times: number) => {
@@ -253,6 +269,25 @@ export const stepDefinitions = [
253
269
  }
254
270
  },
255
271
  ),
272
+ When(
273
+ '{button} is pressed {int} times in Editor B',
274
+ async (context: Context, button: Parameter['button'], times: number) => {
275
+ for (let i = 0; i < times; i++) {
276
+ const previousSelection =
277
+ context.editorB.getSnapshot().context.selection
278
+ await userEvent.keyboard(button)
279
+
280
+ await vi.waitFor(() => {
281
+ const currentSelection =
282
+ context.editorB.getSnapshot().context.selection
283
+
284
+ if (currentSelection) {
285
+ expect(currentSelection).not.toBe(previousSelection)
286
+ }
287
+ })
288
+ }
289
+ },
290
+ ),
256
291
  When(
257
292
  '{shortcut} is pressed',
258
293
  async (context: Context, shortcut: Parameter['shortcut']) => {
@@ -302,6 +337,27 @@ export const stepDefinitions = [
302
337
  })
303
338
  },
304
339
  ),
340
+ When(
341
+ 'the caret is put before {string} in Editor B',
342
+ async (context: Context, text: string) => {
343
+ await vi.waitFor(() => {
344
+ const selection = getSelectionBeforeText(
345
+ context.editorB.getSnapshot().context,
346
+ text,
347
+ )
348
+ expect(selection).not.toBeNull()
349
+
350
+ context.editorB.send({
351
+ type: 'select',
352
+ at: selection,
353
+ })
354
+
355
+ expect(context.editorB.getSnapshot().context.selection).toEqual(
356
+ selection,
357
+ )
358
+ })
359
+ },
360
+ ),
305
361
  Then(
306
362
  'the caret is before {string}',
307
363
  async (context: Context, text: string) => {
@@ -506,6 +562,7 @@ export const stepDefinitions = [
506
562
  },
507
563
  blocks: JSON.parse(blocks),
508
564
  options: {
565
+ normalize: false,
509
566
  removeUnusedMarkDefs: false,
510
567
  validateFields: true,
511
568
  },
@@ -134,7 +134,7 @@ export async function createTestEditors(
134
134
  ...patch,
135
135
  origin: 'remote',
136
136
  })),
137
- snapshot: event.snapshot,
137
+ snapshot: event.value,
138
138
  })
139
139
  editorBRef.current?.send({
140
140
  type: 'update value',
@@ -1,7 +1,7 @@
1
1
  import {compileSchema, defineSchema} from '@portabletext/schema'
2
2
  import {createTestKeyGenerator} from '@portabletext/test'
3
3
  import {describe, expect, test} from 'vitest'
4
- import {parseBlock, parseSpan} from './parse-blocks'
4
+ import {parseBlock, parseInlineObject, parseSpan} from './parse-blocks'
5
5
 
6
6
  describe(parseBlock.name, () => {
7
7
  test('null', () => {
@@ -12,7 +12,11 @@ describe(parseBlock.name, () => {
12
12
  keyGenerator: createTestKeyGenerator(),
13
13
  schema: compileSchema(defineSchema({})),
14
14
  },
15
- options: {removeUnusedMarkDefs: true, validateFields: true},
15
+ options: {
16
+ normalize: false,
17
+ removeUnusedMarkDefs: true,
18
+ validateFields: true,
19
+ },
16
20
  }),
17
21
  ).toBe(undefined)
18
22
  })
@@ -25,7 +29,11 @@ describe(parseBlock.name, () => {
25
29
  keyGenerator: createTestKeyGenerator(),
26
30
  schema: compileSchema(defineSchema({})),
27
31
  },
28
- options: {removeUnusedMarkDefs: true, validateFields: true},
32
+ options: {
33
+ normalize: false,
34
+ removeUnusedMarkDefs: true,
35
+ validateFields: true,
36
+ },
29
37
  }),
30
38
  ).toBe(undefined)
31
39
  })
@@ -39,7 +47,11 @@ describe(parseBlock.name, () => {
39
47
  keyGenerator: createTestKeyGenerator(),
40
48
  schema: compileSchema(defineSchema({})),
41
49
  },
42
- options: {removeUnusedMarkDefs: true, validateFields: true},
50
+ options: {
51
+ normalize: false,
52
+ removeUnusedMarkDefs: true,
53
+ validateFields: true,
54
+ },
43
55
  }),
44
56
  ).toBe(undefined)
45
57
  })
@@ -54,7 +66,11 @@ describe(parseBlock.name, () => {
54
66
  defineSchema({blockObjects: [{name: 'image'}]}),
55
67
  ),
56
68
  },
57
- options: {removeUnusedMarkDefs: true, validateFields: true},
69
+ options: {
70
+ normalize: false,
71
+ removeUnusedMarkDefs: true,
72
+ validateFields: true,
73
+ },
58
74
  }),
59
75
  ).toBe(undefined)
60
76
  })
@@ -69,7 +85,11 @@ describe(parseBlock.name, () => {
69
85
  defineSchema({blockObjects: [{name: 'image'}]}),
70
86
  ),
71
87
  },
72
- options: {removeUnusedMarkDefs: true, validateFields: true},
88
+ options: {
89
+ normalize: false,
90
+ removeUnusedMarkDefs: true,
91
+ validateFields: true,
92
+ },
73
93
  }),
74
94
  ).toEqual({
75
95
  _key: 'k0',
@@ -87,7 +107,11 @@ describe(parseBlock.name, () => {
87
107
  keyGenerator: createTestKeyGenerator(),
88
108
  schema: compileSchema(defineSchema({})),
89
109
  },
90
- options: {removeUnusedMarkDefs: true, validateFields: true},
110
+ options: {
111
+ normalize: false,
112
+ removeUnusedMarkDefs: true,
113
+ validateFields: true,
114
+ },
91
115
  }),
92
116
  ).toEqual({
93
117
  _key: 'k0',
@@ -114,7 +138,11 @@ describe(parseBlock.name, () => {
114
138
  keyGenerator: createTestKeyGenerator(),
115
139
  schema: {...schema, block: {...schema.block, name: 'text'}},
116
140
  },
117
- options: {removeUnusedMarkDefs: true, validateFields: true},
141
+ options: {
142
+ normalize: false,
143
+ removeUnusedMarkDefs: true,
144
+ validateFields: true,
145
+ },
118
146
  }),
119
147
  ).toEqual({
120
148
  _key: 'k0',
@@ -149,7 +177,11 @@ describe(parseBlock.name, () => {
149
177
  keyGenerator: createTestKeyGenerator(),
150
178
  schema: compileSchema(defineSchema({})),
151
179
  },
152
- options: {removeUnusedMarkDefs: true, validateFields: true},
180
+ options: {
181
+ normalize: false,
182
+ removeUnusedMarkDefs: true,
183
+ validateFields: true,
184
+ },
153
185
  }),
154
186
  ).toBe(undefined)
155
187
  })
@@ -166,7 +198,7 @@ describe(parseBlock.name, () => {
166
198
  42,
167
199
  {foo: 'bar'},
168
200
  {
169
- _key: 'k1',
201
+ _key: 'some key',
170
202
  text: 'foo',
171
203
  marks: [],
172
204
  },
@@ -177,6 +209,7 @@ describe(parseBlock.name, () => {
177
209
  {_type: 'span', text: 'foo'},
178
210
  {_type: 'span', marks: ['strong']},
179
211
  {_type: 'span', marks: ['em']},
212
+ {_type: 'image', text: 'inline object or span?'},
180
213
  ],
181
214
  },
182
215
  context: {
@@ -188,12 +221,22 @@ describe(parseBlock.name, () => {
188
221
  }),
189
222
  ),
190
223
  },
191
- options: {removeUnusedMarkDefs: true, validateFields: true},
224
+ options: {
225
+ normalize: false,
226
+ removeUnusedMarkDefs: true,
227
+ validateFields: true,
228
+ },
192
229
  }),
193
230
  ).toEqual({
194
231
  _key: 'k0',
195
232
  _type: 'block',
196
233
  children: [
234
+ {
235
+ _key: 'some key',
236
+ _type: 'span',
237
+ text: 'foo',
238
+ marks: [],
239
+ },
197
240
  {
198
241
  _key: 'k1',
199
242
  _type: 'stock-ticker',
@@ -222,6 +265,12 @@ describe(parseBlock.name, () => {
222
265
  text: '',
223
266
  marks: ['em'],
224
267
  },
268
+ {
269
+ _key: 'k6',
270
+ _type: 'span',
271
+ text: 'inline object or span?',
272
+ marks: [],
273
+ },
225
274
  ],
226
275
  markDefs: [],
227
276
  style: 'normal',
@@ -236,7 +285,11 @@ describe(parseBlock.name, () => {
236
285
  keyGenerator: createTestKeyGenerator(),
237
286
  schema: compileSchema(defineSchema({lists: [{name: 'bullet'}]})),
238
287
  },
239
- options: {removeUnusedMarkDefs: true, validateFields: true},
288
+ options: {
289
+ normalize: false,
290
+ removeUnusedMarkDefs: true,
291
+ validateFields: true,
292
+ },
240
293
  }),
241
294
  ).toEqual({
242
295
  _key: 'k0',
@@ -263,7 +316,11 @@ describe(parseBlock.name, () => {
263
316
  keyGenerator: createTestKeyGenerator(),
264
317
  schema: compileSchema(defineSchema({lists: [{name: 'bullet'}]})),
265
318
  },
266
- options: {removeUnusedMarkDefs: true, validateFields: true},
319
+ options: {
320
+ normalize: false,
321
+ removeUnusedMarkDefs: true,
322
+ validateFields: true,
323
+ },
267
324
  }),
268
325
  ).toEqual({
269
326
  _key: 'k0',
@@ -290,7 +347,11 @@ describe(parseBlock.name, () => {
290
347
  keyGenerator: createTestKeyGenerator(),
291
348
  schema: compileSchema(defineSchema({})),
292
349
  },
293
- options: {removeUnusedMarkDefs: true, validateFields: true},
350
+ options: {
351
+ normalize: false,
352
+ removeUnusedMarkDefs: true,
353
+ validateFields: true,
354
+ },
294
355
  }),
295
356
  ).toEqual({
296
357
  _type: 'block',
@@ -320,7 +381,11 @@ describe(parseBlock.name, () => {
320
381
  }),
321
382
  ),
322
383
  },
323
- options: {removeUnusedMarkDefs: true, validateFields: true},
384
+ options: {
385
+ normalize: false,
386
+ removeUnusedMarkDefs: true,
387
+ validateFields: true,
388
+ },
324
389
  }),
325
390
  ).toEqual({
326
391
  _type: 'block',
@@ -351,7 +416,11 @@ describe(parseBlock.name, () => {
351
416
  }),
352
417
  ),
353
418
  },
354
- options: {removeUnusedMarkDefs: true, validateFields: true},
419
+ options: {
420
+ normalize: false,
421
+ removeUnusedMarkDefs: true,
422
+ validateFields: true,
423
+ },
355
424
  }),
356
425
  ).toEqual({
357
426
  _type: 'block',
@@ -533,3 +602,214 @@ describe(parseSpan.name, () => {
533
602
  })
534
603
  })
535
604
  })
605
+
606
+ describe(parseInlineObject.name, () => {
607
+ test('undefined', () => {
608
+ expect(
609
+ parseInlineObject({
610
+ inlineObject: undefined,
611
+ context: {
612
+ keyGenerator: createTestKeyGenerator(),
613
+ schema: compileSchema(
614
+ defineSchema({inlineObjects: [{name: 'stock-ticker'}]}),
615
+ ),
616
+ },
617
+ options: {validateFields: true},
618
+ }),
619
+ ).toBe(undefined)
620
+ })
621
+
622
+ test('null', () => {
623
+ expect(
624
+ parseInlineObject({
625
+ inlineObject: null,
626
+ context: {
627
+ keyGenerator: createTestKeyGenerator(),
628
+ schema: compileSchema(
629
+ defineSchema({inlineObjects: [{name: 'stock-ticker'}]}),
630
+ ),
631
+ },
632
+ options: {validateFields: true},
633
+ }),
634
+ ).toBe(undefined)
635
+ })
636
+
637
+ test('empty object', () => {
638
+ expect(
639
+ parseInlineObject({
640
+ inlineObject: {},
641
+ context: {
642
+ keyGenerator: createTestKeyGenerator(),
643
+ schema: compileSchema(
644
+ defineSchema({inlineObjects: [{name: 'stock-ticker'}]}),
645
+ ),
646
+ },
647
+ options: {validateFields: true},
648
+ }),
649
+ ).toBe(undefined)
650
+ })
651
+
652
+ test('invalid _type', () => {
653
+ expect(
654
+ parseInlineObject({
655
+ inlineObject: {_type: 'image'},
656
+ context: {
657
+ keyGenerator: createTestKeyGenerator(),
658
+ schema: compileSchema(
659
+ defineSchema({inlineObjects: [{name: 'stock-ticker'}]}),
660
+ ),
661
+ },
662
+ options: {validateFields: true},
663
+ }),
664
+ ).toBe(undefined)
665
+ })
666
+
667
+ test('only _type', () => {
668
+ expect(
669
+ parseInlineObject({
670
+ inlineObject: {_type: 'stock-ticker'},
671
+ context: {
672
+ keyGenerator: createTestKeyGenerator(),
673
+ schema: compileSchema(
674
+ defineSchema({inlineObjects: [{name: 'stock-ticker'}]}),
675
+ ),
676
+ },
677
+ options: {validateFields: true},
678
+ }),
679
+ ).toEqual({
680
+ _key: 'k0',
681
+ _type: 'stock-ticker',
682
+ })
683
+ })
684
+
685
+ describe('looks like text node', () => {
686
+ test('known inline object _type', () => {
687
+ expect(
688
+ parseInlineObject({
689
+ inlineObject: {_type: 'stock-ticker', text: 'foo'},
690
+ context: {
691
+ keyGenerator: createTestKeyGenerator(),
692
+ schema: compileSchema(
693
+ defineSchema({inlineObjects: [{name: 'stock-ticker'}]}),
694
+ ),
695
+ },
696
+ options: {validateFields: true},
697
+ }),
698
+ ).toEqual({_key: 'k0', _type: 'stock-ticker'})
699
+ })
700
+
701
+ test('unknown inline object _type', () => {
702
+ expect(
703
+ parseInlineObject({
704
+ inlineObject: {_type: 'image', text: 'foo'},
705
+ context: {
706
+ keyGenerator: createTestKeyGenerator(),
707
+ schema: compileSchema(
708
+ defineSchema({inlineObjects: [{name: 'stock-ticker'}]}),
709
+ ),
710
+ },
711
+ options: {validateFields: true},
712
+ }),
713
+ ).toBe(undefined)
714
+ })
715
+ })
716
+
717
+ describe('custom props', () => {
718
+ describe('unknown prop', () => {
719
+ test('validateFields: true', () => {
720
+ expect(
721
+ parseInlineObject({
722
+ inlineObject: {_type: 'stock-ticker', foo: 'bar'},
723
+ context: {
724
+ keyGenerator: createTestKeyGenerator(),
725
+ schema: compileSchema(
726
+ defineSchema({
727
+ inlineObjects: [{name: 'stock-ticker'}],
728
+ }),
729
+ ),
730
+ },
731
+ options: {validateFields: true},
732
+ }),
733
+ ).toEqual({
734
+ _key: 'k0',
735
+ _type: 'stock-ticker',
736
+ })
737
+ })
738
+
739
+ test('validateFields: false', () => {
740
+ expect(
741
+ parseInlineObject({
742
+ inlineObject: {_type: 'stock-ticker', foo: 'bar'},
743
+ context: {
744
+ keyGenerator: createTestKeyGenerator(),
745
+ schema: compileSchema(
746
+ defineSchema({
747
+ inlineObjects: [{name: 'stock-ticker'}],
748
+ }),
749
+ ),
750
+ },
751
+ options: {validateFields: false},
752
+ }),
753
+ ).toEqual({
754
+ _key: 'k0',
755
+ _type: 'stock-ticker',
756
+ foo: 'bar',
757
+ })
758
+ })
759
+ })
760
+
761
+ describe('known prop', () => {
762
+ test('validateFields: true', () => {
763
+ expect(
764
+ parseInlineObject({
765
+ inlineObject: {_type: 'stock-ticker', foo: 'bar'},
766
+ context: {
767
+ keyGenerator: createTestKeyGenerator(),
768
+ schema: compileSchema(
769
+ defineSchema({
770
+ inlineObjects: [
771
+ {
772
+ name: 'stock-ticker',
773
+ fields: [{name: 'foo', type: 'string'}],
774
+ },
775
+ ],
776
+ }),
777
+ ),
778
+ },
779
+ options: {validateFields: true},
780
+ }),
781
+ ).toEqual({
782
+ _key: 'k0',
783
+ _type: 'stock-ticker',
784
+ foo: 'bar',
785
+ })
786
+ })
787
+ })
788
+
789
+ test('validateFields: false', () => {
790
+ expect(
791
+ parseInlineObject({
792
+ inlineObject: {_type: 'stock-ticker', foo: 'bar'},
793
+ context: {
794
+ keyGenerator: createTestKeyGenerator(),
795
+ schema: compileSchema(
796
+ defineSchema({
797
+ inlineObjects: [
798
+ {
799
+ name: 'stock-ticker',
800
+ fields: [{name: 'foo', type: 'string'}],
801
+ },
802
+ ],
803
+ }),
804
+ ),
805
+ },
806
+ options: {validateFields: false},
807
+ }),
808
+ ).toEqual({
809
+ _key: 'k0',
810
+ _type: 'stock-ticker',
811
+ foo: 'bar',
812
+ })
813
+ })
814
+ })
815
+ })
@@ -1,4 +1,4 @@
1
- import {isTextBlock} from '@portabletext/schema'
1
+ import {isSpan, isTextBlock} from '@portabletext/schema'
2
2
  import type {
3
3
  PortableTextBlock,
4
4
  PortableTextListBlock,
@@ -9,7 +9,7 @@ import type {
9
9
  } from '@sanity/types'
10
10
  import type {EditorSchema} from '../editor/editor-schema'
11
11
  import type {EditorContext} from '../editor/editor-snapshot'
12
- import {isTypedObject} from './asserters'
12
+ import {isRecord, isTypedObject} from './asserters'
13
13
 
14
14
  export function parseBlocks({
15
15
  context,
@@ -19,6 +19,7 @@ export function parseBlocks({
19
19
  context: Pick<EditorContext, 'keyGenerator' | 'schema'>
20
20
  blocks: unknown
21
21
  options: {
22
+ normalize: boolean
22
23
  removeUnusedMarkDefs: boolean
23
24
  validateFields: boolean
24
25
  }
@@ -42,6 +43,7 @@ export function parseBlock({
42
43
  context: Pick<EditorContext, 'keyGenerator' | 'schema'>
43
44
  block: unknown
44
45
  options: {
46
+ normalize: boolean
45
47
  removeUnusedMarkDefs: boolean
46
48
  validateFields: boolean
47
49
  }
@@ -102,6 +104,7 @@ export function parseTextBlock({
102
104
  block: unknown
103
105
  context: Pick<EditorContext, 'keyGenerator' | 'schema'>
104
106
  options: {
107
+ normalize: boolean
105
108
  removeUnusedMarkDefs: boolean
106
109
  validateFields: boolean
107
110
  }
@@ -186,29 +189,70 @@ export function parseTextBlock({
186
189
  ? block.children
187
190
  : []
188
191
 
189
- const children = unparsedChildren
192
+ const parsedChildren = unparsedChildren
190
193
  .map(
191
194
  (child) =>
192
195
  parseSpan({span: child, context, markDefKeyMap, options}) ??
193
196
  parseInlineObject({inlineObject: child, context, options}),
194
197
  )
195
198
  .filter((child) => child !== undefined)
196
- const marks = children.flatMap((child) => child.marks ?? [])
199
+ const marks = parsedChildren.flatMap((child) => child.marks ?? [])
200
+
201
+ const children =
202
+ parsedChildren.length > 0
203
+ ? parsedChildren
204
+ : [
205
+ {
206
+ _key: context.keyGenerator(),
207
+ _type: context.schema.span.name,
208
+ text: '',
209
+ marks: [],
210
+ },
211
+ ]
212
+
213
+ const normalizedChildren = options.normalize
214
+ ? // Ensure that inline objects re surrounded by spans
215
+ children.reduce<Array<PortableTextObject | PortableTextSpan>>(
216
+ (normalizedChildren, child, index) => {
217
+ if (isSpan(context, child)) {
218
+ return [...normalizedChildren, child]
219
+ }
220
+
221
+ const previousChild = normalizedChildren.at(-1)
222
+
223
+ if (!previousChild || !isSpan(context, previousChild)) {
224
+ return [
225
+ ...normalizedChildren,
226
+ {
227
+ _key: context.keyGenerator(),
228
+ _type: context.schema.span.name,
229
+ text: '',
230
+ marks: [],
231
+ },
232
+ child,
233
+ ...(index === children.length - 1
234
+ ? [
235
+ {
236
+ _key: context.keyGenerator(),
237
+ _type: context.schema.span.name,
238
+ text: '',
239
+ marks: [],
240
+ },
241
+ ]
242
+ : []),
243
+ ]
244
+ }
245
+
246
+ return [...normalizedChildren, child]
247
+ },
248
+ [],
249
+ )
250
+ : children
197
251
 
198
252
  const parsedBlock: PortableTextTextBlock = {
199
253
  _type: context.schema.block.name,
200
254
  _key,
201
- children:
202
- children.length > 0
203
- ? children
204
- : [
205
- {
206
- _key: context.keyGenerator(),
207
- _type: context.schema.span.name,
208
- text: '',
209
- marks: [],
210
- },
211
- ],
255
+ children: normalizedChildren,
212
256
  markDefs: options.removeUnusedMarkDefs
213
257
  ? markDefs.filter((markDef) => marks.includes(markDef._key))
214
258
  : markDefs,
@@ -255,7 +299,7 @@ export function parseSpan({
255
299
  markDefKeyMap: Map<string, string>
256
300
  options: {validateFields: boolean}
257
301
  }): PortableTextSpan | undefined {
258
- if (!isTypedObject(span)) {
302
+ if (!isRecord(span)) {
259
303
  return undefined
260
304
  }
261
305
 
@@ -272,11 +316,6 @@ export function parseSpan({
272
316
  }
273
317
  }
274
318
 
275
- // In reality, the span schema name is always 'span', but we only the check here anyway
276
- if (span._type !== context.schema.span.name || span._type !== 'span') {
277
- return undefined
278
- }
279
-
280
319
  const unparsedMarks: Array<unknown> = Array.isArray(span.marks)
281
320
  ? span.marks
282
321
  : []
@@ -300,8 +339,28 @@ export function parseSpan({
300
339
  return []
301
340
  })
302
341
 
342
+ if (span._type !== context.schema.span.name) {
343
+ if (
344
+ !context.schema.inlineObjects.some(
345
+ (inlineObject) => inlineObject.name === span._type,
346
+ ) &&
347
+ typeof span.text === 'string'
348
+ ) {
349
+ return {
350
+ _type: context.schema.span.name as 'span',
351
+ _key:
352
+ typeof span._key === 'string' ? span._key : context.keyGenerator(),
353
+ text: span.text,
354
+ marks,
355
+ ...(options.validateFields ? {} : customFields),
356
+ }
357
+ }
358
+
359
+ return undefined
360
+ }
361
+
303
362
  return {
304
- _type: 'span',
363
+ _type: context.schema.span.name as 'span',
305
364
  _key: typeof span._key === 'string' ? span._key : context.keyGenerator(),
306
365
  text: typeof span.text === 'string' ? span.text : '',
307
366
  marks,