@portabletext/editor 3.0.0 → 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.
@@ -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,
@@ -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)) {
@@ -17,7 +17,9 @@ export function sliceBlocks({
17
17
  context,
18
18
  blocks,
19
19
  }: {
20
- context: Pick<EditorContext, 'schema' | 'selection'>
20
+ context: Pick<EditorContext, 'schema' | 'selection'> & {
21
+ keyGenerator?: () => string
22
+ }
21
23
  blocks: Array<PortableTextBlock>
22
24
  }): Array<PortableTextBlock> {
23
25
  const slice: Array<PortableTextBlock> = []
@@ -167,11 +169,15 @@ export function sliceBlocks({
167
169
  middleBlocks.push(
168
170
  parseBlock({
169
171
  context: {
170
- ...context,
171
- keyGenerator: defaultKeyGenerator,
172
+ schema: context.schema,
173
+ keyGenerator: context.keyGenerator ?? defaultKeyGenerator,
172
174
  },
173
175
  block,
174
- options: {removeUnusedMarkDefs: true, validateFields: false},
176
+ options: {
177
+ normalize: false,
178
+ removeUnusedMarkDefs: true,
179
+ validateFields: false,
180
+ },
175
181
  }) ?? block,
176
182
  )
177
183
  }
@@ -180,22 +186,30 @@ export function sliceBlocks({
180
186
  const parsedStartBlock = startBlock
181
187
  ? parseBlock({
182
188
  context: {
183
- ...context,
184
- keyGenerator: defaultKeyGenerator,
189
+ schema: context.schema,
190
+ keyGenerator: context.keyGenerator ?? defaultKeyGenerator,
185
191
  },
186
192
  block: startBlock,
187
- options: {removeUnusedMarkDefs: true, validateFields: false},
193
+ options: {
194
+ normalize: false,
195
+ removeUnusedMarkDefs: true,
196
+ validateFields: false,
197
+ },
188
198
  })
189
199
  : undefined
190
200
 
191
201
  const parsedEndBlock = endBlock
192
202
  ? parseBlock({
193
203
  context: {
194
- ...context,
195
- keyGenerator: defaultKeyGenerator,
204
+ schema: context.schema,
205
+ keyGenerator: context.keyGenerator ?? defaultKeyGenerator,
196
206
  },
197
207
  block: endBlock,
198
- options: {removeUnusedMarkDefs: true, validateFields: false},
208
+ options: {
209
+ normalize: false,
210
+ removeUnusedMarkDefs: true,
211
+ validateFields: false,
212
+ },
199
213
  })
200
214
  : undefined
201
215