@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
@@ -86,6 +86,7 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
86
86
  return false
87
87
  }
88
88
 
89
+ const previousInlineObject = selectors.getPreviousInlineObject({context})
89
90
  const blockOffset = spanSelectionPointToBlockOffset({
90
91
  value: context.value,
91
92
  selectionPoint: {
@@ -98,7 +99,7 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
98
99
  },
99
100
  })
100
101
 
101
- if (!blockOffset) {
102
+ if (previousInlineObject || !blockOffset) {
102
103
  return false
103
104
  }
104
105
 
@@ -173,6 +174,7 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
173
174
  return false
174
175
  }
175
176
 
177
+ const previousInlineObject = selectors.getPreviousInlineObject({context})
176
178
  const textBefore = getBlockTextBefore({context})
177
179
  const hrBlockOffsets = {
178
180
  anchor: {
@@ -185,7 +187,10 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
185
187
  },
186
188
  }
187
189
 
188
- if (textBefore === `${hrCharacter}${hrCharacter}`) {
190
+ if (
191
+ !previousInlineObject &&
192
+ textBefore === `${hrCharacter}${hrCharacter}`
193
+ ) {
189
194
  return {hrObject, focusBlock, hrCharacter, hrBlockOffsets}
190
195
  }
191
196
 
@@ -290,6 +295,7 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
290
295
  return false
291
296
  }
292
297
 
298
+ const previousInlineObject = selectors.getPreviousInlineObject({context})
293
299
  const blockText = getTextBlockText(focusTextBlock.node)
294
300
  const markdownHeadingSearch = /^#+/.exec(blockText)
295
301
  const level = markdownHeadingSearch
@@ -297,7 +303,7 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
297
303
  : undefined
298
304
  const caretAtTheEndOfHeading = blockOffset.offset === level
299
305
 
300
- if (!caretAtTheEndOfHeading) {
306
+ if (previousInlineObject || !caretAtTheEndOfHeading) {
301
307
  return false
302
308
  }
303
309
 
@@ -397,6 +403,7 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
397
403
  return false
398
404
  }
399
405
 
406
+ const previousInlineObject = selectors.getPreviousInlineObject({context})
400
407
  const blockOffset = spanSelectionPointToBlockOffset({
401
408
  value: context.value,
402
409
  selectionPoint: {
@@ -409,7 +416,7 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
409
416
  },
410
417
  })
411
418
 
