@portabletext/editor 1.32.0 → 1.33.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.
Files changed (71) hide show
  1. package/lib/_chunks-cjs/behavior.core.cjs +4 -4
  2. package/lib/_chunks-cjs/behavior.core.cjs.map +1 -1
  3. package/lib/_chunks-cjs/behavior.markdown.cjs +19 -11
  4. package/lib/_chunks-cjs/behavior.markdown.cjs.map +1 -1
  5. package/lib/_chunks-cjs/plugin.event-listener.cjs +127 -88
  6. package/lib/_chunks-cjs/plugin.event-listener.cjs.map +1 -1
  7. package/lib/_chunks-cjs/selector.get-trimmed-selection.cjs +97 -0
  8. package/lib/_chunks-cjs/selector.get-trimmed-selection.cjs.map +1 -0
  9. package/lib/_chunks-cjs/{parse-blocks.cjs → util.block-offsets-to-selection.cjs} +21 -2
  10. package/lib/_chunks-cjs/util.block-offsets-to-selection.cjs.map +1 -0
  11. package/lib/_chunks-cjs/util.reverse-selection.cjs +11 -0
  12. package/lib/_chunks-cjs/util.reverse-selection.cjs.map +1 -1
  13. package/lib/_chunks-es/behavior.core.js +1 -1
  14. package/lib/_chunks-es/behavior.core.js.map +1 -1
  15. package/lib/_chunks-es/behavior.markdown.js +18 -11
  16. package/lib/_chunks-es/behavior.markdown.js.map +1 -1
  17. package/lib/_chunks-es/plugin.event-listener.js +127 -87
  18. package/lib/_chunks-es/plugin.event-listener.js.map +1 -1
  19. package/lib/_chunks-es/selector.get-trimmed-selection.js +100 -0
  20. package/lib/_chunks-es/selector.get-trimmed-selection.js.map +1 -0
  21. package/lib/_chunks-es/{parse-blocks.js → util.block-offsets-to-selection.js} +21 -1
  22. package/lib/_chunks-es/util.block-offsets-to-selection.js.map +1 -0
  23. package/lib/_chunks-es/util.reverse-selection.js +11 -0
  24. package/lib/_chunks-es/util.reverse-selection.js.map +1 -1
  25. package/lib/behaviors/index.d.cts +1 -0
  26. package/lib/behaviors/index.d.ts +1 -0
  27. package/lib/index.d.cts +60 -0
  28. package/lib/index.d.ts +60 -0
  29. package/lib/plugins/index.cjs +302 -3
  30. package/lib/plugins/index.cjs.map +1 -1
  31. package/lib/plugins/index.d.cts +74 -1
  32. package/lib/plugins/index.d.ts +74 -1
  33. package/lib/plugins/index.js +307 -4
  34. package/lib/plugins/index.js.map +1 -1
  35. package/lib/selectors/index.cjs +51 -1
  36. package/lib/selectors/index.cjs.map +1 -1
  37. package/lib/selectors/index.d.cts +67 -0
  38. package/lib/selectors/index.d.ts +67 -0
  39. package/lib/selectors/index.js +53 -2
  40. package/lib/selectors/index.js.map +1 -1
  41. package/lib/utils/index.cjs +5 -4
  42. package/lib/utils/index.cjs.map +1 -1
  43. package/lib/utils/index.d.cts +16 -0
  44. package/lib/utils/index.d.ts +16 -0
  45. package/lib/utils/index.js +4 -3
  46. package/package.json +7 -7
  47. package/src/behavior-actions/behavior.action.decorator.add.ts +161 -0
  48. package/src/behavior-actions/behavior.action.delete.text.ts +54 -0
  49. package/src/behavior-actions/behavior.actions.ts +5 -43
  50. package/src/behaviors/behavior.markdown-emphasis.ts +392 -0
  51. package/src/behaviors/behavior.markdown.ts +11 -4
  52. package/src/behaviors/behavior.types.ts +1 -0
  53. package/src/editor/plugins/createWithPortableTextMarkModel.ts +2 -97
  54. package/src/internal-utils/get-text-to-emphasize.test.ts +36 -0
  55. package/src/internal-utils/get-text-to-emphasize.ts +18 -0
  56. package/src/plugins/plugin.markdown.tsx +11 -1
  57. package/src/selectors/index.ts +5 -0
  58. package/src/selectors/selector.get-anchor-block.ts +22 -0
  59. package/src/selectors/selector.get-anchor-child.ts +36 -0
  60. package/src/selectors/selector.get-anchor-span.ts +18 -0
  61. package/src/selectors/selector.get-anchor-text-block.ts +20 -0
  62. package/src/selectors/selector.get-trimmed-selection.test.ts +658 -0
  63. package/src/selectors/selector.get-trimmed-selection.ts +175 -0
  64. package/src/utils/index.ts +1 -0
  65. package/src/utils/util.block-offsets-to-selection.ts +36 -0
  66. package/lib/_chunks-cjs/parse-blocks.cjs.map +0 -1
  67. package/lib/_chunks-cjs/util.is-empty-text-block.cjs +0 -14
  68. package/lib/_chunks-cjs/util.is-empty-text-block.cjs.map +0 -1
  69. package/lib/_chunks-es/parse-blocks.js.map +0 -1
  70. package/lib/_chunks-es/util.is-empty-text-block.js +0 -15
  71. package/lib/_chunks-es/util.is-empty-text-block.js.map +0 -1
