@portabletext/plugin-markdown-shortcuts 4.0.22 → 4.0.24

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portabletext/plugin-markdown-shortcuts",
3
- "version": "4.0.22",
3
+ "version": "4.0.24",
4
4
  "description": "Adds helpful Markdown shortcuts to the editor",
5
5
  "keywords": [
6
6
  "portabletext",
@@ -32,13 +32,12 @@
32
32
  "main": "./dist/index.js",
33
33
  "types": "./dist/index.d.ts",
34
34
  "files": [
35
- "src",
36
35
  "dist"
37
36
  ],
38
37
  "dependencies": {
39
- "react-compiler-runtime": "1.0.0",
40
- "@portabletext/plugin-character-pair-decorator": "^4.0.22",
41
- "@portabletext/plugin-input-rule": "^1.0.22"
38
+ "react-compiler-runtime": "^1.0.0",
39
+ "@portabletext/plugin-character-pair-decorator": "^4.0.24",
40
+ "@portabletext/plugin-input-rule": "^1.0.24"
42
41
  },
43
42
  "devDependencies": {
44
43
  "@sanity/tsconfig": "^1.0.0",
@@ -46,19 +45,19 @@
46
45
  "@vitejs/plugin-react": "^5.0.4",
47
46
  "@vitest/browser": "^4.0.14",
48
47
  "@vitest/browser-playwright": "^4.0.14",
49
- "babel-plugin-react-compiler": "1.0.0",
48
+ "babel-plugin-react-compiler": "^1.0.0",
50
49
  "eslint": "^9.39.1",
51
- "eslint-plugin-react-hooks": "7.0.1",
50
+ "eslint-plugin-react-hooks": "^7.0.1",
52
51
  "react": "^19.2.1",
53
52
  "typescript": "5.9.3",
54
53
  "typescript-eslint": "^8.48.0",
55
54
  "vitest": "^4.0.14",
56
- "@portabletext/editor": "^3.3.2",
57
- "racejar": "2.0.0"
55
+ "@portabletext/editor": "^3.3.4",
56
+ "racejar": "2.0.1"
58
57
  },
59
58
  "peerDependencies": {
60
59
  "react": "^18.3 || ^19",
61
- "@portabletext/editor": "^3.3.2"
60
+ "@portabletext/editor": "^3.3.4"
62
61
  },
63
62
  "engines": {
64
63
  "node": ">=20.19 <22 || >=22.12"
@@ -1,133 +0,0 @@
1
- import type {EditorContext} from '@portabletext/editor'
2
- import {defineBehavior, raise} from '@portabletext/editor/behaviors'
3
- import * as selectors from '@portabletext/editor/selectors'
4
-
5
- export type ObjectWithOptionalKey = {
6
- _type: string
7
- _key?: string
8
- [other: string]: unknown
9
- }
10
-
11
- export type MarkdownBehaviorsConfig = {
12
- horizontalRuleObject?: ({
13
- context,
14
- }: {
15
- context: Pick<EditorContext, 'schema' | 'keyGenerator'>
16
- }) => ObjectWithOptionalKey | undefined
17
- defaultStyle?: ({
18
- context,
19
- schema,
20
- }: {
21
- context: Pick<EditorContext, 'schema'>
22
- /**
23
- * @deprecated Use `context.schema` instead
24
- */
25
- schema: EditorContext['schema']
26
- }) => string | undefined
27
- }
28
-
29
- export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
30
- const automaticHrOnPaste = defineBehavior({
31
- on: 'clipboard.paste',
32
- guard: ({snapshot, event}) => {
33
- const text = event.originEvent.dataTransfer.getData('text/plain')
34
- const hrRegExp = /^(---)$|^(—-)$|^(___)$|^(\*\*\*)$/
35
- const hrCharacters = text.match(hrRegExp)?.[0]
36
- const hrObject = config.horizontalRuleObject?.({
37
- context: {
38
- schema: snapshot.context.schema,
39
- keyGenerator: snapshot.context.keyGenerator,
40
- },
41
- })
42
- const focusBlock = selectors.getFocusBlock(snapshot)
43
- const focusTextBlock = selectors.getFocusTextBlock(snapshot)
44
-
45
- if (!hrCharacters || !hrObject || !focusBlock) {
46
- return false
47
- }
48
-
49
- return {hrCharacters, hrObject, focusBlock, focusTextBlock}
50
- },
51
- actions: [
52
- (_, {hrCharacters}) => [
53
- raise({
54
- type: 'insert.text',
55
- text: hrCharacters,
56
- }),
57
- ],
58
- ({snapshot}, {hrObject, focusBlock, focusTextBlock}) =>
59
- focusTextBlock
60
- ? [
61
- raise({
62
- type: 'insert.block',
63
- block: {
64
- _type: snapshot.context.schema.block.name,
65
- children: focusTextBlock.node.children,
66
- },
67
- placement: 'after',
68
- }),
69
- raise({
70
- type: 'insert.block',
71
- block: hrObject,
72
- placement: 'after',
73
- }),
74
- raise({
75
- type: 'delete.block',
76
- at: focusBlock.path,
77
- }),
78
- ]
79
- : [
80
- raise({
81
- type: 'insert.block',
82
- block: hrObject,
83
- placement: 'after',
84
- }),
85
- ],
86
- ],
87
- })
88
-
89
- const clearStyleOnBackspace = defineBehavior({
90
- on: 'delete.backward',
91
- guard: ({snapshot}) => {
92
- const selectionCollapsed = selectors.isSelectionCollapsed(snapshot)
93
- const focusTextBlock = selectors.getFocusTextBlock(snapshot)
94
- const focusSpan = selectors.getFocusSpan(snapshot)
95
-
96
- if (!selectionCollapsed || !focusTextBlock || !focusSpan) {
97
- return false
98
- }
99
-
100
- const atTheBeginningOfBLock =
101
- focusTextBlock.node.children[0]._key === focusSpan.node._key &&
102
- snapshot.context.selection?.focus.offset === 0
103
-
104
- const defaultStyle = config.defaultStyle?.({
105
- context: {schema: snapshot.context.schema},
106
- schema: snapshot.context.schema,
107
- })
108
-
109
- if (
110
- atTheBeginningOfBLock &&
111
- defaultStyle &&
112
- focusTextBlock.node.style !== defaultStyle
113
- ) {
114
- return {defaultStyle, focusTextBlock}
115
- }
116
-
117
- return false
118
- },
119
- actions: [
120
- (_, {defaultStyle, focusTextBlock}) => [
121
- raise({
122
- type: 'block.set',
123
- props: {style: defaultStyle},
124
- at: focusTextBlock.path,
125
- }),
126
- ],
127
- ],
128
- })
129
-
130
- const markdownBehaviors = [automaticHrOnPaste, clearStyleOnBackspace]
131
-
132
- return markdownBehaviors
133
- }
@@ -1,108 +0,0 @@
1
- Feature: Markdown Behaviors
2
-
3
- Background:
4
- Given a global keymap
5
-
6
- Scenario: Automatic blockquote
7
- Given the text ">"
8
- When "{Space}" is pressed
9
- Then the text is "q:"
10
-
11
- Scenario: Automatic blockquote not toggled by space in the beginning
12
- Given the text ">"
13
- When the caret is put before ">"
14
- When "{Space}" is pressed
15
- Then the text is " >"
16
-
17
- Scenario: Automatic blockquote in non-empty block
18
- Given the text ">foo"
19
- When the caret is put before "f"
20
- And "{Space}" is pressed
21
- Then the text is "q:foo"
22
-
23
- Scenario Outline: Automatic headings
24
- Given the text <text>
25
- When "{Space}" is pressed
26
- Then the text is <new text>
27
-
28
- Examples:
29
- | text | new text |
30
- | "#" | "h1:" |
31
- | "##" | "h2:" |
32
- | "###" | "h3:" |
33
- | "####" | "h4:" |
34
- | "#####" | "h5:" |
35
- | "######" | "h6:" |
36
- | "#######" | "####### " |
37
-
38
- Scenario Outline: Automatic headings not toggled by space in the beginning
39
- Given the text <text>
40
- When the caret is put <position>
41
- When "{Space}" is pressed
42
- Then the text is <new text>
43
-
44
- Examples:
45
- | text | position | new text |
46
- | "#" | before "#" | " #" |
47
- | "##" | before "##" | " ##" |
48
- | "###" | before "###" | " ###" |
49
-
50
- Scenario Outline: Automatic headings toggled by space mid-heading
51
- Given the text <text>
52
- When the caret is put <position>
53
- When "{ArrowRight}" is pressed
54
- When "{Space}" is pressed
55
- Then the text is <new text>
56
-
57
- Examples:
58
- | text | position | new text |
59
- | "##" | before "##" | "h1:#" |
60
- | "###" | before "###" | "h1:##" |
61
-
62
- Scenario Outline: Automatic headings in non-empty block
63
- Given the text <text>
64
- When the caret is put <position>
65
- And "{Space}" is pressed
66
- Then the text is <new text>
67
-
68
- Examples:
69
- | text | position | new text |
70
- | "foo" | before "foo" | " foo" |
71
- | "#foo" | before "foo" | "h1:foo" |
72
- | "##foo" | before "foo" | "h2:foo" |
73
- | "###foo" | before "foo" | "h3:foo" |
74
- | "####foo" | before "foo" | "h4:foo" |
75
- | "#####foo" | before "foo" | "h5:foo" |
76
- | "######foo" | before "foo" | "h6:foo" |
77
- | "#######foo" | before "foo" | "####### foo" |
78
-
79
- Scenario Outline: Clear style on Backspace
80
- Given the text "foo"
81
- When <style> is toggled
82
- And "{Backspace}" is pressed 4 times
83
- Then the text is ""
84
-
85
- Examples:
86
- | style |
87
- | "h1" |
88
- | "h2" |
89
- | "h3" |
90
- | "h4" |
91
- | "h5" |
92
- | "h6" |
93
-
94
- Scenario Outline: Clear style on Backspace in empty block
95
- Given the text "foo"
96
- When <style> is toggled
97
- And "{Backspace}" is pressed 4 times
98
- And "bar" is typed
99
- Then the text is "bar"
100
-
101
- Examples:
102
- | style |
103
- | "h1" |
104
- | "h2" |
105
- | "h3" |
106
- | "h4" |
107
- | "h5" |
108
- | "h6" |
@@ -1,63 +0,0 @@
1
- import {defineSchema} from '@portabletext/editor'
2
- import {parameterTypes} from '@portabletext/editor/test'
3
- import {
4
- createTestEditor,
5
- stepDefinitions,
6
- type Context,
7
- } from '@portabletext/editor/test/vitest'
8
- import {Before} from 'racejar'
9
- import {Feature} from 'racejar/vitest'
10
- import behaviorMarkdownFeature from './behavior.markdown.feature?raw'
11
- import {MarkdownShortcutsPlugin} from './plugin.markdown-shortcuts'
12
-
13
- Feature({
14
- hooks: [
15
- Before(async (context: Context) => {
16
- const {editor, locator} = await createTestEditor({
17
- children: (
18
- <MarkdownShortcutsPlugin
19
- defaultStyle={({context}) => context.schema.styles[0]?.name}
20
- headingStyle={({context, props}) =>
21
- context.schema.styles.find(
22
- (style) => style.name === `h${props.level}`,
23
- )?.name
24
- }
25
- blockquoteStyle={({context}) =>
26
- context.schema.styles.find((style) => style.name === 'blockquote')
27
- ?.name
28
- }
29
- unorderedList={({context}) =>
30
- context.schema.lists.find((list) => list.name === 'bullet')?.name
31
- }
32
- orderedList={({context}) =>
33
- context.schema.lists.find((list) => list.name === 'number')?.name
34
- }
35
- />
36
- ),
37
- schemaDefinition: defineSchema({
38
- annotations: [{name: 'comment'}, {name: 'link'}],
39
- decorators: [{name: 'em'}, {name: 'strong'}],
40
- blockObjects: [{name: 'image'}, {name: 'break'}],
41
- inlineObjects: [{name: 'stock-ticker'}],
42
- lists: [{name: 'bullet'}, {name: 'number'}],
43
- styles: [
44
- {name: 'normal'},
45
- {name: 'h1'},
46
- {name: 'h2'},
47
- {name: 'h3'},
48
- {name: 'h4'},
49
- {name: 'h5'},
50
- {name: 'h6'},
51
- {name: 'blockquote'},
52
- ],
53
- }),
54
- })
55
-
56
- context.locator = locator
57
- context.editor = editor
58
- }),
59
- ],
60
- featureText: behaviorMarkdownFeature,
61
- stepDefinitions: stepDefinitions,
62
- parameterTypes,
63
- })
package/src/global.d.ts DELETED
@@ -1,4 +0,0 @@
1
- declare module '*.feature?raw' {
2
- const content: string
3
- export default content
4
- }
package/src/index.ts DELETED
@@ -1 +0,0 @@
1
- export * from './plugin.markdown-shortcuts'
@@ -1,233 +0,0 @@
1
- import type {EditorContext} from '@portabletext/editor'
2
- import {useEditor} from '@portabletext/editor'
3
- import {CharacterPairDecoratorPlugin} from '@portabletext/plugin-character-pair-decorator'
4
- import {InputRulePlugin} from '@portabletext/plugin-input-rule'
5
- import {useEffect, useMemo} from 'react'
6
- import {
7
- createMarkdownBehaviors,
8
- type MarkdownBehaviorsConfig,
9
- type ObjectWithOptionalKey,
10
- } from './behavior.markdown-shortcuts'
11
- import {createBlockquoteRule} from './rule.blockquote'
12
- import {createHeadingRule} from './rule.heading'
13
- import {createHorizontalRuleRule} from './rule.horizontal-rule'
14
- import {createMarkdownLinkRule} from './rule.markdown-link'
15
- import {createOrderedListRule} from './rule.ordered-list'
16
- import {createUnorderedListRule} from './rule.unordered-list'
17
-
18
- /**
19
- * @public
20
- */
21
- export type MarkdownShortcutsPluginProps = MarkdownBehaviorsConfig & {
22
- blockquoteStyle?: ({
23
- context,
24
- schema,
25
- }: {
26
- context: Pick<EditorContext, 'schema'>
27
- /**
28
- * @deprecated Use `context.schema` instead
29
- */
30
- schema: EditorContext['schema']
31
- }) => string | undefined
32
- defaultStyle?: ({
33
- context,
34
- schema,
35
- }: {
36
- context: Pick<EditorContext, 'schema'>
37
- /**
38
- * @deprecated Use `context.schema` instead
39
- */
40
- schema: EditorContext['schema']
41
- }) => string | undefined
42
- headingStyle?: ({
43
- context,
44
- schema,
45
- props,
46
- level,
47
- }: {
48
- context: Pick<EditorContext, 'schema'>
49
- /**
50
- * @deprecated Use `context.schema` instead
51
- */
52
- schema: EditorContext['schema']
53
- props: {level: number}
54
- /**
55
- * @deprecated Use `props.level` instead
56
- */
57
- level: number
58
- }) => string | undefined
59
- linkObject?: ({
60
- context,
61
- props,
62
- }: {
63
- context: Pick<EditorContext, 'schema' | 'keyGenerator'>
64
- props: {href: string}
65
- }) => ObjectWithOptionalKey | undefined
66
- unorderedList?: ({
67
- context,
68
- schema,
69
- }: {
70
- context: Pick<EditorContext, 'schema'>
71
- /**
72
- * @deprecated Use `context.schema` instead
73
- */
74
- schema: EditorContext['schema']
75
- }) => string | undefined
76
- orderedList?: ({
77
- context,
78
- schema,
79
- }: {
80
- context: Pick<EditorContext, 'schema'>
81
- /**
82
- * @deprecated Use `context.schema` instead
83
- */
84
- schema: EditorContext['schema']
85
- }) => string | undefined
86
- boldDecorator?: ({
87
- context,
88
- schema,
89
- }: {
90
- context: Pick<EditorContext, 'schema'>
91
- /**
92
- * @deprecated Use `context.schema` instead
93
- */
94
- schema: EditorContext['schema']
95
- }) => string | undefined
96
- codeDecorator?: ({
97
- context,
98
- schema,
99
- }: {
100
- context: Pick<EditorContext, 'schema'>
101
- /**
102
- * @deprecated Use `context.schema` instead
103
- */
104
- schema: EditorContext['schema']
105
- }) => string | undefined
106
- italicDecorator?: ({
107
- context,
108
- schema,
109
- }: {
110
- context: Pick<EditorContext, 'schema'>
111
- /**
112
- * @deprecated Use `context.schema` instead
113
- */
114
- schema: EditorContext['schema']
115
- }) => string | undefined
116
- strikeThroughDecorator?: ({
117
- context,
118
- schema,
119
- }: {
120
- context: Pick<EditorContext, 'schema'>
121
- /**
122
- * @deprecated Use `context.schema` instead
123
- */
124
- schema: EditorContext['schema']
125
- }) => string | undefined
126
- }
127
-
128
- /**
129
- * @public
130
- */
131
- export function MarkdownShortcutsPlugin({
132
- blockquoteStyle,
133
- boldDecorator,
134
- codeDecorator,
135
- defaultStyle,
136
- headingStyle,
137
- horizontalRuleObject,
138
- linkObject,
139
- italicDecorator,
140
- orderedList,
141
- strikeThroughDecorator,
142
- unorderedList,
143
- }: MarkdownShortcutsPluginProps) {
144
- const editor = useEditor()
145
-
146
- useEffect(() => {
147
- const behaviors = createMarkdownBehaviors({
148
- defaultStyle,
149
- })
150
-
151
- const unregisterBehaviors = behaviors.map((behavior) =>
152
- editor.registerBehavior({behavior}),
153
- )
154
-
155
- return () => {
156
- for (const unregisterBehavior of unregisterBehaviors) {
157
- unregisterBehavior()
158
- }
159
- }
160
- }, [defaultStyle, editor])
161
-
162
- const inputRules = useMemo(() => {
163
- const rules = []
164
- if (blockquoteStyle) {
165
- rules.push(createBlockquoteRule({blockquoteStyle}))
166
- }
167
- if (headingStyle) {
168
- rules.push(createHeadingRule({headingStyle}))
169
- }
170
- if (horizontalRuleObject) {
171
- rules.push(createHorizontalRuleRule({horizontalRuleObject}))
172
- }
173
- if (linkObject) {
174
- rules.push(createMarkdownLinkRule({linkObject}))
175
- }
176
- if (orderedList) {
177
- rules.push(createOrderedListRule({orderedList}))
178
- }
179
- if (unorderedList) {
180
- rules.push(createUnorderedListRule({unorderedList}))
181
- }
182
- return rules.length > 0 ? rules : null
183
- }, [
184
- blockquoteStyle,
185
- headingStyle,
186
- horizontalRuleObject,
187
- linkObject,
188
- orderedList,
189
- unorderedList,
190
- ])
191
-
192
- return (
193
- <>
194
- {boldDecorator ? (
195
- <>
196
- <CharacterPairDecoratorPlugin
197
- decorator={boldDecorator}
198
- pair={{char: '*', amount: 2}}
199
- />
200
- <CharacterPairDecoratorPlugin
201
- decorator={boldDecorator}
202
- pair={{char: '_', amount: 2}}
203
- />
204
- </>
205
- ) : null}
206
- {codeDecorator ? (
207
- <CharacterPairDecoratorPlugin
208
- decorator={codeDecorator}
209
- pair={{char: '`', amount: 1}}
210
- />
211
- ) : null}
212
- {italicDecorator ? (
213
- <>
214
- <CharacterPairDecoratorPlugin
215
- decorator={italicDecorator}
216
- pair={{char: '*', amount: 1}}
217
- />
218
- <CharacterPairDecoratorPlugin
219
- decorator={italicDecorator}
220
- pair={{char: '_', amount: 1}}
221
- />
222
- </>
223
- ) : null}
224
- {strikeThroughDecorator ? (
225
- <CharacterPairDecoratorPlugin
226
- decorator={strikeThroughDecorator}
227
- pair={{char: '~', amount: 2}}
228
- />
229
- ) : null}
230
- {inputRules ? <InputRulePlugin rules={inputRules} /> : null}
231
- </>
232
- )
233
- }
@@ -1,63 +0,0 @@
1
- import type {EditorContext} from '@portabletext/editor'
2
- import {raise} from '@portabletext/editor/behaviors'
3
- import {getPreviousInlineObject} from '@portabletext/editor/selectors'
4
- import {defineInputRule} from '@portabletext/plugin-input-rule'
5
-
6
- export function createBlockquoteRule(config: {
7
- blockquoteStyle: ({
8
- context,
9
- schema,
10
- }: {
11
- context: Pick<EditorContext, 'schema'>
12
- /**
13
- * @deprecated Use `context.schema` instead
14
- */
15
- schema: EditorContext['schema']
16
- }) => string | undefined
17
- }) {
18
- return defineInputRule({
19
- on: /^> /,
20
- guard: ({snapshot, event}) => {
21
- const style = config.blockquoteStyle({
22
- context: {schema: snapshot.context.schema},
23
- schema: snapshot.context.schema,
24
- })
25
-
26
- if (!style) {
27
- return false
28
- }
29
-
30
- const previousInlineObject = getPreviousInlineObject(snapshot)
31
-
32
- if (previousInlineObject) {
33
- return false
34
- }
35
-
36
- const match = event.matches.at(0)
37
-
38
- if (!match) {
39
- return false
40
- }
41
-
42
- return {style, match}
43
- },
44
- actions: [
45
- ({event}, {style, match}) => [
46
- raise({
47
- type: 'block.unset',
48
- props: ['listItem', 'level'],
49
- at: event.focusBlock.path,
50
- }),
51
- raise({
52
- type: 'block.set',
53
- props: {style},
54
- at: event.focusBlock.path,
55
- }),
56
- raise({
57
- type: 'delete',
58
- at: match.targetOffsets,
59
- }),
60
- ],
61
- ],
62
- })
63
- }
@@ -1,74 +0,0 @@
1
- import type {EditorContext} from '@portabletext/editor'
2
- import {raise} from '@portabletext/editor/behaviors'
3
- import {getPreviousInlineObject} from '@portabletext/editor/selectors'
4
- import {defineInputRule} from '@portabletext/plugin-input-rule'
5
-
6
- export function createHeadingRule(config: {
7
- headingStyle: ({
8
- context,
9
- schema,
10
- props,
11
- level,
12
- }: {
13
- context: Pick<EditorContext, 'schema'>
14
- /**
15
- * @deprecated Use `context.schema` instead
16
- */
17
- schema: EditorContext['schema']
18
- props: {level: number}
19
- /**
20
- * @deprecated Use `props.level` instead
21
- */
22
- level: number
23
- }) => string | undefined
24
- }) {
25
- return defineInputRule({
26
- on: /^#+ /,
27
- guard: ({snapshot, event}) => {
28
- const previousInlineObject = getPreviousInlineObject(snapshot)
29
-
30
- if (previousInlineObject) {
31
- return false
32
- }
33
-
34
- const match = event.matches.at(0)
35
-
36
- if (!match) {
37
- return false
38
- }
39
-
40
- const level = match.text.length - 1
41
-
42
- const style = config.headingStyle({
43
- context: {schema: snapshot.context.schema},
44
- schema: snapshot.context.schema,
45
- props: {level},
46
- level,
47
- })
48
-
49
- if (!style) {
50
- return false
51
- }
52
-
53
- return {match, style}
54
- },
55
- actions: [
56
- ({event}, {match, style}) => [
57
- raise({
58
- type: 'block.unset',
59
- props: ['listItem', 'level'],
60
- at: event.focusBlock.path,
61
- }),
62
- raise({
63
- type: 'block.set',
64
- props: {style},
65
- at: event.focusBlock.path,
66
- }),
67
- raise({
68
- type: 'delete',
69
- at: match.targetOffsets,
70
- }),
71
- ],
72
- ],
73
- })
74
- }
@@ -1,64 +0,0 @@
1
- import type {EditorContext} from '@portabletext/editor'
2
- import {raise} from '@portabletext/editor/behaviors'
3
- import {getPreviousInlineObject} from '@portabletext/editor/selectors'
4
- import {defineInputRule} from '@portabletext/plugin-input-rule'
5
- import type {ObjectWithOptionalKey} from './behavior.markdown-shortcuts'
6
-
7
- export function createHorizontalRuleRule(config: {
8
- horizontalRuleObject: ({
9
- context,
10
- }: {
11
- context: Pick<EditorContext, 'schema' | 'keyGenerator'>
12
- }) => ObjectWithOptionalKey | undefined
13
- }) {
14
- return defineInputRule({
15
- on: /^(---)|^(—-)|^(___)|^(\*\*\*)/,
16
- guard: ({snapshot, event}) => {
17
- const hrObject = config.horizontalRuleObject({
18
- context: {
19
- schema: snapshot.context.schema,
20
- keyGenerator: snapshot.context.keyGenerator,
21
- },
22
- })
23
-
24
- if (!hrObject) {
25
- // If no horizontal rule object is provided, then we can safely skip
26
- // this rule.
27
- return false
28
- }
29
-
30
- const previousInlineObject = getPreviousInlineObject(snapshot)
31
-
32
- if (previousInlineObject) {
33
- // Input Rules only look at the plain text of the text block. This
34
- // means that the RegExp is matched even if there is a leading inline
35
- // object.
36
- return false
37
- }
38
-
39
- // In theory, an Input Rule could return multiple matches. But in this
40
- // case we only expect one match.
41
- const match = event.matches.at(0)
42
-
43
- if (!match) {
44
- return false
45
- }
46
-
47
- return {hrObject, match}
48
- },
49
- actions: [
50
- (_, {hrObject, match}) => [
51
- raise({
52
- type: 'insert.block',
53
- block: hrObject,
54
- placement: 'before',
55
- select: 'none',
56
- }),
57
- raise({
58
- type: 'delete',
59
- at: match.targetOffsets,
60
- }),
61
- ],
62
- ],
63
- })
64
- }
@@ -1,56 +0,0 @@
1
- Feature: Markdown Link Rule
2
-
3
- Background:
4
- Given a global keymap
5
-
6
- Scenario Outline: Transform markdown Link into annotation
7
- Given the text <text>
8
- When <inserted text> is inserted
9
- And "new" is typed
10
- Then the text is <new text>
11
- And <annotated> has <marks>
12
-
13
- Examples:
14
- | text | inserted text | new text | annotated | marks |
15
- | "[foo](bar" | ")" | "foo,new" | "foo" | marks "k4" |
16
- | "[[foo](bar" | ")" | "[,foo,new" | "foo" | marks "k4" |
17
- | "[f[oo](bar" | ")" | "[f,oo,new" | "oo" | marks "k4" |
18
- | "[f[]oo](bar" | ")" | "[f[]oo](bar)new" | "" | no marks |
19
-
20
- Scenario: Preserving decorator in link text
21
- Given the text "[foo](bar"
22
- And "strong" around "foo"
23
- When ")" is inserted
24
- And "new" is typed
25
- Then the text is "foo,new"
26
- And "foo" has marks "strong,k6"
27
-
28
- Scenario: Preserving decorators in link text
29
- Given the text "[foo](bar"
30
- And "strong" around "foo"
31
- And "em" around "oo"
32
- When ")" is inserted
33
- And "new" is typed
34
- Then the text is "f,oo,new"
35
- And "f" has marks "strong,k7"
36
- And "oo" has marks "strong,em,k7"
37
-
38
- Scenario: Overwriting other links
39
- Given the text "[foo](bar"
40
- And a "link" "l1" around "foo"
41
- When the caret is put after "bar"
42
- And ")" is inserted
43
- And "new" is typed
44
- Then the text is "foo,new"
45
- And "foo" has an annotation different than "l1"
46
-
47
- Scenario: Preserving other annotations
48
- Given the text "[foo](bar"
49
- And a "link" "l1" around "foo"
50
- And a "comment" "c1" around "foo"
51
- When the caret is put after "bar"
52
- And ")" is inserted
53
- And "new" is typed
54
- Then the text is "foo,new"
55
- And "foo" has an annotation different than "l1"
56
- And "foo" has marks "c1,k9"
@@ -1,44 +0,0 @@
1
- import {defineSchema} from '@portabletext/editor'
2
- import {parameterTypes} from '@portabletext/editor/test'
3
- import {
4
- createTestEditor,
5
- stepDefinitions,
6
- type Context,
7
- } from '@portabletext/editor/test/vitest'
8
- import {InputRulePlugin} from '@portabletext/plugin-input-rule'
9
- import {Before} from 'racejar'
10
- import {Feature} from 'racejar/vitest'
11
- import {createMarkdownLinkRule} from './rule.markdown-link'
12
- import markdownLinkFeature from './rule.markdown-link.feature?raw'
13
-
14
- const markdownLinkRule = createMarkdownLinkRule({
15
- linkObject: ({props}) => ({
16
- _type: 'link',
17
- href: props.href,
18
- }),
19
- })
20
-
21
- Feature({
22
- hooks: [
23
- Before(async (context: Context) => {
24
- const {editor, locator} = await createTestEditor({
25
- children: (
26
- <>
27
- <InputRulePlugin rules={[markdownLinkRule]} />
28
- </>
29
- ),
30
- schemaDefinition: defineSchema({
31
- decorators: [{name: 'strong'}, {name: 'em'}],
32
- annotations: [{name: 'link'}, {name: 'comment'}],
33
- inlineObjects: [{name: 'stock-ticker'}],
34
- }),
35
- })
36
-
37
- context.locator = locator
38
- context.editor = editor
39
- }),
40
- ],
41
- featureText: markdownLinkFeature,
42
- stepDefinitions,
43
- parameterTypes,
44
- })
@@ -1,108 +0,0 @@
1
- import type {EditorContext} from '@portabletext/editor'
2
- import {raise, type BehaviorAction} from '@portabletext/editor/behaviors'
3
- import {defineInputRule} from '@portabletext/plugin-input-rule'
4
- import type {ObjectWithOptionalKey} from './behavior.markdown-shortcuts'
5
-
6
- export function createMarkdownLinkRule(config: {
7
- linkObject: ({
8
- context,
9
- props,
10
- }: {
11
- context: Pick<EditorContext, 'schema' | 'keyGenerator'>
12
- props: {href: string}
13
- }) => ObjectWithOptionalKey | undefined
14
- }) {
15
- return defineInputRule({
16
- on: /\[([^[\]]+)]\((.+)\)/,
17
- actions: [
18
- ({snapshot, event}) => {
19
- const newText = event.textBefore + event.textInserted
20
- let textLengthDelta = 0
21
- const actions: Array<BehaviorAction> = []
22
-
23
- for (const match of event.matches.reverse()) {
24
- const textMatch = match.groupMatches.at(0)
25
- const hrefMatch = match.groupMatches.at(1)
26
-
27
- if (textMatch === undefined || hrefMatch === undefined) {
28
- continue
29
- }
30
-
31
- textLengthDelta =
32
- textLengthDelta -
33
- (match.targetOffsets.focus.offset -
34
- match.targetOffsets.anchor.offset -
35
- textMatch.text.length)
36
-
37
- const linkObject = config.linkObject({
38
- context: {
39
- schema: snapshot.context.schema,
40
- keyGenerator: snapshot.context.keyGenerator,
41
- },
42
- props: {href: hrefMatch.text},
43
- })
44
-
45
- if (!linkObject) {
46
- continue
47
- }
48
-
49
- const {_type, _key, ...value} = linkObject
50
-
51
- const leftSideOffsets = {
52
- anchor: match.targetOffsets.anchor,
53
- focus: textMatch.targetOffsets.anchor,
54
- }
55
- const rightSideOffsets = {
56
- anchor: textMatch.targetOffsets.focus,
57
- focus: match.targetOffsets.focus,
58
- }
59
-
60
- actions.push(
61
- raise({
62
- type: 'select',
63
- at: textMatch.targetOffsets,
64
- }),
65
- )
66
- actions.push(
67
- raise({
68
- type: 'annotation.add',
69
- annotation: {
70
- name: _type,
71
- _key,
72
- value,
73
- },
74
- }),
75
- )
76
- actions.push(
77
- raise({
78
- type: 'delete',
79
- at: rightSideOffsets,
80
- }),
81
- )
82
- actions.push(
83
- raise({
84
- type: 'delete',
85
- at: leftSideOffsets,
86
- }),
87
- )
88
- }
89
-
90
- const endCaretPosition = {
91
- path: event.focusBlock.path,
92
- offset: newText.length - textLengthDelta * -1,
93
- }
94
-
95
- return [
96
- ...actions,
97
- raise({
98
- type: 'select',
99
- at: {
100
- anchor: endCaretPosition,
101
- focus: endCaretPosition,
102
- },
103
- }),
104
- ]
105
- },
106
- ],
107
- })
108
- }
@@ -1,66 +0,0 @@
1
- import type {EditorContext} from '@portabletext/editor'
2
- import {raise} from '@portabletext/editor/behaviors'
3
- import {getPreviousInlineObject} from '@portabletext/editor/selectors'
4
- import {defineInputRule} from '@portabletext/plugin-input-rule'
5
-
6
- export function createOrderedListRule(config: {
7
- orderedList: ({
8
- context,
9
- schema,
10
- }: {
11
- context: Pick<EditorContext, 'schema'>
12
- /**
13
- * @deprecated Use `context.schema` instead
14
- */
15
- schema: EditorContext['schema']
16
- }) => string | undefined
17
- }) {
18
- return defineInputRule({
19
- on: /^1\. /,
20
- guard: ({snapshot, event}) => {
21
- const orderedList = config.orderedList({
22
- context: {schema: snapshot.context.schema},
23
- schema: snapshot.context.schema,
24
- })
25
-
26
- if (!orderedList) {
27
- return false
28
- }
29
-
30
- const previousInlineObject = getPreviousInlineObject(snapshot)
31
-
32
- if (previousInlineObject) {
33
- return false
34
- }
35
-
36
- const match = event.matches.at(0)
37
-
38
- if (!match) {
39
- return false
40
- }
41
-
42
- return {match, orderedList}
43
- },
44
- actions: [
45
- ({event}, {match, orderedList}) => [
46
- raise({
47
- type: 'block.unset',
48
- props: ['style'],
49
- at: event.focusBlock.path,
50
- }),
51
- raise({
52
- type: 'block.set',
53
- props: {
54
- listItem: orderedList,
55
- level: event.focusBlock.node.level ?? 1,
56
- },
57
- at: event.focusBlock.path,
58
- }),
59
- raise({
60
- type: 'delete',
61
- at: match.targetOffsets,
62
- }),
63
- ],
64
- ],
65
- })
66
- }
@@ -1,66 +0,0 @@
1
- import type {EditorContext} from '@portabletext/editor'
2
- import {raise} from '@portabletext/editor/behaviors'
3
- import {getPreviousInlineObject} from '@portabletext/editor/selectors'
4
- import {defineInputRule} from '@portabletext/plugin-input-rule'
5
-
6
- export function createUnorderedListRule(config: {
7
- unorderedList: ({
8
- context,
9
- schema,
10
- }: {
11
- context: Pick<EditorContext, 'schema'>
12
- /**
13
- * @deprecated Use `context.schema` instead
14
- */
15
- schema: EditorContext['schema']
16
- }) => string | undefined
17
- }) {
18
- return defineInputRule({
19
- on: /^(-|\*) /,
20
- guard: ({snapshot, event}) => {
21
- const unorderedList = config.unorderedList({
22
- context: {schema: snapshot.context.schema},
23
- schema: snapshot.context.schema,
24
- })
25
-
26
- if (!unorderedList) {
27
- return false
28
- }
29
-
30
- const previousInlineObject = getPreviousInlineObject(snapshot)
31
-
32
- if (previousInlineObject) {
33
- return false
34
- }
35
-
36
- const match = event.matches.at(0)
37
-
38
- if (!match) {
39
- return false
40
- }
41
-
42
- return {match, unorderedList}
43
- },
44
- actions: [
45
- ({event}, {match, unorderedList}) => [
46
- raise({
47
- type: 'block.unset',
48
- props: ['style'],
49
- at: event.focusBlock.path,
50
- }),
51
- raise({
52
- type: 'block.set',
53
- props: {
54
- listItem: unorderedList,
55
- level: event.focusBlock.node.level ?? 1,
56
- },
57
- at: event.focusBlock.path,
58
- }),
59
- raise({
60
- type: 'delete',
61
- at: match.targetOffsets,
62
- }),
63
- ],
64
- ],
65
- })
66
- }