@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,359 @@
1
+ import {htmlToBlocks, normalizeBlock} from '@sanity/block-tools'
2
+ import {type PortableTextBlock, type PortableTextChild} from '@sanity/types'
3
+ import {isEqual, uniq} from 'lodash'
4
+ import {type Descendant, Editor, type Node, Range, Transforms} from 'slate'
5
+ import {ReactEditor} from 'slate-react'
6
+
7
+ import {
8
+ type EditorChanges,
9
+ type PortableTextMemberSchemaTypes,
10
+ type PortableTextSlateEditor,
11
+ } from '../../types/editor'
12
+ import {debugWithName} from '../../utils/debug'
13
+ import {validateValue} from '../../utils/validateValue'
14
+ import {fromSlateValue, isEqualToEmptyEditor, toSlateValue} from '../../utils/values'
15
+
16
+ const debug = debugWithName('plugin:withInsertData')
17
+
18
+ /**
19
+ * This plugin handles copy/paste in the editor
20
+ *
21
+ */
22
+ export function createWithInsertData(
23
+ change$: EditorChanges,
24
+ schemaTypes: PortableTextMemberSchemaTypes,
25
+ keyGenerator: () => string,
26
+ ) {
27
+ return function withInsertData(editor: PortableTextSlateEditor): PortableTextSlateEditor {
28
+ const blockTypeName = schemaTypes.block.name
29
+ const spanTypeName = schemaTypes.span.name
30
+ const whitespaceOnPasteMode = schemaTypes.block.options.unstable_whitespaceOnPasteMode
31
+
32
+ const toPlainText = (blocks: PortableTextBlock[]) => {
33
+ return blocks
34
+ .map((block) => {
35
+ if (editor.isTextBlock(block)) {
36
+ return block.children
37
+ .map((child: PortableTextChild) => {
38
+ if (child._type === spanTypeName) {
39
+ return child.text
40
+ }
41
+ return `[${
42
+ schemaTypes.inlineObjects.find((t) => t.name === child._type)?.title || 'Object'
43
+ }]`
44
+ })
45
+ .join('')
46
+ }
47
+ return `[${
48
+ schemaTypes.blockObjects.find((t) => t.name === block._type)?.title || 'Object'
49
+ }]`
50
+ })
51
+ .join('\n\n')
52
+ }
53
+
54
+ editor.setFragmentData = (data: DataTransfer, originEvent) => {
55
+ const {selection} = editor
56
+
57
+ if (!selection) {
58
+ return
59
+ }
60
+
61
+ const [start, end] = Range.edges(selection)
62
+ const startVoid = Editor.void(editor, {at: start.path})
63
+ const endVoid = Editor.void(editor, {at: end.path})
64
+
65
+ if (Range.isCollapsed(selection) && !startVoid) {
66
+ return
67
+ }
68
+
69
+ // Create a fake selection so that we can add a Base64-encoded copy of the
70
+ // fragment to the HTML, to decode on future pastes.
71
+ const domRange = ReactEditor.toDOMRange(editor, selection)
72
+ let contents = domRange.cloneContents()
73
+ // COMPAT: If the end node is a void node, we need to move the end of the
74
+ // range from the void node's spacer span, to the end of the void node's
75
+ // content, since the spacer is before void's content in the DOM.
76
+ if (endVoid) {
77
+ const [voidNode] = endVoid
78
+ const r = domRange.cloneRange()
79
+ const domNode = ReactEditor.toDOMNode(editor, voidNode)
80
+ r.setEndAfter(domNode)
81
+ contents = r.cloneContents()
82
+ }
83
+ // Remove any zero-width space spans from the cloned DOM so that they don't
84
+ // show up elsewhere when pasted.
85
+ Array.from(contents.querySelectorAll('[data-slate-zero-width]')).forEach((zw) => {
86
+ const isNewline = zw.getAttribute('data-slate-zero-width') === 'n'
87
+ zw.textContent = isNewline ? '\n' : ''
88
+ })
89
+ // Clean up the clipboard HTML for editor spesific attributes
90
+ Array.from(contents.querySelectorAll('*')).forEach((elm) => {
91
+ elm.removeAttribute('contentEditable')
92
+ elm.removeAttribute('data-slate-inline')
93
+ elm.removeAttribute('data-slate-leaf')
94
+ elm.removeAttribute('data-slate-node')
95
+ elm.removeAttribute('data-slate-spacer')
96
+ elm.removeAttribute('data-slate-string')
97
+ elm.removeAttribute('data-slate-zero-width')
98
+ elm.removeAttribute('draggable')
99
+ for (const key in elm.attributes) {
100
+ if (elm.hasAttribute(key)) {
101
+ elm.removeAttribute(key)
102
+ }
103
+ }
104
+ })
105
+ const div = contents.ownerDocument.createElement('div')
106
+ div.appendChild(contents)
107
+ div.setAttribute('hidden', 'true')
108
+ contents.ownerDocument.body.appendChild(div)
109
+ const asHTML = div.innerHTML
110
+ contents.ownerDocument.body.removeChild(div)
111
+ const fragment = editor.getFragment()
112
+ const portableText = fromSlateValue(fragment, blockTypeName)
113
+
114
+ const asJSON = JSON.stringify(portableText)
115
+ const asPlainText = toPlainText(portableText)
116
+ data.clearData()
117
+ data.setData('text/plain', asPlainText)
118
+ data.setData('text/html', asHTML)
119
+ data.setData('application/json', asJSON)
120
+ data.setData('application/x-portable-text', asJSON)
121
+ debug('text', asPlainText)
122
+ data.setData('application/x-portable-text-event-origin', originEvent || 'external')
123
+ debug('Set fragment data', asJSON, asHTML)
124
+ }
125
+
126
+ editor.insertPortableTextData = (data: DataTransfer): boolean => {
127
+ if (!editor.selection) {
128
+ return false
129
+ }
130
+ const pText = data.getData('application/x-portable-text')
131
+ const origin = data.getData('application/x-portable-text-event-origin')
132
+ debug(`Inserting portable text from ${origin} event`, pText)
133
+ if (pText) {
134
+ const parsed = JSON.parse(pText) as PortableTextBlock[]
135
+ if (Array.isArray(parsed) && parsed.length > 0) {
136
+ const slateValue = _regenerateKeys(
137
+ editor,
138
+ toSlateValue(parsed, {schemaTypes}),
139
+ keyGenerator,
140
+ spanTypeName,
141
+ )
142
+ // Validate the result
143
+ const validation = validateValue(parsed, schemaTypes, keyGenerator)
144
+ // Bail out if it's not valid
145
+ if (!validation.valid && !validation.resolution?.autoResolve) {
146
+ const errorDescription = `${validation.resolution?.description}`
147
+ change$.next({
148
+ type: 'error',
149
+ level: 'warning',
150
+ name: 'pasteError',
151
+ description: errorDescription,
152
+ data: validation,
153
+ })
154
+ debug('Invalid insert result', validation)
155
+ return false
156
+ }
157
+ _insertFragment(editor, slateValue, schemaTypes)
158
+ return true
159
+ }
160
+ }
161
+ return false
162
+ }
163
+
164
+ editor.insertTextOrHTMLData = (data: DataTransfer): boolean => {
165
+ if (!editor.selection) {
166
+ debug('No selection, not inserting')
167
+ return false
168
+ }
169
+ change$.next({type: 'loading', isLoading: true}) // This could potentially take some time
170
+ const html = data.getData('text/html')
171
+ const text = data.getData('text/plain')
172
+
173
+ if (html || text) {
174
+ debug('Inserting data', data)
175
+ let portableText: PortableTextBlock[]
176
+ let fragment: Node[]
177
+ let insertedType
178
+
179
+ if (html) {
180
+ portableText = htmlToBlocks(html, schemaTypes.portableText, {
181
+ unstable_whitespaceOnPasteMode: whitespaceOnPasteMode,
182
+ }).map((block) => normalizeBlock(block, {blockTypeName})) as PortableTextBlock[]
183
+ fragment = toSlateValue(portableText, {schemaTypes})
184
+ insertedType = 'HTML'
185
+
186
+ if (portableText.length === 0) {
187
+ return false
188
+ }
189
+ } else {
190
+ // plain text
191
+ const blocks = escapeHtml(text)
192
+ .split(/\n{2,}/)
193
+ .map((line) =>
194
+ line ? `<p>${line.replace(/(?:\r\n|\r|\n)/g, '<br/>')}</p>` : '<p></p>',
195
+ )
196
+ .join('')
197
+ const textToHtml = `<html><body>${blocks}</body></html>`
198
+ portableText = htmlToBlocks(textToHtml, schemaTypes.portableText).map((block) =>
199
+ normalizeBlock(block, {blockTypeName}),
200
+ ) as PortableTextBlock[]
201
+ fragment = toSlateValue(portableText, {
202
+ schemaTypes,
203
+ })
204
+ insertedType = 'text'
205
+ }
206
+
207
+ // Validate the result
208
+ const validation = validateValue(portableText, schemaTypes, keyGenerator)
209
+
210
+ // Bail out if it's not valid
211
+ if (!validation.valid) {
212
+ const errorDescription = `Could not validate the resulting portable text to insert.\n${validation.resolution?.description}\nTry to insert as plain text (shift-paste) instead.`
213
+ change$.next({
214
+ type: 'error',
215
+ level: 'warning',
216
+ name: 'pasteError',
217
+ description: errorDescription,
218
+ data: validation,
219
+ })
220
+ debug('Invalid insert result', validation)
221
+ return false
222
+ }
223
+ debug(`Inserting ${insertedType} fragment at ${JSON.stringify(editor.selection)}`)
224
+ _insertFragment(editor, fragment, schemaTypes)
225
+ change$.next({type: 'loading', isLoading: false})
226
+ return true
227
+ }
228
+ change$.next({type: 'loading', isLoading: false})
229
+ return false
230
+ }
231
+
232
+ editor.insertData = (data: DataTransfer) => {
233
+ if (!editor.insertPortableTextData(data)) {
234
+ editor.insertTextOrHTMLData(data)
235
+ }
236
+ }
237
+
238
+ editor.insertFragmentData = (data: DataTransfer): boolean => {
239
+ const fragment = data.getData('application/x-portable-text')
240
+ if (fragment) {
241
+ const parsed = JSON.parse(fragment)
242
+ editor.insertFragment(parsed)
243
+ return true
244
+ }
245
+ return false
246
+ }
247
+
248
+ return editor
249
+ }
250
+ }
251
+
252
+ const entityMap: Record<string, string> = {
253
+ '&': '&amp;',
254
+ '<': '&lt;',
255
+ '>': '&gt;',
256
+ '"': '&quot;',
257
+ "'": '&#39;',
258
+ '/': '&#x2F;',
259
+ '`': '&#x60;',
260
+ '=': '&#x3D;',
261
+ }
262
+ function escapeHtml(str: string) {
263
+ return String(str).replace(/[&<>"'`=/]/g, (s: string) => entityMap[s])
264
+ }
265
+
266
+ /**
267
+ * Shared helper function to regenerate the keys on a fragment.
268
+ *
269
+ * @internal
270
+ */
271
+ function _regenerateKeys(
272
+ editor: PortableTextSlateEditor,
273
+ fragment: Descendant[],
274
+ keyGenerator: () => string,
275
+ spanTypeName: string,
276
+ ): Descendant[] {
277
+ return fragment.map((node) => {
278
+ const newNode: Descendant = {...node}
279
+ // Ensure the copy has new keys
280
+ if (editor.isTextBlock(newNode)) {
281
+ newNode.markDefs = (newNode.markDefs || []).map((def) => {
282
+ const oldKey = def._key
283
+ const newKey = keyGenerator()
284
+ newNode.children = newNode.children.map((child) =>
285
+ child._type === spanTypeName && editor.isTextSpan(child)
286
+ ? {
287
+ ...child,
288
+ marks:
289
+ child.marks && child.marks.includes(oldKey)
290
+ ? // eslint-disable-next-line max-nested-callbacks
291
+ [...child.marks].filter((mark) => mark !== oldKey).concat(newKey)
292
+ : child.marks,
293
+ }
294
+ : child,
295
+ )
296
+ return {...def, _key: newKey}
297
+ })
298
+ }
299
+ const nodeWithNewKeys = {...newNode, _key: keyGenerator()}
300
+ if (editor.isTextBlock(nodeWithNewKeys)) {
301
+ nodeWithNewKeys.children = nodeWithNewKeys.children.map((child) => ({
302
+ ...child,
303
+ _key: keyGenerator(),
304
+ }))
305
+ }
306
+ return nodeWithNewKeys as Descendant
307
+ })
308
+ }
309
+
310
+ /**
311
+ * Shared helper function to insert the final fragment into the editor
312
+ *
313
+ * @internal
314
+ */
315
+ function _insertFragment(
316
+ editor: PortableTextSlateEditor,
317
+ fragment: Descendant[],
318
+ schemaTypes: PortableTextMemberSchemaTypes,
319
+ ) {
320
+ editor.withoutNormalizing(() => {
321
+ if (!editor.selection) {
322
+ return
323
+ }
324
+ // Ensure that markDefs for any annotations inside this fragment are copied over to the focused text block.
325
+ const [focusBlock, focusPath] = Editor.node(editor, editor.selection, {depth: 1})
326
+ if (editor.isTextBlock(focusBlock) && editor.isTextBlock(fragment[0])) {
327
+ const {markDefs} = focusBlock
328
+ debug('Mixing markDefs of focusBlock and fragments[0] block', markDefs, fragment[0].markDefs)
329
+ if (!isEqual(markDefs, fragment[0].markDefs)) {
330
+ Transforms.setNodes(
331
+ editor,
332
+ {
333
+ markDefs: uniq([...(fragment[0].markDefs || []), ...(markDefs || [])]),
334
+ },
335
+ {at: focusPath, mode: 'lowest', voids: false},
336
+ )
337
+ }
338
+ }
339
+
340
+ const isPasteToEmptyEditor = isEqualToEmptyEditor(editor.children, schemaTypes)
341
+
342
+ if (isPasteToEmptyEditor) {
343
+ // Special case for pasting directly into an empty editor (a placeholder block).
344
+ // When pasting content starting with multiple empty blocks,
345
+ // `editor.insertFragment` can potentially duplicate the keys of
346
+ // the placeholder block because of operations that happen
347
+ // inside `editor.insertFragment` (involves an `insert_node` operation).
348
+ // However by splitting the placeholder block first in this situation we are good.
349
+ Transforms.splitNodes(editor, {at: [0, 0]})
350
+ editor.insertFragment(fragment)
351
+ Transforms.removeNodes(editor, {at: [0]})
352
+ } else {
353
+ // All other inserts
354
+ editor.insertFragment(fragment)
355
+ }
356
+ })
357
+
358
+ editor.onChange()
359
+ }
@@ -0,0 +1,24 @@
1
+ import {type PortableTextSlateEditor} from '../../types/editor'
2
+
3
+ /**
4
+ * This plugin makes sure that the PTE maxBlocks prop is respected
5
+ *
6
+ */
7
+ export function createWithMaxBlocks(maxBlocks: number) {
8
+ return function withMaxBlocks(editor: PortableTextSlateEditor): PortableTextSlateEditor {
9
+ const {apply} = editor
10
+ editor.apply = (operation) => {
11
+ const rows = maxBlocks
12
+ if (rows > 0 && editor.children.length >= rows) {
13
+ if (
14
+ (operation.type === 'insert_node' || operation.type === 'split_node') &&
15
+ operation.path.length === 1
16
+ ) {
17
+ return
18
+ }
19
+ }
20
+ apply(operation)
21
+ }
22
+ return editor
23
+ }
24
+ }
@@ -0,0 +1,63 @@
1
+ import {Editor, Element, Node, Transforms} from 'slate'
2
+
3
+ import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../../types/editor'
4
+ import {isPreservingKeys, PRESERVE_KEYS} from '../../utils/withPreserveKeys'
5
+
6
+ /**
7
+ * This plugin makes sure that every new node in the editor get a new _key prop when created
8
+ *
9
+ */
10
+ export function createWithObjectKeys(
11
+ schemaTypes: PortableTextMemberSchemaTypes,
12
+ keyGenerator: () => string,
13
+ ) {
14
+ return function withKeys(editor: PortableTextSlateEditor): PortableTextSlateEditor {
15
+ PRESERVE_KEYS.set(editor, false)
16
+ const {apply, normalizeNode} = editor
17
+
18
+ // The apply function can be called with a scope (withPreserveKeys) that will
19
+ // preserve keys for the produced nodes if they have a _key property set already.
20
+ // The default behavior is to always generate a new key here.
21
+ // For example, when undoing and redoing we want to retain the keys, but
22
+ // when we create a new bold span by splitting a non-bold-span we want the produced node to get a new key.
23
+ editor.apply = (operation) => {
24
+ if (operation.type === 'split_node') {
25
+ const withNewKey = !isPreservingKeys(editor) || !('_key' in operation.properties)
26
+ operation.properties = {
27
+ ...operation.properties,
28
+ ...(withNewKey ? {_key: keyGenerator()} : {}),
29
+ }
30
+ }
31
+ if (operation.type === 'insert_node') {
32
+ // Must be given a new key or adding/removing marks while typing gets in trouble (duped keys)!
33
+ const withNewKey = !isPreservingKeys(editor) || !('_key' in operation.node)
34
+ if (!Editor.isEditor(operation.node)) {
35
+ operation.node = {
36
+ ...operation.node,
37
+ ...(withNewKey ? {_key: keyGenerator()} : {}),
38
+ }
39
+ }
40
+ }
41
+ apply(operation)
42
+ }
43
+ editor.normalizeNode = (entry) => {
44
+ const [node, path] = entry
45
+ if (Element.isElement(node) && node._type === schemaTypes.block.name) {
46
+ // Set key on block itself
47
+ if (!node._key) {
48
+ Transforms.setNodes(editor, {_key: keyGenerator()}, {at: path})
49
+ }
50
+ // Set keys on it's children
51
+ for (const [child, childPath] of Node.children(editor, path)) {
52
+ if (!child._key) {
53
+ Transforms.setNodes(editor, {_key: keyGenerator()}, {at: childPath})
54
+ return
55
+ }
56
+ }
57
+ }
58
+ normalizeNode(entry)
59
+ }
60
+
61
+ return editor
62
+ }
63
+ }