@portabletext/plugin-character-pair-decorator 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2016 - 2025 Sanity.io
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # `@portabletext/plugin-character-pair-decorator`
2
+
3
+ > Automatically match a pair of characters and decorate the text in between
4
+
5
+ Import the `CharacterPairDecoratorPlugin` React component and place it inside the `EditorProvider` to automatically register the necessary Behaviors:
6
+
7
+ ```tsx
8
+ import {
9
+ defineSchema,
10
+ EditorProvider,
11
+ PortableTextEditable,
12
+ } from '@portabletext/editor'
13
+ import {CharacterPairDecoratorPlugin} from '@portabletext/plugin-character-pair-decorator'
14
+
15
+ function App() {
16
+ return (
17
+ <EditorProvider
18
+ initialConfig={{
19
+ schemaDefinition: defineSchema({
20
+ decorators: [{name: 'italic'}],
21
+ }),
22
+ }}
23
+ >
24
+ <PortableTextEditable />
25
+ <CharacterPairDecoratorPlugin
26
+ decorator={({schema}) =>
27
+ schema.decorators.find((d) => d.name === 'italic')?.name
28
+ }
29
+ pair={{char: '#', amount: 1}}
30
+ />
31
+ </EditorProvider>
32
+ )
33
+ }
34
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,291 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: !0 });
3
+ var editor = require("@portabletext/editor"), behaviors = require("@portabletext/editor/behaviors"), utils = require("@portabletext/editor/utils"), react = require("@xstate/react"), remeda = require("remeda"), xstate = require("xstate"), selectors = require("@portabletext/editor/selectors");
4
+ function _interopNamespaceCompat(e) {
5
+ if (e && typeof e == "object" && "default" in e) return e;
6
+ var n = /* @__PURE__ */ Object.create(null);
7
+ return e && Object.keys(e).forEach(function(k) {
8
+ if (k !== "default") {
9
+ var d = Object.getOwnPropertyDescriptor(e, k);
10
+ Object.defineProperty(n, k, d.get ? d : {
11
+ enumerable: !0,
12
+ get: function() {
13
+ return e[k];
14
+ }
15
+ });
16
+ }
17
+ }), n.default = e, Object.freeze(n);
18
+ }
19
+ var utils__namespace = /* @__PURE__ */ _interopNamespaceCompat(utils), selectors__namespace = /* @__PURE__ */ _interopNamespaceCompat(selectors);
20
+ function createCharacterPairRegex(char, amount) {
21
+ const prePrefix = `(?<!\\${char})`, prefix = `\\${char}`.repeat(Math.max(amount, 1)), postPrefix = "(?!\\s)", content = `([^${char}\\n]+?)`, preSuffix = "(?<!\\s)", suffix = `\\${char}`.repeat(Math.max(amount, 1)), postSuffix = `(?!\\${char})`;
22
+ return `${prePrefix}${prefix}${postPrefix}${content}${preSuffix}${suffix}${postSuffix}`;
23
+ }
24
+ function createCharacterPairDecoratorBehavior(config) {
25
+ config.pair.amount < 1 && console.warn(
26
+ "The amount of characters in the pair should be greater than 0"
27
+ );
28
+ const pairRegex = createCharacterPairRegex(
29
+ config.pair.char,
30
+ config.pair.amount
31
+ ), regEx = new RegExp(`(${pairRegex})$`);
32
+ return behaviors.defineBehavior({
33
+ on: "insert.text",
34
+ guard: ({ snapshot, event }) => {
35
+ if (config.pair.amount < 1)
36
+ return !1;
37
+ const decorator = config.decorator({ schema: snapshot.context.schema });
38
+ if (decorator === void 0)
39
+ return !1;
40
+ const focusTextBlock = selectors__namespace.getFocusTextBlock(snapshot), selectionStartPoint = selectors__namespace.getSelectionStartPoint(snapshot), selectionStartOffset = selectionStartPoint ? utils__namespace.spanSelectionPointToBlockOffset({
41
+ value: snapshot.context.value,
42
+ selectionPoint: selectionStartPoint
43
+ }) : void 0;
44
+ if (!focusTextBlock || !selectionStartOffset)
45
+ return !1;
46
+ const newText = `${selectors__namespace.getBlockTextBefore(snapshot)}${event.text}`, textToDecorate = newText.match(regEx)?.at(0);
47
+ if (textToDecorate === void 0)
48
+ return !1;
49
+ const prefixOffsets = {
50
+ anchor: {
51
+ path: focusTextBlock.path,
52
+ // Example: "foo **bar**".length - "**bar**".length = 4
53
+ offset: newText.length - textToDecorate.length
54
+ },
55
+ focus: {
56
+ path: focusTextBlock.path,
57
+ // Example: "foo **bar**".length - "**bar**".length + "*".length * 2 = 6
58
+ offset: newText.length - textToDecorate.length + config.pair.char.length * config.pair.amount
59
+ }
60
+ }, suffixOffsets = {
61
+ anchor: {
62
+ path: focusTextBlock.path,
63
+ // Example: "foo **bar*|" (10) + "*".length - 2 = 9
64
+ offset: selectionStartOffset.offset + event.text.length - config.pair.char.length * config.pair.amount
65
+ },
66
+ focus: {
67
+ path: focusTextBlock.path,
68
+ // Example: "foo **bar*|" (10) + "*".length = 11
69
+ offset: selectionStartOffset.offset + event.text.length
70
+ }
71
+ };
72
+ if (prefixOffsets.focus.offset - prefixOffsets.anchor.offset > 1) {
73
+ const prefixSelection = utils__namespace.blockOffsetsToSelection({
74
+ value: snapshot.context.value,
75
+ offsets: prefixOffsets
76
+ }), inlineObjectBeforePrefixFocus = selectors__namespace.getPreviousInlineObject(
77
+ {
78
+ ...snapshot,
79
+ context: {
80
+ ...snapshot.context,
81
+ selection: prefixSelection ? {
82
+ anchor: prefixSelection.focus,
83
+ focus: prefixSelection.focus
84
+ } : null
85
+ }
86
+ }
87
+ ), inlineObjectBeforePrefixFocusOffset = inlineObjectBeforePrefixFocus ? utils__namespace.childSelectionPointToBlockOffset({
88
+ value: snapshot.context.value,
89
+ selectionPoint: {
90
+ path: inlineObjectBeforePrefixFocus.path,
91
+ offset: 0
92
+ }
93
+ }) : void 0;
94
+ if (inlineObjectBeforePrefixFocusOffset && inlineObjectBeforePrefixFocusOffset.offset > prefixOffsets.anchor.offset && inlineObjectBeforePrefixFocusOffset.offset < prefixOffsets.focus.offset)
95
+ return !1;
96
+ }
97
+ if (suffixOffsets.focus.offset - suffixOffsets.anchor.offset > 1) {
98
+ const previousInlineObject = selectors__namespace.getPreviousInlineObject(snapshot), previousInlineObjectOffset = previousInlineObject ? utils__namespace.childSelectionPointToBlockOffset({
99
+ value: snapshot.context.value,
100
+ selectionPoint: {
101
+ path: previousInlineObject.path,
102
+ offset: 0
103
+ }
104
+ }) : void 0;
105
+ if (previousInlineObjectOffset && previousInlineObjectOffset.offset > suffixOffsets.anchor.offset && previousInlineObjectOffset.offset < suffixOffsets.focus.offset)
106
+ return !1;
107
+ }
108
+ return {
109
+ prefixOffsets,
110
+ suffixOffsets,
111
+ decorator
112
+ };
113
+ },
114
+ actions: [
115
+ // Insert the text as usual in its own undo step
116
+ ({ event }) => [behaviors.execute(event)],
117
+ (_, { prefixOffsets, suffixOffsets, decorator }) => [
118
+ // Decorate the text between the prefix and suffix
119
+ behaviors.execute({
120
+ type: "decorator.add",
121
+ decorator,
122
+ at: {
123
+ anchor: prefixOffsets.focus,
124
+ focus: suffixOffsets.anchor
125
+ }
126
+ }),
127
+ // Delete the suffix
128
+ behaviors.execute({
129
+ type: "delete.text",
130
+ at: suffixOffsets
131
+ }),
132
+ // Delete the prefix
133
+ behaviors.execute({
134
+ type: "delete.text",
135
+ at: prefixOffsets
136
+ }),
137
+ // Toggle the decorator off so the next inserted text isn't emphasized
138
+ behaviors.execute({
139
+ type: "decorator.remove",
140
+ decorator
141
+ }),
142
+ behaviors.effect(() => {
143
+ config.onDecorate({
144
+ ...suffixOffsets.anchor,
145
+ offset: suffixOffsets.anchor.offset - (prefixOffsets.focus.offset - prefixOffsets.anchor.offset)
146
+ });
147
+ })
148
+ ]
149
+ ]
150
+ });
151
+ }
152
+ function CharacterPairDecoratorPlugin(config) {
153
+ const editor$1 = editor.useEditor();
154
+ return react.useActorRef(decoratorPairMachine, {
155
+ input: {
156
+ editor: editor$1,
157
+ decorator: config.decorator,
158
+ pair: config.pair
159
+ }
160
+ }), null;
161
+ }
162
+ const decorateListener = ({ sendBack, input }) => input.editor.registerBehavior({
163
+ behavior: createCharacterPairDecoratorBehavior({
164
+ decorator: input.decorator,
165
+ pair: input.pair,
166
+ onDecorate: (offset) => {
167
+ sendBack({ type: "decorator.add", blockOffset: offset });
168
+ }
169
+ })
170
+ }), selectionListenerCallback = ({ sendBack, input }) => input.editor.registerBehavior({
171
+ behavior: behaviors.defineBehavior({
172
+ on: "select",
173
+ guard: ({ snapshot, event }) => {
174
+ if (!event.at)
175
+ return { blockOffsets: void 0 };
176
+ const anchor = utils__namespace.spanSelectionPointToBlockOffset({
177
+ value: snapshot.context.value,
178
+ selectionPoint: event.at.anchor
179
+ }), focus = utils__namespace.spanSelectionPointToBlockOffset({
180
+ value: snapshot.context.value,
181
+ selectionPoint: event.at.focus
182
+ });
183
+ return !anchor || !focus ? { blockOffsets: void 0 } : {
184
+ blockOffsets: {
185
+ anchor,
186
+ focus
187
+ }
188
+ };
189
+ },
190
+ actions: [
191
+ ({ event }, { blockOffsets }) => [
192
+ {
193
+ type: "effect",
194
+ effect: () => {
195
+ sendBack({ type: "selection", blockOffsets });
196
+ }
197
+ },
198
+ behaviors.forward(event)
199
+ ]
200
+ ]
201
+ })
202
+ }), deleteBackwardListenerCallback = ({ sendBack, input }) => input.editor.registerBehavior({
203
+ behavior: behaviors.defineBehavior({
204
+ on: "delete.backward",
205
+ actions: [
206
+ () => [
207
+ behaviors.execute({
208
+ type: "history.undo"
209
+ }),
210
+ behaviors.effect(() => {
211
+ sendBack({ type: "delete.backward" });
212
+ })
213
+ ]
214
+ ]
215
+ })
216
+ }), decoratorPairMachine = xstate.setup({
217
+ types: {
218
+ context: {},
219
+ input: {},
220
+ events: {}
221
+ },
222
+ actors: {
223
+ "decorate listener": xstate.fromCallback(decorateListener),
224
+ "delete.backward listener": xstate.fromCallback(deleteBackwardListenerCallback),
225
+ "selection listener": xstate.fromCallback(selectionListenerCallback)
226
+ }
227
+ }).createMachine({
228
+ id: "decorator pair",
229
+ context: ({ input }) => ({
230
+ decorator: input.decorator,
231
+ editor: input.editor,
232
+ pair: input.pair
233
+ }),
234
+ initial: "idle",
235
+ states: {
236
+ idle: {
237
+ invoke: [
238
+ {
239
+ src: "decorate listener",
240
+ input: ({ context }) => ({
241
+ decorator: context.decorator,
242
+ editor: context.editor,
243
+ pair: context.pair
244
+ })
245
+ }
246
+ ],
247
+ on: {
248
+ "decorator.add": {
249
+ target: "decorator added",
250
+ actions: xstate.assign({
251
+ offsetAfterDecorator: ({ event }) => event.blockOffset
252
+ })
253
+ }
254
+ }
255
+ },
256
+ "decorator added": {
257
+ exit: [
258
+ xstate.assign({
259
+ offsetAfterDecorator: void 0
260
+ })
261
+ ],
262
+ invoke: [
263
+ {
264
+ src: "selection listener",
265
+ input: ({ context }) => ({ editor: context.editor })
266
+ },
267
+ {
268
+ src: "delete.backward listener",
269
+ input: ({ context }) => ({ editor: context.editor })
270
+ }
271
+ ],
272
+ on: {
273
+ selection: {
274
+ target: "idle",
275
+ guard: ({ context, event }) => !remeda.isDeepEqual(
276
+ {
277
+ anchor: context.offsetAfterDecorator,
278
+ focus: context.offsetAfterDecorator
279
+ },
280
+ event.blockOffsets
281
+ )
282
+ },
283
+ "delete.backward": {
284
+ target: "idle"
285
+ }
286
+ }
287
+ }
288
+ }
289
+ });
290
+ exports.CharacterPairDecoratorPlugin = CharacterPairDecoratorPlugin;
291
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":["../src/regex.character-pair.ts","../src/behavior.character-pair-decorator.ts","../src/plugin.character-pair-decorator.ts"],"sourcesContent":["export function createCharacterPairRegex(char: string, amount: number) {\n // Negative lookbehind: Ensures that the matched sequence is not preceded by the same character\n const prePrefix = `(?<!\\\\${char})`\n\n // Repeats the character `amount` times\n const prefix = `\\\\${char}`.repeat(Math.max(amount, 1))\n\n // Negative lookahead: Ensures that the opening pair (**, *, etc.) is not followed by a space\n const postPrefix = `(?!\\\\s)`\n\n // Captures the content inside the pair\n const content = `([^${char}\\\\n]+?)`\n\n // Negative lookbehind: Ensures that the content is not followed by a space\n const preSuffix = `(?<!\\\\s)`\n\n // Repeats the character `amount` times\n const suffix = `\\\\${char}`.repeat(Math.max(amount, 1))\n\n // Negative lookahead: Ensures that the matched sequence is not followed by the same character\n const postSuffix = `(?!\\\\${char})`\n\n return `${prePrefix}${prefix}${postPrefix}${content}${preSuffix}${suffix}${postSuffix}`\n}\n","import type {BlockOffset, EditorSchema} from '@portabletext/editor'\nimport {defineBehavior, effect, execute} from '@portabletext/editor/behaviors'\nimport * as selectors from '@portabletext/editor/selectors'\nimport * as utils from '@portabletext/editor/utils'\nimport {createCharacterPairRegex} from './regex.character-pair'\n\nexport function createCharacterPairDecoratorBehavior(config: {\n decorator: ({schema}: {schema: EditorSchema}) => string | undefined\n pair: {char: string; amount: number}\n onDecorate: (offset: BlockOffset) => void\n}) {\n if (config.pair.amount < 1) {\n console.warn(\n `The amount of characters in the pair should be greater than 0`,\n )\n }\n\n const pairRegex = createCharacterPairRegex(\n config.pair.char,\n config.pair.amount,\n )\n const regEx = new RegExp(`(${pairRegex})$`)\n\n return defineBehavior({\n on: 'insert.text',\n guard: ({snapshot, event}) => {\n if (config.pair.amount < 1) {\n return false\n }\n\n const decorator = config.decorator({schema: snapshot.context.schema})\n\n if (decorator === undefined) {\n return false\n }\n\n const focusTextBlock = selectors.getFocusTextBlock(snapshot)\n const selectionStartPoint = selectors.getSelectionStartPoint(snapshot)\n const selectionStartOffset = selectionStartPoint\n ? utils.spanSelectionPointToBlockOffset({\n value: snapshot.context.value,\n selectionPoint: selectionStartPoint,\n })\n : undefined\n\n if (!focusTextBlock || !selectionStartOffset) {\n return false\n }\n\n const textBefore = selectors.getBlockTextBefore(snapshot)\n const newText = `${textBefore}${event.text}`\n const textToDecorate = newText.match(regEx)?.at(0)\n\n if (textToDecorate === undefined) {\n return false\n }\n\n const prefixOffsets = {\n anchor: {\n path: focusTextBlock.path,\n // Example: \"foo **bar**\".length - \"**bar**\".length = 4\n offset: newText.length - textToDecorate.length,\n },\n focus: {\n path: focusTextBlock.path,\n // Example: \"foo **bar**\".length - \"**bar**\".length + \"*\".length * 2 = 6\n offset:\n newText.length -\n textToDecorate.length +\n config.pair.char.length * config.pair.amount,\n },\n }\n\n const suffixOffsets = {\n anchor: {\n path: focusTextBlock.path,\n // Example: \"foo **bar*|\" (10) + \"*\".length - 2 = 9\n offset:\n selectionStartOffset.offset +\n event.text.length -\n config.pair.char.length * config.pair.amount,\n },\n focus: {\n path: focusTextBlock.path,\n // Example: \"foo **bar*|\" (10) + \"*\".length = 11\n offset: selectionStartOffset.offset + event.text.length,\n },\n }\n\n // If the prefix is more than one character, then we need to check if\n // there is an inline object inside it\n if (prefixOffsets.focus.offset - prefixOffsets.anchor.offset > 1) {\n const prefixSelection = utils.blockOffsetsToSelection({\n value: snapshot.context.value,\n offsets: prefixOffsets,\n })\n const inlineObjectBeforePrefixFocus = selectors.getPreviousInlineObject(\n {\n ...snapshot,\n context: {\n ...snapshot.context,\n selection: prefixSelection\n ? {\n anchor: prefixSelection.focus,\n focus: prefixSelection.focus,\n }\n : null,\n },\n },\n )\n const inlineObjectBeforePrefixFocusOffset =\n inlineObjectBeforePrefixFocus\n ? utils.childSelectionPointToBlockOffset({\n value: snapshot.context.value,\n selectionPoint: {\n path: inlineObjectBeforePrefixFocus.path,\n offset: 0,\n },\n })\n : undefined\n\n if (\n inlineObjectBeforePrefixFocusOffset &&\n inlineObjectBeforePrefixFocusOffset.offset >\n prefixOffsets.anchor.offset &&\n inlineObjectBeforePrefixFocusOffset.offset <\n prefixOffsets.focus.offset\n ) {\n return false\n }\n }\n\n // If the suffix is more than one character, then we need to check if\n // there is an inline object inside it\n if (suffixOffsets.focus.offset - suffixOffsets.anchor.offset > 1) {\n const previousInlineObject = selectors.getPreviousInlineObject(snapshot)\n const previousInlineObjectOffset = previousInlineObject\n ? utils.childSelectionPointToBlockOffset({\n value: snapshot.context.value,\n selectionPoint: {\n path: previousInlineObject.path,\n offset: 0,\n },\n })\n : undefined\n\n if (\n previousInlineObjectOffset &&\n previousInlineObjectOffset.offset > suffixOffsets.anchor.offset &&\n previousInlineObjectOffset.offset < suffixOffsets.focus.offset\n ) {\n return false\n }\n }\n\n return {\n prefixOffsets,\n suffixOffsets,\n decorator,\n }\n },\n actions: [\n // Insert the text as usual in its own undo step\n ({event}) => [execute(event)],\n (_, {prefixOffsets, suffixOffsets, decorator}) => [\n // Decorate the text between the prefix and suffix\n execute({\n type: 'decorator.add',\n decorator,\n at: {\n anchor: prefixOffsets.focus,\n focus: suffixOffsets.anchor,\n },\n }),\n // Delete the suffix\n execute({\n type: 'delete.text',\n at: suffixOffsets,\n }),\n // Delete the prefix\n execute({\n type: 'delete.text',\n at: prefixOffsets,\n }),\n // Toggle the decorator off so the next inserted text isn't emphasized\n execute({\n type: 'decorator.remove',\n decorator,\n }),\n effect(() => {\n config.onDecorate({\n ...suffixOffsets.anchor,\n offset:\n suffixOffsets.anchor.offset -\n (prefixOffsets.focus.offset - prefixOffsets.anchor.offset),\n })\n }),\n ],\n ],\n })\n}\n","import type {BlockOffset, Editor, EditorSchema} from '@portabletext/editor'\nimport {useEditor} from '@portabletext/editor'\nimport {\n defineBehavior,\n effect,\n execute,\n forward,\n} from '@portabletext/editor/behaviors'\nimport * as utils from '@portabletext/editor/utils'\nimport {useActorRef} from '@xstate/react'\nimport {isDeepEqual} from 'remeda'\nimport {\n assign,\n fromCallback,\n setup,\n type AnyEventObject,\n type CallbackLogicFunction,\n} from 'xstate'\nimport {createCharacterPairDecoratorBehavior} from './behavior.character-pair-decorator'\n\n/**\n * @beta\n */\nexport function CharacterPairDecoratorPlugin(config: {\n decorator: ({schema}: {schema: EditorSchema}) => string | undefined\n pair: {char: string; amount: number}\n}) {\n const editor = useEditor()\n\n useActorRef(decoratorPairMachine, {\n input: {\n editor,\n decorator: config.decorator,\n pair: config.pair,\n },\n })\n\n return null\n}\n\ntype DecoratorPairEvent =\n | {\n type: 'decorator.add'\n blockOffset: BlockOffset\n }\n | {\n type: 'selection'\n blockOffsets?: {\n anchor: BlockOffset\n focus: BlockOffset\n }\n }\n | {\n type: 'delete.backward'\n }\n\nconst decorateListener: CallbackLogicFunction<\n AnyEventObject,\n DecoratorPairEvent,\n {\n decorator: ({schema}: {schema: EditorSchema}) => string | undefined\n editor: Editor\n pair: {char: string; amount: number}\n }\n> = ({sendBack, input}) => {\n const unregister = input.editor.registerBehavior({\n behavior: createCharacterPairDecoratorBehavior({\n decorator: input.decorator,\n pair: input.pair,\n onDecorate: (offset) => {\n sendBack({type: 'decorator.add', blockOffset: offset})\n },\n }),\n })\n\n return unregister\n}\n\nconst selectionListenerCallback: CallbackLogicFunction<\n AnyEventObject,\n DecoratorPairEvent,\n {editor: Editor}\n> = ({sendBack, input}) => {\n const unregister = input.editor.registerBehavior({\n behavior: defineBehavior({\n on: 'select',\n guard: ({snapshot, event}) => {\n if (!event.at) {\n return {blockOffsets: undefined}\n }\n\n const anchor = utils.spanSelectionPointToBlockOffset({\n value: snapshot.context.value,\n selectionPoint: event.at.anchor,\n })\n const focus = utils.spanSelectionPointToBlockOffset({\n value: snapshot.context.value,\n selectionPoint: event.at.focus,\n })\n\n if (!anchor || !focus) {\n return {blockOffsets: undefined}\n }\n\n return {\n blockOffsets: {\n anchor,\n focus,\n },\n }\n },\n actions: [\n ({event}, {blockOffsets}) => [\n {\n type: 'effect',\n effect: () => {\n sendBack({type: 'selection', blockOffsets})\n },\n },\n forward(event),\n ],\n ],\n }),\n })\n\n return unregister\n}\n\nconst deleteBackwardListenerCallback: CallbackLogicFunction<\n AnyEventObject,\n DecoratorPairEvent,\n {editor: Editor}\n> = ({sendBack, input}) => {\n const unregister = input.editor.registerBehavior({\n behavior: defineBehavior({\n on: 'delete.backward',\n actions: [\n () => [\n execute({\n type: 'history.undo',\n }),\n effect(() => {\n sendBack({type: 'delete.backward'})\n }),\n ],\n ],\n }),\n })\n\n return unregister\n}\n\nconst decoratorPairMachine = setup({\n types: {\n context: {} as {\n decorator: ({schema}: {schema: EditorSchema}) => string | undefined\n editor: Editor\n offsetAfterDecorator?: BlockOffset\n pair: {char: string; amount: number}\n },\n input: {} as {\n decorator: ({schema}: {schema: EditorSchema}) => string | undefined\n editor: Editor\n pair: {char: string; amount: number}\n },\n events: {} as DecoratorPairEvent,\n },\n actors: {\n 'decorate listener': fromCallback(decorateListener),\n 'delete.backward listener': fromCallback(deleteBackwardListenerCallback),\n 'selection listener': fromCallback(selectionListenerCallback),\n },\n}).createMachine({\n id: 'decorator pair',\n context: ({input}) => ({\n decorator: input.decorator,\n editor: input.editor,\n pair: input.pair,\n }),\n initial: 'idle',\n states: {\n 'idle': {\n invoke: [\n {\n src: 'decorate listener',\n input: ({context}) => ({\n decorator: context.decorator,\n editor: context.editor,\n pair: context.pair,\n }),\n },\n ],\n on: {\n 'decorator.add': {\n target: 'decorator added',\n actions: assign({\n offsetAfterDecorator: ({event}) => event.blockOffset,\n }),\n },\n },\n },\n 'decorator added': {\n exit: [\n assign({\n offsetAfterDecorator: undefined,\n }),\n ],\n invoke: [\n {\n src: 'selection listener',\n input: ({context}) => ({editor: context.editor}),\n },\n {\n src: 'delete.backward listener',\n input: ({context}) => ({editor: context.editor}),\n },\n ],\n on: {\n 'selection': {\n target: 'idle',\n guard: ({context, event}) => {\n const selectionChanged = !isDeepEqual(\n {\n anchor: context.offsetAfterDecorator,\n focus: context.offsetAfterDecorator,\n },\n event.blockOffsets,\n )\n\n return selectionChanged\n },\n },\n 'delete.backward': {\n target: 'idle',\n },\n },\n },\n },\n})\n"],"names":["defineBehavior","selectors","utils","execute","effect","editor","useEditor","useActorRef","forward","setup","fromCallback","assign","isDeepEqual"],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type {EditorSchema} from '@portabletext/editor'
2
+
3
+ /**
4
+ * @beta
5
+ */
6
+ export declare function CharacterPairDecoratorPlugin(config: {
7
+ decorator: ({schema}: {schema: EditorSchema}) => string | undefined
8
+ pair: {
9
+ char: string
10
+ amount: number
11
+ }
12
+ }): null
13
+
14
+ export {}
@@ -0,0 +1,14 @@
1
+ import type {EditorSchema} from '@portabletext/editor'
2
+
3
+ /**
4
+ * @beta
5
+ */
6
+ export declare function CharacterPairDecoratorPlugin(config: {
7
+ decorator: ({schema}: {schema: EditorSchema}) => string | undefined
8
+ pair: {
9
+ char: string
10
+ amount: number
11
+ }
12
+ }): null
13
+
14
+ export {}
package/dist/index.js ADDED
@@ -0,0 +1,281 @@
1
+ import { useEditor } from "@portabletext/editor";
2
+ import { defineBehavior, execute, effect, forward } from "@portabletext/editor/behaviors";
3
+ import * as utils from "@portabletext/editor/utils";
4
+ import { useActorRef } from "@xstate/react";
5
+ import { isDeepEqual } from "remeda";
6
+ import { setup, fromCallback, assign } from "xstate";
7
+ import * as selectors from "@portabletext/editor/selectors";
8
+ function createCharacterPairRegex(char, amount) {
9
+ const prePrefix = `(?<!\\${char})`, prefix = `\\${char}`.repeat(Math.max(amount, 1)), postPrefix = "(?!\\s)", content = `([^${char}\\n]+?)`, preSuffix = "(?<!\\s)", suffix = `\\${char}`.repeat(Math.max(amount, 1)), postSuffix = `(?!\\${char})`;
10
+ return `${prePrefix}${prefix}${postPrefix}${content}${preSuffix}${suffix}${postSuffix}`;
11
+ }
12
+ function createCharacterPairDecoratorBehavior(config) {
13
+ config.pair.amount < 1 && console.warn(
14
+ "The amount of characters in the pair should be greater than 0"
15
+ );
16
+ const pairRegex = createCharacterPairRegex(
17
+ config.pair.char,
18
+ config.pair.amount
19
+ ), regEx = new RegExp(`(${pairRegex})$`);
20
+ return defineBehavior({
21
+ on: "insert.text",
22
+ guard: ({ snapshot, event }) => {
23
+ if (config.pair.amount < 1)
24
+ return !1;
25
+ const decorator = config.decorator({ schema: snapshot.context.schema });
26
+ if (decorator === void 0)
27
+ return !1;
28
+ const focusTextBlock = selectors.getFocusTextBlock(snapshot), selectionStartPoint = selectors.getSelectionStartPoint(snapshot), selectionStartOffset = selectionStartPoint ? utils.spanSelectionPointToBlockOffset({
29
+ value: snapshot.context.value,
30
+ selectionPoint: selectionStartPoint
31
+ }) : void 0;
32
+ if (!focusTextBlock || !selectionStartOffset)
33
+ return !1;
34
+ const newText = `${selectors.getBlockTextBefore(snapshot)}${event.text}`, textToDecorate = newText.match(regEx)?.at(0);
35
+ if (textToDecorate === void 0)
36
+ return !1;
37
+ const prefixOffsets = {
38
+ anchor: {
39
+ path: focusTextBlock.path,
40
+ // Example: "foo **bar**".length - "**bar**".length = 4
41
+ offset: newText.length - textToDecorate.length
42
+ },
43
+ focus: {
44
+ path: focusTextBlock.path,
45
+ // Example: "foo **bar**".length - "**bar**".length + "*".length * 2 = 6
46
+ offset: newText.length - textToDecorate.length + config.pair.char.length * config.pair.amount
47
+ }
48
+ }, suffixOffsets = {
49
+ anchor: {
50
+ path: focusTextBlock.path,
51
+ // Example: "foo **bar*|" (10) + "*".length - 2 = 9
52
+ offset: selectionStartOffset.offset + event.text.length - config.pair.char.length * config.pair.amount
53
+ },
54
+ focus: {
55
+ path: focusTextBlock.path,
56
+ // Example: "foo **bar*|" (10) + "*".length = 11
57
+ offset: selectionStartOffset.offset + event.text.length
58
+ }
59
+ };
60
+ if (prefixOffsets.focus.offset - prefixOffsets.anchor.offset > 1) {
61
+ const prefixSelection = utils.blockOffsetsToSelection({
62
+ value: snapshot.context.value,
63
+ offsets: prefixOffsets
64
+ }), inlineObjectBeforePrefixFocus = selectors.getPreviousInlineObject(
65
+ {
66
+ ...snapshot,
67
+ context: {
68
+ ...snapshot.context,
69
+ selection: prefixSelection ? {
70
+ anchor: prefixSelection.focus,
71
+ focus: prefixSelection.focus
72
+ } : null
73
+ }
74
+ }
75
+ ), inlineObjectBeforePrefixFocusOffset = inlineObjectBeforePrefixFocus ? utils.childSelectionPointToBlockOffset({
76
+ value: snapshot.context.value,
77
+ selectionPoint: {
78
+ path: inlineObjectBeforePrefixFocus.path,
79
+ offset: 0
80
+ }
81
+ }) : void 0;
82
+ if (inlineObjectBeforePrefixFocusOffset && inlineObjectBeforePrefixFocusOffset.offset > prefixOffsets.anchor.offset && inlineObjectBeforePrefixFocusOffset.offset < prefixOffsets.focus.offset)
83
+ return !1;
84
+ }
85
+ if (suffixOffsets.focus.offset - suffixOffsets.anchor.offset > 1) {
86
+ const previousInlineObject = selectors.getPreviousInlineObject(snapshot), previousInlineObjectOffset = previousInlineObject ? utils.childSelectionPointToBlockOffset({
87
+ value: snapshot.context.value,
88
+ selectionPoint: {
89
+ path: previousInlineObject.path,
90
+ offset: 0
91
+ }
92
+ }) : void 0;
93
+ if (previousInlineObjectOffset && previousInlineObjectOffset.offset > suffixOffsets.anchor.offset && previousInlineObjectOffset.offset < suffixOffsets.focus.offset)
94
+ return !1;
95
+ }
96
+ return {
97
+ prefixOffsets,
98
+ suffixOffsets,
99
+ decorator
100
+ };
101
+ },
102
+ actions: [
103
+ // Insert the text as usual in its own undo step
104
+ ({ event }) => [execute(event)],
105
+ (_, { prefixOffsets, suffixOffsets, decorator }) => [
106
+ // Decorate the text between the prefix and suffix
107
+ execute({
108
+ type: "decorator.add",
109
+ decorator,
110
+ at: {
111
+ anchor: prefixOffsets.focus,
112
+ focus: suffixOffsets.anchor
113
+ }
114
+ }),
115
+ // Delete the suffix
116
+ execute({
117
+ type: "delete.text",
118
+ at: suffixOffsets
119
+ }),
120
+ // Delete the prefix
121
+ execute({
122
+ type: "delete.text",
123
+ at: prefixOffsets
124
+ }),
125
+ // Toggle the decorator off so the next inserted text isn't emphasized
126
+ execute({
127
+ type: "decorator.remove",
128
+ decorator
129
+ }),
130
+ effect(() => {
131
+ config.onDecorate({
132
+ ...suffixOffsets.anchor,
133
+ offset: suffixOffsets.anchor.offset - (prefixOffsets.focus.offset - prefixOffsets.anchor.offset)
134
+ });
135
+ })
136
+ ]
137
+ ]
138
+ });
139
+ }
140
+ function CharacterPairDecoratorPlugin(config) {
141
+ const editor = useEditor();
142
+ return useActorRef(decoratorPairMachine, {
143
+ input: {
144
+ editor,
145
+ decorator: config.decorator,
146
+ pair: config.pair
147
+ }
148
+ }), null;
149
+ }
150
+ const decorateListener = ({ sendBack, input }) => input.editor.registerBehavior({
151
+ behavior: createCharacterPairDecoratorBehavior({
152
+ decorator: input.decorator,
153
+ pair: input.pair,
154
+ onDecorate: (offset) => {
155
+ sendBack({ type: "decorator.add", blockOffset: offset });
156
+ }
157
+ })
158
+ }), selectionListenerCallback = ({ sendBack, input }) => input.editor.registerBehavior({
159
+ behavior: defineBehavior({
160
+ on: "select",
161
+ guard: ({ snapshot, event }) => {
162
+ if (!event.at)
163
+ return { blockOffsets: void 0 };
164
+ const anchor = utils.spanSelectionPointToBlockOffset({
165
+ value: snapshot.context.value,
166
+ selectionPoint: event.at.anchor
167
+ }), focus = utils.spanSelectionPointToBlockOffset({
168
+ value: snapshot.context.value,
169
+ selectionPoint: event.at.focus
170
+ });
171
+ return !anchor || !focus ? { blockOffsets: void 0 } : {
172
+ blockOffsets: {
173
+ anchor,
174
+ focus
175
+ }
176
+ };
177
+ },
178
+ actions: [
179
+ ({ event }, { blockOffsets }) => [
180
+ {
181
+ type: "effect",
182
+ effect: () => {
183
+ sendBack({ type: "selection", blockOffsets });
184
+ }
185
+ },
186
+ forward(event)
187
+ ]
188
+ ]
189
+ })
190
+ }), deleteBackwardListenerCallback = ({ sendBack, input }) => input.editor.registerBehavior({
191
+ behavior: defineBehavior({
192
+ on: "delete.backward",
193
+ actions: [
194
+ () => [
195
+ execute({
196
+ type: "history.undo"
197
+ }),
198
+ effect(() => {
199
+ sendBack({ type: "delete.backward" });
200
+ })
201
+ ]
202
+ ]
203
+ })
204
+ }), decoratorPairMachine = setup({
205
+ types: {
206
+ context: {},
207
+ input: {},
208
+ events: {}
209
+ },
210
+ actors: {
211
+ "decorate listener": fromCallback(decorateListener),
212
+ "delete.backward listener": fromCallback(deleteBackwardListenerCallback),
213
+ "selection listener": fromCallback(selectionListenerCallback)
214
+ }
215
+ }).createMachine({
216
+ id: "decorator pair",
217
+ context: ({ input }) => ({
218
+ decorator: input.decorator,
219
+ editor: input.editor,
220
+ pair: input.pair
221
+ }),
222
+ initial: "idle",
223
+ states: {
224
+ idle: {
225
+ invoke: [
226
+ {
227
+ src: "decorate listener",
228
+ input: ({ context }) => ({
229
+ decorator: context.decorator,
230
+ editor: context.editor,
231
+ pair: context.pair
232
+ })
233
+ }
234
+ ],
235
+ on: {
236
+ "decorator.add": {
237
+ target: "decorator added",
238
+ actions: assign({
239
+ offsetAfterDecorator: ({ event }) => event.blockOffset
240
+ })
241
+ }
242
+ }
243
+ },
244
+ "decorator added": {
245
+ exit: [
246
+ assign({
247
+ offsetAfterDecorator: void 0
248
+ })
249
+ ],
250
+ invoke: [
251
+ {
252
+ src: "selection listener",
253
+ input: ({ context }) => ({ editor: context.editor })
254
+ },
255
+ {
256
+ src: "delete.backward listener",
257
+ input: ({ context }) => ({ editor: context.editor })
258
+ }
259
+ ],
260
+ on: {
261
+ selection: {
262
+ target: "idle",
263
+ guard: ({ context, event }) => !isDeepEqual(
264
+ {
265
+ anchor: context.offsetAfterDecorator,
266
+ focus: context.offsetAfterDecorator
267
+ },
268
+ event.blockOffsets
269
+ )
270
+ },
271
+ "delete.backward": {
272
+ target: "idle"
273
+ }
274
+ }
275
+ }
276
+ }
277
+ });
278
+ export {
279
+ CharacterPairDecoratorPlugin
280
+ };
281
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/regex.character-pair.ts","../src/behavior.character-pair-decorator.ts","../src/plugin.character-pair-decorator.ts"],"sourcesContent":["export function createCharacterPairRegex(char: string, amount: number) {\n // Negative lookbehind: Ensures that the matched sequence is not preceded by the same character\n const prePrefix = `(?<!\\\\${char})`\n\n // Repeats the character `amount` times\n const prefix = `\\\\${char}`.repeat(Math.max(amount, 1))\n\n // Negative lookahead: Ensures that the opening pair (**, *, etc.) is not followed by a space\n const postPrefix = `(?!\\\\s)`\n\n // Captures the content inside the pair\n const content = `([^${char}\\\\n]+?)`\n\n // Negative lookbehind: Ensures that the content is not followed by a space\n const preSuffix = `(?<!\\\\s)`\n\n // Repeats the character `amount` times\n const suffix = `\\\\${char}`.repeat(Math.max(amount, 1))\n\n // Negative lookahead: Ensures that the matched sequence is not followed by the same character\n const postSuffix = `(?!\\\\${char})`\n\n return `${prePrefix}${prefix}${postPrefix}${content}${preSuffix}${suffix}${postSuffix}`\n}\n","import type {BlockOffset, EditorSchema} from '@portabletext/editor'\nimport {defineBehavior, effect, execute} from '@portabletext/editor/behaviors'\nimport * as selectors from '@portabletext/editor/selectors'\nimport * as utils from '@portabletext/editor/utils'\nimport {createCharacterPairRegex} from './regex.character-pair'\n\nexport function createCharacterPairDecoratorBehavior(config: {\n decorator: ({schema}: {schema: EditorSchema}) => string | undefined\n pair: {char: string; amount: number}\n onDecorate: (offset: BlockOffset) => void\n}) {\n if (config.pair.amount < 1) {\n console.warn(\n `The amount of characters in the pair should be greater than 0`,\n )\n }\n\n const pairRegex = createCharacterPairRegex(\n config.pair.char,\n config.pair.amount,\n )\n const regEx = new RegExp(`(${pairRegex})$`)\n\n return defineBehavior({\n on: 'insert.text',\n guard: ({snapshot, event}) => {\n if (config.pair.amount < 1) {\n return false\n }\n\n const decorator = config.decorator({schema: snapshot.context.schema})\n\n if (decorator === undefined) {\n return false\n }\n\n const focusTextBlock = selectors.getFocusTextBlock(snapshot)\n const selectionStartPoint = selectors.getSelectionStartPoint(snapshot)\n const selectionStartOffset = selectionStartPoint\n ? utils.spanSelectionPointToBlockOffset({\n value: snapshot.context.value,\n selectionPoint: selectionStartPoint,\n })\n : undefined\n\n if (!focusTextBlock || !selectionStartOffset) {\n return false\n }\n\n const textBefore = selectors.getBlockTextBefore(snapshot)\n const newText = `${textBefore}${event.text}`\n const textToDecorate = newText.match(regEx)?.at(0)\n\n if (textToDecorate === undefined) {\n return false\n }\n\n const prefixOffsets = {\n anchor: {\n path: focusTextBlock.path,\n // Example: \"foo **bar**\".length - \"**bar**\".length = 4\n offset: newText.length - textToDecorate.length,\n },\n focus: {\n path: focusTextBlock.path,\n // Example: \"foo **bar**\".length - \"**bar**\".length + \"*\".length * 2 = 6\n offset:\n newText.length -\n textToDecorate.length +\n config.pair.char.length * config.pair.amount,\n },\n }\n\n const suffixOffsets = {\n anchor: {\n path: focusTextBlock.path,\n // Example: \"foo **bar*|\" (10) + \"*\".length - 2 = 9\n offset:\n selectionStartOffset.offset +\n event.text.length -\n config.pair.char.length * config.pair.amount,\n },\n focus: {\n path: focusTextBlock.path,\n // Example: \"foo **bar*|\" (10) + \"*\".length = 11\n offset: selectionStartOffset.offset + event.text.length,\n },\n }\n\n // If the prefix is more than one character, then we need to check if\n // there is an inline object inside it\n if (prefixOffsets.focus.offset - prefixOffsets.anchor.offset > 1) {\n const prefixSelection = utils.blockOffsetsToSelection({\n value: snapshot.context.value,\n offsets: prefixOffsets,\n })\n const inlineObjectBeforePrefixFocus = selectors.getPreviousInlineObject(\n {\n ...snapshot,\n context: {\n ...snapshot.context,\n selection: prefixSelection\n ? {\n anchor: prefixSelection.focus,\n focus: prefixSelection.focus,\n }\n : null,\n },\n },\n )\n const inlineObjectBeforePrefixFocusOffset =\n inlineObjectBeforePrefixFocus\n ? utils.childSelectionPointToBlockOffset({\n value: snapshot.context.value,\n selectionPoint: {\n path: inlineObjectBeforePrefixFocus.path,\n offset: 0,\n },\n })\n : undefined\n\n if (\n inlineObjectBeforePrefixFocusOffset &&\n inlineObjectBeforePrefixFocusOffset.offset >\n prefixOffsets.anchor.offset &&\n inlineObjectBeforePrefixFocusOffset.offset <\n prefixOffsets.focus.offset\n ) {\n return false\n }\n }\n\n // If the suffix is more than one character, then we need to check if\n // there is an inline object inside it\n if (suffixOffsets.focus.offset - suffixOffsets.anchor.offset > 1) {\n const previousInlineObject = selectors.getPreviousInlineObject(snapshot)\n const previousInlineObjectOffset = previousInlineObject\n ? utils.childSelectionPointToBlockOffset({\n value: snapshot.context.value,\n selectionPoint: {\n path: previousInlineObject.path,\n offset: 0,\n },\n })\n : undefined\n\n if (\n previousInlineObjectOffset &&\n previousInlineObjectOffset.offset > suffixOffsets.anchor.offset &&\n previousInlineObjectOffset.offset < suffixOffsets.focus.offset\n ) {\n return false\n }\n }\n\n return {\n prefixOffsets,\n suffixOffsets,\n decorator,\n }\n },\n actions: [\n // Insert the text as usual in its own undo step\n ({event}) => [execute(event)],\n (_, {prefixOffsets, suffixOffsets, decorator}) => [\n // Decorate the text between the prefix and suffix\n execute({\n type: 'decorator.add',\n decorator,\n at: {\n anchor: prefixOffsets.focus,\n focus: suffixOffsets.anchor,\n },\n }),\n // Delete the suffix\n execute({\n type: 'delete.text',\n at: suffixOffsets,\n }),\n // Delete the prefix\n execute({\n type: 'delete.text',\n at: prefixOffsets,\n }),\n // Toggle the decorator off so the next inserted text isn't emphasized\n execute({\n type: 'decorator.remove',\n decorator,\n }),\n effect(() => {\n config.onDecorate({\n ...suffixOffsets.anchor,\n offset:\n suffixOffsets.anchor.offset -\n (prefixOffsets.focus.offset - prefixOffsets.anchor.offset),\n })\n }),\n ],\n ],\n })\n}\n","import type {BlockOffset, Editor, EditorSchema} from '@portabletext/editor'\nimport {useEditor} from '@portabletext/editor'\nimport {\n defineBehavior,\n effect,\n execute,\n forward,\n} from '@portabletext/editor/behaviors'\nimport * as utils from '@portabletext/editor/utils'\nimport {useActorRef} from '@xstate/react'\nimport {isDeepEqual} from 'remeda'\nimport {\n assign,\n fromCallback,\n setup,\n type AnyEventObject,\n type CallbackLogicFunction,\n} from 'xstate'\nimport {createCharacterPairDecoratorBehavior} from './behavior.character-pair-decorator'\n\n/**\n * @beta\n */\nexport function CharacterPairDecoratorPlugin(config: {\n decorator: ({schema}: {schema: EditorSchema}) => string | undefined\n pair: {char: string; amount: number}\n}) {\n const editor = useEditor()\n\n useActorRef(decoratorPairMachine, {\n input: {\n editor,\n decorator: config.decorator,\n pair: config.pair,\n },\n })\n\n return null\n}\n\ntype DecoratorPairEvent =\n | {\n type: 'decorator.add'\n blockOffset: BlockOffset\n }\n | {\n type: 'selection'\n blockOffsets?: {\n anchor: BlockOffset\n focus: BlockOffset\n }\n }\n | {\n type: 'delete.backward'\n }\n\nconst decorateListener: CallbackLogicFunction<\n AnyEventObject,\n DecoratorPairEvent,\n {\n decorator: ({schema}: {schema: EditorSchema}) => string | undefined\n editor: Editor\n pair: {char: string; amount: number}\n }\n> = ({sendBack, input}) => {\n const unregister = input.editor.registerBehavior({\n behavior: createCharacterPairDecoratorBehavior({\n decorator: input.decorator,\n pair: input.pair,\n onDecorate: (offset) => {\n sendBack({type: 'decorator.add', blockOffset: offset})\n },\n }),\n })\n\n return unregister\n}\n\nconst selectionListenerCallback: CallbackLogicFunction<\n AnyEventObject,\n DecoratorPairEvent,\n {editor: Editor}\n> = ({sendBack, input}) => {\n const unregister = input.editor.registerBehavior({\n behavior: defineBehavior({\n on: 'select',\n guard: ({snapshot, event}) => {\n if (!event.at) {\n return {blockOffsets: undefined}\n }\n\n const anchor = utils.spanSelectionPointToBlockOffset({\n value: snapshot.context.value,\n selectionPoint: event.at.anchor,\n })\n const focus = utils.spanSelectionPointToBlockOffset({\n value: snapshot.context.value,\n selectionPoint: event.at.focus,\n })\n\n if (!anchor || !focus) {\n return {blockOffsets: undefined}\n }\n\n return {\n blockOffsets: {\n anchor,\n focus,\n },\n }\n },\n actions: [\n ({event}, {blockOffsets}) => [\n {\n type: 'effect',\n effect: () => {\n sendBack({type: 'selection', blockOffsets})\n },\n },\n forward(event),\n ],\n ],\n }),\n })\n\n return unregister\n}\n\nconst deleteBackwardListenerCallback: CallbackLogicFunction<\n AnyEventObject,\n DecoratorPairEvent,\n {editor: Editor}\n> = ({sendBack, input}) => {\n const unregister = input.editor.registerBehavior({\n behavior: defineBehavior({\n on: 'delete.backward',\n actions: [\n () => [\n execute({\n type: 'history.undo',\n }),\n effect(() => {\n sendBack({type: 'delete.backward'})\n }),\n ],\n ],\n }),\n })\n\n return unregister\n}\n\nconst decoratorPairMachine = setup({\n types: {\n context: {} as {\n decorator: ({schema}: {schema: EditorSchema}) => string | undefined\n editor: Editor\n offsetAfterDecorator?: BlockOffset\n pair: {char: string; amount: number}\n },\n input: {} as {\n decorator: ({schema}: {schema: EditorSchema}) => string | undefined\n editor: Editor\n pair: {char: string; amount: number}\n },\n events: {} as DecoratorPairEvent,\n },\n actors: {\n 'decorate listener': fromCallback(decorateListener),\n 'delete.backward listener': fromCallback(deleteBackwardListenerCallback),\n 'selection listener': fromCallback(selectionListenerCallback),\n },\n}).createMachine({\n id: 'decorator pair',\n context: ({input}) => ({\n decorator: input.decorator,\n editor: input.editor,\n pair: input.pair,\n }),\n initial: 'idle',\n states: {\n 'idle': {\n invoke: [\n {\n src: 'decorate listener',\n input: ({context}) => ({\n decorator: context.decorator,\n editor: context.editor,\n pair: context.pair,\n }),\n },\n ],\n on: {\n 'decorator.add': {\n target: 'decorator added',\n actions: assign({\n offsetAfterDecorator: ({event}) => event.blockOffset,\n }),\n },\n },\n },\n 'decorator added': {\n exit: [\n assign({\n offsetAfterDecorator: undefined,\n }),\n ],\n invoke: [\n {\n src: 'selection listener',\n input: ({context}) => ({editor: context.editor}),\n },\n {\n src: 'delete.backward listener',\n input: ({context}) => ({editor: context.editor}),\n },\n ],\n on: {\n 'selection': {\n target: 'idle',\n guard: ({context, event}) => {\n const selectionChanged = !isDeepEqual(\n {\n anchor: context.offsetAfterDecorator,\n focus: context.offsetAfterDecorator,\n },\n event.blockOffsets,\n )\n\n return selectionChanged\n },\n },\n 'delete.backward': {\n target: 'idle',\n },\n },\n },\n },\n})\n"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@portabletext/plugin-character-pair-decorator",
3
+ "version": "1.0.0",
4
+ "description": "Automatically match a pair of characters and decorate the text in between",
5
+ "keywords": [
6
+ "portabletext",
7
+ "plugin",
8
+ "pair",
9
+ "decorator",
10
+ "behaviors"
11
+ ],
12
+ "homepage": "https://portabletext.org",
13
+ "bugs": {
14
+ "url": "https://github.com/portabletext/plugins/issues"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/portabletext/plugins.git",
19
+ "directory": "plugins/character-pair-decorator"
20
+ },
21
+ "license": "MIT",
22
+ "author": "Sanity.io <hello@sanity.io>",
23
+ "sideEffects": false,
24
+ "type": "module",
25
+ "exports": {
26
+ ".": {
27
+ "source": "./src/index.ts",
28
+ "import": "./dist/index.js",
29
+ "require": "./dist/index.cjs",
30
+ "default": "./dist/index.js"
31
+ },
32
+ "./package.json": "./package.json"
33
+ },
34
+ "main": "./dist/index.cjs",
35
+ "module": "./dist/index.js",
36
+ "types": "./dist/index.d.ts",
37
+ "files": [
38
+ "src",
39
+ "dist"
40
+ ],
41
+ "devDependencies": {
42
+ "@portabletext/editor": "^1.48.4",
43
+ "@types/react": "^19.1.2",
44
+ "react": "^19.1.0"
45
+ },
46
+ "peerDependencies": {
47
+ "@portabletext/editor": "^1.48.3",
48
+ "react": "^19.1.0"
49
+ },
50
+ "dependencies": {
51
+ "@xstate/react": "^5.0.3",
52
+ "remeda": "^2.21.3",
53
+ "xstate": "^5.19.2"
54
+ },
55
+ "scripts": {
56
+ "build": "pkg-utils build --strict --check --clean",
57
+ "check:lint": "biome lint .",
58
+ "check:react-compiler": "eslint --cache --no-inline-config --no-eslintrc --ignore-pattern '**/__tests__/**' --ext .cjs,.mjs,.js,.jsx,.ts,.tsx --parser @typescript-eslint/parser --plugin react-compiler --plugin react-hooks --rule 'react-compiler/react-compiler: [warn]' --rule 'react-hooks/rules-of-hooks: [error]' --rule 'react-hooks/exhaustive-deps: [error]' src",
59
+ "check:types": "tsc",
60
+ "check:types:watch": "tsc --watch",
61
+ "clean": "del .turbo && del dist && del node_modules",
62
+ "dev": "pkg-utils watch",
63
+ "lint:fix": "biome lint --write .",
64
+ "test:unit": "vitest --run",
65
+ "test:unit:watch": "vitest"
66
+ }
67
+ }
@@ -0,0 +1,201 @@
1
+ import type {BlockOffset, EditorSchema} from '@portabletext/editor'
2
+ import {defineBehavior, effect, execute} from '@portabletext/editor/behaviors'
3
+ import * as selectors from '@portabletext/editor/selectors'
4
+ import * as utils from '@portabletext/editor/utils'
5
+ import {createCharacterPairRegex} from './regex.character-pair'
6
+
7
+ export function createCharacterPairDecoratorBehavior(config: {
8
+ decorator: ({schema}: {schema: EditorSchema}) => string | undefined
9
+ pair: {char: string; amount: number}
10
+ onDecorate: (offset: BlockOffset) => void
11
+ }) {
12
+ if (config.pair.amount < 1) {
13
+ console.warn(
14
+ `The amount of characters in the pair should be greater than 0`,
15
+ )
16
+ }
17
+
18
+ const pairRegex = createCharacterPairRegex(
19
+ config.pair.char,
20
+ config.pair.amount,
21
+ )
22
+ const regEx = new RegExp(`(${pairRegex})$`)
23
+
24
+ return defineBehavior({
25
+ on: 'insert.text',
26
+ guard: ({snapshot, event}) => {
27
+ if (config.pair.amount < 1) {
28
+ return false
29
+ }
30
+
31
+ const decorator = config.decorator({schema: snapshot.context.schema})
32
+
33
+ if (decorator === undefined) {
34
+ return false
35
+ }
36
+
37
+ const focusTextBlock = selectors.getFocusTextBlock(snapshot)
38
+ const selectionStartPoint = selectors.getSelectionStartPoint(snapshot)
39
+ const selectionStartOffset = selectionStartPoint
40
+ ? utils.spanSelectionPointToBlockOffset({
41
+ value: snapshot.context.value,
42
+ selectionPoint: selectionStartPoint,
43
+ })
44
+ : undefined
45
+
46
+ if (!focusTextBlock || !selectionStartOffset) {
47
+ return false
48
+ }
49
+
50
+ const textBefore = selectors.getBlockTextBefore(snapshot)
51
+ const newText = `${textBefore}${event.text}`
52
+ const textToDecorate = newText.match(regEx)?.at(0)
53
+
54
+ if (textToDecorate === undefined) {
55
+ return false
56
+ }
57
+
58
+ const prefixOffsets = {
59
+ anchor: {
60
+ path: focusTextBlock.path,
61
+ // Example: "foo **bar**".length - "**bar**".length = 4
62
+ offset: newText.length - textToDecorate.length,
63
+ },
64
+ focus: {
65
+ path: focusTextBlock.path,
66
+ // Example: "foo **bar**".length - "**bar**".length + "*".length * 2 = 6
67
+ offset:
68
+ newText.length -
69
+ textToDecorate.length +
70
+ config.pair.char.length * config.pair.amount,
71
+ },
72
+ }
73
+
74
+ const suffixOffsets = {
75
+ anchor: {
76
+ path: focusTextBlock.path,
77
+ // Example: "foo **bar*|" (10) + "*".length - 2 = 9
78
+ offset:
79
+ selectionStartOffset.offset +
80
+ event.text.length -
81
+ config.pair.char.length * config.pair.amount,
82
+ },
83
+ focus: {
84
+ path: focusTextBlock.path,
85
+ // Example: "foo **bar*|" (10) + "*".length = 11
86
+ offset: selectionStartOffset.offset + event.text.length,
87
+ },
88
+ }
89
+
90
+ // If the prefix is more than one character, then we need to check if
91
+ // there is an inline object inside it
92
+ if (prefixOffsets.focus.offset - prefixOffsets.anchor.offset > 1) {
93
+ const prefixSelection = utils.blockOffsetsToSelection({
94
+ value: snapshot.context.value,
95
+ offsets: prefixOffsets,
96
+ })
97
+ const inlineObjectBeforePrefixFocus = selectors.getPreviousInlineObject(
98
+ {
99
+ ...snapshot,
100
+ context: {
101
+ ...snapshot.context,
102
+ selection: prefixSelection
103
+ ? {
104
+ anchor: prefixSelection.focus,
105
+ focus: prefixSelection.focus,
106
+ }
107
+ : null,
108
+ },
109
+ },
110
+ )
111
+ const inlineObjectBeforePrefixFocusOffset =
112
+ inlineObjectBeforePrefixFocus
113
+ ? utils.childSelectionPointToBlockOffset({
114
+ value: snapshot.context.value,
115
+ selectionPoint: {
116
+ path: inlineObjectBeforePrefixFocus.path,
117
+ offset: 0,
118
+ },
119
+ })
120
+ : undefined
121
+
122
+ if (
123
+ inlineObjectBeforePrefixFocusOffset &&
124
+ inlineObjectBeforePrefixFocusOffset.offset >
125
+ prefixOffsets.anchor.offset &&
126
+ inlineObjectBeforePrefixFocusOffset.offset <
127
+ prefixOffsets.focus.offset
128
+ ) {
129
+ return false
130
+ }
131
+ }
132
+
133
+ // If the suffix is more than one character, then we need to check if
134
+ // there is an inline object inside it
135
+ if (suffixOffsets.focus.offset - suffixOffsets.anchor.offset > 1) {
136
+ const previousInlineObject = selectors.getPreviousInlineObject(snapshot)
137
+ const previousInlineObjectOffset = previousInlineObject
138
+ ? utils.childSelectionPointToBlockOffset({
139
+ value: snapshot.context.value,
140
+ selectionPoint: {
141
+ path: previousInlineObject.path,
142
+ offset: 0,
143
+ },
144
+ })
145
+ : undefined
146
+
147
+ if (
148
+ previousInlineObjectOffset &&
149
+ previousInlineObjectOffset.offset > suffixOffsets.anchor.offset &&
150
+ previousInlineObjectOffset.offset < suffixOffsets.focus.offset
151
+ ) {
152
+ return false
153
+ }
154
+ }
155
+
156
+ return {
157
+ prefixOffsets,
158
+ suffixOffsets,
159
+ decorator,
160
+ }
161
+ },
162
+ actions: [
163
+ // Insert the text as usual in its own undo step
164
+ ({event}) => [execute(event)],
165
+ (_, {prefixOffsets, suffixOffsets, decorator}) => [
166
+ // Decorate the text between the prefix and suffix
167
+ execute({
168
+ type: 'decorator.add',
169
+ decorator,
170
+ at: {
171
+ anchor: prefixOffsets.focus,
172
+ focus: suffixOffsets.anchor,
173
+ },
174
+ }),
175
+ // Delete the suffix
176
+ execute({
177
+ type: 'delete.text',
178
+ at: suffixOffsets,
179
+ }),
180
+ // Delete the prefix
181
+ execute({
182
+ type: 'delete.text',
183
+ at: prefixOffsets,
184
+ }),
185
+ // Toggle the decorator off so the next inserted text isn't emphasized
186
+ execute({
187
+ type: 'decorator.remove',
188
+ decorator,
189
+ }),
190
+ effect(() => {
191
+ config.onDecorate({
192
+ ...suffixOffsets.anchor,
193
+ offset:
194
+ suffixOffsets.anchor.offset -
195
+ (prefixOffsets.focus.offset - prefixOffsets.anchor.offset),
196
+ })
197
+ }),
198
+ ],
199
+ ],
200
+ })
201
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './plugin.character-pair-decorator'
@@ -0,0 +1,239 @@
1
+ import type {BlockOffset, Editor, EditorSchema} from '@portabletext/editor'
2
+ import {useEditor} from '@portabletext/editor'
3
+ import {
4
+ defineBehavior,
5
+ effect,
6
+ execute,
7
+ forward,
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
+ * @beta
23
+ */
24
+ export function CharacterPairDecoratorPlugin(config: {
25
+ decorator: ({schema}: {schema: EditorSchema}) => string | undefined
26
+ pair: {char: string; amount: number}
27
+ }) {
28
+ const editor = useEditor()
29
+
30
+ useActorRef(decoratorPairMachine, {
31
+ input: {
32
+ editor,
33
+ decorator: config.decorator,
34
+ pair: config.pair,
35
+ },
36
+ })
37
+
38
+ return null
39
+ }
40
+
41
+ type DecoratorPairEvent =
42
+ | {
43
+ type: 'decorator.add'
44
+ blockOffset: BlockOffset
45
+ }
46
+ | {
47
+ type: 'selection'
48
+ blockOffsets?: {
49
+ anchor: BlockOffset
50
+ focus: BlockOffset
51
+ }
52
+ }
53
+ | {
54
+ type: 'delete.backward'
55
+ }
56
+
57
+ const decorateListener: CallbackLogicFunction<
58
+ AnyEventObject,
59
+ DecoratorPairEvent,
60
+ {
61
+ decorator: ({schema}: {schema: EditorSchema}) => string | undefined
62
+ editor: Editor
63
+ pair: {char: string; amount: number}
64
+ }
65
+ > = ({sendBack, input}) => {
66
+ const unregister = input.editor.registerBehavior({
67
+ behavior: createCharacterPairDecoratorBehavior({
68
+ decorator: input.decorator,
69
+ pair: input.pair,
70
+ onDecorate: (offset) => {
71
+ sendBack({type: 'decorator.add', blockOffset: offset})
72
+ },
73
+ }),
74
+ })
75
+
76
+ return unregister
77
+ }
78
+
79
+ const selectionListenerCallback: CallbackLogicFunction<
80
+ AnyEventObject,
81
+ DecoratorPairEvent,
82
+ {editor: Editor}
83
+ > = ({sendBack, input}) => {
84
+ const unregister = input.editor.registerBehavior({
85
+ behavior: defineBehavior({
86
+ on: 'select',
87
+ guard: ({snapshot, event}) => {
88
+ if (!event.at) {
89
+ return {blockOffsets: undefined}
90
+ }
91
+
92
+ const anchor = utils.spanSelectionPointToBlockOffset({
93
+ value: snapshot.context.value,
94
+ selectionPoint: event.at.anchor,
95
+ })
96
+ const focus = utils.spanSelectionPointToBlockOffset({
97
+ value: snapshot.context.value,
98
+ selectionPoint: event.at.focus,
99
+ })
100
+
101
+ if (!anchor || !focus) {
102
+ return {blockOffsets: undefined}
103
+ }
104
+
105
+ return {
106
+ blockOffsets: {
107
+ anchor,
108
+ focus,
109
+ },
110
+ }
111
+ },
112
+ actions: [
113
+ ({event}, {blockOffsets}) => [
114
+ {
115
+ type: 'effect',
116
+ effect: () => {
117
+ sendBack({type: 'selection', blockOffsets})
118
+ },
119
+ },
120
+ forward(event),
121
+ ],
122
+ ],
123
+ }),
124
+ })
125
+
126
+ return unregister
127
+ }
128
+
129
+ const deleteBackwardListenerCallback: CallbackLogicFunction<
130
+ AnyEventObject,
131
+ DecoratorPairEvent,
132
+ {editor: Editor}
133
+ > = ({sendBack, input}) => {
134
+ const unregister = input.editor.registerBehavior({
135
+ behavior: defineBehavior({
136
+ on: 'delete.backward',
137
+ actions: [
138
+ () => [
139
+ execute({
140
+ type: 'history.undo',
141
+ }),
142
+ effect(() => {
143
+ sendBack({type: 'delete.backward'})
144
+ }),
145
+ ],
146
+ ],
147
+ }),
148
+ })
149
+
150
+ return unregister
151
+ }
152
+
153
+ const decoratorPairMachine = setup({
154
+ types: {
155
+ context: {} as {
156
+ decorator: ({schema}: {schema: EditorSchema}) => string | undefined
157
+ editor: Editor
158
+ offsetAfterDecorator?: BlockOffset
159
+ pair: {char: string; amount: number}
160
+ },
161
+ input: {} as {
162
+ decorator: ({schema}: {schema: EditorSchema}) => string | undefined
163
+ editor: Editor
164
+ pair: {char: string; amount: number}
165
+ },
166
+ events: {} as DecoratorPairEvent,
167
+ },
168
+ actors: {
169
+ 'decorate listener': fromCallback(decorateListener),
170
+ 'delete.backward listener': fromCallback(deleteBackwardListenerCallback),
171
+ 'selection listener': fromCallback(selectionListenerCallback),
172
+ },
173
+ }).createMachine({
174
+ id: 'decorator pair',
175
+ context: ({input}) => ({
176
+ decorator: input.decorator,
177
+ editor: input.editor,
178
+ pair: input.pair,
179
+ }),
180
+ initial: 'idle',
181
+ states: {
182
+ 'idle': {
183
+ invoke: [
184
+ {
185
+ src: 'decorate listener',
186
+ input: ({context}) => ({
187
+ decorator: context.decorator,
188
+ editor: context.editor,
189
+ pair: context.pair,
190
+ }),
191
+ },
192
+ ],
193
+ on: {
194
+ 'decorator.add': {
195
+ target: 'decorator added',
196
+ actions: assign({
197
+ offsetAfterDecorator: ({event}) => event.blockOffset,
198
+ }),
199
+ },
200
+ },
201
+ },
202
+ 'decorator added': {
203
+ exit: [
204
+ assign({
205
+ offsetAfterDecorator: undefined,
206
+ }),
207
+ ],
208
+ invoke: [
209
+ {
210
+ src: 'selection listener',
211
+ input: ({context}) => ({editor: context.editor}),
212
+ },
213
+ {
214
+ src: 'delete.backward listener',
215
+ input: ({context}) => ({editor: context.editor}),
216
+ },
217
+ ],
218
+ on: {
219
+ 'selection': {
220
+ target: 'idle',
221
+ guard: ({context, event}) => {
222
+ const selectionChanged = !isDeepEqual(
223
+ {
224
+ anchor: context.offsetAfterDecorator,
225
+ focus: context.offsetAfterDecorator,
226
+ },
227
+ event.blockOffsets,
228
+ )
229
+
230
+ return selectionChanged
231
+ },
232
+ },
233
+ 'delete.backward': {
234
+ target: 'idle',
235
+ },
236
+ },
237
+ },
238
+ },
239
+ })
@@ -0,0 +1,74 @@
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
+ })
@@ -0,0 +1,24 @@
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
+ }