412
- if (!blockOffset) {
419
+ if (previousInlineObject || !blockOffset) {
413
420
  return false
414
421
  }
415
422
 
@@ -57,6 +57,7 @@ export type SyntheticBehaviorEvent =
57
57
  | {
58
58
  type: 'decorator.add'
59
59
  decorator: string
60
+ selection?: NonNullable<EditorSelection>
60
61
  }
61
62
  | {
62
63
  type: 'decorator.remove'
@@ -8,6 +8,7 @@ import {isPortableTextBlock, isPortableTextSpan} from '@portabletext/toolkit'
8
8
  import type {PortableTextObject, PortableTextSpan} from '@sanity/types'
9
9
  import {isEqual, uniq} from 'lodash'
10
10
  import {Editor, Element, Node, Path, Range, Text, Transforms} from 'slate'
11
+ import {decoratorAddActionImplementation} from '../../behavior-actions/behavior.action.decorator.add'
11
12
  import type {BehaviorActionImplementation} from '../../behavior-actions/behavior.actions'
12
13
  import {debugWithName} from '../../internal-utils/debug'
13
14
  import {getNextSpan, getPreviousSpan} from '../../internal-utils/sibling-utils'
@@ -657,102 +658,6 @@ export function createWithPortableTextMarkModel(
657
658
  }
658
659
  }
659
660
 
660
- export const addDecoratorActionImplementation: BehaviorActionImplementation<
661
- 'decorator.add'
662
- > = ({action}) => {
663
- const editor = action.editor
664
- const mark = action.decorator
665
-
666
- if (editor.selection) {
667
- if (Range.isExpanded(editor.selection)) {
668
- // Split if needed
669
- Transforms.setNodes(
670
- editor,
671
- {},
672
- {match: Text.isText, split: true, hanging: true},
673
- )
674
- // Use new selection
675
- const splitTextNodes = Range.isRange(editor.selection)
676
- ? [
677
- ...Editor.nodes(editor, {
678
- at: editor.selection,
679
- match: Text.isText,
680
- }),
681
- ]
682
- : []
683
- const shouldRemoveMark =
684
- splitTextNodes.length > 1 &&
685
- splitTextNodes.every((node) => node[0].marks?.includes(mark))
686
-
687
- if (shouldRemoveMark) {
688
- editor.removeMark(mark)
689
- } else {
690
- splitTextNodes.forEach(([node, path]) => {
691
- const marks = [
692
- ...(Array.isArray(node.marks) ? node.marks : []).filter(
693
- (eMark: string) => eMark !== mark,
694
- ),
695
- mark,
696
- ]
697
- Transforms.setNodes(
698
- editor,
699
- {marks},
700
- {at: path, match: Text.isText, split: true, hanging: true},
701
- )
702
- })
703
- }
704
- } else {
705
- const [block, blockPath] = Editor.node(editor, editor.selection, {
706
- depth: 1,
707
- })
708
- const lonelyEmptySpan =
709
- editor.isTextBlock(block) &&
710
- block.children.length === 1 &&
711
- editor.isTextSpan(block.children[0]) &&
712
- block.children[0].text === ''
713
- ? block.children[0]
714
- : undefined
715
-
716
- if (lonelyEmptySpan) {
717
- const existingMarks = lonelyEmptySpan.marks ?? []
718
- const existingMarksWithoutDecorator = existingMarks.filter(
719
- (existingMark) => existingMark !== mark,
720
- )
721
-
722
- Transforms.setNodes(
723
- editor,
724
- {
725
- marks:
726
- existingMarks.length === existingMarksWithoutDecorator.length
727
- ? [...existingMarks, mark]
728
- : existingMarksWithoutDecorator,
729
- },
730
- {
731
- at: blockPath,
732
- match: (node) => editor.isTextSpan(node),
733
- },
734
- )
735
- } else {
736
- const existingMarks: string[] =
737
- {
738
- ...(Editor.marks(editor) || {}),
739
- }.marks || []
740
- const marks = {
741
- ...(Editor.marks(editor) || {}),
742
- marks: [...existingMarks, mark],
743
- }
744
- editor.marks = marks as Text
745
- }
746
- }
747
-
748
- if (editor.selection) {
749
- // Reselect
750
- const selection = editor.selection
751
- editor.selection = {...selection}
752
- }
753
- }
754
- }
755
-
756
661
  export const removeDecoratorActionImplementation: BehaviorActionImplementation<
757
662
  'decorator.remove'
758
663
  > = ({action}) => {
@@ -892,7 +797,7 @@ export const toggleDecoratorActionImplementation: BehaviorActionImplementation<
892
797
  },
893
798
  })
