@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,357 @@
1
+ import {type Path, type PortableTextSpan, type PortableTextTextBlock} from '@sanity/types'
2
+ import {get, isUndefined, omitBy} from 'lodash'
3
+ import {
4
+ type Descendant,
5
+ type InsertNodeOperation,
6
+ type InsertTextOperation,
7
+ type MergeNodeOperation,
8
+ type MoveNodeOperation,
9
+ type RemoveNodeOperation,
10
+ type RemoveTextOperation,
11
+ type SetNodeOperation,
12
+ type SplitNodeOperation,
13
+ Text,
14
+ } from 'slate'
15
+
16
+ import {type PatchFunctions} from '../editor/plugins/createWithPatches'
17
+ import {diffMatchPatch, insert, set, setIfMissing, unset} from '../patch/PatchEvent'
18
+ import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../types/editor'
19
+ import {type InsertPosition, type Patch} from '../types/patch'
20
+ import {debugWithName} from './debug'
21
+ import {fromSlateValue} from './values'
22
+
23
+ const debug = debugWithName('operationToPatches')
24
+ debug.enabled = false
25
+
26
+ export function createOperationToPatches(types: PortableTextMemberSchemaTypes): PatchFunctions {
27
+ const textBlockName = types.block.name
28
+ function insertTextPatch(
29
+ editor: PortableTextSlateEditor,
30
+ operation: InsertTextOperation,
31
+ beforeValue: Descendant[],
32
+ ) {
33
+ if (debug.enabled) {
34
+ debug('Operation', JSON.stringify(operation, null, 2))
35
+ }
36
+ const block =
37
+ editor.isTextBlock(editor.children[operation.path[0]]) && editor.children[operation.path[0]]
38
+ if (!block) {
39
+ throw new Error('Could not find block')
40
+ }
41
+ const textChild =
42
+ editor.isTextBlock(block) &&
43
+ editor.isTextSpan(block.children[operation.path[1]]) &&
44
+ (block.children[operation.path[1]] as PortableTextSpan)
45
+ if (!textChild) {
46
+ throw new Error('Could not find child')
47
+ }
48
+ const path: Path = [{_key: block._key}, 'children', {_key: textChild._key}, 'text']
49
+ const prevBlock = beforeValue[operation.path[0]]
50
+ const prevChild = editor.isTextBlock(prevBlock) && prevBlock.children[operation.path[1]]
51
+ const prevText = editor.isTextSpan(prevChild) ? prevChild.text : ''
52
+ const patch = diffMatchPatch(prevText, textChild.text, path)
53
+ return patch.value.length ? [patch] : []
54
+ }
55
+
56
+ function removeTextPatch(
57
+ editor: PortableTextSlateEditor,
58
+ operation: RemoveTextOperation,
59
+ beforeValue: Descendant[],
60
+ ) {
61
+ const block = editor && editor.children[operation.path[0]]
62
+ if (!block) {
63
+ throw new Error('Could not find block')
64
+ }
65
+ const child = (editor.isTextBlock(block) && block.children[operation.path[1]]) || undefined
66
+ const textChild: PortableTextSpan | undefined = editor.isTextSpan(child) ? child : undefined
67
+ if (child && !textChild) {
68
+ throw new Error('Expected span')
69
+ }
70
+ if (!textChild) {
71
+ throw new Error('Could not find child')
72
+ }
73
+ const path: Path = [{_key: block._key}, 'children', {_key: textChild._key}, 'text']
74
+ const beforeBlock = beforeValue[operation.path[0]]
75
+ const prevTextChild = editor.isTextBlock(beforeBlock) && beforeBlock.children[operation.path[1]]
76
+ const prevText = editor.isTextSpan(prevTextChild) && prevTextChild.text
77
+ const patch = diffMatchPatch(prevText || '', textChild.text, path)
78
+ return patch.value ? [patch] : []
79
+ }
80
+
81
+ function setNodePatch(editor: PortableTextSlateEditor, operation: SetNodeOperation) {
82
+ if (operation.path.length === 1) {
83
+ const block = editor.children[operation.path[0]]
84
+ if (typeof block._key !== 'string') {
85
+ throw new Error('Expected block to have a _key')
86
+ }
87
+ const setNode = omitBy(
88
+ {...editor.children[operation.path[0]], ...operation.newProperties},
89
+ isUndefined,
90
+ ) as unknown as Descendant
91
+ return [set(fromSlateValue([setNode], textBlockName)[0], [{_key: block._key}])]
92
+ } else if (operation.path.length === 2) {
93
+ const block = editor.children[operation.path[0]]
94
+ if (editor.isTextBlock(block)) {
95
+ const child = block.children[operation.path[1]]
96
+ if (child) {
97
+ const blockKey = block._key
98
+ const childKey = child._key
99
+ const patches: Patch[] = []
100
+ const keys = Object.keys(operation.newProperties)
101
+ keys.forEach((keyName) => {
102
+ // Special case for setting _key on a child. We have to target it by index and not the _key.
103
+ if (keys.length === 1 && keyName === '_key') {
104
+ const val = get(operation.newProperties, keyName)
105
+ patches.push(
106
+ set(val, [{_key: blockKey}, 'children', block.children.indexOf(child), keyName]),
107
+ )
108
+ } else {
109
+ const val = get(operation.newProperties, keyName)
110
+ patches.push(set(val, [{_key: blockKey}, 'children', {_key: childKey}, keyName]))
111
+ }
112
+ })
113
+ return patches
114
+ }
115
+ throw new Error('Could not find a valid child')
116
+ }
117
+ throw new Error('Could not find a valid block')
118
+ } else {
119
+ throw new Error(`Unexpected path encountered: ${JSON.stringify(operation.path)}`)
120
+ }
121
+ }
122
+
123
+ function insertNodePatch(
124
+ editor: PortableTextSlateEditor,
125
+ operation: InsertNodeOperation,
126
+ beforeValue: Descendant[],
127
+ ): Patch[] {
128
+ const block = beforeValue[operation.path[0]]
129
+ const isTextBlock = editor.isTextBlock(block)
130
+ if (operation.path.length === 1) {
131
+ const position = operation.path[0] === 0 ? 'before' : 'after'
132
+ const beforeBlock = beforeValue[operation.path[0] - 1]
133
+ const targetKey = operation.path[0] === 0 ? block?._key : beforeBlock?._key
134
+ if (targetKey) {
135
+ return [
136
+ insert([fromSlateValue([operation.node as Descendant], textBlockName)[0]], position, [
137
+ {_key: targetKey},
138
+ ]),
139
+ ]
140
+ }
141
+ return [
142
+ setIfMissing(beforeValue, []),
143
+ insert([fromSlateValue([operation.node as Descendant], textBlockName)[0]], 'before', [
144
+ operation.path[0],
145
+ ]),
146
+ ]
147
+ } else if (isTextBlock && operation.path.length === 2 && editor.children[operation.path[0]]) {
148
+ const position =
149
+ block.children.length === 0 || !block.children[operation.path[1] - 1] ? 'before' : 'after'
150
+ const node = {...operation.node} as Descendant
151
+ if (!node._type && Text.isText(node)) {
152
+ node._type = 'span'
153
+ node.marks = []
154
+ }
155
+ const blk = fromSlateValue(
156
+ [
157
+ {
158
+ _key: 'bogus',
159
+ _type: textBlockName,
160
+ children: [node],
161
+ },
162
+ ],
163
+ textBlockName,
164
+ )[0] as PortableTextTextBlock
165
+ const child = blk.children[0]
166
+ return [
167
+ insert([child], position, [
168
+ {_key: block._key},
169
+ 'children',
170
+ block.children.length <= 1 || !block.children[operation.path[1] - 1]
171
+ ? 0
172
+ : {_key: block.children[operation.path[1] - 1]._key},
173
+ ]),
174
+ ]
175
+ }
176
+ debug('Something was inserted into a void block. Not producing editor patches.')
177
+ return []
178
+ }
179
+
180
+ function splitNodePatch(
181
+ editor: PortableTextSlateEditor,
182
+ operation: SplitNodeOperation,
183
+ beforeValue: Descendant[],
184
+ ) {
185
+ const patches: Patch[] = []
186
+ const splitBlock = editor.children[operation.path[0]]
187
+ if (!editor.isTextBlock(splitBlock)) {
188
+ throw new Error(
189
+ `Block with path ${JSON.stringify(
190
+ operation.path[0],
191
+ )} is not a text block and can't be split`,
192
+ )
193
+ }
194
+ if (operation.path.length === 1) {
195
+ const oldBlock = beforeValue[operation.path[0]]
196
+ if (editor.isTextBlock(oldBlock)) {
197
+ const targetValue = fromSlateValue(
198
+ [editor.children[operation.path[0] + 1]],
199
+ textBlockName,
200
+ )[0]
201
+ if (targetValue) {
202
+ patches.push(insert([targetValue], 'after', [{_key: splitBlock._key}]))
203
+ const spansToUnset = oldBlock.children.slice(operation.position)
204
+ spansToUnset.forEach((span) => {
205
+ const path = [{_key: oldBlock._key}, 'children', {_key: span._key}]
206
+ patches.push(unset(path))
207
+ })
208
+ }
209
+ }
210
+ return patches
211
+ }
212
+ if (operation.path.length === 2) {
213
+ const splitSpan = splitBlock.children[operation.path[1]]
214
+ if (editor.isTextSpan(splitSpan)) {
215
+ const targetSpans = (
216
+ fromSlateValue(
217
+ [
218
+ {
219
+ ...splitBlock,
220
+ children: splitBlock.children.slice(operation.path[1] + 1, operation.path[1] + 2),
221
+ } as Descendant,
222
+ ],
223
+ textBlockName,
224
+ )[0] as PortableTextTextBlock
225
+ ).children
226
+
227
+ patches.push(
228
+ insert(targetSpans, 'after', [
229
+ {_key: splitBlock._key},
230
+ 'children',
231
+ {_key: splitSpan._key},
232
+ ]),
233
+ )
234
+ patches.push(
235
+ set(splitSpan.text, [
236
+ {_key: splitBlock._key},
237
+ 'children',
238
+ {_key: splitSpan._key},
239
+ 'text',
240
+ ]),
241
+ )
242
+ }
243
+ return patches
244
+ }
245
+ return patches
246
+ }
247
+
248
+ function removeNodePatch(
249
+ editor: PortableTextSlateEditor,
250
+ operation: RemoveNodeOperation,
251
+ beforeValue: Descendant[],
252
+ ) {
253
+ const block = beforeValue[operation.path[0]]
254
+ if (operation.path.length === 1) {
255
+ // Remove a single block
256
+ if (block && block._key) {
257
+ return [unset([{_key: block._key}])]
258
+ }
259
+ throw new Error('Block not found')
260
+ } else if (editor.isTextBlock(block) && operation.path.length === 2) {
261
+ const spanToRemove =
262
+ editor.isTextBlock(block) && block.children && block.children[operation.path[1]]
263
+ if (spanToRemove) {
264
+ return [unset([{_key: block._key}, 'children', {_key: spanToRemove._key}])]
265
+ }
266
+ debug('Span not found in editor trying to remove node')
267
+ return []
268
+ } else {
269
+ debug('Not creating patch inside object block')
270
+ return []
271
+ }
272
+ }
273
+
274
+ function mergeNodePatch(
275
+ editor: PortableTextSlateEditor,
276
+ operation: MergeNodeOperation,
277
+ beforeValue: Descendant[],
278
+ ) {
279
+ const patches: Patch[] = []
280
+
281
+ const block = beforeValue[operation.path[0]]
282
+ const targetBlock = editor.children[operation.path[0]]
283
+
284
+ if (operation.path.length === 1) {
285
+ if (block?._key) {
286
+ const newBlock = fromSlateValue([editor.children[operation.path[0] - 1]], textBlockName)[0]
287
+ patches.push(set(newBlock, [{_key: newBlock._key}]))
288
+ patches.push(unset([{_key: block._key}]))
289
+ } else {
290
+ throw new Error('Target key not found!')
291
+ }
292
+ } else if (operation.path.length === 2 && editor.isTextBlock(targetBlock)) {
293
+ const mergedSpan =
294
+ (editor.isTextBlock(block) && block.children[operation.path[1]]) || undefined
295
+ const targetSpan = targetBlock.children[operation.path[1] - 1]
296
+ if (editor.isTextSpan(targetSpan)) {
297
+ // Set the merged span with it's new value
298
+ patches.push(
299
+ set(targetSpan.text, [{_key: block._key}, 'children', {_key: targetSpan._key}, 'text']),
300
+ )
301
+ if (mergedSpan) {
302
+ patches.push(unset([{_key: block._key}, 'children', {_key: mergedSpan._key}]))
303
+ }
304
+ }
305
+ } else {
306
+ debug("Void nodes can't be merged, not creating any patches")
307
+ }
308
+ return patches
309
+ }
310
+
311
+ function moveNodePatch(
312
+ editor: PortableTextSlateEditor,
313
+ operation: MoveNodeOperation,
314
+ beforeValue: Descendant[],
315
+ ) {
316
+ const patches: Patch[] = []
317
+ const block = beforeValue[operation.path[0]]
318
+ const targetBlock = beforeValue[operation.newPath[0]]
319
+ if (operation.path.length === 1) {
320
+ const position: InsertPosition = operation.path[0] > operation.newPath[0] ? 'before' : 'after'
321
+ patches.push(unset([{_key: block._key}]))
322
+ patches.push(
323
+ insert([fromSlateValue([block], textBlockName)[0]], position, [{_key: targetBlock._key}]),
324
+ )
325
+ } else if (
326
+ operation.path.length === 2 &&
327
+ editor.isTextBlock(block) &&
328
+ editor.isTextBlock(targetBlock)
329
+ ) {
330
+ const child = block.children[operation.path[1]]
331
+ const targetChild = targetBlock.children[operation.newPath[1]]
332
+ const position = operation.newPath[1] === targetBlock.children.length ? 'after' : 'before'
333
+ const childToInsert = (fromSlateValue([block], textBlockName)[0] as PortableTextTextBlock)
334
+ .children[operation.path[1]]
335
+ patches.push(unset([{_key: block._key}, 'children', {_key: child._key}]))
336
+ patches.push(
337
+ insert([childToInsert], position, [
338
+ {_key: targetBlock._key},
339
+ 'children',
340
+ {_key: targetChild._key},
341
+ ]),
342
+ )
343
+ }
344
+ return patches
345
+ }
346
+
347
+ return {
348
+ insertNodePatch,
349
+ insertTextPatch,
350
+ mergeNodePatch,
351
+ moveNodePatch,
352
+ removeNodePatch,
353
+ removeTextPatch,
354
+ setNodePatch,
355
+ splitNodePatch,
356
+ }
357
+ }
@@ -0,0 +1,36 @@
1
+ import {isEqual} from 'lodash'
2
+
3
+ import {type Patch} from '../types/patch'
4
+
5
+ /**
6
+ * Try to compact a set of patches
7
+ *
8
+ */
9
+ export function compactPatches(patches: Patch[]) {
10
+ // If the last patch is unsetting everything, just do that
11
+ const lastPatch = patches.slice(-1)[0]
12
+ if (lastPatch && lastPatch.type === 'unset' && lastPatch.path.length === 0) {
13
+ return [lastPatch]
14
+ }
15
+ let finalPatches = patches
16
+ // Run through the patches and remove any redundant ones.
17
+ finalPatches = finalPatches.filter((patch, index) => {
18
+ if (!patch) {
19
+ return false
20
+ }
21
+ const nextPatch = finalPatches[index + 1]
22
+ if (
23
+ nextPatch &&
24
+ nextPatch.type === 'set' &&
25
+ patch.type === 'set' &&
26
+ isEqual(patch.path, nextPatch.path)
27
+ ) {
28
+ return false
29
+ }
30
+ return true
31
+ })
32
+ if (finalPatches.length !== patches.length) {
33
+ return finalPatches
34
+ }
35
+ return patches
36
+ }
@@ -0,0 +1,60 @@
1
+ import {isKeySegment, type Path} from '@sanity/types'
2
+ import {isEqual} from 'lodash'
3
+ import {type Descendant, Editor, Element, type Path as SlatePath, type Point} from 'slate'
4
+
5
+ import {type EditorSelectionPoint, type PortableTextMemberSchemaTypes} from '../types/editor'
6
+ import {type ObjectWithKeyAndType} from './ranges'
7
+
8
+ export function createKeyedPath(
9
+ point: Point,
10
+ value: ObjectWithKeyAndType[] | undefined,
11
+ types: PortableTextMemberSchemaTypes,
12
+ ): Path | null {
13
+ const blockPath = [point.path[0]]
14
+ if (!value) {
15
+ return null
16
+ }
17
+ const block = value[blockPath[0]]
18
+ if (!block) {
19
+ return null
20
+ }
21
+ const keyedBlockPath = [{_key: block._key}]
22
+ if (block._type !== types.block.name) {
23
+ return keyedBlockPath as Path
24
+ }
25
+ let keyedChildPath
26
+ const childPath = point.path.slice(0, 2)
27
+ const child = Array.isArray(block.children) && block.children[childPath[1]]
28
+ if (child) {
29
+ keyedChildPath = ['children', {_key: child._key}]
30
+ }
31
+ return (keyedChildPath ? [...keyedBlockPath, ...keyedChildPath] : keyedBlockPath) as Path
32
+ }
33
+
34
+ export function createArrayedPath(point: EditorSelectionPoint, editor: Editor): SlatePath {
35
+ if (!editor) {
36
+ return []
37
+ }
38
+ const [block, blockPath] = Array.from(
39
+ Editor.nodes(editor, {
40
+ at: [],
41
+ match: (n) => isKeySegment(point.path[0]) && (n as Descendant)._key === point.path[0]._key,
42
+ }),
43
+ )[0] || [undefined, undefined]
44
+ if (!block || !Element.isElement(block)) {
45
+ return []
46
+ }
47
+ if (editor.isVoid(block)) {
48
+ return [blockPath[0], 0]
49
+ }
50
+ const childPath = [point.path[2]]
51
+ const childIndex = block.children.findIndex((child) => isEqual([{_key: child._key}], childPath))
52
+ if (childIndex >= 0 && block.children[childIndex]) {
53
+ const child = block.children[childIndex]
54
+ if (Element.isElement(child) && editor.isVoid(child)) {
55
+ return blockPath.concat(childIndex).concat(0)
56
+ }
57
+ return blockPath.concat(childIndex)
58
+ }
59
+ return blockPath
60
+ }
@@ -0,0 +1,77 @@
1
+ /* eslint-disable complexity */
2
+ import {type BaseRange, type Editor, type Operation, Point, Range} from 'slate'
3
+
4
+ import {
5
+ type EditorSelection,
6
+ type EditorSelectionPoint,
7
+ type PortableTextMemberSchemaTypes,
8
+ } from '../types/editor'
9
+ import {createArrayedPath, createKeyedPath} from './paths'
10
+
11
+ export interface ObjectWithKeyAndType {
12
+ _key: string
13
+ _type: string
14
+ children?: ObjectWithKeyAndType[]
15
+ }
16
+
17
+ export function toPortableTextRange(
18
+ value: ObjectWithKeyAndType[] | undefined,
19
+ range: BaseRange | Partial<BaseRange> | null,
20
+ types: PortableTextMemberSchemaTypes,
21
+ ): EditorSelection {
22
+ if (!range) {
23
+ return null
24
+ }
25
+ let anchor: EditorSelectionPoint | null = null
26
+ let focus: EditorSelectionPoint | null = null
27
+ const anchorPath = range.anchor && createKeyedPath(range.anchor, value, types)
28
+ if (anchorPath && range.anchor) {
29
+ anchor = {
30
+ path: anchorPath,
31
+ offset: range.anchor.offset,
32
+ }
33
+ }
34
+ const focusPath = range.focus && createKeyedPath(range.focus, value, types)
35
+ if (focusPath && range.focus) {
36
+ focus = {
37
+ path: focusPath,
38
+ offset: range.focus.offset,
39
+ }
40
+ }
41
+ const backward = Boolean(Range.isRange(range) ? Range.isBackward(range) : undefined)
42
+ return anchor && focus ? {anchor, focus, backward} : null
43
+ }
44
+
45
+ export function toSlateRange(selection: EditorSelection, editor: Editor): Range | null {
46
+ if (!selection || !editor) {
47
+ return null
48
+ }
49
+ const anchor = {
50
+ path: createArrayedPath(selection.anchor, editor),
51
+ offset: selection.anchor.offset,
52
+ }
53
+ const focus = {
54
+ path: createArrayedPath(selection.focus, editor),
55
+ offset: selection.focus.offset,
56
+ }
57
+ if (focus.path.length === 0 || anchor.path.length === 0) {
58
+ return null
59
+ }
60
+ const range = anchor && focus ? {anchor, focus} : null
61
+ return range
62
+ }
63
+
64
+ export function moveRangeByOperation(range: Range, operation: Operation): Range | null {
65
+ const anchor = Point.transform(range.anchor, operation)
66
+ const focus = Point.transform(range.focus, operation)
67
+
68
+ if (anchor === null || focus === null) {
69
+ return null
70
+ }
71
+
72
+ if (Point.equals(anchor, range.anchor) && Point.equals(focus, range.focus)) {
73
+ return range
74
+ }
75
+
76
+ return {anchor, focus}
77
+ }
@@ -0,0 +1,8 @@
1
+ import {Schema} from '@sanity/schema'
2
+
3
+ export function compileType(rawType: any) {
4
+ return Schema.compile({
5
+ name: 'blockTypeSchema',
6
+ types: [rawType],
7
+ }).get(rawType.name)
8
+ }
@@ -0,0 +1,65 @@
1
+ import {type Path, type PortableTextBlock} from '@sanity/types'
2
+ import {isEqual} from 'lodash'
3
+
4
+ import {type EditorSelection, type EditorSelectionPoint} from '../types/editor'
5
+
6
+ export function normalizePoint(
7
+ point: EditorSelectionPoint,
8
+ value: PortableTextBlock[],
9
+ ): EditorSelectionPoint | null {
10
+ if (!point || !value) {
11
+ return null
12
+ }
13
+ const newPath: Path = []
14
+ let newOffset: number = point.offset || 0
15
+ const blockKey =
16
+ typeof point.path[0] === 'object' && '_key' in point.path[0] && point.path[0]._key
17
+ const childKey =
18
+ typeof point.path[2] === 'object' && '_key' in point.path[2] && point.path[2]._key
19
+ const block: PortableTextBlock | undefined = value.find((blk) => blk._key === blockKey)
20
+ if (block) {
21
+ newPath.push({_key: block._key})
22
+ } else {
23
+ return null
24
+ }
25
+ if (block && point.path[1] === 'children') {
26
+ if (!block.children || (Array.isArray(block.children) && block.children.length === 0)) {
27
+ return null
28
+ }
29
+ const child =
30
+ Array.isArray(block.children) && block.children.find((cld) => cld._key === childKey)
31
+ if (child) {
32
+ newPath.push('children')
33
+ newPath.push({_key: child._key})
34
+ newOffset =
35
+ child.text && child.text.length >= point.offset
36
+ ? point.offset
37
+ : (child.text && child.text.length) || 0
38
+ } else {
39
+ return null
40
+ }
41
+ }
42
+ return {path: newPath, offset: newOffset}
43
+ }
44
+
45
+ export function normalizeSelection(
46
+ selection: EditorSelection,
47
+ value: PortableTextBlock[] | undefined,
48
+ ): EditorSelection | null {
49
+ if (!selection || !value || value.length === 0) {
50
+ return null
51
+ }
52
+ let newAnchor: EditorSelectionPoint | null = null
53
+ let newFocus: EditorSelectionPoint | null = null
54
+ const {anchor, focus} = selection
55
+ if (anchor && value.find((blk) => isEqual({_key: blk._key}, anchor.path[0]))) {
56
+ newAnchor = normalizePoint(anchor, value)
57
+ }
58
+ if (focus && value.find((blk) => isEqual({_key: blk._key}, focus.path[0]))) {
59
+ newFocus = normalizePoint(focus, value)
60
+ }
61
+ if (newAnchor && newFocus) {
62
+ return {anchor: newAnchor, focus: newFocus, backward: selection.backward}
63
+ }
64
+ return null
65
+ }
@@ -0,0 +1,67 @@
1
+ import {type Patch} from '@sanity/diff-match-patch'
2
+
3
+ /**
4
+ * Takes a `patches` array as produced by diff-match-patch and adjusts the
5
+ * `start1` and `start2` properties so that they refer to UCS-2 index instead
6
+ * of a UTF-8 index.
7
+ *
8
+ * @param patches - The patches to adjust
9
+ * @param base - The base string to use for counting bytes
10
+ * @returns A new array of patches with adjusted indicies
11
+ * @beta
12
+ */
13
+ export function adjustIndiciesToUcs2(patches: Patch[], base: string): Patch[] {
14
+ let byteOffset = 0
15
+ let idx = 0 // index into the input.
16
+
17
+ function advanceTo(target: number) {
18
+ for (; byteOffset < target; ) {
19
+ const codePoint = base.codePointAt(idx)
20
+ if (typeof codePoint === 'undefined') {
21
+ // Reached the end of the base string - the indicies won't be correct,
22
+ // but we also cannot advance any further to find a closer index.
23
+ return idx
24
+ }
25
+
26
+ byteOffset += utf8len(codePoint)
27
+
28
+ // This is encoded as a surrogate pair.
29
+ if (codePoint > 0xffff) {
30
+ idx += 2
31
+ } else {
32
+ idx += 1
33
+ }
34
+ }
35
+
36
+ // Theoretically, we should have reached target - however, due to differences in
37
+ // `base` from the string that the patch was originally based upon, occurences
38
+ // _can_ happen where we go beyond the target due to surrogate pairs or similar.
39
+ // In the PTE, this is okayish - best effort matching is good enough.
40
+ return idx
41
+ }
42
+
43
+ const adjusted: Patch[] = []
44
+ for (const patch of patches) {
45
+ adjusted.push({
46
+ diffs: patch.diffs.map((diff) => [...diff]),
47
+ start1: advanceTo(patch.start1),
48
+ start2: advanceTo(patch.start2),
49
+ utf8Start1: patch.utf8Start1,
50
+ utf8Start2: patch.utf8Start2,
51
+ length1: patch.length1,
52
+ length2: patch.length2,
53
+ utf8Length1: patch.utf8Length1,
54
+ utf8Length2: patch.utf8Length2,
55
+ })
56
+ }
57
+
58
+ return adjusted
59
+ }
60
+
61
+ function utf8len(codePoint: number): 1 | 2 | 3 | 4 {
62
+ // See table at https://en.wikipedia.org/wiki/UTF-8
63
+ if (codePoint <= 0x007f) return 1
64
+ if (codePoint <= 0x07ff) return 2
65
+ if (codePoint <= 0xffff) return 3
66
+ return 4
67
+ }