@portabletext/editor 3.0.0 → 3.0.2

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.
@@ -1,7 +1,12 @@
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 {
5
+ parseBlock,
6
+ parseChild,
7
+ parseInlineObject,
8
+ parseSpan,
9
+ } from './parse-blocks'
5
10
 
6
11
  describe(parseBlock.name, () => {
7
12
  test('null', () => {
@@ -12,7 +17,11 @@ describe(parseBlock.name, () => {
12
17
  keyGenerator: createTestKeyGenerator(),
13
18
  schema: compileSchema(defineSchema({})),
14
19
  },
15
- options: {removeUnusedMarkDefs: true, validateFields: true},
20
+ options: {
21
+ normalize: false,
22
+ removeUnusedMarkDefs: true,
23
+ validateFields: true,
24
+ },
16
25
  }),
17
26
  ).toBe(undefined)
18
27
  })
@@ -25,7 +34,11 @@ describe(parseBlock.name, () => {
25
34
  keyGenerator: createTestKeyGenerator(),
26
35
  schema: compileSchema(defineSchema({})),
27
36
  },
28
- options: {removeUnusedMarkDefs: true, validateFields: true},
37
+ options: {
38
+ normalize: false,
39
+ removeUnusedMarkDefs: true,
40
+ validateFields: true,
41
+ },
29
42
  }),
30
43
  ).toBe(undefined)
31
44
  })
@@ -39,7 +52,11 @@ describe(parseBlock.name, () => {
39
52
  keyGenerator: createTestKeyGenerator(),
40
53
  schema: compileSchema(defineSchema({})),
41
54
  },
42
- options: {removeUnusedMarkDefs: true, validateFields: true},
55
+ options: {
56
+ normalize: false,
57
+ removeUnusedMarkDefs: true,
58
+ validateFields: true,
59
+ },
43
60
  }),
44
61
  ).toBe(undefined)
45
62
  })
@@ -54,7 +71,11 @@ describe(parseBlock.name, () => {
54
71
  defineSchema({blockObjects: [{name: 'image'}]}),
55
72
  ),
56
73
  },
57
- options: {removeUnusedMarkDefs: true, validateFields: true},
74
+ options: {
75
+ normalize: false,
76
+ removeUnusedMarkDefs: true,
77
+ validateFields: true,
78
+ },
58
79
  }),
59
80
  ).toBe(undefined)
60
81
  })
@@ -69,7 +90,11 @@ describe(parseBlock.name, () => {
69
90
  defineSchema({blockObjects: [{name: 'image'}]}),
70
91
  ),
71
92
  },
72
- options: {removeUnusedMarkDefs: true, validateFields: true},
93
+ options: {
94
+ normalize: false,
95
+ removeUnusedMarkDefs: true,
96
+ validateFields: true,
97
+ },
73
98
  }),
74
99
  ).toEqual({
75
100
  _key: 'k0',
@@ -87,7 +112,11 @@ describe(parseBlock.name, () => {
87
112
  keyGenerator: createTestKeyGenerator(),
88
113
  schema: compileSchema(defineSchema({})),
89
114
  },
90
- options: {removeUnusedMarkDefs: true, validateFields: true},
115
+ options: {
116
+ normalize: false,
117
+ removeUnusedMarkDefs: true,
118
+ validateFields: true,
119
+ },
91
120
  }),
92
121
  ).toEqual({
93
122
  _key: 'k0',
@@ -114,7 +143,11 @@ describe(parseBlock.name, () => {
114
143
  keyGenerator: createTestKeyGenerator(),
115
144
  schema: {...schema, block: {...schema.block, name: 'text'}},
116
145
  },
117
- options: {removeUnusedMarkDefs: true, validateFields: true},
146
+ options: {
147
+ normalize: false,
148
+ removeUnusedMarkDefs: true,
149
+ validateFields: true,
150
+ },
118
151
  }),
119
152
  ).toEqual({
120
153
  _key: 'k0',
@@ -149,7 +182,11 @@ describe(parseBlock.name, () => {
149
182
  keyGenerator: createTestKeyGenerator(),
150
183
  schema: compileSchema(defineSchema({})),
151
184
  },
152
- options: {removeUnusedMarkDefs: true, validateFields: true},
185
+ options: {
186
+ normalize: false,
187
+ removeUnusedMarkDefs: true,
188
+ validateFields: true,
189
+ },
153
190
  }),
154
191
  ).toBe(undefined)