894
799
  } else {
895
- addDecoratorActionImplementation({
800
+ decoratorAddActionImplementation({
896
801
  context,
897
802
  action: {
898
803
  type: 'decorator.add',
@@ -0,0 +1,36 @@
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
+
14
+ expect(getTextToItalic('_Hello*world_')).toBe('_Hello*world_')
15
+ expect(getTextToItalic('*Hello_world*')).toBe('*Hello_world*')
16
+
17
+ expect(getTextToItalic('*hello\nworld*')).toBe(undefined)
18
+ expect(getTextToItalic('_hello\nworld_')).toBe(undefined)
19
+ })
20
+
21
+ test(getTextToBold.name, () => {
22
+ expect(getTextToBold('Hello **world**')).toBe('**world**')
23
+ expect(getTextToBold('Hello __world__')).toBe('__world__')
24
+ expect(getTextToBold('**Hello**world**')).toBe('**world**')
25
+ expect(getTextToBold('__Hello__world__')).toBe('__world__')
26
+
27
+ expect(getTextToBold('Hello **world')).toBe(undefined)
28
+ expect(getTextToBold('Hello world**')).toBe(undefined)
29
+ expect(getTextToBold('Hello **world** **')).toBe(undefined)
30
+
31
+ expect(getTextToBold('__Hello**world__')).toBe('__Hello**world__')
32
+ expect(getTextToBold('**Hello__world**')).toBe('**Hello__world**')
33
+
34
+ expect(getTextToBold('**hello\nworld**')).toBe(undefined)
35
+ expect(getTextToBold('__hello\nworld__')).toBe(undefined)
36
+ })
@@ -0,0 +1,18 @@
1
+ const asteriskPairRegex = '(?<!\\*)\\*(?!\\s)([^*\\n]+?)(?<!\\s)\\*(?!\\*)'
2
+ const underscorePairRegex = '(?<!_)_(?!\\s)([^_\\n]+?)(?<!\\s)_(?!_)'
3
+ const italicRegex = new RegExp(`(${asteriskPairRegex}|${underscorePairRegex})$`)
4
+
5
+ const doubleAsteriskPairRegex =
6
+ '(?<!\\*)\\*\\*(?!\\s)([^*\\n]+?)(?<!\\s)\\*\\*(?!\\*)'
7
+ const doubleUnderscorePairRegex = '(?<!_)__(?!\\s)([^_\\n]+?)(?<!\\s)__(?!_)'
8
+ const boldRegex = new RegExp(
9
+ `(${doubleAsteriskPairRegex}|${doubleUnderscorePairRegex})$`,
10
+ )
11
+
12
+ export function getTextToItalic(text: string) {
13
+ return text.match(italicRegex)?.at(0)
14
+ }
15
+
16
+ export function getTextToBold(text: string) {
17
+ return text.match(boldRegex)?.at(0)
18
+ }
@@ -3,12 +3,17 @@ import {
3
3
  createMarkdownBehaviors,
4
4
  type MarkdownBehaviorsConfig,
5
5
  } from '../behaviors/behavior.markdown'
6
+ import {
7
+ useMarkdownEmphasisBehaviors,
8
+ type MarkdownEmphasisBehaviorsConfig,
9
+ } from '../behaviors/behavior.markdown-emphasis'
6
10
  import {useEditor} from '../editor/editor-provider'
7
11
 
8
12
  /**
9
13
  * @beta
10
14
  */
11
- export type MarkdownPluginConfig = MarkdownBehaviorsConfig
15
+ export type MarkdownPluginConfig = MarkdownBehaviorsConfig &
16
+ MarkdownEmphasisBehaviorsConfig
12
17
 
13
18
  /**
14
19
  * @beta
@@ -25,6 +30,10 @@ export type MarkdownPluginConfig = MarkdownBehaviorsConfig
25
30
  * <EditorProvider>
26
31
  * <MarkdownPlugin
27
32
  * config={{
33
+ * boldDecorator: ({schema}) =>
34
+ * schema.decorators.find((decorator) => decorator.value === 'strong')?.value,
35
+ * italicDecorator: ({schema}) =>
36
+ * schema.decorators.find((decorator) => decorator.value === 'em')?.value,
28
37
  * horizontalRuleObject: ({schema}) => {
29
38
  * const name = schema.blockObjects.find(
30
39
  * (object) => object.name === 'break',
@@ -51,6 +60,7 @@ export type MarkdownPluginConfig = MarkdownBehaviorsConfig
51
60
  */
52
61
  export function MarkdownPlugin(props: {config: MarkdownPluginConfig}) {
53
62
  const editor = useEditor()
63
+ useMarkdownEmphasisBehaviors({config: props.config})
54
64
 
55
65
  useEffect(() => {
56
66
  const behaviors = createMarkdownBehaviors(props.config)
@@ -9,6 +9,10 @@ export type {
9
9
  export {getActiveAnnotations} from './selector.get-active-annotations'
10
10
  export {getActiveListItem} from './selector.get-active-list-item'
11
11
  export {getActiveStyle} from './selector.get-active-style'
12
+ export {getAnchorBlock} from './selector.get-anchor-block'
13
+ export {getAnchorChild} from './selector.get-anchor-child'
14
+ export {getAnchorSpan} from './selector.get-anchor-span'
15
+ export {getAnchorTextBlock} from './selector.get-anchor-text-block'
12
16
  export {getBlockOffsets} from './selector.get-block-offsets'
13
17
  export {getCaretWordSelection} from './selector.get-caret-word-selection'
14
18
  export {getNextInlineObject} from './selector.get-next-inline-object'
@@ -20,6 +24,7 @@ export {getSelectionEndPoint} from './selector.get-selection-end-point'
20
24
  export {getSelectionStartPoint} from './selector.get-selection-start-point'
21
25
  export {getSelectionText} from './selector.get-selection-text'
22
26
  export {getBlockTextBefore} from './selector.get-text-before'
27
+ export {getTrimmedSelection} from './selector.get-trimmed-selection'
23
28
  export {getValue} from './selector.get-value'
24
29
  export {isActiveAnnotation} from './selector.is-active-annotation'
25
30
  export {isActiveDecorator} from './selector.is-active-decorator'
@@ -0,0 +1,22 @@
1
+ import type {KeyedSegment, PortableTextBlock} from '@sanity/types'
2
+ import type {EditorSelector} from '../editor/editor-selector'
3
+ import {isKeyedSegment} from '../utils'
4
+
5
+ /**
6
+ * @public
7
+ */
8
+ export const getAnchorBlock: EditorSelector<
9
+ {node: PortableTextBlock; path: [KeyedSegment]} | undefined
10
+ > = ({context}) => {
11
+ const key = context.selection
12
+ ? isKeyedSegment(context.selection.anchor.path[0])
13
+ ? context.selection.anchor.path[0]._key
14
+ : undefined
15
+ : undefined
16
+
17
+ const node = key
18
+ ? context.value.find((block) => block._key === key)
19
+ : undefined
20
+
21
+ return node && key ? {node, path: [{_key: key}]} : undefined
22
+ }
@@ -0,0 +1,36 @@
1
+ import type {KeyedSegment} from '@portabletext/patches'
2
+ import type {PortableTextObject, PortableTextSpan} from '@sanity/types'
3
+ import type {EditorSelector} from '../editor/editor-selector'
4
+ import {isKeyedSegment} from '../utils'
5
+ import {getAnchorTextBlock} from './selector.get-anchor-text-block'
6
+
7
+ /**
8
+ * @public
9
+ */
10
+ export const getAnchorChild: EditorSelector<
11
+ | {
12
+ node: PortableTextObject | PortableTextSpan
13
+ path: [KeyedSegment, 'children', KeyedSegment]
14
+ }
15
+ | undefined
16
+ > = ({context}) => {
17
+ const anchorBlock = getAnchorTextBlock({context})
18
+
19
+ if (!anchorBlock) {
20
+ return undefined
21
+ }
22
+
23
+ const key = context.selection
24
+ ? isKeyedSegment(context.selection.anchor.path[2])
25
+ ? context.selection.anchor.path[2]._key
26
+ : undefined
27
+ : undefined
28
+
29
+ const node = key
30
+ ? anchorBlock.node.children.find((span) => span._key === key)
31
+ : undefined
32
+
33
+ return node && key
34
+ ? {node, path: [...anchorBlock.path, 'children', {_key: key}]}
35
+ : undefined
36
+ }
@@ -0,0 +1,18 @@
1
+ import type {KeyedSegment} from '@portabletext/patches'
2
+ import {isPortableTextSpan, type PortableTextSpan} from '@sanity/types'
3
+ import type {EditorSelector} from '../editor/editor-selector'
4
+ import {getAnchorChild} from './selector.get-anchor-child'
5
+
6
+ /**
7
+ * @public
8
+ */
9
+ export const getAnchorSpan: EditorSelector<
10
+ | {node: PortableTextSpan; path: [KeyedSegment, 'children', KeyedSegment]}
11
+ | undefined
12
+ > = ({context}) => {
13
+ const anchorChild = getAnchorChild({context})
14
+
15
+ return anchorChild && isPortableTextSpan(anchorChild.node)
16
+ ? {node: anchorChild.node, path: anchorChild.path}
17
+ : undefined
18
+ }
@@ -0,0 +1,20 @@
1
+ import {
2
+ isPortableTextTextBlock,
3
+ type KeyedSegment,
4
+ type PortableTextTextBlock,
5
+ } from '@sanity/types'
6
+ import type {EditorSelector} from '../editor/editor-selector'
7
+ import {getAnchorBlock} from './selector.get-anchor-block'
8
+
9
+ /**
10
+ * @public
11
+ */
12
+ export const getAnchorTextBlock: EditorSelector<
13
+ {node: PortableTextTextBlock; path: [KeyedSegment]} | undefined
14
+ > = ({context}) => {
15
+ const anchorBlock = getAnchorBlock({context})
16
+
17
+ return anchorBlock && isPortableTextTextBlock(anchorBlock.node)
18
+ ? {node: anchorBlock.node, path: anchorBlock.path}
19
+ : undefined
20
+ }