@portabletext/plugin-input-rule 0.1.3 → 0.3.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.
- package/dist/index.cjs +76 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -12
- package/dist/index.d.ts +28 -12
- package/dist/index.js +78 -46
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/edge-cases.feature +88 -0
- package/src/edge-cases.test.tsx +31 -0
- package/src/input-rule-match-location.ts +50 -7
- package/src/input-rule.ts +10 -5
- package/src/plugin.input-rule.tsx +10 -6
- package/src/rule.markdown-link.feature +54 -0
- package/src/rule.markdown-link.test.tsx +46 -0
- package/src/rule.markdown-link.ts +98 -0
- package/src/text-transform-rule.ts +56 -45
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type {EditorSchema} from '@portabletext/editor'
|
|
2
|
+
import {raise, type BehaviorAction} from '@portabletext/editor/behaviors'
|
|
3
|
+
import {defineInputRule} from './input-rule'
|
|
4
|
+
|
|
5
|
+
export function createMarkdownLinkRule(config: {
|
|
6
|
+
linkObject: (context: {
|
|
7
|
+
schema: EditorSchema
|
|
8
|
+
href: string
|
|
9
|
+
}) => {name: string; value?: {[prop: string]: unknown}} | undefined
|
|
10
|
+
}) {
|
|
11
|
+
return defineInputRule({
|
|
12
|
+
on: /\[(.+)]\((.+)\)/,
|
|
13
|
+
actions: [
|
|
14
|
+
({snapshot, event}) => {
|
|
15
|
+
const newText = event.textBefore + event.textInserted
|
|
16
|
+
let textLengthDelta = 0
|
|
17
|
+
const actions: Array<BehaviorAction> = []
|
|
18
|
+
|
|
19
|
+
for (const match of event.matches.reverse()) {
|
|
20
|
+
const textMatch = match.groupMatches.at(0)
|
|
21
|
+
const hrefMatch = match.groupMatches.at(1)
|
|
22
|
+
|
|
23
|
+
if (textMatch === undefined || hrefMatch === undefined) {
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
textLengthDelta =
|
|
28
|
+
textLengthDelta -
|
|
29
|
+
(match.targetOffsets.focus.offset -
|
|
30
|
+
match.targetOffsets.anchor.offset -
|
|
31
|
+
textMatch.text.length)
|
|
32
|
+
|
|
33
|
+
const linkObject = config.linkObject({
|
|
34
|
+
schema: snapshot.context.schema,
|
|
35
|
+
href: hrefMatch.text,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
if (!linkObject) {
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const leftSideOffsets = {
|
|
43
|
+
anchor: match.targetOffsets.anchor,
|
|
44
|
+
focus: textMatch.targetOffsets.anchor,
|
|
45
|
+
}
|
|
46
|
+
const rightSideOffsets = {
|
|
47
|
+
anchor: textMatch.targetOffsets.focus,
|
|
48
|
+
focus: match.targetOffsets.focus,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
actions.push(
|
|
52
|
+
raise({
|
|
53
|
+
type: 'select',
|
|
54
|
+
at: textMatch.targetOffsets,
|
|
55
|
+
}),
|
|
56
|
+
)
|
|
57
|
+
actions.push(
|
|
58
|
+
raise({
|
|
59
|
+
type: 'annotation.add',
|
|
60
|
+
annotation: {
|
|
61
|
+
name: linkObject.name,
|
|
62
|
+
value: linkObject.value ?? {},
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
)
|
|
66
|
+
actions.push(
|
|
67
|
+
raise({
|
|
68
|
+
type: 'delete',
|
|
69
|
+
at: rightSideOffsets,
|
|
70
|
+
}),
|
|
71
|
+
)
|
|
72
|
+
actions.push(
|
|
73
|
+
raise({
|
|
74
|
+
type: 'delete',
|
|
75
|
+
at: leftSideOffsets,
|
|
76
|
+
}),
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const endCaretPosition = {
|
|
81
|
+
path: event.focusTextBlock.path,
|
|
82
|
+
offset: newText.length - textLengthDelta * -1,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return [
|
|
86
|
+
...actions,
|
|
87
|
+
raise({
|
|
88
|
+
type: 'select',
|
|
89
|
+
at: {
|
|
90
|
+
anchor: endCaretPosition,
|
|
91
|
+
focus: endCaretPosition,
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
})
|
|
98
|
+
}
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import {raise} from '@portabletext/editor/behaviors'
|
|
1
|
+
import {raise, type BehaviorAction} from '@portabletext/editor/behaviors'
|
|
2
2
|
import {getMarkState} from '@portabletext/editor/selectors'
|
|
3
3
|
import type {InputRule, InputRuleGuard} from './input-rule'
|
|
4
|
+
import type {InputRuleMatchLocation} from './input-rule-match-location'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @alpha
|
|
7
8
|
*/
|
|
8
|
-
export type TextTransformRule = {
|
|
9
|
+
export type TextTransformRule<TGuardResponse = true> = {
|
|
9
10
|
on: RegExp
|
|
10
|
-
guard?: InputRuleGuard
|
|
11
|
-
transform: (
|
|
11
|
+
guard?: InputRuleGuard<TGuardResponse>
|
|
12
|
+
transform: (
|
|
13
|
+
{location}: {location: InputRuleMatchLocation},
|
|
14
|
+
guardResponse: TGuardResponse,
|
|
15
|
+
) => string
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
/**
|
|
@@ -25,59 +29,66 @@ export type TextTransformRule = {
|
|
|
25
29
|
*
|
|
26
30
|
* @alpha
|
|
27
31
|
*/
|
|
28
|
-
export function defineTextTransformRule
|
|
32
|
+
export function defineTextTransformRule<TGuardResponse = true>(
|
|
33
|
+
config: TextTransformRule<TGuardResponse>,
|
|
34
|
+
): InputRule<TGuardResponse> {
|
|
29
35
|
return {
|
|
30
36
|
on: config.on,
|
|
31
|
-
guard: config.guard ?? (() => true),
|
|
37
|
+
guard: config.guard ?? (() => true as TGuardResponse),
|
|
32
38
|
actions: [
|
|
33
|
-
({snapshot, event}) => {
|
|
34
|
-
const
|
|
39
|
+
({snapshot, event}, guardResponse) => {
|
|
40
|
+
const locations = event.matches.flatMap((match) =>
|
|
35
41
|
match.groupMatches.length === 0 ? [match] : match.groupMatches,
|
|
36
42
|
)
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
const newText = event.textBefore + event.textInserted
|
|
44
|
+
|
|
45
|
+
let textLengthDelta = 0
|
|
46
|
+
const actions: Array<BehaviorAction> = []
|
|
47
|
+
|
|
48
|
+
for (const location of locations.reverse()) {
|
|
49
|
+
const text = config.transform({location}, guardResponse)
|
|
50
|
+
|
|
51
|
+
textLengthDelta =
|
|
52
|
+
textLengthDelta -
|
|
53
|
+
(text.length -
|
|
54
|
+
(location.targetOffsets.focus.offset -
|
|
55
|
+
location.targetOffsets.anchor.offset))
|
|
56
|
+
|
|
57
|
+
actions.push(raise({type: 'select', at: location.targetOffsets}))
|
|
58
|
+
actions.push(raise({type: 'delete', at: location.targetOffsets}))
|
|
59
|
+
actions.push(
|
|
60
|
+
raise({
|
|
61
|
+
type: 'insert.child',
|
|
62
|
+
child: {
|
|
63
|
+
_type: snapshot.context.schema.span.name,
|
|
64
|
+
text,
|
|
65
|
+
marks:
|
|
66
|
+
getMarkState({
|
|
67
|
+
...snapshot,
|
|
68
|
+
context: {
|
|
69
|
+
...snapshot.context,
|
|
70
|
+
selection: {
|
|
71
|
+
anchor: location.selection.anchor,
|
|
72
|
+
focus: {
|
|
73
|
+
path: location.selection.focus.path,
|
|
74
|
+
offset: Math.min(
|
|
75
|
+
location.selection.focus.offset,
|
|
76
|
+
event.textBefore.length,
|
|
77
|
+
),
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
})?.marks ?? [],
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
43
84
|
)
|
|
44
|
-
}
|
|
85
|
+
}
|
|
45
86
|
|
|
46
|
-
const newText = event.textBefore + event.textInserted
|
|
47
87
|
const endCaretPosition = {
|
|
48
88
|
path: event.focusTextBlock.path,
|
|
49
89
|
offset: newText.length - textLengthDelta,
|
|
50
90
|
}
|
|
51
91
|
|
|
52
|
-
const actions = matches.reverse().flatMap((match) => [
|
|
53
|
-
raise({type: 'select', at: match.targetOffsets}),
|
|
54
|
-
raise({type: 'delete', at: match.targetOffsets}),
|
|
55
|
-
raise({
|
|
56
|
-
type: 'insert.child',
|
|
57
|
-
child: {
|
|
58
|
-
_type: snapshot.context.schema.span.name,
|
|
59
|
-
text: config.transform(),
|
|
60
|
-
marks:
|
|
61
|
-
getMarkState({
|
|
62
|
-
...snapshot,
|
|
63
|
-
context: {
|
|
64
|
-
...snapshot.context,
|
|
65
|
-
selection: {
|
|
66
|
-
anchor: match.selection.anchor,
|
|
67
|
-
focus: {
|
|
68
|
-
path: match.selection.focus.path,
|
|
69
|
-
offset: Math.min(
|
|
70
|
-
match.selection.focus.offset,
|
|
71
|
-
event.textBefore.length,
|
|
72
|
-
),
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
})?.marks ?? [],
|
|
77
|
-
},
|
|
78
|
-
}),
|
|
79
|
-
])
|
|
80
|
-
|
|
81
92
|
return [
|
|
82
93
|
...actions,
|
|
83
94
|
raise({
|