@@ -0,0 +1,161 @@
1
+ import {Editor, Range, Text, Transforms} from 'slate'
2
+ import {toPortableTextRange, toSlateRange} from '../internal-utils/ranges'
3
+ import {fromSlateValue} from '../internal-utils/values'
4
+ import {KEY_TO_VALUE_ELEMENT} from '../internal-utils/weakMaps'
5
+ import * as selectors from '../selectors'
6
+ import * as utils from '../utils'
7
+ import type {BehaviorActionImplementation} from './behavior.actions'
8
+
9
+ export const decoratorAddActionImplementation: BehaviorActionImplementation<
10
+ 'decorator.add'
11
+ > = ({context, action}) => {
12
+ const editor = action.editor
13
+ const mark = action.decorator
14
+ const selection = action.selection
15
+ ? (toSlateRange(action.selection, action.editor) ?? editor.selection)
16
+ : editor.selection
17
+
18
+ if (!selection) {
19
+ return
20
+ }
21
+
22
+ const value = fromSlateValue(
23
+ editor.children,
24
+ context.schema.block.name,
25
+ KEY_TO_VALUE_ELEMENT.get(editor),
26
+ )
27
+
28
+ const editorSelection = toPortableTextRange(value, selection, context.schema)
29
+ const anchorOffset = editorSelection
30
+ ? utils.spanSelectionPointToBlockOffset({
31
+ value,
32
+ selectionPoint: editorSelection.anchor,
33
+ })
34
+ : undefined
35
+ const focusOffset = editorSelection
36
+ ? utils.spanSelectionPointToBlockOffset({
37
+ value,
38
+ selectionPoint: editorSelection.focus,
39
+ })
40
+ : undefined
41
+
42
+ if (!anchorOffset || !focusOffset) {
43
+ throw new Error('Unable to find anchor or focus offset')
44
+ }
45
+
46
+ if (Range.isExpanded(selection)) {
47
+ // Split if needed
48
+ Transforms.setNodes(
49
+ editor,
50
+ {},
51
+ {at: selection, match: Text.isText, split: true, hanging: true},
52
+ )
53
+
54
+ // The value might have changed after splitting
55
+ const newValue = fromSlateValue(
56
+ editor.children,
57
+ context.schema.block.name,
58
+ KEY_TO_VALUE_ELEMENT.get(editor),
59
+ )
60
+ // We need to find the new selection from the original offsets because the
61
+ // split operation might have changed the value.
62
+ const newSelection = utils.blockOffsetsToSelection({
63
+ value: newValue,
64
+ offsets: {anchor: anchorOffset, focus: focusOffset},
65
+ backward: editorSelection?.backward,
66
+ })
67
+
68
+ const trimmedSelection = selectors.getTrimmedSelection({
69
+ context: {
70
+ activeDecorators: [],
71
+ converters: [],
72
+ keyGenerator: context.keyGenerator,
73
+ schema: context.schema,
74
+ selection: newSelection,
75
+ value: newValue,
76
+ },
77
+ })
78
+
79
+ if (!trimmedSelection) {
80
+ throw new Error('Unable to find trimmed selection')
81
+ }
82
+
83
+ const newRange = toSlateRange(trimmedSelection, editor)
84
+
85
+ if (!newRange) {
86
+ throw new Error('Unable to find new selection')
87
+ }
88
+
89
+ // Use new selection to find nodes to decorate
90
+ const splitTextNodes = Range.isRange(newRange)
91
+ ? [
92
+ ...Editor.nodes(editor, {
93
+ at: newRange,
94
+ match: (node) => Text.isText(node),
95
+ }),
96
+ ]
97
+ : []
98
+
99
+ for (const [node, path] of splitTextNodes) {
100
+ const marks = [
101
+ ...(Array.isArray(node.marks) ? node.marks : []).filter(
102
+ (eMark: string) => eMark !== mark,
103
+ ),
104
+ mark,
105
+ ]
106
+ Transforms.setNodes(
107
+ editor,
108
+ {marks},
109
+ {at: path, match: Text.isText, split: true, hanging: true},
110
+ )
111
+ }
112
+ } else {
113
+ const [block, blockPath] = Editor.node(editor, selection, {
114
+ depth: 1,
115
+ })
116
+ const lonelyEmptySpan =
117
+ editor.isTextBlock(block) &&
118
+ block.children.length === 1 &&
119
+ editor.isTextSpan(block.children[0]) &&
120
+ block.children[0].text === ''
121
+ ? block.children[0]
122
+ : undefined
123
+
124
+ if (lonelyEmptySpan) {
125
+ const existingMarks = lonelyEmptySpan.marks ?? []
126
+ const existingMarksWithoutDecorator = existingMarks.filter(
127
+ (existingMark) => existingMark !== mark,
128
+ )
129
+
130
+ Transforms.setNodes(
131
+ editor,
132
+ {
133
+ marks:
134
+ existingMarks.length === existingMarksWithoutDecorator.length
135
+ ? [...existingMarks, mark]
136
+ : existingMarksWithoutDecorator,
137
+ },
138
+ {
139
+ at: blockPath,
140
+ match: (node) => editor.isTextSpan(node),
141
+ },
142
+ )
143
+ } else {
144
+ const existingMarks: string[] =
145
+ {
146
+ ...(Editor.marks(editor) || {}),
147
+ }.marks || []
148
+ const marks = {
149
+ ...(Editor.marks(editor) || {}),
150
+ marks: [...existingMarks, mark],
151
+ }
152
+ editor.marks = marks as Text
153
+ }
154
+ }
155
+
156
+ if (editor.selection) {
157
+ // Reselect
158
+ const selection = editor.selection
159
+ editor.selection = {...selection}
160
+ }
161
+ }
@@ -0,0 +1,54 @@
1
+ import {Transforms} from 'slate'
2
+ import {toSlateRange} from '../internal-utils/ranges'
3
+ import {fromSlateValue} from '../internal-utils/values'
4
+ import {KEY_TO_VALUE_ELEMENT} from '../internal-utils/weakMaps'
5
+ import * as selectors from '../selectors'
6
+ import * as utils from '../utils'
7
+ import type {BehaviorActionImplementation} from './behavior.actions'
8
+
9
+ export const deleteTextActionImplementation: BehaviorActionImplementation<
10
+ 'delete.text'
11
+ > = ({context, action}) => {
12
+ const value = fromSlateValue(
13
+ action.editor.children,
14
+ context.schema.block.name,
15
+ KEY_TO_VALUE_ELEMENT.get(action.editor),
16
+ )
17
+
18
+ const selection = utils.blockOffsetsToSelection({
19
+ value,
20
+ offsets: {
21
+ anchor: action.anchor,
22
+ focus: action.focus,
23
+ },
24
+ })
25
+
26
+ if (!selection) {
27
+ throw new Error('Unable to find selection from block offsets')
28
+ }
29
+
30
+ const trimmedSelection = selectors.getTrimmedSelection({
31
+ context: {
32
+ converters: [],
33
+ schema: context.schema,
34
+ keyGenerator: context.keyGenerator,
35
+ activeDecorators: [],
36
+ value,
37
+ selection,
38
+ },
39
+ })
40
+
41
+ if (!trimmedSelection) {
42
+ throw new Error('Unable to find trimmed selection')
43
+ }
44
+
45
+ const range = toSlateRange(trimmedSelection, action.editor)
46
+
47
+ if (!range) {
48
+ throw new Error('Unable to find Slate range from trimmed selection')
49
+ }
50
+
51
+ Transforms.delete(action.editor, {
52
+ at: range,
53
+ })
54
+ }
@@ -17,7 +17,6 @@ import {
17
17
  toggleAnnotationActionImplementation,
18
18
  } from '../editor/plugins/createWithEditableAPI'
