@portabletext/editor 3.0.8 → 3.1.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.
@@ -1,478 +0,0 @@
1
- import {isTextBlock} from '@portabletext/schema'
2
- import type {EditorSchema} from '../editor/editor-schema'
3
- import {getFocusBlock} from '../selectors/selector.get-focus-block'
4
- import {getFocusSpan} from '../selectors/selector.get-focus-span'
5
- import {getFocusTextBlock} from '../selectors/selector.get-focus-text-block'
6
- import {getPreviousInlineObject} from '../selectors/selector.get-previous-inline-object'
7
- import {getBlockTextBefore} from '../selectors/selector.get-text-before'
8
- import {isSelectionCollapsed} from '../selectors/selector.is-selection-collapsed'
9
- import {spanSelectionPointToBlockOffset} from '../utils/util.block-offset'
10
- import {getTextBlockText} from '../utils/util.get-text-block-text'
11
- import {execute} from './behavior.types.action'
12
- import {defineBehavior} from './behavior.types.behavior'
13
-
14
- export type MarkdownBehaviorsConfig = {
15
- horizontalRuleObject?: (context: {
16
- schema: EditorSchema
17
- }) => {name: string; value?: {[prop: string]: unknown}} | undefined
18
- defaultStyle?: (context: {schema: EditorSchema}) => string | undefined
19
- headingStyle?: (context: {
20
- schema: EditorSchema
21
- level: number
22
- }) => string | undefined
23
- blockquoteStyle?: (context: {schema: EditorSchema}) => string | undefined
24
- unorderedListStyle?: (context: {schema: EditorSchema}) => string | undefined
25
- orderedListStyle?: (context: {schema: EditorSchema}) => string | undefined
26
- }
27
-
28
- export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
29
- const automaticBlockquoteOnSpace = defineBehavior({
30
- on: 'insert.text',
31
- guard: ({snapshot, event}) => {
32
- const isSpace = event.text === ' '
33
-
34
- if (!isSpace) {
35
- return false
36
- }
37
-
38
- const selectionCollapsed = isSelectionCollapsed(snapshot)
39
- const focusTextBlock = getFocusTextBlock(snapshot)
40
- const focusSpan = getFocusSpan(snapshot)
41
-
42
- if (!selectionCollapsed || !focusTextBlock || !focusSpan) {
43
- return false
44
- }
45
-
46
- const previousInlineObject = getPreviousInlineObject(snapshot)
47
- const blockOffset = spanSelectionPointToBlockOffset({
48
- context: snapshot.context,
49
- selectionPoint: {
50
- path: [
51
- {_key: focusTextBlock.node._key},
52
- 'children',
53
- {_key: focusSpan.node._key},
54
- ],
55
- offset: snapshot.context.selection?.focus.offset ?? 0,
56
- },
57
- })
58
-
59
- if (previousInlineObject || !blockOffset) {
60
- return false
61
- }
62
-
63
- const blockText = getTextBlockText(focusTextBlock.node)
64
- const caretAtTheEndOfQuote = blockOffset.offset === 1
65
- const looksLikeMarkdownQuote = /^>/.test(blockText)
66
- const blockquoteStyle = config.blockquoteStyle?.(snapshot.context)
67
-
68
- if (
69
- caretAtTheEndOfQuote &&
70
- looksLikeMarkdownQuote &&
71
- blockquoteStyle !== undefined
72
- ) {
73
- return {focusTextBlock, style: blockquoteStyle}
74
- }
75
-
76
- return false
77
- },
78
- actions: [
79
- () => [
80
- execute({
81
- type: 'insert.text',
82
- text: ' ',
83
- }),
84
- ],
85
- (_, {focusTextBlock, style}) => [
86
- execute({
87
- type: 'block.unset',
88
- props: ['listItem', 'level'],
89
- at: focusTextBlock.path,
90
- }),
91
- execute({
92
- type: 'block.set',
93
- props: {style},
94
- at: focusTextBlock.path,
95
- }),
96
- execute({
97
- type: 'delete.text',
98
- at: {
99
- anchor: {
100
- path: focusTextBlock.path,
101
- offset: 0,
102
- },
103
- focus: {
104
- path: focusTextBlock.path,
105
- offset: 2,
106
- },
107
- },
108
- }),
109
- ],
110
- ],
111
- })
112
- const automaticHr = defineBehavior({
113
- on: 'insert.text',
114
- guard: ({snapshot, event}) => {
115
- const hrCharacter =
116
- event.text === '-'
117
- ? '-'
118
- : event.text === '*'
119
- ? '*'
120
- : event.text === '_'
121
- ? '_'
122
- : undefined
123
-
124
- if (hrCharacter === undefined) {
125
- return false
126
- }
127
-
128
- const hrObject = config.horizontalRuleObject?.(snapshot.context)
129
- const focusBlock = getFocusTextBlock(snapshot)
130
- const selectionCollapsed = isSelectionCollapsed(snapshot)
131
-
132
- if (!hrObject || !focusBlock || !selectionCollapsed) {
133
- return false
134
- }
135
-
136
- const previousInlineObject = getPreviousInlineObject(snapshot)
137
- const textBefore = getBlockTextBefore(snapshot)
138
- const hrBlockOffsets = {
139
- anchor: {
140
- path: focusBlock.path,
141
- offset: 0,
142
- },
143
- focus: {
144
- path: focusBlock.path,
145
- offset: 3,
146
- },
147
- }
148
-
149
- if (
150
- !previousInlineObject &&
151
- textBefore === `${hrCharacter}${hrCharacter}`
152
- ) {
153
- return {hrObject, focusBlock, hrCharacter, hrBlockOffsets}
154
- }
155
-
156
- return false
157
- },
158
- actions: [
159
- (_, {hrCharacter}) => [
160
- execute({
161
- type: 'insert.text',
162
- text: hrCharacter,
163
- }),
164
- ],
165
- (_, {hrObject, hrBlockOffsets}) => [
166
- execute({
167
- type: 'insert.block',
168
- placement: 'before',
169
- block: {
170
- _type: hrObject.name,
171
- ...(hrObject.value ?? {}),
172
- },
173
- }),
174
- execute({
175
- type: 'delete.text',
176
- at: hrBlockOffsets,
177
- }),
178
- ],
179
- ],
180
- })
181
- const automaticHrOnPaste = defineBehavior({
182
- on: 'clipboard.paste',
183
- guard: ({snapshot, event}) => {
184
- const text = event.originEvent.dataTransfer.getData('text/plain')
185
- const hrRegExp = /^(---)$|(___)$|(\*\*\*)$/
186
- const hrCharacters = text.match(hrRegExp)?.[0]
187
- const hrObject = config.horizontalRuleObject?.(snapshot.context)
188
- const focusBlock = getFocusBlock(snapshot)
189
-
190
- if (!hrCharacters || !hrObject || !focusBlock) {
191
- return false
192
- }
193
-
194
- return {hrCharacters, hrObject, focusBlock}
195
- },
196
- actions: [
197
- (_, {hrCharacters}) => [
198
- execute({
199
- type: 'insert.text',
200
- text: hrCharacters,
201
- }),
202
- ],
203
- ({snapshot}, {hrObject, focusBlock}) =>
204
- isTextBlock(snapshot.context, focusBlock.node)
205
- ? [
206
- execute({
207
- type: 'insert.block',
208
- block: {
209
- _type: snapshot.context.schema.block.name,
210
- children: focusBlock.node.children,
211
- },
212
- placement: 'after',
213
- }),
214
- execute({
215
- type: 'insert.block',
216
- block: {
217
- _type: hrObject.name,
218
- ...(hrObject.value ?? {}),
219
- },
220
- placement: 'after',
221
- }),
222
- execute({
223
- type: 'delete.block',
224
- at: focusBlock.path,
225
- }),
226
- ]
227
- : [
228
- execute({
229
- type: 'insert.block',
230
- block: {
231
- _type: hrObject.name,
232
- ...(hrObject.value ?? {}),
233
- },
234
- placement: 'after',
235
- }),
236
- ],
237
- ],
238
- })
239
- const automaticHeadingOnSpace = defineBehavior({
240
- on: 'insert.text',
241
- guard: ({snapshot, event}) => {
242
- const isSpace = event.text === ' '
243
-
244
- if (!isSpace) {
245
- return false
246
- }
247
-
248
- const selectionCollapsed = isSelectionCollapsed(snapshot)
249
- const focusTextBlock = getFocusTextBlock(snapshot)
250
- const focusSpan = getFocusSpan(snapshot)
251
-
252
- if (!selectionCollapsed || !focusTextBlock || !focusSpan) {
253
- return false
254
- }
255
-
256
- const blockOffset = spanSelectionPointToBlockOffset({
257
- context: snapshot.context,
258
- selectionPoint: {
259
- path: [
260
- {_key: focusTextBlock.node._key},
261
- 'children',
262
- {_key: focusSpan.node._key},
263
- ],
264
- offset: snapshot.context.selection?.focus.offset ?? 0,
265
- },
266
- })
267
-
268
- if (!blockOffset) {
269
- return false
270
- }
271
-
272
- const previousInlineObject = getPreviousInlineObject(snapshot)
273
- const blockText = getTextBlockText(focusTextBlock.node)
274
- const markdownHeadingSearch = /^#+/.exec(blockText)
275
- const level = markdownHeadingSearch
276
- ? markdownHeadingSearch[0].length
277
- : undefined
278
- const caretAtTheEndOfHeading = blockOffset.offset === level
279
-
280
- if (previousInlineObject || !caretAtTheEndOfHeading) {
281
- return false
282
- }
283
-
284
- const style =
285
- level !== undefined
286
- ? config.headingStyle?.({schema: snapshot.context.schema, level})
287
- : undefined
288
-
289
- if (level !== undefined && style !== undefined) {
290
- return {
291
- focusTextBlock,
292
- style: style,
293
- level,
294
- }
295
- }
296
-
297
- return false
298
- },
299
- actions: [
300
- ({event}) => [execute(event)],
301
- (_, {focusTextBlock, style, level}) => [
302
- execute({
303
- type: 'block.unset',
304
- props: ['listItem', 'level'],
305
- at: focusTextBlock.path,
306
- }),
307
- execute({
308
- type: 'block.set',
309
- props: {style},
310
- at: focusTextBlock.path,
311
- }),
312
- execute({
313
- type: 'delete.text',
314
- at: {
315
- anchor: {
316
- path: focusTextBlock.path,
317
- offset: 0,
318
- },
319
- focus: {
320
- path: focusTextBlock.path,
321
- offset: level + 1,
322
- },
323
- },
324
- }),
325
- ],
326
- ],
327
- })
328
- const clearStyleOnBackspace = defineBehavior({
329
- on: 'delete.backward',
330
- guard: ({snapshot}) => {
331
- const selectionCollapsed = isSelectionCollapsed(snapshot)
332
- const focusTextBlock = getFocusTextBlock(snapshot)
333
- const focusSpan = getFocusSpan(snapshot)
334
-
335
- if (!selectionCollapsed || !focusTextBlock || !focusSpan) {
336
- return false
337
- }
338
-
339
- const atTheBeginningOfBLock =
340
- focusTextBlock.node.children[0]._key === focusSpan.node._key &&
341
- snapshot.context.selection?.focus.offset === 0
342
-
343
- const defaultStyle = config.defaultStyle?.(snapshot.context)
344
-
345
- if (
346
- atTheBeginningOfBLock &&
347
- defaultStyle &&
348
- focusTextBlock.node.style !== defaultStyle
349
- ) {
350
- return {defaultStyle, focusTextBlock}
351
- }
352
-
353
- return false
354
- },
355
- actions: [
356
- (_, {defaultStyle, focusTextBlock}) => [
357
- execute({
358
- type: 'block.set',
359
- props: {style: defaultStyle},
360
- at: focusTextBlock.path,
361
- }),
362
- ],
363
- ],
364
- })
365
- const automaticListOnSpace = defineBehavior({
366
- on: 'insert.text',
367
- guard: ({snapshot, event}) => {
368
- const isSpace = event.text === ' '
369
-
370
- if (!isSpace) {
371
- return false
372
- }
373
-
374
- const selectionCollapsed = isSelectionCollapsed(snapshot)
375
- const focusTextBlock = getFocusTextBlock(snapshot)
376
- const focusSpan = getFocusSpan(snapshot)
377
-
378
- if (!selectionCollapsed || !focusTextBlock || !focusSpan) {
379
- return false
380
- }
381
-
382
- const previousInlineObject = getPreviousInlineObject(snapshot)
383
- const blockOffset = spanSelectionPointToBlockOffset({
384
- context: snapshot.context,
385
- selectionPoint: {
386
- path: [
387
- {_key: focusTextBlock.node._key},
388
- 'children',
389
- {_key: focusSpan.node._key},
390
- ],
391
- offset: snapshot.context.selection?.focus.offset ?? 0,
392
- },
393
- })
394
-
395
- if (previousInlineObject || !blockOffset) {
396
- return false
397
- }
398
-
399
- const blockText = getTextBlockText(focusTextBlock.node)
400
- const defaultStyle = config.defaultStyle?.(snapshot.context)
401
- const looksLikeUnorderedList = /^(-|\*)/.test(blockText)
402
- const unorderedListStyle = config.unorderedListStyle?.(snapshot.context)
403
- const caretAtTheEndOfUnorderedList = blockOffset.offset === 1
404
-
405
- if (
406
- defaultStyle &&
407
- caretAtTheEndOfUnorderedList &&
408
- looksLikeUnorderedList &&
409
- unorderedListStyle !== undefined
410
- ) {
411
- return {
412
- focusTextBlock,
413
- listItem: unorderedListStyle,
414
- listItemLength: 1,
415
- style: defaultStyle,
416
- }
417
- }
418
-
419
- const looksLikeOrderedList = /^1\./.test(blockText)
420
- const orderedListStyle = config.orderedListStyle?.(snapshot.context)
421
- const caretAtTheEndOfOrderedList = blockOffset.offset === 2
422
-
423
- if (
424
- defaultStyle &&
425
- caretAtTheEndOfOrderedList &&
426
- looksLikeOrderedList &&
427
- orderedListStyle !== undefined
428
- ) {
429
- return {
430
- focusTextBlock,
431
- listItem: orderedListStyle,
432
- listItemLength: 2,
433
- style: defaultStyle,
434
- }
435
- }
436
-
437
- return false
438
- },
439
- actions: [
440
- ({event}) => [execute(event)],
441
- (_, {focusTextBlock, style, listItem, listItemLength}) => [
442
- execute({
443
- type: 'block.set',
444
- props: {
445
- listItem,
446
- level: 1,
447
- style,
448
- },
449
- at: focusTextBlock.path,
450
- }),
451
- execute({
452
- type: 'delete.text',
453
- at: {
454
- anchor: {
455
- path: focusTextBlock.path,
456
- offset: 0,
457
- },
458
- focus: {
459
- path: focusTextBlock.path,
460
- offset: listItemLength + 1,
461
- },
462
- },
463
- }),
464
- ],
465
- ],
466
- })
467
-
468
- const markdownBehaviors = [
469
- automaticBlockquoteOnSpace,
470
- automaticHeadingOnSpace,
471
- automaticHr,
472
- automaticHrOnPaste,
473
- clearStyleOnBackspace,
474
- automaticListOnSpace,
475
- ]
476
-
477
- return markdownBehaviors
478
- }
@@ -1,52 +0,0 @@
1
- import {toSlateBlock} from '../../internal-utils/values'
2
- import type {PortableTextSlateEditor} from '../../types/editor'
3
- import type {EditorActor} from '../editor-machine'
4
-
5
- interface Options {
6
- editorActor: EditorActor
7
- }
8
-
9
- /**
10
- * This plugin makes various util commands available in the editor
11
- *
12
- */
13
- export function createWithUtils({editorActor}: Options) {
14
- return function withUtils(
15
- editor: PortableTextSlateEditor,
16
- ): PortableTextSlateEditor {
17
- editor.pteCreateTextBlock = (options: {
18
- decorators: Array<string>
19
- listItem?: string
20
- level?: number
21
- }) => {
22
- return toSlateBlock(
23
- {
24
- _type: editorActor.getSnapshot().context.schema.block.name,
25
- _key: editorActor.getSnapshot().context.keyGenerator(),
26
- style:
27
- editorActor.getSnapshot().context.schema.styles[0].name || 'normal',
28
- ...(options.listItem ? {listItem: options.listItem} : {}),
29
- ...(options.level ? {level: options.level} : {}),
30
- markDefs: [],
31
- children: [
32
- {
33
- _type: 'span',
34
- _key: editorActor.getSnapshot().context.keyGenerator(),
35
- text: '',
36
- marks: options.decorators.filter((decorator) =>
37
- editorActor
38
- .getSnapshot()
39
- .context.schema.decorators.find(
40
- ({name}) => name === decorator,
41
- ),
42
- ),
43
- },
44
- ],
45
- },
46
-
47
- {schemaTypes: editorActor.getSnapshot().context.schema},
48
- )
49
- }
50
- return editor
51
- }
52
- }
@@ -1,60 +0,0 @@
1
- import {expect, test} from 'vitest'
2
- import {getTextToBold, getTextToItalic} from './get-text-to-emphasize'
3
-
4
- test(getTextToItalic.name, () => {
5
- expect(getTextToItalic('Hello *world*')).toBe('*world*')
6
- expect(getTextToItalic('Hello _world_')).toBe('_world_')
7
- expect(getTextToItalic('*Hello*world*')).toBe('*world*')
8
- expect(getTextToItalic('_Hello_world_')).toBe('_world_')
9
-
10
- expect(getTextToItalic('* Hello world *')).toBe(undefined)
11
- expect(getTextToItalic('* Hello world*')).toBe(undefined)
12
- expect(getTextToItalic('*Hello world *')).toBe(undefined)
13
- expect(getTextToItalic('_ Hello world _')).toBe(undefined)
14
- expect(getTextToItalic('_ Hello world_')).toBe(undefined)
15
- expect(getTextToItalic('_Hello world _')).toBe(undefined)
16
-
17
- expect(getTextToItalic('Hello *world')).toBe(undefined)
18
- expect(getTextToItalic('Hello world*')).toBe(undefined)
19
- expect(getTextToItalic('Hello *world* *')).toBe(undefined)
20
-
21
- expect(getTextToItalic('_Hello*world_')).toBe('_Hello*world_')
22
- expect(getTextToItalic('*Hello_world*')).toBe('*Hello_world*')
23
-
24
- expect(getTextToItalic('*hello\nworld*')).toBe(undefined)
25
- expect(getTextToItalic('_hello\nworld_')).toBe(undefined)
26
-
27
- expect(getTextToItalic('*')).toBe(undefined)
28
- expect(getTextToItalic('_')).toBe(undefined)
29
- expect(getTextToItalic('**')).toBe(undefined)
30
- expect(getTextToItalic('__')).toBe(undefined)
31
- })
32
-
33
- test(getTextToBold.name, () => {
34
- expect(getTextToBold('Hello **world**')).toBe('**world**')
35
- expect(getTextToBold('Hello __world__')).toBe('__world__')
36
- expect(getTextToBold('**Hello**world**')).toBe('**world**')
37
- expect(getTextToBold('__Hello__world__')).toBe('__world__')
38
-
39
- expect(getTextToBold('** Hello world **')).toBe(undefined)
40
- expect(getTextToBold('** Hello world**')).toBe(undefined)
41
- expect(getTextToBold('**Hello world **')).toBe(undefined)
42
- expect(getTextToBold('__ Hello world __')).toBe(undefined)
43
- expect(getTextToBold('__ Hello world__')).toBe(undefined)
44
- expect(getTextToBold('__Hello world __')).toBe(undefined)
45
-
46
- expect(getTextToBold('Hello **world')).toBe(undefined)
47
- expect(getTextToBold('Hello world**')).toBe(undefined)
48
- expect(getTextToBold('Hello **world** **')).toBe(undefined)
49
-
50
- expect(getTextToBold('__Hello**world__')).toBe('__Hello**world__')
51
- expect(getTextToBold('**Hello__world**')).toBe('**Hello__world**')
52
-
53
- expect(getTextToBold('**hello\nworld**')).toBe(undefined)
54
- expect(getTextToBold('__hello\nworld__')).toBe(undefined)
55
-
56
- expect(getTextToBold('**')).toBe(undefined)
57
- expect(getTextToBold('__')).toBe(undefined)
58
- expect(getTextToBold('****')).toBe(undefined)
59
- expect(getTextToBold('____')).toBe(undefined)
60
- })
@@ -1,40 +0,0 @@
1
- export function createPairRegex(char: string, amount: number) {
2
- // Negative lookbehind: Ensures that the matched sequence is not preceded by the same character
3
- const prePrefix = `(?<!\\${char})`
4
-
5
- // Repeats the character `amount` times
6
- const prefix = `\\${char}`.repeat(Math.max(amount, 1))
7
-
8
- // Negative lookahead: Ensures that the opening pair (**, *, etc.) is not followed by a space
9
- const postPrefix = `(?!\\s)`
10
-
11
- // Captures the content inside the pair
12
- const content = `([^${char}\\n]+?)`
13
-
14
- // Negative lookbehind: Ensures that the content is not followed by a space
15
- const preSuffix = `(?<!\\s)`
16
-
17
- // Repeats the character `amount` times
18
- const suffix = `\\${char}`.repeat(Math.max(amount, 1))
19
-
20
- // Negative lookahead: Ensures that the matched sequence is not followed by the same character
21
- const postSuffix = `(?!\\${char})`
22
-
23
- return `${prePrefix}${prefix}${postPrefix}${content}${preSuffix}${suffix}${postSuffix}`
24
- }
25
-
26
- const italicRegex = new RegExp(
27
- `(${createPairRegex('*', 1)}|${createPairRegex('_', 1)})$`,
28
- )
29
-
30
- const boldRegex = new RegExp(
31
- `(${createPairRegex('*', 2)}|${createPairRegex('_', 2)})$`,
32
- )
33
-
34
- export function getTextToItalic(text: string) {
35
- return text.match(italicRegex)?.at(0)
36
- }
37
-
38
- export function getTextToBold(text: string) {
39
- return text.match(boldRegex)?.at(0)
40
- }