@portabletext/plugin-character-pair-decorator 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-character-pair-decorator",
3
- "version": "4.0.22",
3
+ "version": "4.0.24",
4
4
  "description": "Automatically match a pair of characters and decorate the text in between",
5
5
  "keywords": [
6
6
  "portabletext",
@@ -32,30 +32,29 @@
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
38
  "@xstate/react": "^6.0.0",
40
- "react-compiler-runtime": "1.0.0",
39
+ "react-compiler-runtime": "^1.0.0",
41
40
  "remeda": "^2.32.0",
42
41
  "xstate": "^5.24.0"
43
42
  },
44
43
  "devDependencies": {
45
44
  "@sanity/tsconfig": "^1.0.0",
46
45
  "@types/react": "^19.2.7",
47
- "babel-plugin-react-compiler": "1.0.0",
46
+ "babel-plugin-react-compiler": "^1.0.0",
48
47
  "eslint": "^9.39.1",
49
- "eslint-plugin-react-hooks": "7.0.1",
48
+ "eslint-plugin-react-hooks": "^7.0.1",
50
49
  "react": "^19.2.1",
51
50
  "typescript": "5.9.3",
52
51
  "typescript-eslint": "^8.48.0",
53
52
  "vitest": "^4.0.14",
54
- "@portabletext/editor": "^3.3.2"
53
+ "@portabletext/editor": "^3.3.4"
55
54
  },
56
55
  "peerDependencies": {
57
56
  "react": "^18.3 || ^19",
58
- "@portabletext/editor": "^3.3.2"
57
+ "@portabletext/editor": "^3.3.4"
59
58
  },