19
19
  import {
20
- addDecoratorActionImplementation,
21
20
  removeDecoratorActionImplementation,
22
21
  toggleDecoratorActionImplementation,
23
22
  } from '../editor/plugins/createWithPortableTextMarkModel'
@@ -27,15 +26,15 @@ import {
27
26
  } from '../editor/plugins/createWithUndoRedo'
28
27
  import {toSlatePath} from '../internal-utils/paths'
29
28
  import {toSlateRange} from '../internal-utils/ranges'
30
- import {fromSlateValue, toSlateValue} from '../internal-utils/values'
31
- import {KEY_TO_VALUE_ELEMENT} from '../internal-utils/weakMaps'
29
+ import {toSlateValue} from '../internal-utils/values'
32
30
  import type {PickFromUnion} from '../type-utils'
33
- import {blockOffsetToSpanSelectionPoint} from '../utils/util.block-offset'
34
31
  import {insertBlock} from './behavior.action-utils.insert-block'
35
32
  import {blockSetBehaviorActionImplementation} from './behavior.action.block.set'
36
33
  import {blockUnsetBehaviorActionImplementation} from './behavior.action.block.unset'
37
34
  import {dataTransferSetActionImplementation} from './behavior.action.data-transfer-set'
35
+ import {decoratorAddActionImplementation} from './behavior.action.decorator.add'
38
36
  import {deleteActionImplementation} from './behavior.action.delete'