155
192
  })
@@ -166,7 +203,7 @@ describe(parseBlock.name, () => {
166
203
  42,
167
204
  {foo: 'bar'},
168
205
  {
169
- _key: 'k1',
206
+ _key: 'some key',
170
207
  text: 'foo',
171
208
  marks: [],
172
209
  },
@@ -177,6 +214,7 @@ describe(parseBlock.name, () => {
177
214
  {_type: 'span', text: 'foo'},
178
215
  {_type: 'span', marks: ['strong']},
179
216
  {_type: 'span', marks: ['em']},
217
+ {_type: 'image', text: 'inline object or span?'},
180
218
  ],
181
219
  },
182
220
  context: {
@@ -188,12 +226,22 @@ describe(parseBlock.name, () => {
188
226
  }),
189
227
  ),
190
228
  },
191
- options: {removeUnusedMarkDefs: true, validateFields: true},
229
+ options: {
230
+ normalize: false,
231
+ removeUnusedMarkDefs: true,
232
+ validateFields: true,
233
+ },
192
234
  }),
193
235
  ).toEqual({
194
236
  _key: 'k0',
195
237
  _type: 'block',
196
238
  children: [
239
+ {
240
+ _key: 'some key',
241
+ _type: 'span',
242
+ text: 'foo',
243
+ marks: [],
244
+ },
197
245
  {
198
246
  _key: 'k1',
199
247
  _type: 'stock-ticker',
@@ -236,7 +284,11 @@ describe(parseBlock.name, () => {
236
284
  keyGenerator: createTestKeyGenerator(),
237
285
  schema: compileSchema(defineSchema({lists: [{name: 'bullet'}]})),
238
286
  },
239
- options: {removeUnusedMarkDefs: true, validateFields: true},
287
+ options: {
288
+ normalize: false,
289
+ removeUnusedMarkDefs: true,
290
+ validateFields: true,
291
+ },
240
292
  }),
241
293
  ).toEqual({
242
294
  _key: 'k0',
@@ -263,7 +315,11 @@ describe(parseBlock.name, () => {
263
315
  keyGenerator: createTestKeyGenerator(),
264
316
  schema: compileSchema(defineSchema({lists: [{name: 'bullet'}]})),
265
317
  },
266
- options: {removeUnusedMarkDefs: true, validateFields: true},
318
+ options: {
319
+ normalize: false,
320
+ removeUnusedMarkDefs: true,
321
+ validateFields: true,
322
+ },
267
323
  }),
268
324
  ).toEqual({
269
325
  _key: 'k0',
@@ -290,7 +346,11 @@ describe(parseBlock.name, () => {
290
346
  keyGenerator: createTestKeyGenerator(),
291
347
  schema: compileSchema(defineSchema({})),
292
348
  },
293
- options: {removeUnusedMarkDefs: true, validateFields: true},
349
+ options: {
350
+ normalize: false,
351
+ removeUnusedMarkDefs: true,
352
+ validateFields: true,
353
+ },
294
354
  }),
295
355
  ).toEqual({
296
356
  _type: 'block',
@@ -320,7 +380,11 @@ describe(parseBlock.name, () => {
320
380
  }),
321
381
  ),
322
382
  },
323
- options: {removeUnusedMarkDefs: true, validateFields: true},
383
+ options: {
384
+ normalize: false,
385
+ removeUnusedMarkDefs: true,
386
+ validateFields: true,
387
+ },
324
388
  }),
325
389
  ).toEqual({
326
390
  _type: 'block',
@@ -351,7 +415,11 @@ describe(parseBlock.name, () => {
351
415
  }),
352
416
  ),
353
417
  },
354
- options: {removeUnusedMarkDefs: true, validateFields: true},
418
+ options: {
419
+ normalize: false,
420
+ removeUnusedMarkDefs: true,
421
+ validateFields: true,
422
+ },
355
423
  }),
356
424
  ).toEqual({
357
425
  _type: 'block',
@@ -533,3 +601,252 @@ describe(parseSpan.name, () => {
533
601
  })
534
602
  })
535
603
  })