60
59
  "engines": {
61
60
  "node": ">=20.19 <22 || >=22.12"
@@ -1,218 +0,0 @@
1
- import type {BlockOffset, EditorContext} from '@portabletext/editor'
2
- import {
3
- defineBehavior,
4
- effect,
5
- forward,
6
- raise,
7
- } from '@portabletext/editor/behaviors'
8
- import * as selectors from '@portabletext/editor/selectors'
9
- import * as utils from '@portabletext/editor/utils'
10
- import {createCharacterPairRegex} from './regex.character-pair'
11
-
12
- export function createCharacterPairDecoratorBehavior(config: {
13
- decorator: ({
14
- context,
15
- schema,
16
- }: {
17
- context: Pick<EditorContext, 'schema'>
18
- /**
19
- * @deprecated Use `context.schema` instead
20
- */
21
- schema: EditorContext['schema']
22
- }) => string | undefined
23
- pair: {char: string; amount: number}
24
- onDecorate: (offset: BlockOffset) => void
25
- }) {
26
- if (config.pair.amount < 1) {
27
- console.warn(
28
- `The amount of characters in the pair should be greater than 0`,
29
- )
30
- }
31
-
32
- const pairRegex = createCharacterPairRegex(
33
- config.pair.char,
34
- config.pair.amount,
35
- )
36
- const regEx = new RegExp(`(${pairRegex})$`)
37
-
38
- return defineBehavior({
39
- on: 'insert.text',
40
- guard: ({snapshot, event}) => {
41
- if (config.pair.amount < 1) {
42
- return false
43
- }
44
-
45
- const decorator = config.decorator({
46
- context: {schema: snapshot.context.schema},
47
- schema: snapshot.context.schema,
48
- })
49
-
50
- if (decorator === undefined) {
51
- return false
52
- }
53
-
54
- const focusTextBlock = selectors.getFocusTextBlock(snapshot)
55
- const selectionStartPoint = selectors.getSelectionStartPoint(snapshot)
56
- const selectionStartOffset = selectionStartPoint
57
- ? utils.spanSelectionPointToBlockOffset({
58
- context: snapshot.context,
59
- selectionPoint: selectionStartPoint,
60
- })
61
- : undefined
62
-
63
- if (!focusTextBlock || !selectionStartOffset) {
64
- return false
65
- }
66
-
67
- const textBefore = selectors.getBlockTextBefore(snapshot)
68
- const newText = `${textBefore}${event.text}`
69
- const textToDecorate = newText.match(regEx)?.at(0)
70
-
71
- if (textToDecorate === undefined) {
72
- return false
73
- }
74
-
75
- const prefixOffsets = {
76
- anchor: {
77
- path: focusTextBlock.path,
78
- // Example: "foo **bar**".length - "**bar**".length = 4
79
- offset: newText.length - textToDecorate.length,
80
- },
81
- focus: {
82
- path: focusTextBlock.path,
83
- // Example: "foo **bar**".length - "**bar**".length + "*".length * 2 = 6
84
- offset:
85
- newText.length -
86
- textToDecorate.length +
87
- config.pair.char.length * config.pair.amount,
88
- },
89
- }
90
-
91
- const suffixOffsets = {
92
- anchor: {
93
- path: focusTextBlock.path,
94
- // Example: "foo **bar*|" (10) + "*".length - 2 = 9
95
- offset:
96
- selectionStartOffset.offset +
97
- event.text.length -
98
- config.pair.char.length * config.pair.amount,
99
- },
100
- focus: {
101
- path: focusTextBlock.path,
102
- // Example: "foo **bar*|" (10) + "*".length = 11
103
- offset: selectionStartOffset.offset + event.text.length,
104
- },
105
- }
106
-
107
- // If the prefix is more than one character, then we need to check if
108
- // there is an inline object inside it
109
- if (prefixOffsets.focus.offset - prefixOffsets.anchor.offset > 1) {
110
- const prefixSelection = utils.blockOffsetsToSelection({
111
- context: snapshot.context,
112
- offsets: prefixOffsets,
113
- })
114
- const inlineObjectBeforePrefixFocus = selectors.getPreviousInlineObject(
115
- {
116
- ...snapshot,
117
- context: {
118
- ...snapshot.context,
119
- selection: prefixSelection
120
- ? {
121
- anchor: prefixSelection.focus,
122
- focus: prefixSelection.focus,
123
- }
124
- : null,
125
- },
126
- },
127
- )
128
- const inlineObjectBeforePrefixFocusOffset =
129
- inlineObjectBeforePrefixFocus
130
- ? utils.childSelectionPointToBlockOffset({
131
- context: snapshot.context,
132
- selectionPoint: {
133
- path: inlineObjectBeforePrefixFocus.path,
134
- offset: 0,
135
- },
136
- })
137
- : undefined
138
-
139
- if (
140
- inlineObjectBeforePrefixFocusOffset &&
141
- inlineObjectBeforePrefixFocusOffset.offset >
142
- prefixOffsets.anchor.offset &&
143
- inlineObjectBeforePrefixFocusOffset.offset <
144
- prefixOffsets.focus.offset
145
- ) {
146
- return false
147
- }
148
- }
149
-
150
- // If the suffix is more than one character, then we need to check if
151
- // there is an inline object inside it
152
- if (suffixOffsets.focus.offset - suffixOffsets.anchor.offset > 1) {
153
- const previousInlineObject = selectors.getPreviousInlineObject(snapshot)
154
- const previousInlineObjectOffset = previousInlineObject
155
- ? utils.childSelectionPointToBlockOffset({
156
- context: snapshot.context,
157
- selectionPoint: {
158
- path: previousInlineObject.path,
159
- offset: 0,
160
- },
161
- })
162
- : undefined
163
-
164
- if (
165
- previousInlineObjectOffset &&
166
- previousInlineObjectOffset.offset > suffixOffsets.anchor.offset &&
167
- previousInlineObjectOffset.offset < suffixOffsets.focus.offset
168
- ) {
169
- return false
170
- }
171
- }
172
-
173
- return {
174
- prefixOffsets,
175
- suffixOffsets,
176
- decorator,
177
- }
178
- },
179
- actions: [
180
- // Insert the text as usual in its own undo step
181
- ({event}) => [forward(event)],
182
- (_, {prefixOffsets, suffixOffsets, decorator}) => [
183
- // Decorate the text between the prefix and suffix
184
- raise({
185
- type: 'decorator.add',
186
- decorator,
187
- at: {
188
- anchor: prefixOffsets.focus,
189
- focus: suffixOffsets.anchor,
190
- },
191
- }),
192
- // Delete the suffix
193
- raise({
194
- type: 'delete.text',
195
- at: suffixOffsets,
196
- }),
197
- // Delete the prefix
198
- raise({
199
- type: 'delete.text',
200
- at: prefixOffsets,
201
- }),
202
- // Toggle the decorator off so the next inserted text isn't emphasized
203
- raise({
204
- type: 'decorator.remove',
205
- decorator,
206
- }),
207
- effect(() => {
208
- config.onDecorate({
209
- ...suffixOffsets.anchor,
210
- offset:
211
- suffixOffsets.anchor.offset -
212
- (prefixOffsets.focus.offset - prefixOffsets.anchor.offset),
213
- })
214
- }),
215
- ],
216
- ],
217
- })
218
- }
package/src/index.ts DELETED
@@ -1 +0,0 @@
1
- export * from './plugin.character-pair-decorator'
@@ -1,275 +0,0 @@
1
- import type {BlockOffset, Editor, EditorContext} from '@portabletext/editor'
2
- import {useEditor} from '@portabletext/editor'
3
- import {
4
- defineBehavior,
5
- effect,
6
- forward,
7
- raise,
8
- } from '@portabletext/editor/behaviors'
9
- import * as utils from '@portabletext/editor/utils'
10
- import {useActorRef} from '@xstate/react'
11
- import {isDeepEqual} from 'remeda'
12
- import {
13
- assign,
14
- fromCallback,
15
- setup,
16
- type AnyEventObject,
17
- type CallbackLogicFunction,
18
- } from 'xstate'
19
- import {createCharacterPairDecoratorBehavior} from './behavior.character-pair-decorator'
20
-
21
- /**
22
- * @public
23
- */
24
- export function CharacterPairDecoratorPlugin(props: {
25
- decorator: ({
26
- context,
27
- schema,
28
- }: {
29
- context: Pick<EditorContext, 'schema'>
30
- /**
31
- * @deprecated Use `context.schema` instead
32
- */
33
- schema: EditorContext['schema']
34
- }) => string | undefined
35
- pair: {char: string; amount: number}
36
- }) {
37
- const editor = useEditor()
38
-
39
- useActorRef(decoratorPairMachine, {
40
- input: {
41
- editor,
42
- decorator: props.decorator,
43
- pair: props.pair,
44
- },
45
- })
46
-
47
- return null
48
- }
49
-
50
- type DecoratorPairEvent =
51
- | {
52
- type: 'decorator.add'
53
- blockOffset: BlockOffset
54
- }
55
- | {
56
- type: 'selection'
57
- blockOffsets?: {
58
- anchor: BlockOffset
59
- focus: BlockOffset
60
- }
61
- }
62
- | {
63
- type: 'delete.backward'
64
- }
65
-
66
- const decorateListener: CallbackLogicFunction<
67
- AnyEventObject,
68
- DecoratorPairEvent,
69
- {
70
- decorator: ({
71
- context,
72
- schema,
73
- }: {
74
- context: Pick<EditorContext, 'schema'>
75
- /**
76
- * @deprecated Use `context.schema` instead
77
- */
78
- schema: EditorContext['schema']
79
- }) => string | undefined
80
- editor: Editor
81
- pair: {char: string; amount: number}
82
- }
83
- > = ({sendBack, input}) => {
84
- const unregister = input.editor.registerBehavior({
85
- behavior: createCharacterPairDecoratorBehavior({
86
- decorator: input.decorator,
87
- pair: input.pair,
88
- onDecorate: (offset) => {
89
- sendBack({type: 'decorator.add', blockOffset: offset})
90
- },
91
- }),
92
- })
93
-
94
- return unregister
95
- }
96
-
97
- const selectionListenerCallback: CallbackLogicFunction<
98
- AnyEventObject,
99
- DecoratorPairEvent,
100
- {editor: Editor}
101
- > = ({sendBack, input}) => {
102
- const unregister = input.editor.registerBehavior({
103
- behavior: defineBehavior({
104
- on: 'select',
105
- guard: ({snapshot, event}) => {
106
- if (!event.at) {
107
- return {blockOffsets: undefined}
108
- }
109
-
110
- const anchor = utils.spanSelectionPointToBlockOffset({
111
- context: snapshot.context,
112
- selectionPoint: event.at.anchor,
113
- })
114
- const focus = utils.spanSelectionPointToBlockOffset({
115
- context: snapshot.context,
116
- selectionPoint: event.at.focus,
117
- })
118
-
119
- if (!anchor || !focus) {
120
- return {blockOffsets: undefined}
121
- }
122
-
123
- return {
124
- blockOffsets: {
125
- anchor,
126
- focus,
127
- },
128
- }
129
- },
130
- actions: [
131
- ({event}, {blockOffsets}) => [
132
- {
133
- type: 'effect',
134
- effect: () => {
135
- sendBack({type: 'selection', blockOffsets})
136
- },
137
- },
138
- forward(event),
139
- ],
140
- ],
141
- }),
142
- })
143
-
144
- return unregister
145
- }
146
-
147
- const deleteBackwardListenerCallback: CallbackLogicFunction<
148
- AnyEventObject,
149
- DecoratorPairEvent,
150
- {editor: Editor}
151
- > = ({sendBack, input}) => {
152
- const unregister = input.editor.registerBehavior({
153
- behavior: defineBehavior({
154
- on: 'delete.backward',
155
- actions: [
156
- () => [
157
- raise({
158
- type: 'history.undo',
159
- }),
160
- effect(() => {
161
- sendBack({type: 'delete.backward'})
162
- }),
163
- ],
164
- ],
165
- }),
166
- })
167
-
168
- return unregister
169
- }
170
-
171
- const decoratorPairMachine = setup({
172
- types: {
173
- context: {} as {
174
- decorator: ({
175
- context,
176
- schema,
177
- }: {
178
- context: Pick<EditorContext, 'schema'>
179
- /**
180
- * @deprecated Use `context.schema` instead
181
- */
182
- schema: EditorContext['schema']
183
- }) => string | undefined
184
- editor: Editor
185
- offsetAfterDecorator?: BlockOffset
186
- pair: {char: string; amount: number}
187
- },
188
- input: {} as {
189
- decorator: ({
190
- context,
191
- schema,
192
- }: {
193
- context: Pick<EditorContext, 'schema'>
194
- /**
195
- * @deprecated Use `context.schema` instead
196
- */
197
- schema: EditorContext['schema']
198
- }) => string | undefined
199
- editor: Editor
200
- pair: {char: string; amount: number}
201
- },
202
- events: {} as DecoratorPairEvent,
203
- },
204
- actors: {
205
- 'decorate listener': fromCallback(decorateListener),
206
- 'delete.backward listener': fromCallback(deleteBackwardListenerCallback),
207
- 'selection listener': fromCallback(selectionListenerCallback),
208
- },
209
- }).createMachine({
210
- id: 'decorator pair',
211
- context: ({input}) => ({
212
- decorator: input.decorator,
213
- editor: input.editor,
214
- pair: input.pair,
215
- }),
216
- initial: 'idle',
217
- states: {
218
- 'idle': {
219
- invoke: [
220
- {
221
- src: 'decorate listener',
222
- input: ({context}) => ({
223
- decorator: context.decorator,
224
- editor: context.editor,
225
- pair: context.pair,
226
- }),
227
- },
228
- ],
229
- on: {
230
- 'decorator.add': {
231
- target: 'decorator added',
232
- actions: assign({
233
- offsetAfterDecorator: ({event}) => event.blockOffset,
234
- }),
235
- },
236
- },
237
- },
238
- 'decorator added': {
239
- exit: [
240
- assign({
241
- offsetAfterDecorator: undefined,
242
- }),
243
- ],
244
- invoke: [
245
- {
246
- src: 'selection listener',
247
- input: ({context}) => ({editor: context.editor}),
248
- },
249
- {
250
- src: 'delete.backward listener',
251
- input: ({context}) => ({editor: context.editor}),
252
- },
253
- ],
254
- on: {
255
- 'selection': {
256
- target: 'idle',
257
- guard: ({context, event}) => {
258
- const selectionChanged = !isDeepEqual(
259
- {
260
- anchor: context.offsetAfterDecorator,
261
- focus: context.offsetAfterDecorator,
262
- },
263
- event.blockOffsets,
264
- )
265
-
266
- return selectionChanged
267
- },
268
- },
269
- 'delete.backward': {
270
- target: 'idle',
271
- },
272
- },
273
- },
274
- },
275
- })
@@ -1,74 +0,0 @@
1
- import {expect, test} from 'vitest'
2
- import {createCharacterPairRegex} from './regex.character-pair'
3
-
4
- const italicRegex = new RegExp(
5
- `(${createCharacterPairRegex('*', 1)}|${createCharacterPairRegex('_', 1)})$`,
6
- )
7
- function getTextToItalic(text: string) {
8
- return text.match(italicRegex)?.at(0)
9
- }
10
-
11
- const boldRegex = new RegExp(
12
- `(${createCharacterPairRegex('*', 2)}|${createCharacterPairRegex('_', 2)})$`,
13
- )
14
- function getTextToBold(text: string) {
15
- return text.match(boldRegex)?.at(0)
16
- }
17
-
18
- test(getTextToItalic.name, () => {
19
- expect(getTextToItalic('Hello *world*')).toBe('*world*')
20
- expect(getTextToItalic('Hello _world_')).toBe('_world_')
21
- expect(getTextToItalic('*Hello*world*')).toBe('*world*')
22
- expect(getTextToItalic('_Hello_world_')).toBe('_world_')
23
-
24
- expect(getTextToItalic('* Hello world *')).toBe(undefined)
25
- expect(getTextToItalic('* Hello world*')).toBe(undefined)
26
- expect(getTextToItalic('*Hello world *')).toBe(undefined)
27
- expect(getTextToItalic('_ Hello world _')).toBe(undefined)
28
- expect(getTextToItalic('_ Hello world_')).toBe(undefined)
29
- expect(getTextToItalic('_Hello world _')).toBe(undefined)
30
-
31
- expect(getTextToItalic('Hello *world')).toBe(undefined)
32
- expect(getTextToItalic('Hello world*')).toBe(undefined)
33
- expect(getTextToItalic('Hello *world* *')).toBe(undefined)
34
-
35
- expect(getTextToItalic('_Hello*world_')).toBe('_Hello*world_')
36
- expect(getTextToItalic('*Hello_world*')).toBe('*Hello_world*')
37
-
38
- expect(getTextToItalic('*hello\nworld*')).toBe(undefined)
39
- expect(getTextToItalic('_hello\nworld_')).toBe(undefined)
40
-
41
- expect(getTextToItalic('*')).toBe(undefined)
42
- expect(getTextToItalic('_')).toBe(undefined)
43
- expect(getTextToItalic('**')).toBe(undefined)
44
- expect(getTextToItalic('__')).toBe(undefined)
45
- })
46
-
47
- test(getTextToBold.name, () => {
48
- expect(getTextToBold('Hello **world**')).toBe('**world**')
49
- expect(getTextToBold('Hello __world__')).toBe('__world__')
50
- expect(getTextToBold('**Hello**world**')).toBe('**world**')
51
- expect(getTextToBold('__Hello__world__')).toBe('__world__')
52
-
53
- expect(getTextToBold('** Hello world **')).toBe(undefined)
54
- expect(getTextToBold('** Hello world**')).toBe(undefined)
55
- expect(getTextToBold('**Hello world **')).toBe(undefined)
56
- expect(getTextToBold('__ Hello world __')).toBe(undefined)
57
- expect(getTextToBold('__ Hello world__')).toBe(undefined)
58
- expect(getTextToBold('__Hello world __')).toBe(undefined)
59
-
60
- expect(getTextToBold('Hello **world')).toBe(undefined)
61
- expect(getTextToBold('Hello world**')).toBe(undefined)
62
- expect(getTextToBold('Hello **world** **')).toBe(undefined)
63
-
64
- expect(getTextToBold('__Hello**world__')).toBe('__Hello**world__')
65
- expect(getTextToBold('**Hello__world**')).toBe('**Hello__world**')
66
-
67
- expect(getTextToBold('**hello\nworld**')).toBe(undefined)
68
- expect(getTextToBold('__hello\nworld__')).toBe(undefined)
69
-
70
- expect(getTextToBold('**')).toBe(undefined)
71
- expect(getTextToBold('__')).toBe(undefined)
72
- expect(getTextToBold('****')).toBe(undefined)
73
- expect(getTextToBold('____')).toBe(undefined)
74
- })
@@ -1,24 +0,0 @@
1
- export function createCharacterPairRegex(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
- }