@portabletext/editor 0.0.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 (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/lib/index.d.mts +911 -0
  4. package/lib/index.d.ts +911 -0
  5. package/lib/index.esm.js +4896 -0
  6. package/lib/index.esm.js.map +1 -0
  7. package/lib/index.js +4874 -0
  8. package/lib/index.js.map +1 -0
  9. package/lib/index.mjs +4896 -0
  10. package/lib/index.mjs.map +1 -0
  11. package/package.json +119 -0
  12. package/src/editor/Editable.tsx +683 -0
  13. package/src/editor/PortableTextEditor.tsx +308 -0
  14. package/src/editor/__tests__/PortableTextEditor.test.tsx +386 -0
  15. package/src/editor/__tests__/PortableTextEditorTester.tsx +116 -0
  16. package/src/editor/__tests__/RangeDecorations.test.tsx +115 -0
  17. package/src/editor/__tests__/handleClick.test.tsx +218 -0
  18. package/src/editor/__tests__/pteWarningsSelfSolving.test.tsx +389 -0
  19. package/src/editor/__tests__/utils.ts +39 -0
  20. package/src/editor/components/DraggableBlock.tsx +287 -0
  21. package/src/editor/components/Element.tsx +279 -0
  22. package/src/editor/components/Leaf.tsx +288 -0
  23. package/src/editor/components/SlateContainer.tsx +81 -0
  24. package/src/editor/components/Synchronizer.tsx +190 -0
  25. package/src/editor/hooks/usePortableTextEditor.ts +23 -0
  26. package/src/editor/hooks/usePortableTextEditorKeyGenerator.ts +24 -0
  27. package/src/editor/hooks/usePortableTextEditorSelection.ts +22 -0
  28. package/src/editor/hooks/usePortableTextEditorValue.ts +16 -0
  29. package/src/editor/hooks/usePortableTextReadOnly.ts +20 -0
  30. package/src/editor/hooks/useSyncValue.test.tsx +125 -0
  31. package/src/editor/hooks/useSyncValue.ts +372 -0
  32. package/src/editor/nodes/DefaultAnnotation.tsx +16 -0
  33. package/src/editor/nodes/DefaultObject.tsx +15 -0
  34. package/src/editor/nodes/index.ts +189 -0
  35. package/src/editor/plugins/__tests__/withEditableAPIDelete.test.tsx +244 -0
  36. package/src/editor/plugins/__tests__/withEditableAPIGetFragment.test.tsx +142 -0
  37. package/src/editor/plugins/__tests__/withEditableAPIInsert.test.tsx +346 -0
  38. package/src/editor/plugins/__tests__/withEditableAPISelectionsOverlapping.test.tsx +162 -0
  39. package/src/editor/plugins/__tests__/withHotkeys.test.tsx +212 -0
  40. package/src/editor/plugins/__tests__/withInsertBreak.test.tsx +204 -0
  41. package/src/editor/plugins/__tests__/withPlaceholderBlock.test.tsx +133 -0
  42. package/src/editor/plugins/__tests__/withPortableTextLists.test.tsx +65 -0
  43. package/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx +1377 -0
  44. package/src/editor/plugins/__tests__/withPortableTextSelections.test.tsx +91 -0
  45. package/src/editor/plugins/__tests__/withUndoRedo.test.tsx +115 -0
  46. package/src/editor/plugins/createWithEditableAPI.ts +573 -0
  47. package/src/editor/plugins/createWithHotKeys.ts +304 -0
  48. package/src/editor/plugins/createWithInsertBreak.ts +45 -0
  49. package/src/editor/plugins/createWithInsertData.ts +359 -0
  50. package/src/editor/plugins/createWithMaxBlocks.ts +24 -0
  51. package/src/editor/plugins/createWithObjectKeys.ts +63 -0
  52. package/src/editor/plugins/createWithPatches.ts +274 -0
  53. package/src/editor/plugins/createWithPlaceholderBlock.ts +36 -0
  54. package/src/editor/plugins/createWithPortableTextBlockStyle.ts +91 -0
  55. package/src/editor/plugins/createWithPortableTextLists.ts +160 -0
  56. package/src/editor/plugins/createWithPortableTextMarkModel.ts +441 -0
  57. package/src/editor/plugins/createWithPortableTextSelections.ts +65 -0
  58. package/src/editor/plugins/createWithSchemaTypes.ts +76 -0
  59. package/src/editor/plugins/createWithUndoRedo.ts +494 -0
  60. package/src/editor/plugins/createWithUtils.ts +81 -0
  61. package/src/editor/plugins/index.ts +155 -0
  62. package/src/index.ts +11 -0
  63. package/src/patch/PatchEvent.ts +33 -0
  64. package/src/patch/applyPatch.ts +29 -0
  65. package/src/patch/array.ts +89 -0
  66. package/src/patch/arrayInsert.ts +27 -0
  67. package/src/patch/object.ts +39 -0
  68. package/src/patch/patches.ts +53 -0
  69. package/src/patch/primitive.ts +43 -0
  70. package/src/patch/string.ts +51 -0
  71. package/src/types/editor.ts +576 -0
  72. package/src/types/options.ts +17 -0
  73. package/src/types/patch.ts +65 -0
  74. package/src/types/slate.ts +25 -0
  75. package/src/utils/__tests__/dmpToOperations.test.ts +181 -0
  76. package/src/utils/__tests__/operationToPatches.test.ts +421 -0
  77. package/src/utils/__tests__/patchToOperations.test.ts +293 -0
  78. package/src/utils/__tests__/ranges.test.ts +18 -0
  79. package/src/utils/__tests__/valueNormalization.test.tsx +62 -0
  80. package/src/utils/__tests__/values.test.ts +253 -0
  81. package/src/utils/applyPatch.ts +407 -0
  82. package/src/utils/bufferUntil.ts +15 -0
  83. package/src/utils/debug.ts +12 -0
  84. package/src/utils/getPortableTextMemberSchemaTypes.ts +100 -0
  85. package/src/utils/operationToPatches.ts +357 -0
  86. package/src/utils/patches.ts +36 -0
  87. package/src/utils/paths.ts +60 -0
  88. package/src/utils/ranges.ts +77 -0
  89. package/src/utils/schema.ts +8 -0
  90. package/src/utils/selection.ts +65 -0
  91. package/src/utils/ucs2Indices.ts +67 -0
  92. package/src/utils/validateValue.ts +394 -0
  93. package/src/utils/values.ts +208 -0
  94. package/src/utils/weakMaps.ts +24 -0
  95. package/src/utils/withChanges.ts +25 -0
  96. package/src/utils/withPreserveKeys.ts +14 -0
  97. package/src/utils/withoutPatching.ts +14 -0
@@ -0,0 +1,407 @@
1
+ /* eslint-disable max-statements */
2
+ import {
3
+ applyPatches as diffMatchPatchApplyPatches,
4
+ cleanupEfficiency,
5
+ DIFF_DELETE,
6
+ DIFF_EQUAL,
7
+ DIFF_INSERT,
8
+ makeDiff,
9
+ parsePatch,
10
+ } from '@sanity/diff-match-patch'
11
+ import {
12
+ type KeyedSegment,
13
+ type Path,
14
+ type PathSegment,
15
+ type PortableTextBlock,
16
+ type PortableTextChild,
17
+ } from '@sanity/types'
18
+ import {type Descendant, Element, type Node, type Path as SlatePath, Text, Transforms} from 'slate'
19
+
20
+ import {applyAll} from '../patch/applyPatch'
21
+ import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../types/editor'
22
+ import {
23
+ type DiffMatchPatch,
24
+ type InsertPatch,
25
+ type Patch,
26
+ type SetPatch,
27
+ type UnsetPatch,
28
+ } from '../types/patch'
29
+ import {debugWithName} from './debug'
30
+ import {toSlateValue} from './values'
31
+ import {KEY_TO_SLATE_ELEMENT} from './weakMaps'
32
+
33
+ const debug = debugWithName('applyPatches')
34
+ const debugVerbose = debug.enabled && true
35
+
36
+ /**
37
+ * Creates a function that can apply a patch onto a PortableTextSlateEditor.
38
+ */
39
+ export function createApplyPatch(
40
+ schemaTypes: PortableTextMemberSchemaTypes,
41
+ ): (editor: PortableTextSlateEditor, patch: Patch) => boolean {
42
+ let previousPatch: Patch | undefined
43
+
44
+ return function (editor: PortableTextSlateEditor, patch: Patch): boolean {
45
+ let changed = false
46
+
47
+ // Save some CPU cycles by not stringifying unless enabled
48
+ if (debugVerbose) {
49
+ debug('\n\nNEW PATCH =============================================================')
50
+ debug(JSON.stringify(patch, null, 2))
51
+ }
52
+
53
+ try {
54
+ switch (patch.type) {
55
+ case 'insert':
56
+ changed = insertPatch(editor, patch, schemaTypes)
57
+ break
58
+ case 'unset':
59
+ changed = unsetPatch(editor, patch, previousPatch)
60
+ break
61
+ case 'set':
62
+ changed = setPatch(editor, patch)
63
+ break
64
+ case 'diffMatchPatch':
65
+ changed = diffMatchPatch(editor, patch)
66
+ break
67
+ default:
68
+ debug('Unhandled patch', patch.type)
69
+ }
70
+ } catch (err) {
71
+ console.error(err)
72
+ }
73
+ previousPatch = patch
74
+ return changed
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Apply a remote diff match patch to the current PTE instance.
80
+ * Note meant for external consumption, only exported for testing purposes.
81
+ *
82
+ * @param editor - Portable text slate editor instance
83
+ * @param patch - The PTE diff match patch operation to apply
84
+ * @returns true if the patch was applied, false otherwise
85
+ * @internal
86
+ */
87
+ export function diffMatchPatch(
88
+ editor: Pick<
89
+ PortableTextSlateEditor,
90
+ 'children' | 'isTextBlock' | 'apply' | 'selection' | 'onChange'
91
+ >,
92
+ patch: DiffMatchPatch,
93
+ ): boolean {
94
+ const {block, child, childPath} = findBlockAndChildFromPath(editor, patch.path)
95
+ if (!block) {
96
+ debug('Block not found')
97
+ return false
98
+ }
99
+ if (!child || !childPath) {
100
+ debug('Child not found')
101
+ return false
102
+ }
103
+ const isSpanTextDiffMatchPatch =
104
+ block &&
105
+ editor.isTextBlock(block) &&
106
+ patch.path.length === 4 &&
107
+ patch.path[1] === 'children' &&
108
+ patch.path[3] === 'text'
109
+
110
+ if (!isSpanTextDiffMatchPatch || !Text.isText(child)) {
111
+ return false
112
+ }
113
+
114
+ const patches = parsePatch(patch.value)
115
+ const [newValue] = diffMatchPatchApplyPatches(patches, child.text, {allowExceedingIndices: true})
116
+ const diff = cleanupEfficiency(makeDiff(child.text, newValue), 5)
117
+
118
+ debugState(editor, 'before')
119
+ let offset = 0
120
+ for (const [op, text] of diff) {
121
+ if (op === DIFF_INSERT) {
122
+ editor.apply({type: 'insert_text', path: childPath, offset, text})
123
+ offset += text.length
124
+ } else if (op === DIFF_DELETE) {
125
+ editor.apply({type: 'remove_text', path: childPath, offset: offset, text})
126
+ } else if (op === DIFF_EQUAL) {
127
+ offset += text.length
128
+ }
129
+ }
130
+ debugState(editor, 'after')
131
+
132
+ return true
133
+ }
134
+
135
+ function insertPatch(
136
+ editor: PortableTextSlateEditor,
137
+ patch: InsertPatch,
138
+ schemaTypes: PortableTextMemberSchemaTypes,
139
+ ) {
140
+ const {
141
+ block: targetBlock,
142
+ child: targetChild,
143
+ blockPath: targetBlockPath,
144
+ childPath: targetChildPath,
145
+ } = findBlockAndChildFromPath(editor, patch.path)
146
+ if (!targetBlock || !targetBlockPath) {
147
+ debug('Block not found')
148
+ return false
149
+ }
150
+ if (patch.path.length > 1 && patch.path[1] !== 'children') {
151
+ debug('Ignoring patch targeting void value')
152
+ return false
153
+ }
154
+ // Insert blocks
155
+ if (patch.path.length === 1) {
156
+ const {items, position} = patch
157
+ const blocksToInsert = toSlateValue(
158
+ items as PortableTextBlock[],
159
+ {schemaTypes},
160
+ KEY_TO_SLATE_ELEMENT.get(editor),
161
+ ) as Descendant[]
162
+ const targetBlockIndex = targetBlockPath[0]
163
+ const normalizedIdx = position === 'after' ? targetBlockIndex + 1 : targetBlockIndex
164
+ debug(`Inserting blocks at path [${normalizedIdx}]`)
165
+ debugState(editor, 'before')
166
+ Transforms.insertNodes(editor, blocksToInsert, {at: [normalizedIdx]})
167
+ debugState(editor, 'after')
168
+ return true
169
+ }
170
+ // Insert children
171
+ const {items, position} = patch
172
+ if (!targetChild || !targetChildPath) {
173
+ debug('Child not found')
174
+ return false
175
+ }
176
+ const childrenToInsert =
177
+ targetBlock &&
178
+ toSlateValue(
179
+ [{...targetBlock, children: items as PortableTextChild[]}],
180
+ {schemaTypes},
181
+ KEY_TO_SLATE_ELEMENT.get(editor),
182
+ )
183
+ const targetChildIndex = targetChildPath[1]
184
+ const normalizedIdx = position === 'after' ? targetChildIndex + 1 : targetChildIndex
185
+ const childInsertPath = [targetChildPath[0], normalizedIdx]
186
+ debug(`Inserting children at path ${childInsertPath}`)
187
+ debugState(editor, 'before')
188
+ if (childrenToInsert && Element.isElement(childrenToInsert[0])) {
189
+ Transforms.insertNodes(editor, childrenToInsert[0].children, {at: childInsertPath})
190
+ }
191
+ debugState(editor, 'after')
192
+ return true
193
+ }
194
+
195
+ function setPatch(editor: PortableTextSlateEditor, patch: SetPatch) {
196
+ let value = patch.value
197
+ if (typeof patch.path[3] === 'string') {
198
+ value = {}
199
+ value[patch.path[3]] = patch.value
200
+ }
201
+ const {block, blockPath, child, childPath} = findBlockAndChildFromPath(editor, patch.path)
202
+
203
+ if (!block) {
204
+ debug('Block not found')
205
+ return false
206
+ }
207
+ const isTextBlock = editor.isTextBlock(block)
208
+
209
+ // Ignore patches targeting nested void data, like 'markDefs'
210
+ if (isTextBlock && patch.path.length > 1 && patch.path[1] !== 'children') {
211
+ debug('Ignoring setting void value')
212
+ return false
213
+ }
214
+
215
+ debugState(editor, 'before')
216
+
217
+ // If this is targeting a text block child
218
+ if (isTextBlock && child && childPath) {
219
+ if (Text.isText(value) && Text.isText(child)) {
220
+ const newText = child.text
221
+ const oldText = value.text
222
+ if (oldText !== newText) {
223
+ debug('Setting text property')
224
+ editor.apply({
225
+ type: 'remove_text',
226
+ path: childPath,
227
+ offset: 0,
228
+ text: newText,
229
+ })
230
+ editor.apply({
231
+ type: 'insert_text',
232
+ path: childPath,
233
+ offset: 0,
234
+ text: value.text,
235
+ })
236
+ // call OnChange here to emit the new selection
237
+ // the user's selection might be interfering with
238
+ editor.onChange()
239
+ }
240
+ } else {
241
+ debug('Setting non-text property')
242
+ editor.apply({
243
+ type: 'set_node',
244
+ path: childPath,
245
+ properties: {},
246
+ newProperties: value as Partial<Node>,
247
+ })
248
+ }
249
+ return true
250
+ } else if (Element.isElement(block) && patch.path.length === 1 && blockPath) {
251
+ debug('Setting block property')
252
+ const {children, ...nextRest} = value as unknown as PortableTextBlock
253
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
254
+ const {children: prevChildren, ...prevRest} = block || {children: undefined}
255
+ // Set any block properties
256
+ editor.apply({
257
+ type: 'set_node',
258
+ path: blockPath,
259
+ properties: {...prevRest},
260
+ newProperties: nextRest,
261
+ })
262
+ // Replace the children in the block
263
+ // Note that children must be explicitly inserted, and can't be set with set_node
264
+ debug('Setting children')
265
+ block.children.forEach((c, cIndex) => {
266
+ editor.apply({
267
+ type: 'remove_node',
268
+ path: blockPath.concat(block.children.length - 1 - cIndex),
269
+ node: c,
270
+ })
271
+ })
272
+ if (Array.isArray(children)) {
273
+ children.forEach((c, cIndex) => {
274
+ editor.apply({
275
+ type: 'insert_node',
276
+ path: blockPath.concat(cIndex),
277
+ node: c,
278
+ })
279
+ })
280
+ }
281
+ } else if (block && 'value' in block) {
282
+ const newVal = applyAll([block.value], [patch])[0]
283
+ Transforms.setNodes(editor, {...block, value: newVal}, {at: blockPath})
284
+ return true
285
+ }
286
+ debugState(editor, 'after')
287
+ return true
288
+ }
289
+
290
+ function unsetPatch(editor: PortableTextSlateEditor, patch: UnsetPatch, previousPatch?: Patch) {
291
+ // Value
292
+ if (patch.path.length === 0) {
293
+ debug('Removing everything')
294
+ debugState(editor, 'before')
295
+ const previousSelection = editor.selection
296
+ Transforms.deselect(editor)
297
+ editor.children.forEach((c, i) => {
298
+ Transforms.removeNodes(editor, {at: [i]})
299
+ })
300
+ Transforms.insertNodes(editor, editor.pteCreateEmptyBlock())
301
+ if (previousSelection) {
302
+ Transforms.select(editor, {
303
+ anchor: {path: [0, 0], offset: 0},
304
+ focus: {path: [0, 0], offset: 0},
305
+ })
306
+ }
307
+ // call OnChange here to emit the new selection
308
+ editor.onChange()
309
+ debugState(editor, 'after')
310
+ return true
311
+ }
312
+ const {block, blockPath, child, childPath} = findBlockAndChildFromPath(editor, patch.path)
313
+
314
+ // Single blocks
315
+ if (patch.path.length === 1) {
316
+ if (!block || !blockPath) {
317
+ debug('Block not found')
318
+ return false
319
+ }
320
+ const blockIndex = blockPath[0]
321
+ debug(`Removing block at path [${blockIndex}]`)
322
+ debugState(editor, 'before')
323
+
324
+ Transforms.removeNodes(editor, {at: [blockIndex]})
325
+ debugState(editor, 'after')
326
+ return true
327
+ }
328
+
329
+ // Unset on text block children
330
+ if (editor.isTextBlock(block) && patch.path[1] === 'children' && patch.path.length === 3) {
331
+ if (!child || !childPath) {
332
+ debug('Child not found')
333
+ return false
334
+ }
335
+ debug(`Unsetting child at path ${JSON.stringify(childPath)}`)
336
+ debugState(editor, 'before')
337
+ if (debugVerbose) {
338
+ debug(`Removing child at path ${JSON.stringify(childPath)}`)
339
+ }
340
+ Transforms.removeNodes(editor, {at: childPath})
341
+ debugState(editor, 'after')
342
+ return true
343
+ }
344
+ return false
345
+ }
346
+
347
+ function isKeyedSegment(segment: PathSegment): segment is KeyedSegment {
348
+ return typeof segment === 'object' && '_key' in segment
349
+ }
350
+
351
+ function debugState(
352
+ editor: Pick<PortableTextSlateEditor, 'children' | 'isTextBlock' | 'apply' | 'selection'>,
353
+ stateName: string,
354
+ ) {
355
+ if (!debugVerbose) {
356
+ return
357
+ }
358
+
359
+ debug(`Children ${stateName}:`, JSON.stringify(editor.children, null, 2))
360
+ debug(`Selection ${stateName}: `, JSON.stringify(editor.selection, null, 2))
361
+ }
362
+
363
+ function findBlockFromPath(
364
+ editor: Pick<
365
+ PortableTextSlateEditor,
366
+ 'children' | 'isTextBlock' | 'apply' | 'selection' | 'onChange'
367
+ >,
368
+ path: Path,
369
+ ): {block?: Descendant; path?: SlatePath} {
370
+ let blockIndex = -1
371
+ const block = editor.children.find((node: Descendant, index: number) => {
372
+ const isMatch = isKeyedSegment(path[0]) ? node._key === path[0]._key : index === path[0]
373
+ if (isMatch) {
374
+ blockIndex = index
375
+ }
376
+ return isMatch
377
+ })
378
+ if (!block) {
379
+ return {}
380
+ }
381
+ return {block, path: [blockIndex] as SlatePath}
382
+ }
383
+
384
+ function findBlockAndChildFromPath(
385
+ editor: Pick<
386
+ PortableTextSlateEditor,
387
+ 'children' | 'isTextBlock' | 'apply' | 'selection' | 'onChange'
388
+ >,
389
+ path: Path,
390
+ ): {child?: Descendant; childPath?: SlatePath; block?: Descendant; blockPath?: SlatePath} {
391
+ const {block, path: blockPath} = findBlockFromPath(editor, path)
392
+ if (!(Element.isElement(block) && path[1] === 'children')) {
393
+ return {block, blockPath, child: undefined, childPath: undefined}
394
+ }
395
+ let childIndex = -1
396
+ const child = block.children.find((node, index: number) => {
397
+ const isMatch = isKeyedSegment(path[2]) ? node._key === path[2]._key : index === path[2]
398
+ if (isMatch) {
399
+ childIndex = index
400
+ }
401
+ return isMatch
402
+ })
403
+ if (!child) {
404
+ return {block, blockPath, child: undefined, childPath: undefined}
405
+ }
406
+ return {block, child, blockPath, childPath: blockPath?.concat(childIndex) as SlatePath}
407
+ }
@@ -0,0 +1,15 @@
1
+ import {defer, EMPTY, type Observable, of, type OperatorFunction, switchMap, tap} from 'rxjs'
2
+
3
+ export function bufferUntil<T>(
4
+ emitWhen: (currentBuffer: T[]) => boolean,
5
+ ): OperatorFunction<T, T[]> {
6
+ return (source: Observable<T>) =>
7
+ defer(() => {
8
+ let buffer: T[] = [] // custom buffer
9
+ return source.pipe(
10
+ tap((v) => buffer.push(v)), // add values to buffer
11
+ switchMap(() => (emitWhen(buffer) ? of(buffer) : EMPTY)), // emit the buffer when the condition is met
12
+ tap(() => (buffer = [])), // clear the buffer
13
+ )
14
+ })
15
+ }
@@ -0,0 +1,12 @@
1
+ import debug from 'debug'
2
+
3
+ const rootName = 'sanity-pte:'
4
+
5
+ export default debug(rootName)
6
+ export function debugWithName(name: string): debug.Debugger {
7
+ const namespace = `${rootName}${name}`
8
+ if (debug && debug.enabled(namespace)) {
9
+ return debug(namespace)
10
+ }
11
+ return debug(rootName)
12
+ }
@@ -0,0 +1,100 @@
1
+ import {
2
+ type ArraySchemaType,
3
+ type BlockSchemaType,
4
+ type ObjectSchemaType,
5
+ type PortableTextBlock,
6
+ type SchemaType,
7
+ type SpanSchemaType,
8
+ } from '@sanity/types'
9
+
10
+ import {type PortableTextMemberSchemaTypes} from '../types/editor'
11
+
12
+ export function getPortableTextMemberSchemaTypes(
13
+ portableTextType: ArraySchemaType<PortableTextBlock>,
14
+ ): PortableTextMemberSchemaTypes {
15
+ if (!portableTextType) {
16
+ throw new Error("Parameter 'portabletextType' missing (required)")
17
+ }
18
+ const blockType = portableTextType.of?.find(findBlockType) as BlockSchemaType | undefined
19
+ if (!blockType) {
20
+ throw new Error('Block type is not defined in this schema (required)')
21
+ }
22
+ const childrenField = blockType.fields?.find((field) => field.name === 'children') as
23
+ | {type: ArraySchemaType}
24
+ | undefined
25
+ if (!childrenField) {
26
+ throw new Error('Children field for block type found in schema (required)')
27
+ }
28
+ const ofType = childrenField.type.of
29
+ if (!ofType) {
30
+ throw new Error('Valid types for block children not found in schema (required)')
31
+ }
32
+ const spanType = ofType.find((memberType) => memberType.name === 'span') as
33
+ | ObjectSchemaType
34
+ | undefined
35
+ if (!spanType) {
36
+ throw new Error('Span type not found in schema (required)')
37
+ }
38
+ const inlineObjectTypes = (ofType.filter((memberType) => memberType.name !== 'span') ||
39
+ []) as ObjectSchemaType[]
40
+ const blockObjectTypes = (portableTextType.of?.filter((field) => field.name !== blockType.name) ||
41
+ []) as ObjectSchemaType[]
42
+ return {
43
+ styles: resolveEnabledStyles(blockType),
44
+ decorators: resolveEnabledDecorators(spanType),
45
+ lists: resolveEnabledListItems(blockType),
46
+ block: blockType,
47
+ span: spanType,
48
+ portableText: portableTextType,
49
+ inlineObjects: inlineObjectTypes,
50
+ blockObjects: blockObjectTypes,
51
+ annotations: (spanType as SpanSchemaType).annotations,
52
+ }
53
+ }
54
+
55
+ function resolveEnabledStyles(blockType: ObjectSchemaType) {
56
+ const styleField = blockType.fields?.find((btField) => btField.name === 'style')
57
+ if (!styleField) {
58
+ throw new Error("A field with name 'style' is not defined in the block type (required).")
59
+ }
60
+ const textStyles =
61
+ styleField.type.options?.list &&
62
+ styleField.type.options.list?.filter((style: {value: string}) => style.value)
63
+ if (!textStyles || textStyles.length === 0) {
64
+ throw new Error(
65
+ 'The style fields need at least one style ' +
66
+ "defined. I.e: {title: 'Normal', value: 'normal'}.",
67
+ )
68
+ }
69
+ return textStyles
70
+ }
71
+
72
+ function resolveEnabledDecorators(spanType: ObjectSchemaType) {
73
+ return (spanType as any).decorators
74
+ }
75
+
76
+ function resolveEnabledListItems(blockType: ObjectSchemaType) {
77
+ const listField = blockType.fields?.find((btField) => btField.name === 'listItem')
78
+ if (!listField) {
79
+ throw new Error("A field with name 'listItem' is not defined in the block type (required).")
80
+ }
81
+ const listItems =
82
+ listField.type.options?.list &&
83
+ listField.type.options.list.filter((list: {value: string}) => list.value)
84
+ if (!listItems) {
85
+ throw new Error('The list field need at least to be an empty array')
86
+ }
87
+ return listItems
88
+ }
89
+
90
+ function findBlockType(type: SchemaType): BlockSchemaType | null {
91
+ if (type.type) {
92
+ return findBlockType(type.type)
93
+ }
94
+
95
+ if (type.name === 'block') {
96
+ return type as BlockSchemaType
97
+ }
98
+
99
+ return null
100
+ }