604
+
605
+ describe(parseInlineObject.name, () => {
606
+ test('undefined', () => {
607
+ expect(
608
+ parseInlineObject({
609
+ inlineObject: undefined,
610
+ context: {
611
+ keyGenerator: createTestKeyGenerator(),
612
+ schema: compileSchema(
613
+ defineSchema({inlineObjects: [{name: 'stock-ticker'}]}),
614
+ ),
615
+ },
616
+ options: {validateFields: true},
617
+ }),
618
+ ).toBe(undefined)
619
+ })
620
+
621
+ test('null', () => {
622
+ expect(
623
+ parseInlineObject({
624
+ inlineObject: null,
625
+ context: {
626
+ keyGenerator: createTestKeyGenerator(),
627
+ schema: compileSchema(
628
+ defineSchema({inlineObjects: [{name: 'stock-ticker'}]}),
629
+ ),
630
+ },
631
+ options: {validateFields: true},
632
+ }),
633
+ ).toBe(undefined)
634
+ })
635
+
636
+ test('empty object', () => {
637
+ expect(
638
+ parseInlineObject({
639
+ inlineObject: {},
640
+ context: {
641
+ keyGenerator: createTestKeyGenerator(),
642
+ schema: compileSchema(
643
+ defineSchema({inlineObjects: [{name: 'stock-ticker'}]}),
644
+ ),
645
+ },
646
+ options: {validateFields: true},
647
+ }),
648
+ ).toBe(undefined)
649
+ })
650
+
651
+ test('invalid _type', () => {
652
+ expect(
653
+ parseInlineObject({
654
+ inlineObject: {_type: 'image'},
655
+ context: {
656
+ keyGenerator: createTestKeyGenerator(),
657
+ schema: compileSchema(
658
+ defineSchema({inlineObjects: [{name: 'stock-ticker'}]}),
659
+ ),
660
+ },
661
+ options: {validateFields: true},
662
+ }),
663
+ ).toBe(undefined)
664
+ })
665
+
666
+ test('only _type', () => {
667
+ expect(
668
+ parseInlineObject({
669
+ inlineObject: {_type: 'stock-ticker'},
670
+ context: {
671
+ keyGenerator: createTestKeyGenerator(),
672
+ schema: compileSchema(
673
+ defineSchema({inlineObjects: [{name: 'stock-ticker'}]}),
674
+ ),
675
+ },
676
+ options: {validateFields: true},
677
+ }),
678
+ ).toEqual({
679
+ _key: 'k0',
680
+ _type: 'stock-ticker',
681
+ })
682
+ })
683
+
684
+ describe('looks like text node', () => {
685
+ test('known inline object _type', () => {
686
+ expect(
687
+ parseInlineObject({
688
+ inlineObject: {_type: 'stock-ticker', text: 'foo'},
689
+ context: {
690
+ keyGenerator: createTestKeyGenerator(),
691
+ schema: compileSchema(
692
+ defineSchema({inlineObjects: [{name: 'stock-ticker'}]}),
693
+ ),
694
+ },
695
+ options: {validateFields: true},
696
+ }),
697
+ ).toEqual({_key: 'k0', _type: 'stock-ticker'})
698
+ })
699
+
700
+ test('unknown inline object _type', () => {
701
+ expect(
702
+ parseInlineObject({
703
+ inlineObject: {_type: 'image', text: 'foo'},
704
+ context: {
705
+ keyGenerator: createTestKeyGenerator(),
706
+ schema: compileSchema(
707
+ defineSchema({inlineObjects: [{name: 'stock-ticker'}]}),
708
+ ),
709
+ },
710
+ options: {validateFields: true},
711
+ }),
712
+ ).toBe(undefined)
713
+ })
714
+ })
715
+
716
+ describe('custom props', () => {
717
+ describe('unknown prop', () => {
718
+ test('validateFields: true', () => {
719
+ expect(
720
+ parseInlineObject({
721
+ inlineObject: {_type: 'stock-ticker', foo: 'bar'},
722
+ context: {
723
+ keyGenerator: createTestKeyGenerator(),
724
+ schema: compileSchema(
725
+ defineSchema({
726
+ inlineObjects: [{name: 'stock-ticker'}],
727
+ }),
728
+ ),
729
+ },
730
+ options: {validateFields: true},
731
+ }),
732
+ ).toEqual({
733
+ _key: 'k0',
734
+ _type: 'stock-ticker',
735
+ })
736
+ })
737
+
738
+ test('validateFields: false', () => {
739
+ expect(
740
+ parseInlineObject({
741
+ inlineObject: {_type: 'stock-ticker', foo: 'bar'},
742
+ context: {
743
+ keyGenerator: createTestKeyGenerator(),
744
+ schema: compileSchema(
745
+ defineSchema({
746
+ inlineObjects: [{name: 'stock-ticker'}],
747
+ }),
748
+ ),
749
+ },
750
+ options: {validateFields: false},
751
+ }),
752
+ ).toEqual({
753
+ _key: 'k0',
754
+ _type: 'stock-ticker',
755
+ foo: 'bar',
756
+ })
757
+ })
758
+ })
759
+
760
+ describe('known prop', () => {
761
+ test('validateFields: true', () => {
762
+ expect(
763
+ parseInlineObject({
764
+ inlineObject: {_type: 'stock-ticker', foo: 'bar'},
765
+ context: {
766
+ keyGenerator: createTestKeyGenerator(),
767
+ schema: compileSchema(
768
+ defineSchema({
769
+ inlineObjects: [
770
+ {
771
+ name: 'stock-ticker',
772
+ fields: [{name: 'foo', type: 'string'}],
773
+ },
774
+ ],
775
+ }),
776
+ ),
777
+ },
778
+ options: {validateFields: true},
779
+ }),
780
+ ).toEqual({
781
+ _key: 'k0',
782
+ _type: 'stock-ticker',
783
+ foo: 'bar',
784
+ })
785
+ })
786
+ })
787
+
788
+ test('validateFields: false', () => {
789
+ expect(
790
+ parseInlineObject({
791
+ inlineObject: {_type: 'stock-ticker', foo: 'bar'},
792
+ context: {
793
+ keyGenerator: createTestKeyGenerator(),
794
+ schema: compileSchema(
795
+ defineSchema({
796
+ inlineObjects: [
797
+ {
798
+ name: 'stock-ticker',
799
+ fields: [{name: 'foo', type: 'string'}],
800
+ },
801
+ ],
802
+ }),
803
+ ),
804
+ },
805
+ options: {validateFields: false},
806
+ }),
807
+ ).toEqual({
808
+ _key: 'k0',
809
+ _type: 'stock-ticker',
810
+ foo: 'bar',
811
+ })
812
+ })
813
+ })
814
+ })
815
+
816
+ describe(parseChild.name, () => {
817
+ describe('inline object', () => {
818
+ describe('looks like text node', () => {
819
+ test('known inline object _type', () => {
820
+ expect(
821
+ parseChild({
822
+ context: {
823
+ keyGenerator: createTestKeyGenerator(),
824
+ schema: compileSchema(
825
+ defineSchema({inlineObjects: [{name: 'stock-ticker'}]}),
826
+ ),
827
+ },
828
+ markDefKeyMap: new Map(),
829
+ options: {validateFields: true},
830
+ child: {_type: 'stock-ticker', text: 'foo'},
831
+ }),
832
+ ).toEqual({_key: 'k0', _type: 'stock-ticker'})
833
+ })
834
+
835
+ test('unknown inline object _type', () => {
836
+ expect(
837
+ parseChild({
838
+ context: {
839
+ keyGenerator: createTestKeyGenerator(),
840
+ schema: compileSchema(
841
+ defineSchema({inlineObjects: [{name: 'stock-ticker'}]}),
842
+ ),
843
+ },
844
+ markDefKeyMap: new Map(),
845
+ options: {validateFields: true},
846
+ child: {_type: 'image', text: 'foo'},
847
+ }),
848
+ ).toBe(undefined)
849
+ })
850
+ })
851
+ })
852
+ })
@@ -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,66 @@ export function parseTextBlock({
186
189
  ? block.children
187
190
  : []
188
191
 
189
- const children = unparsedChildren
190
- .map(
191
- (child) =>
192
- parseSpan({span: child, context, markDefKeyMap, options}) ??
193
- parseInlineObject({inlineObject: child, context, options}),
194
- )
192
+ const parsedChildren = unparsedChildren
193
+ .map((child) => parseChild({child, context, markDefKeyMap, options}))
195
194
  .filter((child) => child !== undefined)
196
- const marks = children.flatMap((child) => child.marks ?? [])
195
+ const marks = parsedChildren.flatMap((child) => child.marks ?? [])
196
+
197
+ const children =
198
+ parsedChildren.length > 0
199
+ ? parsedChildren
200
+ : [
201
+ {
202
+ _key: context.keyGenerator(),
203
+ _type: context.schema.span.name,
204
+ text: '',
205
+ marks: [],
206
+ },
207
+ ]
208
+
209
+ const normalizedChildren = options.normalize
210
+ ? // Ensure that inline objects re surrounded by spans
211
+ children.reduce<Array<PortableTextObject | PortableTextSpan>>(
212
+ (normalizedChildren, child, index) => {
213
+ if (isSpan(context, child)) {
214
+ return [...normalizedChildren, child]
215
+ }
216
+
217
+ const previousChild = normalizedChildren.at(-1)
218
+
219
+ if (!previousChild || !isSpan(context, previousChild)) {
220
+ return [
221
+ ...normalizedChildren,
222
+ {
223
+ _key: context.keyGenerator(),
224
+ _type: context.schema.span.name,
225
+ text: '',
226
+ marks: [],
227
+ },
228
+ child,
229
+ ...(index === children.length - 1
230
+ ? [
231
+ {
232
+ _key: context.keyGenerator(),
233
+ _type: context.schema.span.name,
234
+ text: '',
235
+ marks: [],
236
+ },
237
+ ]
238
+ : []),
239
+ ]
240
+ }
241
+
242
+ return [...normalizedChildren, child]
243
+ },
244
+ [],
245
+ )
246
+ : children
197
247
 
198
248
  const parsedBlock: PortableTextTextBlock = {
199
249
  _type: context.schema.block.name,
200
250
  _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
- ],
251
+ children: normalizedChildren,
212
252
  markDefs: options.removeUnusedMarkDefs
213
253
  ? markDefs.filter((markDef) => marks.includes(markDef._key))
214
254
  : markDefs,
@@ -244,6 +284,23 @@ export function parseTextBlock({
244
284
  return parsedBlock
245
285
  }
246
286
 
287
+ export function parseChild({
288
+ child,
289
+ context,
290
+ markDefKeyMap,
291
+ options,
292
+ }: {
293
+ child: unknown
294
+ context: Pick<EditorContext, 'keyGenerator' | 'schema'>
295
+ markDefKeyMap: Map<string, string>
296
+ options: {validateFields: boolean}
297
+ }): PortableTextSpan | PortableTextObject | undefined {
298
+ return (
299
+ parseSpan({span: child, context, markDefKeyMap, options}) ??
300
+ parseInlineObject({inlineObject: child, context, options})
301
+ )
302
+ }
303
+
247
304
  export function parseSpan({
248
305
  span,
249
306
  context,
@@ -255,7 +312,7 @@ export function parseSpan({
255
312
  markDefKeyMap: Map<string, string>
256
313
  options: {validateFields: boolean}
257
314
  }): PortableTextSpan | undefined {
258
- if (!isTypedObject(span)) {
315
+ if (!isRecord(span)) {
259
316
  return undefined
260
317
  }
261
318
 
@@ -272,11 +329,6 @@ export function parseSpan({
272
329
  }
273
330
  }
274
331
 
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
332
  const unparsedMarks: Array<unknown> = Array.isArray(span.marks)
281
333
  ? span.marks
282
334
  : []
@@ -300,8 +352,30 @@ export function parseSpan({
300
352
  return []
301
353
  })
302
354
 
355
+ if (
356
+ typeof span._type === 'string' &&
357
+ span._type !== context.schema.span.name
358
+ ) {
359
+ return undefined
360
+ }
361
+
362
+ if (typeof span._type !== 'string') {
363
+ if (typeof span.text === 'string') {
364
+ return {
365
+ _type: context.schema.span.name as 'span',
366
+ _key:
367
+ typeof span._key === 'string' ? span._key : context.keyGenerator(),
368
+ text: span.text,
369
+ marks,
370
+ ...(options.validateFields ? {} : customFields),
371
+ }
372
+ }
373
+
374
+ return undefined
375
+ }
376
+
303
377
  return {
304
- _type: 'span',
378
+ _type: context.schema.span.name as 'span',
305
379
  _key: typeof span._key === 'string' ? span._key : context.keyGenerator(),
306
380
  text: typeof span.text === 'string' ? span.text : '',
307
381
  marks,
@@ -18,7 +18,11 @@ export function mergeTextBlocks({
18
18
  const parsedIncomingBlock = parseBlock({
19
19
  context,
20
20
  block: incomingBlock,
21
- options: {removeUnusedMarkDefs: true, validateFields: false},
21
+ options: {
22
+ normalize: false,
23
+ removeUnusedMarkDefs: true,
24
+ validateFields: false,
25
+ },
22
26
  })
23
27
 
24
28
  if (!parsedIncomingBlock || !isTextBlock(context, parsedIncomingBlock)) {