@portabletext/editor 2.12.0 → 2.12.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.
@@ -0,0 +1,776 @@
1
+ import {defineSchema} from '@portabletext/schema'
2
+ import {getTersePt, parseTersePt} from '@portabletext/test'
3
+ import {userEvent} from '@vitest/browser/context'
4
+ import {Given, Then, When} from 'racejar'
5
+ import {assert, expect, vi} from 'vitest'
6
+ import {getEditorSelection} from '../../internal-utils/editor-selection'
7
+ import {
8
+ parseBlocks,
9
+ parseInlineObject,
10
+ parseSpan,
11
+ } from '../../internal-utils/parse-blocks'
12
+ import {getSelectionText} from '../../internal-utils/selection-text'
13
+ import {getTextBlockKey} from '../../internal-utils/text-block-key'
14
+ import {getTextMarks} from '../../internal-utils/text-marks'
15
+ import {
16
+ getSelectionAfterText,
17
+ getSelectionBeforeText,
18
+ getTextSelection,
19
+ } from '../../internal-utils/text-selection'
20
+ import {getValueAnnotations} from '../../internal-utils/value-annotations'
21
+ import {createTestEditor} from '../../test/vitest'
22
+ import {
23
+ reverseSelection,
24
+ selectionPointToBlockOffset,
25
+ spanSelectionPointToBlockOffset,
26
+ } from '../../utils'
27
+ import type {Parameter} from '../gherkin-parameter-types'
28
+ import type {Context} from './step-context'
29
+
30
+ /**
31
+ * @internal
32
+ */
33
+ export const stepDefinitions = [
34
+ Given('one editor', async (context: Context) => {
35
+ const {editor, locator} = await createTestEditor({
36
+ schemaDefinition: defineSchema({
37
+ annotations: [{name: 'comment'}, {name: 'link'}],
38
+ decorators: [{name: 'em'}, {name: 'strong'}],
39
+ blockObjects: [{name: 'image'}, {name: 'break'}],
40
+ inlineObjects: [{name: 'stock-ticker'}],
41
+ lists: [{name: 'bullet'}, {name: 'number'}],
42
+ styles: [
43
+ {name: 'normal'},
44
+ {name: 'h1'},
45
+ {name: 'h2'},
46
+ {name: 'h3'},
47
+ {name: 'h4'},
48
+ {name: 'h5'},
49
+ {name: 'h6'},
50
+ {name: 'blockquote'},
51
+ ],
52
+ }),
53
+ })
54
+
55
+ context.locator = locator
56
+ context.editor = editor
57
+ }),
58
+
59
+ Given('a global keymap', (context: Context) => {
60
+ context.keyMap = new Map()
61
+ }),
62
+
63
+ Given('the editor is focused', async (context: Context) => {
64
+ context.editor.send({
65
+ type: 'focus',
66
+ })
67
+
68
+ await vi.waitFor(() => {
69
+ const selection = context.editor.getSnapshot().context.selection
70
+ expect(selection).not.toBeNull()
71
+ })
72
+ }),
73
+
74
+ Given(
75
+ 'the text {terse-pt}',
76
+ (context: Context, tersePt: Parameter['tersePt']) => {
77
+ const blocks = parseTersePt(
78
+ {
79
+ keyGenerator: context.editor.getSnapshot().context.keyGenerator,
80
+ schema: context.editor.getSnapshot().context.schema,
81
+ },
82
+ tersePt,
83
+ )
84
+
85
+ context.editor.send({
86
+ type: 'insert.blocks',
87
+ blocks,
88
+ placement: 'auto',
89
+ select: 'end',
90
+ })
91
+ },
92
+ ),
93
+
94
+ /**
95
+ * Block steps
96
+ */
97
+ Given(
98
+ 'blocks {placement}',
99
+ (context: Context, placement: Parameter['placement'], blocks: string) => {
100
+ context.editor.send({
101
+ type: 'insert.blocks',
102
+ blocks: parseBlocks({
103
+ context: {
104
+ schema: context.editor.getSnapshot().context.schema,
105
+ keyGenerator: context.editor.getSnapshot().context.keyGenerator,
106
+ },
107
+ blocks: JSON.parse(blocks),
108
+ options: {
109
+ removeUnusedMarkDefs: false,
110
+ validateFields: true,
111
+ },
112
+ }),
113
+ placement,
114
+ })
115
+ },
116
+ ),
117
+
118
+ /**
119
+ * Child steps
120
+ */
121
+ When('a child is inserted', (context: Context, child: string) => {
122
+ const parsedChild =
123
+ parseSpan({
124
+ context: {
125
+ schema: context.editor.getSnapshot().context.schema,
126
+ keyGenerator: context.editor.getSnapshot().context.keyGenerator,
127
+ },
128
+ span: JSON.parse(child),
129
+ markDefKeyMap: new Map(),
130
+ options: {validateFields: true},
131
+ }) ??
132
+ parseInlineObject({
133
+ context: {
134
+ schema: context.editor.getSnapshot().context.schema,
135
+ keyGenerator: context.editor.getSnapshot().context.keyGenerator,
136
+ },
137
+ inlineObject: JSON.parse(child),
138
+ options: {validateFields: true},
139
+ })
140
+
141
+ if (!parsedChild) {
142
+ throw new Error(`Unable to parse child ${child}`)
143
+ }
144
+
145
+ context.editor.send({
146
+ type: 'insert.child',
147
+ child: parsedChild,
148
+ })
149
+ }),
150
+ When(
151
+ 'a(n) {inline-object} is inserted',
152
+ (context: Context, inlineObject: Parameter['inlineObject']) => {
153
+ context.editor.send({
154
+ type: 'insert.inline object',
155
+ inlineObject: {
156
+ name: inlineObject,
157
+ value: {},
158
+ },
159
+ })
160
+ },
161
+ ),
162
+
163
+ /**
164
+ * Text steps
165
+ */
166
+ Given(
167
+ 'a block {key} with text {text}',
168
+ (context: Context, key: string, text: Parameter['text']) => {
169
+ context.editor.send({
170
+ type: 'insert.block',
171
+ block: {
172
+ _key: key,
173
+ _type: 'block',
174
+ children: [{_type: 'span', text, marks: []}],
175
+ },
176
+ placement: 'auto',
177
+ select: 'end',
178
+ })
179
+ },
180
+ ),
181
+ When('{string} is typed', async (context: Context, text: string) => {
182
+ await userEvent.type(context.locator, text)
183
+ }),
184
+ Then(
185
+ '{terse-pt} is in block {key}',
186
+ (context: Context, text: Array<string>, key: string) => {
187
+ const string = text.at(0)
188
+
189
+ if (string === undefined) {
190
+ assert.fail('Expected at least one text string')
191
+ }
192
+
193
+ if (text.length > 1) {
194
+ assert.fail('Expected at most one text string')
195
+ }
196
+
197
+ expect(
198
+ getTextBlockKey(context.editor.getSnapshot().context, string),
199
+ ).toBe(key)
200
+ },
201
+ ),
202
+
203
+ /**
204
+ * Button steps
205
+ */
206
+ When(
207
+ '{button} is pressed',
208
+ async (_: Context, button: Parameter['button']) => {
209
+ await userEvent.keyboard(button)
210
+ },
211
+ ),
212
+ When(
213
+ '{button} is pressed {int} times',
214
+ async (_: Context, button: Parameter['button'], times: number) => {
215
+ for (let i = 0; i < times; i++) {
216
+ await userEvent.keyboard(button)
217
+ }
218
+ },
219
+ ),
220
+
221
+ /**
222
+ * Selection steps
223
+ */
224
+ When(
225
+ 'the caret is put before {string}',
226
+ async (context: Context, text: string) => {
227
+ await vi.waitFor(() => {
228
+ const selection = getSelectionBeforeText(
229
+ context.editor.getSnapshot().context,
230
+ text,
231
+ )
232
+ expect(selection).not.toBeNull()
233
+
234
+ context.editor.send({
235
+ type: 'select',
236
+ at: selection,
237
+ })
238
+ })
239
+ },
240
+ ),
241
+ Then(
242
+ 'the caret is before {string}',
243
+ async (context: Context, text: string) => {
244
+ await vi.waitFor(() => {
245
+ const selection = getSelectionBeforeText(
246
+ context.editor.getSnapshot().context,
247
+ text,
248
+ )
249
+ expect(selection).not.toBeNull()
250
+ expect(context.editor.getSnapshot().context.selection).toEqual(
251
+ selection,
252
+ )
253
+ })
254
+ },
255
+ ),
256
+ When(
257
+ 'the caret is put after {string}',
258
+ async (context: Context, text: string) => {
259
+ await vi.waitFor(() => {
260
+ const selection = getSelectionAfterText(
261
+ context.editor.getSnapshot().context,
262
+ text,
263
+ )
264
+ expect(selection).not.toBeNull()
265
+
266
+ context.editor.send({
267
+ type: 'select',
268
+ at: getSelectionAfterText(context.editor.getSnapshot().context, text),
269
+ })
270
+ })
271
+ },
272
+ ),
273
+ Then(
274
+ 'the caret is after {string}',
275
+ async (context: Context, text: string) => {
276
+ await vi.waitFor(() => {
277
+ const selection = getSelectionAfterText(
278
+ context.editor.getSnapshot().context,
279
+ text,
280
+ )
281
+ expect(selection).not.toBeNull()
282
+ expect(context.editor.getSnapshot().context.selection).toEqual(
283
+ selection,
284
+ )
285
+ })
286
+ },
287
+ ),
288
+ Then('nothing is selected', async (context: Context) => {
289
+ await vi.waitFor(() => {
290
+ expect(context.editor.getSnapshot().context.selection).toBeNull()
291
+ })
292
+ }),
293
+ When('everything is selected', (context: Context) => {
294
+ const editorSelection = getEditorSelection(
295
+ context.editor.getSnapshot().context,
296
+ )
297
+
298
+ context.editor.send({
299
+ type: 'select',
300
+ at: editorSelection,
301
+ })
302
+ }),
303
+ When('everything is selected backwards', (context: Context) => {
304
+ const editorSelection = reverseSelection(
305
+ getEditorSelection(context.editor.getSnapshot().context),
306
+ )
307
+
308
+ context.editor.send({
309
+ type: 'select',
310
+ at: editorSelection,
311
+ })
312
+ }),
313
+ When('{string} is selected', async (context: Context, text: string) => {
314
+ await vi.waitFor(() => {
315
+ const selection = getTextSelection(
316
+ context.editor.getSnapshot().context,
317
+ text,
318
+ )
319
+ expect(selection).not.toBeNull()
320
+
321
+ context.editor.send({
322
+ type: 'select',
323
+ at: selection,
324
+ })
325
+ })
326
+ }),
327
+ When(
328
+ '{string} is selected backwards',
329
+ async (context: Context, text: string) => {
330
+ await vi.waitFor(() => {
331
+ const selection = reverseSelection(
332
+ getTextSelection(context.editor.getSnapshot().context, text),
333
+ )
334
+ expect(selection).not.toBeNull()
335
+
336
+ context.editor.send({
337
+ type: 'select',
338
+ at: selection,
339
+ })
340
+ })
341
+ },
342
+ ),
343
+ Then(
344
+ '{terse-pt} is selected',
345
+ async (context: Context, text: Array<string>) => {
346
+ await vi.waitFor(() => {
347
+ expect(
348
+ getSelectionText(context.editor.getSnapshot().context),
349
+ 'Unexpected selection',
350
+ ).toEqual(text)
351
+ })
352
+ },
353
+ ),
354
+
355
+ When(
356
+ '{terse-pt} is inserted at {placement}',
357
+ (
358
+ context: Context,
359
+ tersePt: Parameter['tersePt'],
360
+ placement: Parameter['placement'],
361
+ ) => {
362
+ const blocks = parseTersePt(
363
+ {
364
+ keyGenerator: context.editor.getSnapshot().context.keyGenerator,
365
+ schema: context.editor.getSnapshot().context.schema,
366
+ },
367
+ tersePt,
368
+ )
369
+
370
+ context.editor.send({
371
+ type: 'insert.blocks',
372
+ blocks,
373
+ placement,
374
+ })
375
+ },
376
+ ),
377
+ When(
378
+ '{terse-pt} is inserted at {placement} and selected at the {select-position}',
379
+ (
380
+ context: Context,
381
+ tersePt: Parameter['tersePt'],
382
+ placement: Parameter['placement'],
383
+ selectPosition: Parameter['selectPosition'],
384
+ ) => {
385
+ const blocks = parseTersePt(
386
+ {
387
+ keyGenerator: context.editor.getSnapshot().context.keyGenerator,
388
+ schema: context.editor.getSnapshot().context.schema,
389
+ },
390
+ tersePt,
391
+ )
392
+
393
+ context.editor.send({
394
+ type: 'insert.blocks',
395
+ blocks,
396
+ placement,
397
+ select: selectPosition,
398
+ })
399
+ },
400
+ ),
401
+ When(
402
+ 'blocks are inserted at {placement} and selected at the {select-position}',
403
+ (
404
+ context: Context,
405
+ placement: Parameter['placement'],
406
+ selectPosition: Parameter['selectPosition'],
407
+ blocks: string,
408
+ ) => {
409
+ context.editor.send({
410
+ type: 'insert.blocks',
411
+ blocks: parseBlocks({
412
+ context: {
413
+ schema: context.editor.getSnapshot().context.schema,
414
+ keyGenerator: context.editor.getSnapshot().context.keyGenerator,
415
+ },
416
+ blocks: JSON.parse(blocks),
417
+ options: {
418
+ removeUnusedMarkDefs: false,
419
+ validateFields: true,
420
+ },
421
+ }),
422
+ placement,
423
+ select: selectPosition,
424
+ })
425
+ },
426
+ ),
427
+
428
+ Then(
429
+ 'the text is {terse-pt}',
430
+ async (context: Context, tersePt: Parameter['tersePt']) => {
431
+ await vi.waitFor(() => {
432
+ expect(
433
+ getTersePt(context.editor.getSnapshot().context),
434
+ 'Unexpected editor text',
435
+ ).toEqual(tersePt)
436
+ })
437
+ },
438
+ ),
439
+
440
+ /**
441
+ * Annotation steps
442
+ */
443
+ Given(
444
+ 'a(n) {annotation} {keyKeys} around {string}',
445
+ async (
446
+ context: Context,
447
+ annotation: Parameter['annotation'],
448
+ keyKeys: Array<string>,
449
+ text: string,
450
+ ) => {
451
+ await vi.waitFor(() => {
452
+ const selection = getTextSelection(
453
+ context.editor.getSnapshot().context,
454
+ text,
455
+ )
456
+ expect(selection).not.toBeNull()
457
+
458
+ context.editor.send({
459
+ type: 'select',
460
+ at: selection,
461
+ })
462
+ })
463
+
464
+ const value = context.editor.getSnapshot().context.value
465
+ const priorAnnotationKeys = getValueAnnotations(
466
+ context.editor.getSnapshot().context.schema,
467
+ value,
468
+ )
469
+
470
+ context.editor.send({
471
+ type: 'annotation.toggle',
472
+ annotation: {
473
+ name: annotation,
474
+ value: {},
475
+ },
476
+ })
477
+
478
+ let newAnnotationKeys: Array<string> = []
479
+
480
+ await vi.waitFor(() => {
481
+ const newValue = context.editor.getSnapshot().context.value
482
+
483
+ expect(priorAnnotationKeys).not.toEqual(
484
+ getValueAnnotations(
485
+ context.editor.getSnapshot().context.schema,
486
+ newValue,
487
+ ),
488
+ )
489
+
490
+ newAnnotationKeys = getValueAnnotations(
491
+ context.editor.getSnapshot().context.schema,
492
+ newValue,
493
+ ).filter(
494
+ (newAnnotationKey) => !priorAnnotationKeys.includes(newAnnotationKey),
495
+ )
496
+ })
497
+
498
+ if (newAnnotationKeys.length !== keyKeys.length) {
499
+ assert.fail(
500
+ `Expected ${keyKeys.length} new annotation keys, but got ${newAnnotationKeys.length}`,
501
+ )
502
+ }
503
+
504
+ keyKeys.forEach((keyKey, index) => {
505
+ context.keyMap?.set(keyKey, newAnnotationKeys[index])
506
+ })
507
+ },
508
+ ),
509
+ When(
510
+ '{annotation} is toggled',
511
+ (context: Context, annotation: Parameter['annotation']) => {
512
+ context.editor.send({
513
+ type: 'annotation.toggle',
514
+ annotation: {
515
+ name: annotation,
516
+ value: {},
517
+ },
518
+ })
519
+ },
520
+ ),
521
+ When(
522
+ '{annotation} {keyKeys} is toggled',
523
+ async (
524
+ context: Context,
525
+ annotation: Parameter['annotation'],
526
+ keyKeys: Array<string>,
527
+ ) => {
528
+ const value = context.editor.getSnapshot().context.value
529
+ const priorAnnotationKeys = getValueAnnotations(
530
+ context.editor.getSnapshot().context.schema,
531
+ value,
532
+ )
533
+
534
+ context.editor.send({
535
+ type: 'annotation.toggle',
536
+ annotation: {
537
+ name: annotation,
538
+ value: {},
539
+ },
540
+ })
541
+
542
+ let newAnnotationKeys: Array<string> = []
543
+
544
+ await vi.waitFor(() => {
545
+ const newValue = context.editor.getSnapshot().context.value
546
+
547
+ expect(priorAnnotationKeys).not.toEqual(
548
+ getValueAnnotations(
549
+ context.editor.getSnapshot().context.schema,
550
+ newValue,
551
+ ),
552
+ )
553
+
554
+ newAnnotationKeys = getValueAnnotations(
555
+ context.editor.getSnapshot().context.schema,
556
+ newValue,
557
+ ).filter(
558
+ (newAnnotationKey) => !priorAnnotationKeys.includes(newAnnotationKey),
559
+ )
560
+ })
561
+
562
+ if (newAnnotationKeys.length !== keyKeys.length) {
563
+ assert.fail(
564
+ `Expected ${keyKeys.length} new annotation keys, but got ${newAnnotationKeys.length}`,
565
+ )
566
+ }
567
+
568
+ keyKeys.forEach((keyKey, index) => {
569
+ context.keyMap?.set(keyKey, newAnnotationKeys[index])
570
+ })
571
+ },
572
+ ),
573
+ Then(
574
+ '{string} has an annotation different than {key}',
575
+ (context: Context, text: string, key: string) => {
576
+ const marks = getTextMarks(context.editor.getSnapshot().context, text)
577
+ const expectedMarks = [context.keyMap?.get(key) ?? key]
578
+
579
+ expect(marks).not.toEqual(expectedMarks)
580
+ },
581
+ ),
582
+
583
+ /**
584
+ * Decorator steps
585
+ */
586
+ Given(
587
+ '{decorator} around {string}',
588
+ async (
589
+ context: Context,
590
+ decorator: Parameter['decorator'],
591
+ text: string,
592
+ ) => {
593
+ await vi.waitFor(() => {
594
+ const selection = getTextSelection(
595
+ context.editor.getSnapshot().context,
596
+ text,
597
+ )
598
+ const anchorOffset = selection
599
+ ? selectionPointToBlockOffset({
600
+ context: {
601
+ schema: context.editor.getSnapshot().context.schema,
602
+ value: context.editor.getSnapshot().context.value,
603
+ },
604
+ selectionPoint: selection.anchor,
605
+ })
606
+ : undefined
607
+ const focusOffset = selection
608
+ ? selectionPointToBlockOffset({
609
+ context: {
610
+ schema: context.editor.getSnapshot().context.schema,
611
+ value: context.editor.getSnapshot().context.value,
612
+ },
613
+ selectionPoint: selection.focus,
614
+ })
615
+ : undefined
616
+ expect(anchorOffset).toBeDefined()
617
+ expect(focusOffset).toBeDefined()
618
+
619
+ context.editor.send({
620
+ type: 'decorator.toggle',
621
+ decorator,
622
+ at: {
623
+ anchor: anchorOffset!,
624
+ focus: focusOffset!,
625
+ },
626
+ })
627
+ })
628
+ },
629
+ ),
630
+ When(
631
+ '{decorator} is toggled',
632
+ async (context: Context, decorator: Parameter['decorator']) => {
633
+ await vi.waitFor(() => {
634
+ context.editor.send({
635
+ type: 'decorator.toggle',
636
+ decorator,
637
+ })
638
+ })
639
+ },
640
+ ),
641
+ When(
642
+ '{string} is marked with {decorator}',
643
+ async (
644
+ context: Context,
645
+ text: string,
646
+ decorator: Parameter['decorator'],
647
+ ) => {
648
+ await vi.waitFor(() => {
649
+ const selection = getTextSelection(
650
+ context.editor.getSnapshot().context,
651
+ text,
652
+ )
653
+ const anchorOffset = selection
654
+ ? spanSelectionPointToBlockOffset({
655
+ context: {
656
+ schema: context.editor.getSnapshot().context.schema,
657
+ value: context.editor.getSnapshot().context.value,
658
+ },
659
+ selectionPoint: selection.anchor,
660
+ })
661
+ : undefined
662
+ const focusOffset = selection
663
+ ? spanSelectionPointToBlockOffset({
664
+ context: {
665
+ schema: context.editor.getSnapshot().context.schema,
666
+ value: context.editor.getSnapshot().context.value,
667
+ },
668
+ selectionPoint: selection.focus,
669
+ })
670
+ : undefined
671
+
672
+ expect(anchorOffset).toBeDefined()
673
+ expect(focusOffset).toBeDefined()
674
+
675
+ context.editor.send({
676
+ type: 'decorator.toggle',
677
+ decorator,
678
+ at: {
679
+ anchor: anchorOffset!,
680
+ focus: focusOffset!,
681
+ },
682
+ })
683
+ })
684
+ },
685
+ ),
686
+
687
+ /**
688
+ * Mark steps
689
+ */
690
+ Then(
691
+ '{string} has marks {marks}',
692
+ async (context: Context, text: string, marks: Parameter['marks']) => {
693
+ await vi.waitFor(() => {
694
+ const actualMarks =
695
+ getTextMarks(context.editor.getSnapshot().context, text) ?? []
696
+ const expectedMarks = marks.map(
697
+ (mark) => context.keyMap?.get(mark) ?? mark,
698
+ )
699
+
700
+ expect(actualMarks).toEqual(expectedMarks)
701
+ })
702
+ },
703
+ ),
704
+ Then('{string} has no marks', async (context: Context, text: string) => {
705
+ await vi.waitFor(() => {
706
+ const textMarks = getTextMarks(context.editor.getSnapshot().context, text)
707
+ expect(textMarks).toEqual([])
708
+ })
709
+ }),
710
+ Then(
711
+ '{string} and {string} have the same marks',
712
+ (context: Context, textA: string, textB: string) => {
713
+ const marksA = getTextMarks(context.editor.getSnapshot().context, textA)
714
+ const marksB = getTextMarks(context.editor.getSnapshot().context, textB)
715
+
716
+ expect(
717
+ marksA,
718
+ `Expected "${textA}" and "${textB}" to have the same marks`,
719
+ ).toEqual(marksB)
720
+ },
721
+ ),
722
+
723
+ /**
724
+ * Style steps
725
+ */
726
+ When('{style} is toggled', (context: Context, style: Parameter['style']) => {
727
+ context.editor.send({type: 'style.toggle', style})
728
+ }),
729
+
730
+ /**
731
+ * Clipboard steps
732
+ */
733
+ When('x-portable-text is pasted', (context: Context, blocks: string) => {
734
+ const dataTransfer = new DataTransfer()
735
+ dataTransfer.setData('application/x-portable-text', blocks)
736
+ dataTransfer.setData('application/json', blocks)
737
+
738
+ context.editor.send({
739
+ type: 'clipboard.paste',
740
+ originEvent: {
741
+ dataTransfer,
742
+ },
743
+ position: {
744
+ selection: context.editor.getSnapshot().context.selection!,
745
+ },
746
+ })
747
+ }),
748
+ When(
749
+ 'data is pasted',
750
+ (context: Context, dataTable: Array<[mime: string, data: string]>) => {
751
+ const dataTransfer = new DataTransfer()
752
+
753
+ for (const data of dataTable) {
754
+ dataTransfer.setData(data[0], data[1])
755
+ }
756
+
757
+ context.editor.send({
758
+ type: 'clipboard.paste',
759
+ originEvent: {dataTransfer},
760
+ position: {
761
+ selection: context.editor.getSnapshot().context.selection!,
762
+ },
763
+ })
764
+ },
765
+ ),
766
+
767
+ /**
768
+ * Undo/Redo steps
769
+ */
770
+ When('undo is performed', (context: Context) => {
771
+ context.editor.send({type: 'history.undo'})
772
+ }),
773
+ When('redo is performed', (context: Context) => {
774
+ context.editor.send({type: 'history.redo'})
775
+ }),
776
+ ]