37
+ import {deleteTextActionImplementation} from './behavior.action.delete.text'
39
38
  import {insertBlockObjectActionImplementation} from './behavior.action.insert-block-object'
40
39
  import {insertBlocksActionImplementation} from './behavior.action.insert-blocks'
41
40
  import {
@@ -88,7 +87,7 @@ const behaviorActionImplementations: BehaviorActionImplementations = {
88
87
  ReactEditor.blur(action.editor)
89
88
  },
90
89
  'data transfer.set': dataTransferSetActionImplementation,
91
- 'decorator.add': addDecoratorActionImplementation,
90
+ 'decorator.add': decoratorAddActionImplementation,
92
91
  'decorator.remove': removeDecoratorActionImplementation,
93
92
  'decorator.toggle': toggleDecoratorActionImplementation,
94
93
  'focus': ({action}) => {
@@ -119,44 +118,7 @@ const behaviorActionImplementations: BehaviorActionImplementations = {
119
118
  at: range,
120
119
  })
121
120
  },
122
- 'delete.text': ({context, action}) => {
123
- const value = fromSlateValue(
124
- action.editor.children,
125
- context.schema.block.name,
126
- KEY_TO_VALUE_ELEMENT.get(action.editor),
127
- )
128
-
129
- const anchor = blockOffsetToSpanSelectionPoint({
130
- value,
131
- blockOffset: action.anchor,
132
- })
133
- const focus = blockOffsetToSpanSelectionPoint({
134
- value,
135
- blockOffset: action.focus,
136
- })
137
-
138
- if (!anchor || !focus) {
139
- console.error('Unable to find anchor or focus selection point')
140
- return
141
- }
142
-
143
- const range = toSlateRange(
144
- {
145
- anchor,
146
- focus,
147
- },
148
- action.editor,
149
- )
150
-
151
- if (!range) {
152
- console.error('Unable to find Slate range from selection points')
153
- return
154
- }
155
-
156
- Transforms.delete(action.editor, {
157
- at: range,
158
- })
159
- },
121
+ 'delete.text': deleteTextActionImplementation,
160
122
  'deserialization.failure': ({action}) => {
161
123
  console.error(
162
124
  `Deserialization of ${action.mimeType} failed with reason ${action.reason}`,
@@ -0,0 +1,392 @@
1
+ import {useActorRef} from '@xstate/react'
2
+ import {isEqual} from 'lodash'
3
+ import {
4
+ assign,
5
+ fromCallback,
6
+ setup,
7
+ type AnyEventObject,
8
+ type CallbackLogicFunction,
9
+ } from 'xstate'
10
+ import type {Editor} from '../editor/create-editor'
11
+ import {useEditor} from '../editor/editor-provider'
12
+ import {
13
+ getTextToBold,
14
+ getTextToItalic,
15
+ } from '../internal-utils/get-text-to-emphasize'
16
+ import type {EditorSchema} from '../selectors'
17
+ import * as selectors from '../selectors'
18
+ import * as utils from '../utils'
19
+ import {defineBehavior} from './behavior.types'
20
+
21
+ /**
22
+ * @beta
23
+ */
24
+ export type MarkdownEmphasisBehaviorsConfig = {
25
+ boldDecorator?: ({schema}: {schema: EditorSchema}) => string | undefined
26
+ italicDecorator?: ({schema}: {schema: EditorSchema}) => string | undefined
27
+ }
28
+
29
+ /**
30
+ * @beta
31
+ */
32
+ export function useMarkdownEmphasisBehaviors(props: {
33
+ config: MarkdownEmphasisBehaviorsConfig
34
+ }) {
35
+ const editor = useEditor()
36
+
37
+ useActorRef(emphasisMachine, {
38
+ input: {
39
+ editor,
40
+ boldDecorator: props.config.boldDecorator?.({
41
+ schema: editor.getSnapshot().context.schema,
42
+ }),
43
+ italicDecorator: props.config.italicDecorator?.({
44
+ schema: editor.getSnapshot().context.schema,
45
+ }),
46
+ },
47
+ })
48
+ }
49
+
50
+ type MarkdownEmphasisEvent =
51
+ | {
52
+ type: 'emphasis.add'
53
+ blockOffset: utils.BlockOffset
54
+ }
55
+ | {
56
+ type: 'selection'
57
+ blockOffsets?: {
58
+ anchor: utils.BlockOffset
59
+ focus: utils.BlockOffset
60
+ }
61
+ }
62
+ | {
63
+ type: 'delete.backward'
64
+ }
65
+
66
+ const emphasisListener: CallbackLogicFunction<
67
+ AnyEventObject,
68
+ MarkdownEmphasisEvent,
69
+ {editor: Editor; boldDecorator?: string; italicDecorator?: string}
70
+ > = ({sendBack, input}) => {
71
+ const unregister = input.editor.registerBehavior({
72
+ behavior: defineBehavior({
73
+ on: 'insert.text',
74
+ guard: ({context, event}) => {
75
+ const boldDecorator = input.boldDecorator
76
+ const italicDecorator = input.italicDecorator
77
+
78
+ if (boldDecorator === undefined && italicDecorator === undefined) {
79
+ return false
80
+ }
81
+
82
+ if (event.text !== '*' && event.text !== '_') {
83
+ return false
84
+ }
85
+
86
+ const focusTextBlock = selectors.getFocusTextBlock({context})
87
+ const selectionStartPoint = selectors.getSelectionStartPoint({context})
88
+ const selectionStartOffset = selectionStartPoint
89
+ ? utils.spanSelectionPointToBlockOffset({
90
+ value: context.value,
91
+ selectionPoint: selectionStartPoint,
92
+ })
93
+ : undefined
94
+
95
+ if (!focusTextBlock || !selectionStartOffset) {
96
+ return false
97
+ }
98
+
99
+ const textBefore = selectors.getBlockTextBefore({context})
100
+
101
+ const textToItalic = getTextToItalic(`${textBefore}${event.text}`)
102
+
103
+ if (textToItalic !== undefined && italicDecorator !== undefined) {
104
+ const prefixOffsets = {
105
+ anchor: {
106
+ path: focusTextBlock.path,
107
+ offset: textBefore.length - textToItalic.length + 1,
108
+ },
109
+ focus: {
110
+ path: focusTextBlock.path,
111
+ offset: textBefore.length - textToItalic.length + 1 + 1,
112
+ },
113
+ }
114
+ const suffixOffsets = {
115
+ anchor: {
116
+ path: focusTextBlock.path,
117
+ offset: selectionStartOffset.offset,
118
+ },
119
+ focus: {
120
+ path: focusTextBlock.path,
121
+ offset: selectionStartOffset.offset + 1,
122
+ },
123
+ }
124
+
125
+ const anchor = utils.blockOffsetToSpanSelectionPoint({
126
+ value: context.value,
127
+ blockOffset: prefixOffsets.focus,
128
+ })
129
+ const focus = utils.blockOffsetToSpanSelectionPoint({
130
+ value: context.value,
131
+ blockOffset: suffixOffsets.anchor,
132
+ })
133
+
134
+ if (!anchor || !focus) {
135
+ return false
136
+ }
137
+
138
+ return {
139
+ prefixOffsets,
140
+ suffixOffsets,
141
+ decorator: italicDecorator,
142
+ selection: {anchor, focus},
143
+ }
144
+ }
145
+
146
+ const textToBold = getTextToBold(`${textBefore}${event.text}`)
147
+
148
+ if (textToBold !== undefined && boldDecorator !== undefined) {
149
+ const prefixOffsets = {
150
+ anchor: {
151
+ path: focusTextBlock.path,
152
+ offset: textBefore.length - textToBold.length + 1,
153
+ },
154
+ focus: {
155
+ path: focusTextBlock.path,
156
+ offset: textBefore.length - textToBold.length + 1 + 2,
157
+ },
158
+ }
159
+ const suffixOffsets = {
160
+ anchor: {
161
+ path: focusTextBlock.path,
162
+ offset: selectionStartOffset.offset - 1,
163
+ },
164
+ focus: {
165
+ path: focusTextBlock.path,
166
+ offset: selectionStartOffset.offset + 1,
167
+ },
168
+ }
169
+ const anchor = utils.blockOffsetToSpanSelectionPoint({
170
+ value: context.value,
171
+ blockOffset: prefixOffsets.focus,
172
+ })
173
+ const focus = utils.blockOffsetToSpanSelectionPoint({
174
+ value: context.value,
175
+ blockOffset: suffixOffsets.anchor,
176
+ })
177
+
178
+ if (!anchor || !focus) {
179
+ return false
180
+ }
181
+
182
+ return {
183
+ prefixOffsets,
184
+ suffixOffsets,
185
+ decorator: boldDecorator,
186
+ selection: {anchor, focus},
187
+ }
188
+ }
189
+
190
+ return false
191
+ },
192
+ actions: [
193
+ ({event}) => [event],
194
+ (_, {prefixOffsets, suffixOffsets, decorator, selection}) => [
195
+ {
196
+ type: 'decorator.add',
197
+ decorator,
198
+ selection,
199
+ },
200
+ {
201
+ type: 'delete.text',
202
+ ...suffixOffsets,
203
+ },
204
+ {
205
+ type: 'delete.text',
206
+ ...prefixOffsets,
207
+ },
208
+ {
209
+ type: 'effect',
210
+ effect: () => {
211
+ sendBack({
212
+ type: 'emphasis.add',
213
+ blockOffset: {
214
+ ...suffixOffsets.anchor,
215
+ offset:
216
+ suffixOffsets.anchor.offset -
217
+ (prefixOffsets.focus.offset - prefixOffsets.anchor.offset),
218
+ },
219
+ })
220
+ },
221
+ },
222
+ ],
223
+ ],
224
+ }),
225
+ })
226
+
227
+ return unregister
228
+ }
229
+
230
+ const selectionListenerCallback: CallbackLogicFunction<
231
+ AnyEventObject,
232
+ MarkdownEmphasisEvent,
233
+ {editor: Editor}
234
+ > = ({sendBack, input}) => {
235
+ const unregister = input.editor.registerBehavior({
236
+ behavior: defineBehavior({
237
+ on: 'select',
238
+ guard: ({context, event}) => {
239
+ if (!event.selection) {
240
+ return {blockOffsets: undefined}
241
+ }
242
+
243
+ const anchor = utils.spanSelectionPointToBlockOffset({
244
+ value: context.value,
245
+ selectionPoint: event.selection.anchor,
246
+ })
247
+ const focus = utils.spanSelectionPointToBlockOffset({
248
+ value: context.value,
249
+ selectionPoint: event.selection.focus,
250
+ })
251
+
252
+ if (!anchor || !focus) {
253
+ return {blockOffsets: undefined}
254
+ }
255
+
256
+ return {
257
+ blockOffsets: {
258
+ anchor,
259
+ focus,
260
+ },
261
+ }
262
+ },
263
+ actions: [
264
+ (_, {blockOffsets}) => [
265
+ {
266
+ type: 'effect',
267
+ effect: () => {
268
+ sendBack({type: 'selection', blockOffsets})
269
+ },
270
+ },
271
+ ],
272
+ ],
273
+ }),
274
+ })
275
+
276
+ return unregister
277
+ }
278
+
279
+ const deleteBackwardListenerCallback: CallbackLogicFunction<
280
+ AnyEventObject,
281
+ MarkdownEmphasisEvent,
282
+ {editor: Editor}
283
+ > = ({sendBack, input}) => {
284
+ const unregister = input.editor.registerBehavior({
285
+ behavior: defineBehavior({
286
+ on: 'delete.backward',
287
+ actions: [
288
+ () => [
289
+ {
290
+ type: 'history.undo',
291
+ },
292
+ {
293
+ type: 'effect',
294
+ effect: () => {
295
+ sendBack({type: 'delete.backward'})
296
+ },
297
+ },
298
+ ],
299
+ ],
300
+ }),
301
+ })
302
+
303
+ return unregister
304
+ }
305
+
306
+ const emphasisMachine = setup({
307
+ types: {
308
+ context: {} as {
309
+ boldDecorator?: string
310
+ italicDecorator?: string
311
+ offsetAfterEmphasis?: utils.BlockOffset
312
+ editor: Editor
313
+ },
314
+ input: {} as {
315
+ boldDecorator?: string
316
+ italicDecorator?: string
317
+ editor: Editor
318
+ },
319
+ events: {} as MarkdownEmphasisEvent,
320
+ },
321
+ actors: {
322
+ 'emphasis listener': fromCallback(emphasisListener),
323
+ 'delete.backward listener': fromCallback(deleteBackwardListenerCallback),
324
+ 'selection listener': fromCallback(selectionListenerCallback),
325
+ },
326
+ }).createMachine({
327
+ id: 'emphasis',
328
+ context: ({input}) => ({
329
+ boldDecorator: input.boldDecorator,
330
+ italicDecorator: input.italicDecorator,
331
+ editor: input.editor,
332
+ }),
333
+ initial: 'idle',
334
+ states: {
335
+ 'idle': {
336
+ invoke: [
337
+ {
338
+ src: 'emphasis listener',
339
+ input: ({context}) => ({
340
+ editor: context.editor,
341
+ boldDecorator: context.boldDecorator,
342
+ italicDecorator: context.italicDecorator,
343
+ }),
344
+ },
345
+ ],
346
+ on: {
347
+ 'emphasis.add': {
348
+ target: 'emphasis added',
349
+ actions: assign({
350
+ offsetAfterEmphasis: ({event}) => event.blockOffset,
351
+ }),
352
+ },
353
+ },
354
+ },
355
+ 'emphasis added': {
356
+ exit: [
357
+ assign({
358
+ offsetAfterEmphasis: undefined,
359
+ }),
360
+ ],
361
+ invoke: [
362
+ {
363
+ src: 'selection listener',
364
+ input: ({context}) => ({editor: context.editor}),
365
+ },
366
+ {
367
+ src: 'delete.backward listener',
368
+ input: ({context}) => ({editor: context.editor}),
369
+ },
370
+ ],
371
+ on: {
372
+ 'selection': {
373
+ target: 'idle',
374
+ guard: ({context, event}) => {
375
+ const selectionChanged = !isEqual(
376
+ {
377
+ anchor: context.offsetAfterEmphasis,
378
+ focus: context.offsetAfterEmphasis,
379
+ },
380
+ event.blockOffsets,
381
+ )
382
+
383
+ return selectionChanged
384
+ },
385
+ },
386
+ 'delete.backward': {
387
+ target: 'idle',
388
+ },
389
+ },
390
+ },
391
+ },
392
+ })