@portabletext/plugin-input-rule 0.1.2 → 0.2.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 +74 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +76 -44
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/edge-cases.feature +77 -0
- package/src/edge-cases.test.tsx +25 -0
- package/src/input-rule-match-location.ts +50 -7
- package/src/plugin.input-rule.tsx +5 -3
- 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 +46 -40
package/dist/index.cjs
CHANGED
|
@@ -11,7 +11,7 @@ function getInputRuleMatchLocation({
|
|
|
11
11
|
focusTextBlock,
|
|
12
12
|
originalTextBefore
|
|
13
13
|
}) {
|
|
14
|
-
const [start, end] = match, adjustedIndex = start + adjustIndexBy, targetOffsets = {
|
|
14
|
+
const [text, start, end] = match, adjustedIndex = start + adjustIndexBy, targetOffsets = {
|
|
15
15
|
anchor: {
|
|
16
16
|
path: focusTextBlock.path,
|
|
17
17
|
offset: adjustedIndex
|
|
@@ -29,15 +29,34 @@ function getInputRuleMatchLocation({
|
|
|
29
29
|
focus: {
|
|
30
30
|
path: focusTextBlock.path,
|
|
31
31
|
offset: Math.min(targetOffsets.focus.offset, originalTextBefore.length)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
}, selection = utils.blockOffsetsToSelection({
|
|
32
|
+
}
|
|
33
|
+
}, anchorBackwards = utils.blockOffsetToSpanSelectionPoint({
|
|
35
34
|
context: snapshot.context,
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
blockOffset: normalizedOffsets.anchor,
|
|
36
|
+
direction: "backward"
|
|
37
|
+
}), focusForwards = utils.blockOffsetToSpanSelectionPoint({
|
|
38
|
+
context: snapshot.context,
|
|
39
|
+
blockOffset: normalizedOffsets.focus,
|
|
40
|
+
direction: "forward"
|
|
38
41
|
});
|
|
39
|
-
if (
|
|
42
|
+
if (!anchorBackwards || !focusForwards)
|
|
43
|
+
return;
|
|
44
|
+
const selection = {
|
|
45
|
+
anchor: anchorBackwards,
|
|
46
|
+
focus: focusForwards
|
|
47
|
+
}, inlineObjectsAfterMatch = selectors.getNextInlineObjects({
|
|
48
|
+
...snapshot,
|
|
49
|
+
context: {
|
|
50
|
+
...snapshot.context,
|
|
51
|
+
selection: {
|
|
52
|
+
anchor: selection.anchor,
|
|
53
|
+
focus: selection.anchor
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}), inlineObjectsBefore = selectors.getPreviousInlineObjects(snapshot);
|
|
57
|
+
if (!inlineObjectsAfterMatch.some((inlineObjectAfter) => inlineObjectsBefore.some((inlineObjectBefore) => inlineObjectAfter.node._key === inlineObjectBefore.node._key)))
|
|
40
58
|
return {
|
|
59
|
+
text,
|
|
41
60
|
selection,
|
|
42
61
|
targetOffsets
|
|
43
62
|
};
|
|
@@ -68,7 +87,7 @@ function createInputRuleBehavior(config) {
|
|
|
68
87
|
if (!match)
|
|
69
88
|
return [];
|
|
70
89
|
const matchLocation = getInputRuleMatchLocation({
|
|
71
|
-
match,
|
|
90
|
+
match: [regExpMatch.at(0) ?? "", ...match],
|
|
72
91
|
adjustIndexBy: originalNewText.length - newText.length,
|
|
73
92
|
snapshot,
|
|
74
93
|
focusTextBlock,
|
|
@@ -82,15 +101,19 @@ function createInputRuleBehavior(config) {
|
|
|
82
101
|
return [];
|
|
83
102
|
const groupMatches = regExpMatch.indices.length > 1 ? regExpMatch.indices.slice(1) : [];
|
|
84
103
|
return [{
|
|
104
|
+
text: matchLocation.text,
|
|
85
105
|
selection: matchLocation.selection,
|
|
86
106
|
targetOffsets: matchLocation.targetOffsets,
|
|
87
|
-
groupMatches: groupMatches.flatMap((match2) =>
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
107
|
+
groupMatches: groupMatches.flatMap((match2, index) => {
|
|
108
|
+
const text = regExpMatch.at(index + 1) ?? "";
|
|
109
|
+
return getInputRuleMatchLocation({
|
|
110
|
+
match: [text, ...match2],
|
|
111
|
+
adjustIndexBy: originalNewText.length - newText.length,
|
|
112
|
+
snapshot,
|
|
113
|
+
focusTextBlock,
|
|
114
|
+
originalTextBefore
|
|
115
|
+
}) || [];
|
|
116
|
+
})
|
|
94
117
|
}];
|
|
95
118
|
});
|
|
96
119
|
if (ruleMatches.length > 0) {
|
|
@@ -311,36 +334,45 @@ function defineTextTransformRule(config) {
|
|
|
311
334
|
snapshot,
|
|
312
335
|
event
|
|
313
336
|
}) => {
|
|
314
|
-
const
|
|
337
|
+
const locations = event.matches.flatMap((match) => match.groupMatches.length === 0 ? [match] : match.groupMatches), newText = event.textBefore + event.textInserted;
|
|
338
|
+
let textLengthDelta = 0;
|
|
339
|
+
const actions = [];
|
|
340
|
+
for (const location of locations.reverse()) {
|
|
341
|
+
const text = config.transform({
|
|
342
|
+
location
|
|
343
|
+
});
|
|
344
|
+
textLengthDelta = textLengthDelta - (text.length - (location.targetOffsets.focus.offset - location.targetOffsets.anchor.offset)), actions.push(behaviors.raise({
|
|
345
|
+
type: "select",
|
|
346
|
+
at: location.targetOffsets
|
|
347
|
+
})), actions.push(behaviors.raise({
|
|
348
|
+
type: "delete",
|
|
349
|
+
at: location.targetOffsets
|
|
350
|
+
})), actions.push(behaviors.raise({
|
|
351
|
+
type: "insert.child",
|
|
352
|
+
child: {
|
|
353
|
+
_type: snapshot.context.schema.span.name,
|
|
354
|
+
text,
|
|
355
|
+
marks: selectors.getMarkState({
|
|
356
|
+
...snapshot,
|
|
357
|
+
context: {
|
|
358
|
+
...snapshot.context,
|
|
359
|
+
selection: {
|
|
360
|
+
anchor: location.selection.anchor,
|
|
361
|
+
focus: {
|
|
362
|
+
path: location.selection.focus.path,
|
|
363
|
+
offset: Math.min(location.selection.focus.offset, event.textBefore.length)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
})?.marks ?? []
|
|
368
|
+
}
|
|
369
|
+
}));
|
|
370
|
+
}
|
|
371
|
+
const endCaretPosition = {
|
|
315
372
|
path: event.focusTextBlock.path,
|
|
316
373
|
offset: newText.length - textLengthDelta
|
|
317
374
|
};
|
|
318
|
-
return [...
|
|
319
|
-
type: "select",
|
|
320
|
-
at: match.targetOffsets
|
|
321
|
-
}), behaviors.raise({
|
|
322
|
-
type: "delete",
|
|
323
|
-
at: match.targetOffsets
|
|
324
|
-
}), behaviors.raise({
|
|
325
|
-
type: "insert.child",
|
|
326
|
-
child: {
|
|
327
|
-
_type: snapshot.context.schema.span.name,
|
|
328
|
-
text: config.transform(),
|
|
329
|
-
marks: selectors.getMarkState({
|
|
330
|
-
...snapshot,
|
|
331
|
-
context: {
|
|
332
|
-
...snapshot.context,
|
|
333
|
-
selection: {
|
|
334
|
-
anchor: match.selection.anchor,
|
|
335
|
-
focus: {
|
|
336
|
-
path: match.selection.focus.path,
|
|
337
|
-
offset: Math.min(match.selection.focus.offset, event.textBefore.length)
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
})?.marks ?? []
|
|
342
|
-
}
|
|
343
|
-
})]), behaviors.raise({
|
|
375
|
+
return [...actions, behaviors.raise({
|
|
344
376
|
type: "select",
|
|
345
377
|
at: {
|
|
346
378
|
anchor: endCaretPosition,
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":["../src/input-rule.ts","../src/input-rule-match-location.ts","../src/plugin.input-rule.tsx","../src/text-transform-rule.ts"],"sourcesContent":["import type {BlockPath, PortableTextTextBlock} from '@portabletext/editor'\nimport type {\n BehaviorActionSet,\n BehaviorGuard,\n} from '@portabletext/editor/behaviors'\nimport type {InputRuleMatchLocation} from './input-rule-match-location'\n\n/**\n * Match found in the text after the insertion\n * @alpha\n */\nexport type InputRuleMatch = InputRuleMatchLocation & {\n groupMatches: Array<InputRuleMatchLocation>\n}\n\n/**\n * @alpha\n */\nexport type InputRuleEvent = {\n type: 'custom.input rule'\n /**\n * Matches found by the input rule\n */\n matches: Array<InputRuleMatch>\n /**\n * The text before the insertion\n */\n textBefore: string\n /**\n * The text is destined to be inserted\n */\n textInserted: string\n /**\n * The text block where the insertion takes place\n */\n focusTextBlock: {\n path: BlockPath\n node: PortableTextTextBlock\n }\n}\n\n/**\n * @alpha\n */\nexport type InputRuleGuard = BehaviorGuard<InputRuleEvent, boolean>\n\n/**\n * @alpha\n */\nexport type InputRule = {\n on: RegExp\n guard?: InputRuleGuard\n actions: Array<BehaviorActionSet<InputRuleEvent, boolean>>\n}\n\n/**\n * @alpha\n */\nexport function defineInputRule(config: InputRule): InputRule {\n return config\n}\n","import type {\n BlockOffset,\n BlockPath,\n EditorSelection,\n EditorSnapshot,\n} from '@portabletext/editor'\nimport {blockOffsetsToSelection} from '@portabletext/editor/utils'\n\nexport type InputRuleMatchLocation = {\n /**\n * Estimated selection of where in the original text the match is located.\n * The selection is estimated since the match is found in the text after\n * insertion.\n */\n selection: NonNullable<EditorSelection>\n /**\n * Block offsets of the match in the text after the insertion\n */\n targetOffsets: {\n anchor: BlockOffset\n focus: BlockOffset\n backward: boolean\n }\n}\n\nexport function getInputRuleMatchLocation({\n match,\n adjustIndexBy,\n snapshot,\n focusTextBlock,\n originalTextBefore,\n}: {\n match: [number, number]\n adjustIndexBy: number\n snapshot: EditorSnapshot\n focusTextBlock: {\n path: BlockPath\n }\n originalTextBefore: string\n}): InputRuleMatchLocation | undefined {\n const [start, end] = match\n const adjustedIndex = start + adjustIndexBy\n\n const targetOffsets = {\n anchor: {\n path: focusTextBlock.path,\n offset: adjustedIndex,\n },\n focus: {\n path: focusTextBlock.path,\n offset: adjustedIndex + end - start,\n },\n backward: false,\n }\n const normalizedOffsets = {\n anchor: {\n path: focusTextBlock.path,\n offset: Math.min(targetOffsets.anchor.offset, originalTextBefore.length),\n },\n focus: {\n path: focusTextBlock.path,\n offset: Math.min(targetOffsets.focus.offset, originalTextBefore.length),\n },\n backward: false,\n }\n const selection = blockOffsetsToSelection({\n context: snapshot.context,\n offsets: normalizedOffsets,\n backward: false,\n })\n\n if (!selection) {\n return undefined\n }\n\n return {\n selection,\n targetOffsets,\n }\n}\n","import {useEditor, type BlockOffset, type Editor} from '@portabletext/editor'\nimport {\n defineBehavior,\n effect,\n forward,\n raise,\n type BehaviorAction,\n} from '@portabletext/editor/behaviors'\nimport {\n getBlockOffsets,\n getBlockTextBefore,\n getFocusTextBlock,\n} from '@portabletext/editor/selectors'\nimport {useActorRef} from '@xstate/react'\nimport {\n fromCallback,\n setup,\n type AnyEventObject,\n type CallbackLogicFunction,\n} from 'xstate'\nimport type {InputRule, InputRuleMatch} from './input-rule'\nimport {getInputRuleMatchLocation} from './input-rule-match-location'\n\nfunction createInputRuleBehavior(config: {\n rules: Array<InputRule>\n onApply: ({\n endOffsets,\n }: {\n endOffsets: {start: BlockOffset; end: BlockOffset} | undefined\n }) => void\n}) {\n return defineBehavior({\n on: 'insert.text',\n guard: ({snapshot, event, dom}) => {\n const focusTextBlock = getFocusTextBlock(snapshot)\n\n if (!focusTextBlock) {\n return false\n }\n\n const originalTextBefore = getBlockTextBefore(snapshot)\n let textBefore = originalTextBefore\n const originalNewText = textBefore + event.text\n let newText = originalNewText\n\n const foundMatches: Array<InputRuleMatch['groupMatches'][number]> = []\n const foundActions: Array<BehaviorAction> = []\n\n for (const rule of config.rules) {\n const matcher = new RegExp(rule.on.source, 'gd')\n\n while (true) {\n // Find matches in the text after the insertion\n const ruleMatches = [...newText.matchAll(matcher)].flatMap(\n (regExpMatch) => {\n if (regExpMatch.indices === undefined) {\n return []\n }\n\n const match = regExpMatch.indices.at(0)\n\n if (!match) {\n return []\n }\n\n const matchLocation = getInputRuleMatchLocation({\n match,\n adjustIndexBy: originalNewText.length - newText.length,\n snapshot,\n focusTextBlock,\n originalTextBefore,\n })\n\n if (!matchLocation) {\n return []\n }\n\n const existsInTextBefore =\n matchLocation.targetOffsets.focus.offset <=\n originalTextBefore.length\n\n // Ignore if this match occurs in the text before the insertion\n if (existsInTextBefore) {\n return []\n }\n\n const alreadyFound = foundMatches.some(\n (foundMatch) =>\n foundMatch.targetOffsets.anchor.offset ===\n matchLocation.targetOffsets.anchor.offset,\n )\n\n // Ignore if this match has already been found\n if (alreadyFound) {\n return []\n }\n\n const groupMatches =\n regExpMatch.indices.length > 1\n ? regExpMatch.indices.slice(1)\n : []\n\n const ruleMatch = {\n selection: matchLocation.selection,\n targetOffsets: matchLocation.targetOffsets,\n groupMatches: groupMatches.flatMap((match) => {\n const groupMatchLocation = getInputRuleMatchLocation({\n match,\n adjustIndexBy: originalNewText.length - newText.length,\n snapshot,\n focusTextBlock,\n originalTextBefore,\n })\n\n if (!groupMatchLocation) {\n return []\n }\n\n return groupMatchLocation\n }),\n }\n\n return [ruleMatch]\n },\n )\n\n if (ruleMatches.length > 0) {\n const guardResult =\n rule.guard?.({\n snapshot,\n event: {\n type: 'custom.input rule',\n matches: ruleMatches,\n focusTextBlock,\n textBefore: originalTextBefore,\n textInserted: event.text,\n },\n dom,\n }) ?? true\n\n if (!guardResult) {\n break\n }\n\n const actionSets = rule.actions.map((action) =>\n action(\n {\n snapshot,\n event: {\n type: 'custom.input rule',\n matches: ruleMatches,\n focusTextBlock,\n textBefore: originalTextBefore,\n textInserted: event.text,\n },\n dom,\n },\n guardResult,\n ),\n )\n\n for (const actionSet of actionSets) {\n for (const action of actionSet) {\n foundActions.push(action)\n }\n }\n\n const matches = ruleMatches.flatMap((match) =>\n match.groupMatches.length === 0 ? [match] : match.groupMatches,\n )\n\n for (const match of matches) {\n // Remember each match and adjust `textBefore` and `newText` so\n // no subsequent matches can overlap with this one\n foundMatches.push(match)\n textBefore = newText.slice(\n 0,\n match.targetOffsets.focus.offset ?? 0,\n )\n newText = originalNewText.slice(\n match.targetOffsets.focus.offset ?? 0,\n )\n }\n } else {\n // If no match was found, break out of the loop to try the next\n // rule\n break\n }\n }\n }\n\n if (foundActions.length === 0) {\n return false\n }\n\n return {actions: foundActions}\n },\n actions: [\n ({event}) => [forward(event)],\n (_, {actions}) => actions,\n ({snapshot}) => [\n effect(() => {\n const blockOffsets = getBlockOffsets(snapshot)\n\n config.onApply({endOffsets: blockOffsets})\n }),\n ],\n ],\n })\n}\n\ntype InputRulePluginProps = {\n rules: Array<InputRule>\n}\n\n/**\n * Turn an array of `InputRule`s into a Behavior that can be used to apply the\n * rules to the editor.\n *\n * The plugin handles undo/redo out of the box including smart undo with\n * Backspace.\n *\n * @example\n * ```tsx\n * <InputRulePlugin rules={smartQuotesRules} />\n * ```\n *\n * @alpha\n */\nexport function InputRulePlugin(props: InputRulePluginProps) {\n const editor = useEditor()\n\n useActorRef(inputRuleMachine, {\n input: {editor, rules: props.rules},\n })\n\n return null\n}\n\ntype InputRuleMachineEvent =\n | {\n type: 'input rule raised'\n endOffsets: {start: BlockOffset; end: BlockOffset} | undefined\n }\n | {type: 'history.undo raised'}\n | {\n type: 'selection changed'\n blockOffsets: {start: BlockOffset; end: BlockOffset} | undefined\n }\n\nconst inputRuleListenerCallback: CallbackLogicFunction<\n AnyEventObject,\n InputRuleMachineEvent,\n {\n editor: Editor\n rules: Array<InputRule>\n }\n> = ({input, sendBack}) => {\n const unregister = input.editor.registerBehavior({\n behavior: createInputRuleBehavior({\n rules: input.rules,\n onApply: ({endOffsets}) => {\n sendBack({type: 'input rule raised', endOffsets})\n },\n }),\n })\n\n return () => {\n unregister()\n }\n}\n\nconst deleteBackwardListenerCallback: CallbackLogicFunction<\n AnyEventObject,\n InputRuleMachineEvent,\n {editor: Editor}\n> = ({input, sendBack}) => {\n return input.editor.registerBehavior({\n behavior: defineBehavior({\n on: 'delete.backward',\n actions: [\n () => [\n raise({type: 'history.undo'}),\n effect(() => {\n sendBack({type: 'history.undo raised'})\n }),\n ],\n ],\n }),\n })\n}\n\nconst selectionListenerCallback: CallbackLogicFunction<\n AnyEventObject,\n InputRuleMachineEvent,\n {editor: Editor}\n> = ({sendBack, input}) => {\n const unregister = input.editor.registerBehavior({\n behavior: defineBehavior({\n on: 'select',\n guard: ({snapshot, event}) => {\n const blockOffsets = getBlockOffsets({\n ...snapshot,\n context: {\n ...snapshot.context,\n selection: event.at,\n },\n })\n\n return {blockOffsets}\n },\n actions: [\n ({event}, {blockOffsets}) => [\n effect(() => {\n sendBack({type: 'selection changed', blockOffsets})\n }),\n forward(event),\n ],\n ],\n }),\n })\n\n return unregister\n}\n\nconst inputRuleSetup = setup({\n types: {\n context: {} as {\n editor: Editor\n rules: Array<InputRule>\n endOffsets: {start: BlockOffset; end: BlockOffset} | undefined\n },\n input: {} as {\n editor: Editor\n rules: Array<InputRule>\n },\n events: {} as InputRuleMachineEvent,\n },\n actors: {\n 'delete.backward listener': fromCallback(deleteBackwardListenerCallback),\n 'input rule listener': fromCallback(inputRuleListenerCallback),\n 'selection listener': fromCallback(selectionListenerCallback),\n },\n guards: {\n 'block offset changed': ({context, event}) => {\n if (event.type !== 'selection changed') {\n return false\n }\n\n if (!event.blockOffsets || !context.endOffsets) {\n return true\n }\n\n const startChanged =\n context.endOffsets.start.path[0]._key !==\n event.blockOffsets.start.path[0]._key ||\n context.endOffsets.start.offset !== event.blockOffsets.start.offset\n const endChanged =\n context.endOffsets.end.path[0]._key !==\n event.blockOffsets.end.path[0]._key ||\n context.endOffsets.end.offset !== event.blockOffsets.end.offset\n\n return startChanged || endChanged\n },\n },\n})\n\nconst assignEndOffsets = inputRuleSetup.assign({\n endOffsets: ({context, event}) =>\n event.type === 'input rule raised' ? event.endOffsets : context.endOffsets,\n})\n\nconst inputRuleMachine = inputRuleSetup.createMachine({\n id: 'input rule',\n context: ({input}) => ({\n editor: input.editor,\n rules: input.rules,\n endOffsets: undefined,\n }),\n initial: 'idle',\n invoke: {\n src: 'input rule listener',\n input: ({context}) => ({\n editor: context.editor,\n rules: context.rules,\n }),\n },\n on: {\n 'input rule raised': {\n target: '.input rule applied',\n actions: assignEndOffsets,\n },\n },\n states: {\n 'idle': {},\n 'input rule applied': {\n invoke: [\n {\n src: 'delete.backward listener',\n input: ({context}) => ({editor: context.editor}),\n },\n {\n src: 'selection listener',\n input: ({context}) => ({editor: context.editor}),\n },\n ],\n on: {\n 'selection changed': {\n target: 'idle',\n guard: 'block offset changed',\n },\n 'history.undo raised': {\n target: 'idle',\n },\n },\n },\n },\n})\n","import {raise} from '@portabletext/editor/behaviors'\nimport {getMarkState} from '@portabletext/editor/selectors'\nimport type {InputRule, InputRuleGuard} from './input-rule'\n\n/**\n * @alpha\n */\nexport type TextTransformRule = {\n on: RegExp\n guard?: InputRuleGuard\n transform: () => string\n}\n\n/**\n * Define an `InputRule` specifically designed to transform matched text into\n * some other text.\n *\n * @example\n * ```tsx\n * const transformRule = defineTextTransformRule({\n * on: /--/,\n * transform: () => '—',\n * })\n * ```\n *\n * @alpha\n */\nexport function defineTextTransformRule(config: TextTransformRule): InputRule {\n return {\n on: config.on,\n guard: config.guard ?? (() => true),\n actions: [\n ({snapshot, event}) => {\n const matches = event.matches.flatMap((match) =>\n match.groupMatches.length === 0 ? [match] : match.groupMatches,\n )\n const textLengthDelta = matches.reduce((length, match) => {\n return (\n length -\n (config.transform().length -\n (match.targetOffsets.focus.offset -\n match.targetOffsets.anchor.offset))\n )\n }, 0)\n\n const newText = event.textBefore + event.textInserted\n const endCaretPosition = {\n path: event.focusTextBlock.path,\n offset: newText.length - textLengthDelta,\n }\n\n const actions = matches.reverse().flatMap((match) => [\n raise({type: 'select', at: match.targetOffsets}),\n raise({type: 'delete', at: match.targetOffsets}),\n raise({\n type: 'insert.child',\n child: {\n _type: snapshot.context.schema.span.name,\n text: config.transform(),\n marks:\n getMarkState({\n ...snapshot,\n context: {\n ...snapshot.context,\n selection: {\n anchor: match.selection.anchor,\n focus: {\n path: match.selection.focus.path,\n offset: Math.min(\n match.selection.focus.offset,\n event.textBefore.length,\n ),\n },\n },\n },\n })?.marks ?? [],\n },\n }),\n ])\n\n return [\n ...actions,\n raise({\n type: 'select',\n at: {\n anchor: endCaretPosition,\n focus: endCaretPosition,\n },\n }),\n ]\n },\n ],\n }\n}\n"],"names":["defineInputRule","config","getInputRuleMatchLocation","match","adjustIndexBy","snapshot","focusTextBlock","originalTextBefore","start","end","adjustedIndex","targetOffsets","anchor","path","offset","focus","backward","normalizedOffsets","Math","min","length","selection","blockOffsetsToSelection","context","offsets","createInputRuleBehavior","defineBehavior","on","guard","event","dom","getFocusTextBlock","getBlockTextBefore","textBefore","originalNewText","text","newText","foundMatches","foundActions","rule","rules","matcher","RegExp","source","ruleMatches","matchAll","flatMap","regExpMatch","indices","undefined","at","matchLocation","some","foundMatch","groupMatches","slice","guardResult","type","matches","textInserted","actionSets","actions","map","action","actionSet","push","forward","_","effect","blockOffsets","getBlockOffsets","onApply","endOffsets","InputRulePlugin","props","$","_c","editor","useEditor","t0","input","useActorRef","inputRuleMachine","inputRuleListenerCallback","sendBack","unregister","registerBehavior","behavior","deleteBackwardListenerCallback","raise","selectionListenerCallback","inputRuleSetup","setup","types","events","actors","fromCallback","guards","block offset changed","startChanged","_key","endChanged","assignEndOffsets","assign","createMachine","id","initial","invoke","src","target","states","defineTextTransformRule","textLengthDelta","reduce","transform","endCaretPosition","reverse","child","_type","schema","span","name","marks","getMarkState"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/input-rule.ts","../src/input-rule-match-location.ts","../src/plugin.input-rule.tsx","../src/text-transform-rule.ts"],"sourcesContent":["import type {BlockPath, PortableTextTextBlock} from '@portabletext/editor'\nimport type {\n BehaviorActionSet,\n BehaviorGuard,\n} from '@portabletext/editor/behaviors'\nimport type {InputRuleMatchLocation} from './input-rule-match-location'\n\n/**\n * Match found in the text after the insertion\n * @alpha\n */\nexport type InputRuleMatch = InputRuleMatchLocation & {\n groupMatches: Array<InputRuleMatchLocation>\n}\n\n/**\n * @alpha\n */\nexport type InputRuleEvent = {\n type: 'custom.input rule'\n /**\n * Matches found by the input rule\n */\n matches: Array<InputRuleMatch>\n /**\n * The text before the insertion\n */\n textBefore: string\n /**\n * The text is destined to be inserted\n */\n textInserted: string\n /**\n * The text block where the insertion takes place\n */\n focusTextBlock: {\n path: BlockPath\n node: PortableTextTextBlock\n }\n}\n\n/**\n * @alpha\n */\nexport type InputRuleGuard = BehaviorGuard<InputRuleEvent, boolean>\n\n/**\n * @alpha\n */\nexport type InputRule = {\n on: RegExp\n guard?: InputRuleGuard\n actions: Array<BehaviorActionSet<InputRuleEvent, boolean>>\n}\n\n/**\n * @alpha\n */\nexport function defineInputRule(config: InputRule): InputRule {\n return config\n}\n","import type {\n BlockOffset,\n BlockPath,\n EditorSelection,\n EditorSnapshot,\n} from '@portabletext/editor'\nimport {\n getNextInlineObjects,\n getPreviousInlineObjects,\n} from '@portabletext/editor/selectors'\nimport {blockOffsetToSpanSelectionPoint} from '@portabletext/editor/utils'\n\nexport type InputRuleMatchLocation = {\n /**\n * The matched text\n */\n text: string\n /**\n * Estimated selection of where in the original text the match is located.\n * The selection is estimated since the match is found in the text after\n * insertion.\n */\n selection: NonNullable<EditorSelection>\n /**\n * Block offsets of the match in the text after the insertion\n */\n targetOffsets: {\n anchor: BlockOffset\n focus: BlockOffset\n backward: boolean\n }\n}\n\nexport function getInputRuleMatchLocation({\n match,\n adjustIndexBy,\n snapshot,\n focusTextBlock,\n originalTextBefore,\n}: {\n match: [string, number, number]\n adjustIndexBy: number\n snapshot: EditorSnapshot\n focusTextBlock: {\n path: BlockPath\n }\n originalTextBefore: string\n}): InputRuleMatchLocation | undefined {\n const [text, start, end] = match\n const adjustedIndex = start + adjustIndexBy\n\n const targetOffsets = {\n anchor: {\n path: focusTextBlock.path,\n offset: adjustedIndex,\n },\n focus: {\n path: focusTextBlock.path,\n offset: adjustedIndex + end - start,\n },\n backward: false,\n }\n const normalizedOffsets = {\n anchor: {\n path: focusTextBlock.path,\n offset: Math.min(targetOffsets.anchor.offset, originalTextBefore.length),\n },\n focus: {\n path: focusTextBlock.path,\n offset: Math.min(targetOffsets.focus.offset, originalTextBefore.length),\n },\n backward: false,\n }\n\n const anchorBackwards = blockOffsetToSpanSelectionPoint({\n context: snapshot.context,\n blockOffset: normalizedOffsets.anchor,\n direction: 'backward',\n })\n const focusForwards = blockOffsetToSpanSelectionPoint({\n context: snapshot.context,\n blockOffset: normalizedOffsets.focus,\n direction: 'forward',\n })\n\n if (!anchorBackwards || !focusForwards) {\n return undefined\n }\n\n const selection = {\n anchor: anchorBackwards,\n focus: focusForwards,\n }\n\n const inlineObjectsAfterMatch = getNextInlineObjects({\n ...snapshot,\n context: {\n ...snapshot.context,\n selection: {\n anchor: selection.anchor,\n focus: selection.anchor,\n },\n },\n })\n const inlineObjectsBefore = getPreviousInlineObjects(snapshot)\n\n if (\n inlineObjectsAfterMatch.some((inlineObjectAfter) =>\n inlineObjectsBefore.some(\n (inlineObjectBefore) =>\n inlineObjectAfter.node._key === inlineObjectBefore.node._key,\n ),\n )\n ) {\n return undefined\n }\n\n return {\n text,\n selection,\n targetOffsets,\n }\n}\n","import {useEditor, type BlockOffset, type Editor} from '@portabletext/editor'\nimport {\n defineBehavior,\n effect,\n forward,\n raise,\n type BehaviorAction,\n} from '@portabletext/editor/behaviors'\nimport {\n getBlockOffsets,\n getBlockTextBefore,\n getFocusTextBlock,\n} from '@portabletext/editor/selectors'\nimport {useActorRef} from '@xstate/react'\nimport {\n fromCallback,\n setup,\n type AnyEventObject,\n type CallbackLogicFunction,\n} from 'xstate'\nimport type {InputRule, InputRuleMatch} from './input-rule'\nimport {getInputRuleMatchLocation} from './input-rule-match-location'\n\nfunction createInputRuleBehavior(config: {\n rules: Array<InputRule>\n onApply: ({\n endOffsets,\n }: {\n endOffsets: {start: BlockOffset; end: BlockOffset} | undefined\n }) => void\n}) {\n return defineBehavior({\n on: 'insert.text',\n guard: ({snapshot, event, dom}) => {\n const focusTextBlock = getFocusTextBlock(snapshot)\n\n if (!focusTextBlock) {\n return false\n }\n\n const originalTextBefore = getBlockTextBefore(snapshot)\n let textBefore = originalTextBefore\n const originalNewText = textBefore + event.text\n let newText = originalNewText\n\n const foundMatches: Array<InputRuleMatch['groupMatches'][number]> = []\n const foundActions: Array<BehaviorAction> = []\n\n for (const rule of config.rules) {\n const matcher = new RegExp(rule.on.source, 'gd')\n\n while (true) {\n // Find matches in the text after the insertion\n const ruleMatches = [...newText.matchAll(matcher)].flatMap(\n (regExpMatch) => {\n if (regExpMatch.indices === undefined) {\n return []\n }\n\n const match = regExpMatch.indices.at(0)\n\n if (!match) {\n return []\n }\n\n const matchLocation = getInputRuleMatchLocation({\n match: [regExpMatch.at(0) ?? '', ...match],\n adjustIndexBy: originalNewText.length - newText.length,\n snapshot,\n focusTextBlock,\n originalTextBefore,\n })\n\n if (!matchLocation) {\n return []\n }\n\n const existsInTextBefore =\n matchLocation.targetOffsets.focus.offset <=\n originalTextBefore.length\n\n // Ignore if this match occurs in the text before the insertion\n if (existsInTextBefore) {\n return []\n }\n\n const alreadyFound = foundMatches.some(\n (foundMatch) =>\n foundMatch.targetOffsets.anchor.offset ===\n matchLocation.targetOffsets.anchor.offset,\n )\n\n // Ignore if this match has already been found\n if (alreadyFound) {\n return []\n }\n\n const groupMatches =\n regExpMatch.indices.length > 1\n ? regExpMatch.indices.slice(1)\n : []\n\n const ruleMatch = {\n text: matchLocation.text,\n selection: matchLocation.selection,\n targetOffsets: matchLocation.targetOffsets,\n groupMatches: groupMatches.flatMap((match, index) => {\n const text = regExpMatch.at(index + 1) ?? ''\n const groupMatchLocation = getInputRuleMatchLocation({\n match: [text, ...match],\n adjustIndexBy: originalNewText.length - newText.length,\n snapshot,\n focusTextBlock,\n originalTextBefore,\n })\n\n if (!groupMatchLocation) {\n return []\n }\n\n return groupMatchLocation\n }),\n }\n\n return [ruleMatch]\n },\n )\n\n if (ruleMatches.length > 0) {\n const guardResult =\n rule.guard?.({\n snapshot,\n event: {\n type: 'custom.input rule',\n matches: ruleMatches,\n focusTextBlock,\n textBefore: originalTextBefore,\n textInserted: event.text,\n },\n dom,\n }) ?? true\n\n if (!guardResult) {\n break\n }\n\n const actionSets = rule.actions.map((action) =>\n action(\n {\n snapshot,\n event: {\n type: 'custom.input rule',\n matches: ruleMatches,\n focusTextBlock,\n textBefore: originalTextBefore,\n textInserted: event.text,\n },\n dom,\n },\n guardResult,\n ),\n )\n\n for (const actionSet of actionSets) {\n for (const action of actionSet) {\n foundActions.push(action)\n }\n }\n\n const matches = ruleMatches.flatMap((match) =>\n match.groupMatches.length === 0 ? [match] : match.groupMatches,\n )\n\n for (const match of matches) {\n // Remember each match and adjust `textBefore` and `newText` so\n // no subsequent matches can overlap with this one\n foundMatches.push(match)\n textBefore = newText.slice(\n 0,\n match.targetOffsets.focus.offset ?? 0,\n )\n newText = originalNewText.slice(\n match.targetOffsets.focus.offset ?? 0,\n )\n }\n } else {\n // If no match was found, break out of the loop to try the next\n // rule\n break\n }\n }\n }\n\n if (foundActions.length === 0) {\n return false\n }\n\n return {actions: foundActions}\n },\n actions: [\n ({event}) => [forward(event)],\n (_, {actions}) => actions,\n ({snapshot}) => [\n effect(() => {\n const blockOffsets = getBlockOffsets(snapshot)\n\n config.onApply({endOffsets: blockOffsets})\n }),\n ],\n ],\n })\n}\n\ntype InputRulePluginProps = {\n rules: Array<InputRule>\n}\n\n/**\n * Turn an array of `InputRule`s into a Behavior that can be used to apply the\n * rules to the editor.\n *\n * The plugin handles undo/redo out of the box including smart undo with\n * Backspace.\n *\n * @example\n * ```tsx\n * <InputRulePlugin rules={smartQuotesRules} />\n * ```\n *\n * @alpha\n */\nexport function InputRulePlugin(props: InputRulePluginProps) {\n const editor = useEditor()\n\n useActorRef(inputRuleMachine, {\n input: {editor, rules: props.rules},\n })\n\n return null\n}\n\ntype InputRuleMachineEvent =\n | {\n type: 'input rule raised'\n endOffsets: {start: BlockOffset; end: BlockOffset} | undefined\n }\n | {type: 'history.undo raised'}\n | {\n type: 'selection changed'\n blockOffsets: {start: BlockOffset; end: BlockOffset} | undefined\n }\n\nconst inputRuleListenerCallback: CallbackLogicFunction<\n AnyEventObject,\n InputRuleMachineEvent,\n {\n editor: Editor\n rules: Array<InputRule>\n }\n> = ({input, sendBack}) => {\n const unregister = input.editor.registerBehavior({\n behavior: createInputRuleBehavior({\n rules: input.rules,\n onApply: ({endOffsets}) => {\n sendBack({type: 'input rule raised', endOffsets})\n },\n }),\n })\n\n return () => {\n unregister()\n }\n}\n\nconst deleteBackwardListenerCallback: CallbackLogicFunction<\n AnyEventObject,\n InputRuleMachineEvent,\n {editor: Editor}\n> = ({input, sendBack}) => {\n return input.editor.registerBehavior({\n behavior: defineBehavior({\n on: 'delete.backward',\n actions: [\n () => [\n raise({type: 'history.undo'}),\n effect(() => {\n sendBack({type: 'history.undo raised'})\n }),\n ],\n ],\n }),\n })\n}\n\nconst selectionListenerCallback: CallbackLogicFunction<\n AnyEventObject,\n InputRuleMachineEvent,\n {editor: Editor}\n> = ({sendBack, input}) => {\n const unregister = input.editor.registerBehavior({\n behavior: defineBehavior({\n on: 'select',\n guard: ({snapshot, event}) => {\n const blockOffsets = getBlockOffsets({\n ...snapshot,\n context: {\n ...snapshot.context,\n selection: event.at,\n },\n })\n\n return {blockOffsets}\n },\n actions: [\n ({event}, {blockOffsets}) => [\n effect(() => {\n sendBack({type: 'selection changed', blockOffsets})\n }),\n forward(event),\n ],\n ],\n }),\n })\n\n return unregister\n}\n\nconst inputRuleSetup = setup({\n types: {\n context: {} as {\n editor: Editor\n rules: Array<InputRule>\n endOffsets: {start: BlockOffset; end: BlockOffset} | undefined\n },\n input: {} as {\n editor: Editor\n rules: Array<InputRule>\n },\n events: {} as InputRuleMachineEvent,\n },\n actors: {\n 'delete.backward listener': fromCallback(deleteBackwardListenerCallback),\n 'input rule listener': fromCallback(inputRuleListenerCallback),\n 'selection listener': fromCallback(selectionListenerCallback),\n },\n guards: {\n 'block offset changed': ({context, event}) => {\n if (event.type !== 'selection changed') {\n return false\n }\n\n if (!event.blockOffsets || !context.endOffsets) {\n return true\n }\n\n const startChanged =\n context.endOffsets.start.path[0]._key !==\n event.blockOffsets.start.path[0]._key ||\n context.endOffsets.start.offset !== event.blockOffsets.start.offset\n const endChanged =\n context.endOffsets.end.path[0]._key !==\n event.blockOffsets.end.path[0]._key ||\n context.endOffsets.end.offset !== event.blockOffsets.end.offset\n\n return startChanged || endChanged\n },\n },\n})\n\nconst assignEndOffsets = inputRuleSetup.assign({\n endOffsets: ({context, event}) =>\n event.type === 'input rule raised' ? event.endOffsets : context.endOffsets,\n})\n\nconst inputRuleMachine = inputRuleSetup.createMachine({\n id: 'input rule',\n context: ({input}) => ({\n editor: input.editor,\n rules: input.rules,\n endOffsets: undefined,\n }),\n initial: 'idle',\n invoke: {\n src: 'input rule listener',\n input: ({context}) => ({\n editor: context.editor,\n rules: context.rules,\n }),\n },\n on: {\n 'input rule raised': {\n target: '.input rule applied',\n actions: assignEndOffsets,\n },\n },\n states: {\n 'idle': {},\n 'input rule applied': {\n invoke: [\n {\n src: 'delete.backward listener',\n input: ({context}) => ({editor: context.editor}),\n },\n {\n src: 'selection listener',\n input: ({context}) => ({editor: context.editor}),\n },\n ],\n on: {\n 'selection changed': {\n target: 'idle',\n guard: 'block offset changed',\n },\n 'history.undo raised': {\n target: 'idle',\n },\n },\n },\n },\n})\n","import {raise, type BehaviorAction} from '@portabletext/editor/behaviors'\nimport {getMarkState} from '@portabletext/editor/selectors'\nimport type {InputRule, InputRuleGuard} from './input-rule'\nimport type {InputRuleMatchLocation} from './input-rule-match-location'\n\n/**\n * @alpha\n */\nexport type TextTransformRule = {\n on: RegExp\n guard?: InputRuleGuard\n transform: ({location}: {location: InputRuleMatchLocation}) => string\n}\n\n/**\n * Define an `InputRule` specifically designed to transform matched text into\n * some other text.\n *\n * @example\n * ```tsx\n * const transformRule = defineTextTransformRule({\n * on: /--/,\n * transform: () => '—',\n * })\n * ```\n *\n * @alpha\n */\nexport function defineTextTransformRule(config: TextTransformRule): InputRule {\n return {\n on: config.on,\n guard: config.guard ?? (() => true),\n actions: [\n ({snapshot, event}) => {\n const locations = event.matches.flatMap((match) =>\n match.groupMatches.length === 0 ? [match] : match.groupMatches,\n )\n const newText = event.textBefore + event.textInserted\n\n let textLengthDelta = 0\n const actions: Array<BehaviorAction> = []\n\n for (const location of locations.reverse()) {\n const text = config.transform({location})\n\n textLengthDelta =\n textLengthDelta -\n (text.length -\n (location.targetOffsets.focus.offset -\n location.targetOffsets.anchor.offset))\n\n actions.push(raise({type: 'select', at: location.targetOffsets}))\n actions.push(raise({type: 'delete', at: location.targetOffsets}))\n actions.push(\n raise({\n type: 'insert.child',\n child: {\n _type: snapshot.context.schema.span.name,\n text,\n marks:\n getMarkState({\n ...snapshot,\n context: {\n ...snapshot.context,\n selection: {\n anchor: location.selection.anchor,\n focus: {\n path: location.selection.focus.path,\n offset: Math.min(\n location.selection.focus.offset,\n event.textBefore.length,\n ),\n },\n },\n },\n })?.marks ?? [],\n },\n }),\n )\n }\n\n const endCaretPosition = {\n path: event.focusTextBlock.path,\n offset: newText.length - textLengthDelta,\n }\n\n return [\n ...actions,\n raise({\n type: 'select',\n at: {\n anchor: endCaretPosition,\n focus: endCaretPosition,\n },\n }),\n ]\n },\n ],\n }\n}\n"],"names":["defineInputRule","config","getInputRuleMatchLocation","match","adjustIndexBy","snapshot","focusTextBlock","originalTextBefore","text","start","end","adjustedIndex","targetOffsets","anchor","path","offset","focus","backward","normalizedOffsets","Math","min","length","anchorBackwards","blockOffsetToSpanSelectionPoint","context","blockOffset","direction","focusForwards","selection","inlineObjectsAfterMatch","getNextInlineObjects","inlineObjectsBefore","getPreviousInlineObjects","some","inlineObjectAfter","inlineObjectBefore","node","_key","createInputRuleBehavior","defineBehavior","on","guard","event","dom","getFocusTextBlock","getBlockTextBefore","textBefore","originalNewText","newText","foundMatches","foundActions","rule","rules","matcher","RegExp","source","ruleMatches","matchAll","flatMap","regExpMatch","indices","undefined","at","matchLocation","foundMatch","groupMatches","slice","index","guardResult","type","matches","textInserted","actionSets","actions","map","action","actionSet","push","forward","_","effect","blockOffsets","getBlockOffsets","onApply","endOffsets","InputRulePlugin","props","$","_c","editor","useEditor","t0","input","useActorRef","inputRuleMachine","inputRuleListenerCallback","sendBack","unregister","registerBehavior","behavior","deleteBackwardListenerCallback","raise","selectionListenerCallback","inputRuleSetup","setup","types","events","actors","fromCallback","guards","block offset changed","startChanged","endChanged","assignEndOffsets","assign","createMachine","id","initial","invoke","src","target","states","defineTextTransformRule","locations","textLengthDelta","location","reverse","transform","child","_type","schema","span","name","marks","getMarkState","endCaretPosition"],"mappings":"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"}
|
package/dist/index.d.cts
CHANGED
|
@@ -81,6 +81,10 @@ export declare type InputRuleMatch = InputRuleMatchLocation & {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
declare type InputRuleMatchLocation = {
|
|
84
|
+
/**
|
|
85
|
+
* The matched text
|
|
86
|
+
*/
|
|
87
|
+
text: string
|
|
84
88
|
/**
|
|
85
89
|
* Estimated selection of where in the original text the match is located.
|
|
86
90
|
* The selection is estimated since the match is found in the text after
|
|
@@ -123,7 +127,7 @@ declare type InputRulePluginProps = {
|
|
|
123
127
|
export declare type TextTransformRule = {
|
|
124
128
|
on: RegExp
|
|
125
129
|
guard?: InputRuleGuard
|
|
126
|
-
transform: () => string
|
|
130
|
+
transform: ({location}: {location: InputRuleMatchLocation}) => string
|
|
127
131
|
}
|
|
128
132
|
|
|
129
133
|
export {}
|
package/dist/index.d.ts
CHANGED
|
@@ -81,6 +81,10 @@ export declare type InputRuleMatch = InputRuleMatchLocation & {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
declare type InputRuleMatchLocation = {
|
|
84
|
+
/**
|
|
85
|
+
* The matched text
|
|
86
|
+
*/
|
|
87
|
+
text: string
|
|
84
88
|
/**
|
|
85
89
|
* Estimated selection of where in the original text the match is located.
|
|
86
90
|
* The selection is estimated since the match is found in the text after
|
|
@@ -123,7 +127,7 @@ declare type InputRulePluginProps = {
|
|
|
123
127
|
export declare type TextTransformRule = {
|
|
124
128
|
on: RegExp
|
|
125
129
|
guard?: InputRuleGuard
|
|
126
|
-
transform: () => string
|
|
130
|
+
transform: ({location}: {location: InputRuleMatchLocation}) => string
|
|
127
131
|
}
|
|
128
132
|
|
|
129
133
|
export {}
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { c } from "react/compiler-runtime";
|
|
2
2
|
import { useEditor } from "@portabletext/editor";
|
|
3
3
|
import { defineBehavior, effect, forward, raise } from "@portabletext/editor/behaviors";
|
|
4
|
-
import { getBlockOffsets, getFocusTextBlock, getBlockTextBefore, getMarkState } from "@portabletext/editor/selectors";
|
|
4
|
+
import { getNextInlineObjects, getPreviousInlineObjects, getBlockOffsets, getFocusTextBlock, getBlockTextBefore, getMarkState } from "@portabletext/editor/selectors";
|
|
5
5
|
import { useActorRef } from "@xstate/react";
|
|
6
6
|
import { setup, fromCallback } from "xstate";
|
|
7
|
-
import {
|
|
7
|
+
import { blockOffsetToSpanSelectionPoint } from "@portabletext/editor/utils";
|
|
8
8
|
function defineInputRule(config) {
|
|
9
9
|
return config;
|
|
10
10
|
}
|
|
@@ -15,7 +15,7 @@ function getInputRuleMatchLocation({
|
|
|
15
15
|
focusTextBlock,
|
|
16
16
|
originalTextBefore
|
|
17
17
|
}) {
|
|
18
|
-
const [start, end] = match, adjustedIndex = start + adjustIndexBy, targetOffsets = {
|
|
18
|
+
const [text, start, end] = match, adjustedIndex = start + adjustIndexBy, targetOffsets = {
|
|
19
19
|
anchor: {
|
|
20
20
|
path: focusTextBlock.path,
|
|
21
21
|
offset: adjustedIndex
|
|
@@ -33,15 +33,34 @@ function getInputRuleMatchLocation({
|
|
|
33
33
|
focus: {
|
|
34
34
|
path: focusTextBlock.path,
|
|
35
35
|
offset: Math.min(targetOffsets.focus.offset, originalTextBefore.length)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
}, selection = blockOffsetsToSelection({
|
|
36
|
+
}
|
|
37
|
+
}, anchorBackwards = blockOffsetToSpanSelectionPoint({
|
|
39
38
|
context: snapshot.context,
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
blockOffset: normalizedOffsets.anchor,
|
|
40
|
+
direction: "backward"
|
|
41
|
+
}), focusForwards = blockOffsetToSpanSelectionPoint({
|
|
42
|
+
context: snapshot.context,
|
|
43
|
+
blockOffset: normalizedOffsets.focus,
|
|
44
|
+
direction: "forward"
|
|
42
45
|
});
|
|
43
|
-
if (
|
|
46
|
+
if (!anchorBackwards || !focusForwards)
|
|
47
|
+
return;
|
|
48
|
+
const selection = {
|
|
49
|
+
anchor: anchorBackwards,
|
|
50
|
+
focus: focusForwards
|
|
51
|
+
}, inlineObjectsAfterMatch = getNextInlineObjects({
|
|
52
|
+
...snapshot,
|
|
53
|
+
context: {
|
|
54
|
+
...snapshot.context,
|
|
55
|
+
selection: {
|
|
56
|
+
anchor: selection.anchor,
|
|
57
|
+
focus: selection.anchor
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}), inlineObjectsBefore = getPreviousInlineObjects(snapshot);
|
|
61
|
+
if (!inlineObjectsAfterMatch.some((inlineObjectAfter) => inlineObjectsBefore.some((inlineObjectBefore) => inlineObjectAfter.node._key === inlineObjectBefore.node._key)))
|
|
44
62
|
return {
|
|
63
|
+
text,
|
|
45
64
|
selection,
|
|
46
65
|
targetOffsets
|
|
47
66
|
};
|
|
@@ -72,7 +91,7 @@ function createInputRuleBehavior(config) {
|
|
|
72
91
|
if (!match)
|
|
73
92
|
return [];
|
|
74
93
|
const matchLocation = getInputRuleMatchLocation({
|
|
75
|
-
match,
|
|
94
|
+
match: [regExpMatch.at(0) ?? "", ...match],
|
|
76
95
|
adjustIndexBy: originalNewText.length - newText.length,
|
|
77
96
|
snapshot,
|
|
78
97
|
focusTextBlock,
|
|
@@ -86,15 +105,19 @@ function createInputRuleBehavior(config) {
|
|
|
86
105
|
return [];
|
|
87
106
|
const groupMatches = regExpMatch.indices.length > 1 ? regExpMatch.indices.slice(1) : [];
|
|
88
107
|
return [{
|
|
108
|
+
text: matchLocation.text,
|
|
89
109
|
selection: matchLocation.selection,
|
|
90
110
|
targetOffsets: matchLocation.targetOffsets,
|
|
91
|
-
groupMatches: groupMatches.flatMap((match2) =>
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
111
|
+
groupMatches: groupMatches.flatMap((match2, index) => {
|
|
112
|
+
const text = regExpMatch.at(index + 1) ?? "";
|
|
113
|
+
return getInputRuleMatchLocation({
|
|
114
|
+
match: [text, ...match2],
|
|
115
|
+
adjustIndexBy: originalNewText.length - newText.length,
|
|
116
|
+
snapshot,
|
|
117
|
+
focusTextBlock,
|
|
118
|
+
originalTextBefore
|
|
119
|
+
}) || [];
|
|
120
|
+
})
|
|
98
121
|
}];
|
|
99
122
|
});
|
|
100
123
|
if (ruleMatches.length > 0) {
|
|
@@ -315,36 +338,45 @@ function defineTextTransformRule(config) {
|
|
|
315
338
|
snapshot,
|
|
316
339
|
event
|
|
317
340
|
}) => {
|
|
318
|
-
const
|
|
341
|
+
const locations = event.matches.flatMap((match) => match.groupMatches.length === 0 ? [match] : match.groupMatches), newText = event.textBefore + event.textInserted;
|
|
342
|
+
let textLengthDelta = 0;
|
|
343
|
+
const actions = [];
|
|
344
|
+
for (const location of locations.reverse()) {
|
|
345
|
+
const text = config.transform({
|
|
346
|
+
location
|
|
347
|
+
});
|
|
348
|
+
textLengthDelta = textLengthDelta - (text.length - (location.targetOffsets.focus.offset - location.targetOffsets.anchor.offset)), actions.push(raise({
|
|
349
|
+
type: "select",
|
|
350
|
+
at: location.targetOffsets
|
|
351
|
+
})), actions.push(raise({
|
|
352
|
+
type: "delete",
|
|
353
|
+
at: location.targetOffsets
|
|
354
|
+
})), actions.push(raise({
|
|
355
|
+
type: "insert.child",
|
|
356
|
+
child: {
|
|
357
|
+
_type: snapshot.context.schema.span.name,
|
|
358
|
+
text,
|
|
359
|
+
marks: getMarkState({
|
|
360
|
+
...snapshot,
|
|
361
|
+
context: {
|
|
362
|
+
...snapshot.context,
|
|
363
|
+
selection: {
|
|
364
|
+
anchor: location.selection.anchor,
|
|
365
|
+
focus: {
|
|
366
|
+
path: location.selection.focus.path,
|
|
367
|
+
offset: Math.min(location.selection.focus.offset, event.textBefore.length)
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
})?.marks ?? []
|
|
372
|
+
}
|
|
373
|
+
}));
|
|
374
|
+
}
|
|
375
|
+
const endCaretPosition = {
|
|
319
376
|
path: event.focusTextBlock.path,
|
|
320
377
|
offset: newText.length - textLengthDelta
|
|
321
378
|
};
|
|
322
|
-
return [...
|
|
323
|
-
type: "select",
|
|
324
|
-
at: match.targetOffsets
|
|
325
|
-
}), raise({
|
|
326
|
-
type: "delete",
|
|
327
|
-
at: match.targetOffsets
|
|
328
|
-
}), raise({
|
|
329
|
-
type: "insert.child",
|
|
330
|
-
child: {
|
|
331
|
-
_type: snapshot.context.schema.span.name,
|
|
332
|
-
text: config.transform(),
|
|
333
|
-
marks: getMarkState({
|
|
334
|
-
...snapshot,
|
|
335
|
-
context: {
|
|
336
|
-
...snapshot.context,
|
|
337
|
-
selection: {
|
|
338
|
-
anchor: match.selection.anchor,
|
|
339
|
-
focus: {
|
|
340
|
-
path: match.selection.focus.path,
|
|
341
|
-
offset: Math.min(match.selection.focus.offset, event.textBefore.length)
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
})?.marks ?? []
|
|
346
|
-
}
|
|
347
|
-
})]), raise({
|
|
379
|
+
return [...actions, raise({
|
|
348
380
|
type: "select",
|
|
349
381
|
at: {
|
|
350
382
|
anchor: endCaretPosition,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/input-rule.ts","../src/input-rule-match-location.ts","../src/plugin.input-rule.tsx","../src/text-transform-rule.ts"],"sourcesContent":["import type {BlockPath, PortableTextTextBlock} from '@portabletext/editor'\nimport type {\n BehaviorActionSet,\n BehaviorGuard,\n} from '@portabletext/editor/behaviors'\nimport type {InputRuleMatchLocation} from './input-rule-match-location'\n\n/**\n * Match found in the text after the insertion\n * @alpha\n */\nexport type InputRuleMatch = InputRuleMatchLocation & {\n groupMatches: Array<InputRuleMatchLocation>\n}\n\n/**\n * @alpha\n */\nexport type InputRuleEvent = {\n type: 'custom.input rule'\n /**\n * Matches found by the input rule\n */\n matches: Array<InputRuleMatch>\n /**\n * The text before the insertion\n */\n textBefore: string\n /**\n * The text is destined to be inserted\n */\n textInserted: string\n /**\n * The text block where the insertion takes place\n */\n focusTextBlock: {\n path: BlockPath\n node: PortableTextTextBlock\n }\n}\n\n/**\n * @alpha\n */\nexport type InputRuleGuard = BehaviorGuard<InputRuleEvent, boolean>\n\n/**\n * @alpha\n */\nexport type InputRule = {\n on: RegExp\n guard?: InputRuleGuard\n actions: Array<BehaviorActionSet<InputRuleEvent, boolean>>\n}\n\n/**\n * @alpha\n */\nexport function defineInputRule(config: InputRule): InputRule {\n return config\n}\n","import type {\n BlockOffset,\n BlockPath,\n EditorSelection,\n EditorSnapshot,\n} from '@portabletext/editor'\nimport {blockOffsetsToSelection} from '@portabletext/editor/utils'\n\nexport type InputRuleMatchLocation = {\n /**\n * Estimated selection of where in the original text the match is located.\n * The selection is estimated since the match is found in the text after\n * insertion.\n */\n selection: NonNullable<EditorSelection>\n /**\n * Block offsets of the match in the text after the insertion\n */\n targetOffsets: {\n anchor: BlockOffset\n focus: BlockOffset\n backward: boolean\n }\n}\n\nexport function getInputRuleMatchLocation({\n match,\n adjustIndexBy,\n snapshot,\n focusTextBlock,\n originalTextBefore,\n}: {\n match: [number, number]\n adjustIndexBy: number\n snapshot: EditorSnapshot\n focusTextBlock: {\n path: BlockPath\n }\n originalTextBefore: string\n}): InputRuleMatchLocation | undefined {\n const [start, end] = match\n const adjustedIndex = start + adjustIndexBy\n\n const targetOffsets = {\n anchor: {\n path: focusTextBlock.path,\n offset: adjustedIndex,\n },\n focus: {\n path: focusTextBlock.path,\n offset: adjustedIndex + end - start,\n },\n backward: false,\n }\n const normalizedOffsets = {\n anchor: {\n path: focusTextBlock.path,\n offset: Math.min(targetOffsets.anchor.offset, originalTextBefore.length),\n },\n focus: {\n path: focusTextBlock.path,\n offset: Math.min(targetOffsets.focus.offset, originalTextBefore.length),\n },\n backward: false,\n }\n const selection = blockOffsetsToSelection({\n context: snapshot.context,\n offsets: normalizedOffsets,\n backward: false,\n })\n\n if (!selection) {\n return undefined\n }\n\n return {\n selection,\n targetOffsets,\n }\n}\n","import {useEditor, type BlockOffset, type Editor} from '@portabletext/editor'\nimport {\n defineBehavior,\n effect,\n forward,\n raise,\n type BehaviorAction,\n} from '@portabletext/editor/behaviors'\nimport {\n getBlockOffsets,\n getBlockTextBefore,\n getFocusTextBlock,\n} from '@portabletext/editor/selectors'\nimport {useActorRef} from '@xstate/react'\nimport {\n fromCallback,\n setup,\n type AnyEventObject,\n type CallbackLogicFunction,\n} from 'xstate'\nimport type {InputRule, InputRuleMatch} from './input-rule'\nimport {getInputRuleMatchLocation} from './input-rule-match-location'\n\nfunction createInputRuleBehavior(config: {\n rules: Array<InputRule>\n onApply: ({\n endOffsets,\n }: {\n endOffsets: {start: BlockOffset; end: BlockOffset} | undefined\n }) => void\n}) {\n return defineBehavior({\n on: 'insert.text',\n guard: ({snapshot, event, dom}) => {\n const focusTextBlock = getFocusTextBlock(snapshot)\n\n if (!focusTextBlock) {\n return false\n }\n\n const originalTextBefore = getBlockTextBefore(snapshot)\n let textBefore = originalTextBefore\n const originalNewText = textBefore + event.text\n let newText = originalNewText\n\n const foundMatches: Array<InputRuleMatch['groupMatches'][number]> = []\n const foundActions: Array<BehaviorAction> = []\n\n for (const rule of config.rules) {\n const matcher = new RegExp(rule.on.source, 'gd')\n\n while (true) {\n // Find matches in the text after the insertion\n const ruleMatches = [...newText.matchAll(matcher)].flatMap(\n (regExpMatch) => {\n if (regExpMatch.indices === undefined) {\n return []\n }\n\n const match = regExpMatch.indices.at(0)\n\n if (!match) {\n return []\n }\n\n const matchLocation = getInputRuleMatchLocation({\n match,\n adjustIndexBy: originalNewText.length - newText.length,\n snapshot,\n focusTextBlock,\n originalTextBefore,\n })\n\n if (!matchLocation) {\n return []\n }\n\n const existsInTextBefore =\n matchLocation.targetOffsets.focus.offset <=\n originalTextBefore.length\n\n // Ignore if this match occurs in the text before the insertion\n if (existsInTextBefore) {\n return []\n }\n\n const alreadyFound = foundMatches.some(\n (foundMatch) =>\n foundMatch.targetOffsets.anchor.offset ===\n matchLocation.targetOffsets.anchor.offset,\n )\n\n // Ignore if this match has already been found\n if (alreadyFound) {\n return []\n }\n\n const groupMatches =\n regExpMatch.indices.length > 1\n ? regExpMatch.indices.slice(1)\n : []\n\n const ruleMatch = {\n selection: matchLocation.selection,\n targetOffsets: matchLocation.targetOffsets,\n groupMatches: groupMatches.flatMap((match) => {\n const groupMatchLocation = getInputRuleMatchLocation({\n match,\n adjustIndexBy: originalNewText.length - newText.length,\n snapshot,\n focusTextBlock,\n originalTextBefore,\n })\n\n if (!groupMatchLocation) {\n return []\n }\n\n return groupMatchLocation\n }),\n }\n\n return [ruleMatch]\n },\n )\n\n if (ruleMatches.length > 0) {\n const guardResult =\n rule.guard?.({\n snapshot,\n event: {\n type: 'custom.input rule',\n matches: ruleMatches,\n focusTextBlock,\n textBefore: originalTextBefore,\n textInserted: event.text,\n },\n dom,\n }) ?? true\n\n if (!guardResult) {\n break\n }\n\n const actionSets = rule.actions.map((action) =>\n action(\n {\n snapshot,\n event: {\n type: 'custom.input rule',\n matches: ruleMatches,\n focusTextBlock,\n textBefore: originalTextBefore,\n textInserted: event.text,\n },\n dom,\n },\n guardResult,\n ),\n )\n\n for (const actionSet of actionSets) {\n for (const action of actionSet) {\n foundActions.push(action)\n }\n }\n\n const matches = ruleMatches.flatMap((match) =>\n match.groupMatches.length === 0 ? [match] : match.groupMatches,\n )\n\n for (const match of matches) {\n // Remember each match and adjust `textBefore` and `newText` so\n // no subsequent matches can overlap with this one\n foundMatches.push(match)\n textBefore = newText.slice(\n 0,\n match.targetOffsets.focus.offset ?? 0,\n )\n newText = originalNewText.slice(\n match.targetOffsets.focus.offset ?? 0,\n )\n }\n } else {\n // If no match was found, break out of the loop to try the next\n // rule\n break\n }\n }\n }\n\n if (foundActions.length === 0) {\n return false\n }\n\n return {actions: foundActions}\n },\n actions: [\n ({event}) => [forward(event)],\n (_, {actions}) => actions,\n ({snapshot}) => [\n effect(() => {\n const blockOffsets = getBlockOffsets(snapshot)\n\n config.onApply({endOffsets: blockOffsets})\n }),\n ],\n ],\n })\n}\n\ntype InputRulePluginProps = {\n rules: Array<InputRule>\n}\n\n/**\n * Turn an array of `InputRule`s into a Behavior that can be used to apply the\n * rules to the editor.\n *\n * The plugin handles undo/redo out of the box including smart undo with\n * Backspace.\n *\n * @example\n * ```tsx\n * <InputRulePlugin rules={smartQuotesRules} />\n * ```\n *\n * @alpha\n */\nexport function InputRulePlugin(props: InputRulePluginProps) {\n const editor = useEditor()\n\n useActorRef(inputRuleMachine, {\n input: {editor, rules: props.rules},\n })\n\n return null\n}\n\ntype InputRuleMachineEvent =\n | {\n type: 'input rule raised'\n endOffsets: {start: BlockOffset; end: BlockOffset} | undefined\n }\n | {type: 'history.undo raised'}\n | {\n type: 'selection changed'\n blockOffsets: {start: BlockOffset; end: BlockOffset} | undefined\n }\n\nconst inputRuleListenerCallback: CallbackLogicFunction<\n AnyEventObject,\n InputRuleMachineEvent,\n {\n editor: Editor\n rules: Array<InputRule>\n }\n> = ({input, sendBack}) => {\n const unregister = input.editor.registerBehavior({\n behavior: createInputRuleBehavior({\n rules: input.rules,\n onApply: ({endOffsets}) => {\n sendBack({type: 'input rule raised', endOffsets})\n },\n }),\n })\n\n return () => {\n unregister()\n }\n}\n\nconst deleteBackwardListenerCallback: CallbackLogicFunction<\n AnyEventObject,\n InputRuleMachineEvent,\n {editor: Editor}\n> = ({input, sendBack}) => {\n return input.editor.registerBehavior({\n behavior: defineBehavior({\n on: 'delete.backward',\n actions: [\n () => [\n raise({type: 'history.undo'}),\n effect(() => {\n sendBack({type: 'history.undo raised'})\n }),\n ],\n ],\n }),\n })\n}\n\nconst selectionListenerCallback: CallbackLogicFunction<\n AnyEventObject,\n InputRuleMachineEvent,\n {editor: Editor}\n> = ({sendBack, input}) => {\n const unregister = input.editor.registerBehavior({\n behavior: defineBehavior({\n on: 'select',\n guard: ({snapshot, event}) => {\n const blockOffsets = getBlockOffsets({\n ...snapshot,\n context: {\n ...snapshot.context,\n selection: event.at,\n },\n })\n\n return {blockOffsets}\n },\n actions: [\n ({event}, {blockOffsets}) => [\n effect(() => {\n sendBack({type: 'selection changed', blockOffsets})\n }),\n forward(event),\n ],\n ],\n }),\n })\n\n return unregister\n}\n\nconst inputRuleSetup = setup({\n types: {\n context: {} as {\n editor: Editor\n rules: Array<InputRule>\n endOffsets: {start: BlockOffset; end: BlockOffset} | undefined\n },\n input: {} as {\n editor: Editor\n rules: Array<InputRule>\n },\n events: {} as InputRuleMachineEvent,\n },\n actors: {\n 'delete.backward listener': fromCallback(deleteBackwardListenerCallback),\n 'input rule listener': fromCallback(inputRuleListenerCallback),\n 'selection listener': fromCallback(selectionListenerCallback),\n },\n guards: {\n 'block offset changed': ({context, event}) => {\n if (event.type !== 'selection changed') {\n return false\n }\n\n if (!event.blockOffsets || !context.endOffsets) {\n return true\n }\n\n const startChanged =\n context.endOffsets.start.path[0]._key !==\n event.blockOffsets.start.path[0]._key ||\n context.endOffsets.start.offset !== event.blockOffsets.start.offset\n const endChanged =\n context.endOffsets.end.path[0]._key !==\n event.blockOffsets.end.path[0]._key ||\n context.endOffsets.end.offset !== event.blockOffsets.end.offset\n\n return startChanged || endChanged\n },\n },\n})\n\nconst assignEndOffsets = inputRuleSetup.assign({\n endOffsets: ({context, event}) =>\n event.type === 'input rule raised' ? event.endOffsets : context.endOffsets,\n})\n\nconst inputRuleMachine = inputRuleSetup.createMachine({\n id: 'input rule',\n context: ({input}) => ({\n editor: input.editor,\n rules: input.rules,\n endOffsets: undefined,\n }),\n initial: 'idle',\n invoke: {\n src: 'input rule listener',\n input: ({context}) => ({\n editor: context.editor,\n rules: context.rules,\n }),\n },\n on: {\n 'input rule raised': {\n target: '.input rule applied',\n actions: assignEndOffsets,\n },\n },\n states: {\n 'idle': {},\n 'input rule applied': {\n invoke: [\n {\n src: 'delete.backward listener',\n input: ({context}) => ({editor: context.editor}),\n },\n {\n src: 'selection listener',\n input: ({context}) => ({editor: context.editor}),\n },\n ],\n on: {\n 'selection changed': {\n target: 'idle',\n guard: 'block offset changed',\n },\n 'history.undo raised': {\n target: 'idle',\n },\n },\n },\n },\n})\n","import {raise} from '@portabletext/editor/behaviors'\nimport {getMarkState} from '@portabletext/editor/selectors'\nimport type {InputRule, InputRuleGuard} from './input-rule'\n\n/**\n * @alpha\n */\nexport type TextTransformRule = {\n on: RegExp\n guard?: InputRuleGuard\n transform: () => string\n}\n\n/**\n * Define an `InputRule` specifically designed to transform matched text into\n * some other text.\n *\n * @example\n * ```tsx\n * const transformRule = defineTextTransformRule({\n * on: /--/,\n * transform: () => '—',\n * })\n * ```\n *\n * @alpha\n */\nexport function defineTextTransformRule(config: TextTransformRule): InputRule {\n return {\n on: config.on,\n guard: config.guard ?? (() => true),\n actions: [\n ({snapshot, event}) => {\n const matches = event.matches.flatMap((match) =>\n match.groupMatches.length === 0 ? [match] : match.groupMatches,\n )\n const textLengthDelta = matches.reduce((length, match) => {\n return (\n length -\n (config.transform().length -\n (match.targetOffsets.focus.offset -\n match.targetOffsets.anchor.offset))\n )\n }, 0)\n\n const newText = event.textBefore + event.textInserted\n const endCaretPosition = {\n path: event.focusTextBlock.path,\n offset: newText.length - textLengthDelta,\n }\n\n const actions = matches.reverse().flatMap((match) => [\n raise({type: 'select', at: match.targetOffsets}),\n raise({type: 'delete', at: match.targetOffsets}),\n raise({\n type: 'insert.child',\n child: {\n _type: snapshot.context.schema.span.name,\n text: config.transform(),\n marks:\n getMarkState({\n ...snapshot,\n context: {\n ...snapshot.context,\n selection: {\n anchor: match.selection.anchor,\n focus: {\n path: match.selection.focus.path,\n offset: Math.min(\n match.selection.focus.offset,\n event.textBefore.length,\n ),\n },\n },\n },\n })?.marks ?? [],\n },\n }),\n ])\n\n return [\n ...actions,\n raise({\n type: 'select',\n at: {\n anchor: endCaretPosition,\n focus: endCaretPosition,\n },\n }),\n ]\n },\n ],\n }\n}\n"],"names":["defineInputRule","config","getInputRuleMatchLocation","match","adjustIndexBy","snapshot","focusTextBlock","originalTextBefore","start","end","adjustedIndex","targetOffsets","anchor","path","offset","focus","backward","normalizedOffsets","Math","min","length","selection","blockOffsetsToSelection","context","offsets","createInputRuleBehavior","defineBehavior","on","guard","event","dom","getFocusTextBlock","getBlockTextBefore","textBefore","originalNewText","text","newText","foundMatches","foundActions","rule","rules","matcher","RegExp","source","ruleMatches","matchAll","flatMap","regExpMatch","indices","undefined","at","matchLocation","some","foundMatch","groupMatches","slice","guardResult","type","matches","textInserted","actionSets","actions","map","action","actionSet","push","forward","_","effect","blockOffsets","getBlockOffsets","onApply","endOffsets","InputRulePlugin","props","$","_c","editor","useEditor","t0","input","useActorRef","inputRuleMachine","inputRuleListenerCallback","sendBack","unregister","registerBehavior","behavior","deleteBackwardListenerCallback","raise","selectionListenerCallback","inputRuleSetup","setup","types","events","actors","fromCallback","guards","block offset changed","startChanged","_key","endChanged","assignEndOffsets","assign","createMachine","id","initial","invoke","src","target","states","defineTextTransformRule","textLengthDelta","reduce","transform","endCaretPosition","reverse","child","_type","schema","span","name","marks","getMarkState"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/input-rule.ts","../src/input-rule-match-location.ts","../src/plugin.input-rule.tsx","../src/text-transform-rule.ts"],"sourcesContent":["import type {BlockPath, PortableTextTextBlock} from '@portabletext/editor'\nimport type {\n BehaviorActionSet,\n BehaviorGuard,\n} from '@portabletext/editor/behaviors'\nimport type {InputRuleMatchLocation} from './input-rule-match-location'\n\n/**\n * Match found in the text after the insertion\n * @alpha\n */\nexport type InputRuleMatch = InputRuleMatchLocation & {\n groupMatches: Array<InputRuleMatchLocation>\n}\n\n/**\n * @alpha\n */\nexport type InputRuleEvent = {\n type: 'custom.input rule'\n /**\n * Matches found by the input rule\n */\n matches: Array<InputRuleMatch>\n /**\n * The text before the insertion\n */\n textBefore: string\n /**\n * The text is destined to be inserted\n */\n textInserted: string\n /**\n * The text block where the insertion takes place\n */\n focusTextBlock: {\n path: BlockPath\n node: PortableTextTextBlock\n }\n}\n\n/**\n * @alpha\n */\nexport type InputRuleGuard = BehaviorGuard<InputRuleEvent, boolean>\n\n/**\n * @alpha\n */\nexport type InputRule = {\n on: RegExp\n guard?: InputRuleGuard\n actions: Array<BehaviorActionSet<InputRuleEvent, boolean>>\n}\n\n/**\n * @alpha\n */\nexport function defineInputRule(config: InputRule): InputRule {\n return config\n}\n","import type {\n BlockOffset,\n BlockPath,\n EditorSelection,\n EditorSnapshot,\n} from '@portabletext/editor'\nimport {\n getNextInlineObjects,\n getPreviousInlineObjects,\n} from '@portabletext/editor/selectors'\nimport {blockOffsetToSpanSelectionPoint} from '@portabletext/editor/utils'\n\nexport type InputRuleMatchLocation = {\n /**\n * The matched text\n */\n text: string\n /**\n * Estimated selection of where in the original text the match is located.\n * The selection is estimated since the match is found in the text after\n * insertion.\n */\n selection: NonNullable<EditorSelection>\n /**\n * Block offsets of the match in the text after the insertion\n */\n targetOffsets: {\n anchor: BlockOffset\n focus: BlockOffset\n backward: boolean\n }\n}\n\nexport function getInputRuleMatchLocation({\n match,\n adjustIndexBy,\n snapshot,\n focusTextBlock,\n originalTextBefore,\n}: {\n match: [string, number, number]\n adjustIndexBy: number\n snapshot: EditorSnapshot\n focusTextBlock: {\n path: BlockPath\n }\n originalTextBefore: string\n}): InputRuleMatchLocation | undefined {\n const [text, start, end] = match\n const adjustedIndex = start + adjustIndexBy\n\n const targetOffsets = {\n anchor: {\n path: focusTextBlock.path,\n offset: adjustedIndex,\n },\n focus: {\n path: focusTextBlock.path,\n offset: adjustedIndex + end - start,\n },\n backward: false,\n }\n const normalizedOffsets = {\n anchor: {\n path: focusTextBlock.path,\n offset: Math.min(targetOffsets.anchor.offset, originalTextBefore.length),\n },\n focus: {\n path: focusTextBlock.path,\n offset: Math.min(targetOffsets.focus.offset, originalTextBefore.length),\n },\n backward: false,\n }\n\n const anchorBackwards = blockOffsetToSpanSelectionPoint({\n context: snapshot.context,\n blockOffset: normalizedOffsets.anchor,\n direction: 'backward',\n })\n const focusForwards = blockOffsetToSpanSelectionPoint({\n context: snapshot.context,\n blockOffset: normalizedOffsets.focus,\n direction: 'forward',\n })\n\n if (!anchorBackwards || !focusForwards) {\n return undefined\n }\n\n const selection = {\n anchor: anchorBackwards,\n focus: focusForwards,\n }\n\n const inlineObjectsAfterMatch = getNextInlineObjects({\n ...snapshot,\n context: {\n ...snapshot.context,\n selection: {\n anchor: selection.anchor,\n focus: selection.anchor,\n },\n },\n })\n const inlineObjectsBefore = getPreviousInlineObjects(snapshot)\n\n if (\n inlineObjectsAfterMatch.some((inlineObjectAfter) =>\n inlineObjectsBefore.some(\n (inlineObjectBefore) =>\n inlineObjectAfter.node._key === inlineObjectBefore.node._key,\n ),\n )\n ) {\n return undefined\n }\n\n return {\n text,\n selection,\n targetOffsets,\n }\n}\n","import {useEditor, type BlockOffset, type Editor} from '@portabletext/editor'\nimport {\n defineBehavior,\n effect,\n forward,\n raise,\n type BehaviorAction,\n} from '@portabletext/editor/behaviors'\nimport {\n getBlockOffsets,\n getBlockTextBefore,\n getFocusTextBlock,\n} from '@portabletext/editor/selectors'\nimport {useActorRef} from '@xstate/react'\nimport {\n fromCallback,\n setup,\n type AnyEventObject,\n type CallbackLogicFunction,\n} from 'xstate'\nimport type {InputRule, InputRuleMatch} from './input-rule'\nimport {getInputRuleMatchLocation} from './input-rule-match-location'\n\nfunction createInputRuleBehavior(config: {\n rules: Array<InputRule>\n onApply: ({\n endOffsets,\n }: {\n endOffsets: {start: BlockOffset; end: BlockOffset} | undefined\n }) => void\n}) {\n return defineBehavior({\n on: 'insert.text',\n guard: ({snapshot, event, dom}) => {\n const focusTextBlock = getFocusTextBlock(snapshot)\n\n if (!focusTextBlock) {\n return false\n }\n\n const originalTextBefore = getBlockTextBefore(snapshot)\n let textBefore = originalTextBefore\n const originalNewText = textBefore + event.text\n let newText = originalNewText\n\n const foundMatches: Array<InputRuleMatch['groupMatches'][number]> = []\n const foundActions: Array<BehaviorAction> = []\n\n for (const rule of config.rules) {\n const matcher = new RegExp(rule.on.source, 'gd')\n\n while (true) {\n // Find matches in the text after the insertion\n const ruleMatches = [...newText.matchAll(matcher)].flatMap(\n (regExpMatch) => {\n if (regExpMatch.indices === undefined) {\n return []\n }\n\n const match = regExpMatch.indices.at(0)\n\n if (!match) {\n return []\n }\n\n const matchLocation = getInputRuleMatchLocation({\n match: [regExpMatch.at(0) ?? '', ...match],\n adjustIndexBy: originalNewText.length - newText.length,\n snapshot,\n focusTextBlock,\n originalTextBefore,\n })\n\n if (!matchLocation) {\n return []\n }\n\n const existsInTextBefore =\n matchLocation.targetOffsets.focus.offset <=\n originalTextBefore.length\n\n // Ignore if this match occurs in the text before the insertion\n if (existsInTextBefore) {\n return []\n }\n\n const alreadyFound = foundMatches.some(\n (foundMatch) =>\n foundMatch.targetOffsets.anchor.offset ===\n matchLocation.targetOffsets.anchor.offset,\n )\n\n // Ignore if this match has already been found\n if (alreadyFound) {\n return []\n }\n\n const groupMatches =\n regExpMatch.indices.length > 1\n ? regExpMatch.indices.slice(1)\n : []\n\n const ruleMatch = {\n text: matchLocation.text,\n selection: matchLocation.selection,\n targetOffsets: matchLocation.targetOffsets,\n groupMatches: groupMatches.flatMap((match, index) => {\n const text = regExpMatch.at(index + 1) ?? ''\n const groupMatchLocation = getInputRuleMatchLocation({\n match: [text, ...match],\n adjustIndexBy: originalNewText.length - newText.length,\n snapshot,\n focusTextBlock,\n originalTextBefore,\n })\n\n if (!groupMatchLocation) {\n return []\n }\n\n return groupMatchLocation\n }),\n }\n\n return [ruleMatch]\n },\n )\n\n if (ruleMatches.length > 0) {\n const guardResult =\n rule.guard?.({\n snapshot,\n event: {\n type: 'custom.input rule',\n matches: ruleMatches,\n focusTextBlock,\n textBefore: originalTextBefore,\n textInserted: event.text,\n },\n dom,\n }) ?? true\n\n if (!guardResult) {\n break\n }\n\n const actionSets = rule.actions.map((action) =>\n action(\n {\n snapshot,\n event: {\n type: 'custom.input rule',\n matches: ruleMatches,\n focusTextBlock,\n textBefore: originalTextBefore,\n textInserted: event.text,\n },\n dom,\n },\n guardResult,\n ),\n )\n\n for (const actionSet of actionSets) {\n for (const action of actionSet) {\n foundActions.push(action)\n }\n }\n\n const matches = ruleMatches.flatMap((match) =>\n match.groupMatches.length === 0 ? [match] : match.groupMatches,\n )\n\n for (const match of matches) {\n // Remember each match and adjust `textBefore` and `newText` so\n // no subsequent matches can overlap with this one\n foundMatches.push(match)\n textBefore = newText.slice(\n 0,\n match.targetOffsets.focus.offset ?? 0,\n )\n newText = originalNewText.slice(\n match.targetOffsets.focus.offset ?? 0,\n )\n }\n } else {\n // If no match was found, break out of the loop to try the next\n // rule\n break\n }\n }\n }\n\n if (foundActions.length === 0) {\n return false\n }\n\n return {actions: foundActions}\n },\n actions: [\n ({event}) => [forward(event)],\n (_, {actions}) => actions,\n ({snapshot}) => [\n effect(() => {\n const blockOffsets = getBlockOffsets(snapshot)\n\n config.onApply({endOffsets: blockOffsets})\n }),\n ],\n ],\n })\n}\n\ntype InputRulePluginProps = {\n rules: Array<InputRule>\n}\n\n/**\n * Turn an array of `InputRule`s into a Behavior that can be used to apply the\n * rules to the editor.\n *\n * The plugin handles undo/redo out of the box including smart undo with\n * Backspace.\n *\n * @example\n * ```tsx\n * <InputRulePlugin rules={smartQuotesRules} />\n * ```\n *\n * @alpha\n */\nexport function InputRulePlugin(props: InputRulePluginProps) {\n const editor = useEditor()\n\n useActorRef(inputRuleMachine, {\n input: {editor, rules: props.rules},\n })\n\n return null\n}\n\ntype InputRuleMachineEvent =\n | {\n type: 'input rule raised'\n endOffsets: {start: BlockOffset; end: BlockOffset} | undefined\n }\n | {type: 'history.undo raised'}\n | {\n type: 'selection changed'\n blockOffsets: {start: BlockOffset; end: BlockOffset} | undefined\n }\n\nconst inputRuleListenerCallback: CallbackLogicFunction<\n AnyEventObject,\n InputRuleMachineEvent,\n {\n editor: Editor\n rules: Array<InputRule>\n }\n> = ({input, sendBack}) => {\n const unregister = input.editor.registerBehavior({\n behavior: createInputRuleBehavior({\n rules: input.rules,\n onApply: ({endOffsets}) => {\n sendBack({type: 'input rule raised', endOffsets})\n },\n }),\n })\n\n return () => {\n unregister()\n }\n}\n\nconst deleteBackwardListenerCallback: CallbackLogicFunction<\n AnyEventObject,\n InputRuleMachineEvent,\n {editor: Editor}\n> = ({input, sendBack}) => {\n return input.editor.registerBehavior({\n behavior: defineBehavior({\n on: 'delete.backward',\n actions: [\n () => [\n raise({type: 'history.undo'}),\n effect(() => {\n sendBack({type: 'history.undo raised'})\n }),\n ],\n ],\n }),\n })\n}\n\nconst selectionListenerCallback: CallbackLogicFunction<\n AnyEventObject,\n InputRuleMachineEvent,\n {editor: Editor}\n> = ({sendBack, input}) => {\n const unregister = input.editor.registerBehavior({\n behavior: defineBehavior({\n on: 'select',\n guard: ({snapshot, event}) => {\n const blockOffsets = getBlockOffsets({\n ...snapshot,\n context: {\n ...snapshot.context,\n selection: event.at,\n },\n })\n\n return {blockOffsets}\n },\n actions: [\n ({event}, {blockOffsets}) => [\n effect(() => {\n sendBack({type: 'selection changed', blockOffsets})\n }),\n forward(event),\n ],\n ],\n }),\n })\n\n return unregister\n}\n\nconst inputRuleSetup = setup({\n types: {\n context: {} as {\n editor: Editor\n rules: Array<InputRule>\n endOffsets: {start: BlockOffset; end: BlockOffset} | undefined\n },\n input: {} as {\n editor: Editor\n rules: Array<InputRule>\n },\n events: {} as InputRuleMachineEvent,\n },\n actors: {\n 'delete.backward listener': fromCallback(deleteBackwardListenerCallback),\n 'input rule listener': fromCallback(inputRuleListenerCallback),\n 'selection listener': fromCallback(selectionListenerCallback),\n },\n guards: {\n 'block offset changed': ({context, event}) => {\n if (event.type !== 'selection changed') {\n return false\n }\n\n if (!event.blockOffsets || !context.endOffsets) {\n return true\n }\n\n const startChanged =\n context.endOffsets.start.path[0]._key !==\n event.blockOffsets.start.path[0]._key ||\n context.endOffsets.start.offset !== event.blockOffsets.start.offset\n const endChanged =\n context.endOffsets.end.path[0]._key !==\n event.blockOffsets.end.path[0]._key ||\n context.endOffsets.end.offset !== event.blockOffsets.end.offset\n\n return startChanged || endChanged\n },\n },\n})\n\nconst assignEndOffsets = inputRuleSetup.assign({\n endOffsets: ({context, event}) =>\n event.type === 'input rule raised' ? event.endOffsets : context.endOffsets,\n})\n\nconst inputRuleMachine = inputRuleSetup.createMachine({\n id: 'input rule',\n context: ({input}) => ({\n editor: input.editor,\n rules: input.rules,\n endOffsets: undefined,\n }),\n initial: 'idle',\n invoke: {\n src: 'input rule listener',\n input: ({context}) => ({\n editor: context.editor,\n rules: context.rules,\n }),\n },\n on: {\n 'input rule raised': {\n target: '.input rule applied',\n actions: assignEndOffsets,\n },\n },\n states: {\n 'idle': {},\n 'input rule applied': {\n invoke: [\n {\n src: 'delete.backward listener',\n input: ({context}) => ({editor: context.editor}),\n },\n {\n src: 'selection listener',\n input: ({context}) => ({editor: context.editor}),\n },\n ],\n on: {\n 'selection changed': {\n target: 'idle',\n guard: 'block offset changed',\n },\n 'history.undo raised': {\n target: 'idle',\n },\n },\n },\n },\n})\n","import {raise, type BehaviorAction} from '@portabletext/editor/behaviors'\nimport {getMarkState} from '@portabletext/editor/selectors'\nimport type {InputRule, InputRuleGuard} from './input-rule'\nimport type {InputRuleMatchLocation} from './input-rule-match-location'\n\n/**\n * @alpha\n */\nexport type TextTransformRule = {\n on: RegExp\n guard?: InputRuleGuard\n transform: ({location}: {location: InputRuleMatchLocation}) => string\n}\n\n/**\n * Define an `InputRule` specifically designed to transform matched text into\n * some other text.\n *\n * @example\n * ```tsx\n * const transformRule = defineTextTransformRule({\n * on: /--/,\n * transform: () => '—',\n * })\n * ```\n *\n * @alpha\n */\nexport function defineTextTransformRule(config: TextTransformRule): InputRule {\n return {\n on: config.on,\n guard: config.guard ?? (() => true),\n actions: [\n ({snapshot, event}) => {\n const locations = event.matches.flatMap((match) =>\n match.groupMatches.length === 0 ? [match] : match.groupMatches,\n )\n const newText = event.textBefore + event.textInserted\n\n let textLengthDelta = 0\n const actions: Array<BehaviorAction> = []\n\n for (const location of locations.reverse()) {\n const text = config.transform({location})\n\n textLengthDelta =\n textLengthDelta -\n (text.length -\n (location.targetOffsets.focus.offset -\n location.targetOffsets.anchor.offset))\n\n actions.push(raise({type: 'select', at: location.targetOffsets}))\n actions.push(raise({type: 'delete', at: location.targetOffsets}))\n actions.push(\n raise({\n type: 'insert.child',\n child: {\n _type: snapshot.context.schema.span.name,\n text,\n marks:\n getMarkState({\n ...snapshot,\n context: {\n ...snapshot.context,\n selection: {\n anchor: location.selection.anchor,\n focus: {\n path: location.selection.focus.path,\n offset: Math.min(\n location.selection.focus.offset,\n event.textBefore.length,\n ),\n },\n },\n },\n })?.marks ?? [],\n },\n }),\n )\n }\n\n const endCaretPosition = {\n path: event.focusTextBlock.path,\n offset: newText.length - textLengthDelta,\n }\n\n return [\n ...actions,\n raise({\n type: 'select',\n at: {\n anchor: endCaretPosition,\n focus: endCaretPosition,\n },\n }),\n ]\n },\n ],\n }\n}\n"],"names":["defineInputRule","config","getInputRuleMatchLocation","match","adjustIndexBy","snapshot","focusTextBlock","originalTextBefore","text","start","end","adjustedIndex","targetOffsets","anchor","path","offset","focus","backward","normalizedOffsets","Math","min","length","anchorBackwards","blockOffsetToSpanSelectionPoint","context","blockOffset","direction","focusForwards","selection","inlineObjectsAfterMatch","getNextInlineObjects","inlineObjectsBefore","getPreviousInlineObjects","some","inlineObjectAfter","inlineObjectBefore","node","_key","createInputRuleBehavior","defineBehavior","on","guard","event","dom","getFocusTextBlock","getBlockTextBefore","textBefore","originalNewText","newText","foundMatches","foundActions","rule","rules","matcher","RegExp","source","ruleMatches","matchAll","flatMap","regExpMatch","indices","undefined","at","matchLocation","foundMatch","groupMatches","slice","index","guardResult","type","matches","textInserted","actionSets","actions","map","action","actionSet","push","forward","_","effect","blockOffsets","getBlockOffsets","onApply","endOffsets","InputRulePlugin","props","$","_c","editor","useEditor","t0","input","useActorRef","inputRuleMachine","inputRuleListenerCallback","sendBack","unregister","registerBehavior","behavior","deleteBackwardListenerCallback","raise","selectionListenerCallback","inputRuleSetup","setup","types","events","actors","fromCallback","guards","block offset changed","startChanged","endChanged","assignEndOffsets","assign","createMachine","id","initial","invoke","src","target","states","defineTextTransformRule","locations","textLengthDelta","location","reverse","transform","child","_type","schema","span","name","marks","getMarkState","endCaretPosition"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@portabletext/plugin-input-rule",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Easily configure input rules in the Portable Text Editor",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"portabletext",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@xstate/react": "^6.0.0",
|
|
41
41
|
"react-compiler-runtime": "19.1.0-rc.3",
|
|
42
|
-
"xstate": "^5.
|
|
42
|
+
"xstate": "^5.23.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@sanity/pkg-utils": "^8.1.4",
|
|
@@ -53,12 +53,12 @@
|
|
|
53
53
|
"typescript": "5.9.3",
|
|
54
54
|
"typescript-eslint": "^8.41.0",
|
|
55
55
|
"vitest": "^3.2.4",
|
|
56
|
-
"@portabletext/editor": "2.
|
|
56
|
+
"@portabletext/editor": "2.14.0",
|
|
57
57
|
"@portabletext/schema": "1.2.0",
|
|
58
58
|
"racejar": "1.3.1"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"@portabletext/editor": "^2.
|
|
61
|
+
"@portabletext/editor": "^2.14.0",
|
|
62
62
|
"react": "^19.1.1"
|
|
63
63
|
},
|
|
64
64
|
"publishConfig": {
|
package/src/edge-cases.feature
CHANGED
|
@@ -65,6 +65,17 @@ Feature: Edge Cases
|
|
|
65
65
|
| "" | "xfyxoy" | "zfzzoznew" |
|
|
66
66
|
| "" | "xfyxoyxoy" | "zfzzozzoznew" |
|
|
67
67
|
|
|
68
|
+
Scenario Outline: Replacing 'a' and 'c'
|
|
69
|
+
Given the text <text>
|
|
70
|
+
When <inserted text> is inserted
|
|
71
|
+
And "new" is typed
|
|
72
|
+
Then the text is <new text>
|
|
73
|
+
|
|
74
|
+
Examples:
|
|
75
|
+
| text | inserted text | new text |
|
|
76
|
+
| "" | "ABC" | "CBAnew" |
|
|
77
|
+
| "AB" | "C" | "CBAnew" |
|
|
78
|
+
|
|
68
79
|
Scenario Outline: Undoing Multiple Groups Rule
|
|
69
80
|
Given the text <text>
|
|
70
81
|
When <inserted text> is inserted
|
|
@@ -79,3 +90,69 @@ Feature: Edge Cases
|
|
|
79
90
|
| "xfooy" | "z" | "xfooyz" | "xfooy" |
|
|
80
91
|
| "" | "xfyxoy" | "zfzzoz" | "xfyxoy" |
|
|
81
92
|
| "" | "xfyxoyxoy" | "zfzzozzoz" | "xfyxoyxoy" |
|
|
93
|
+
|
|
94
|
+
Scenario Outline: Preserving inline objects
|
|
95
|
+
Given the text <text>
|
|
96
|
+
When <inserted text> is inserted
|
|
97
|
+
And "new" is typed
|
|
98
|
+
Then the text is <new text>
|
|
99
|
+
|
|
100
|
+
Examples:
|
|
101
|
+
| text | inserted text | new text |
|
|
102
|
+
| "(,{stock-ticker},c" | ")" | "(,{stock-ticker},c)new" |
|
|
103
|
+
| "-,{stock-ticker}," | ">" | "-,{stock-ticker},>new" |
|
|
104
|
+
| ",{stock-ticker},-,{stock-ticker}," | ">" | ",{stock-ticker},-,{stock-ticker},>new" |
|
|
105
|
+
| ",{stock-ticker},-" | ">" | ",{stock-ticker},→new" |
|
|
106
|
+
|
|
107
|
+
Scenario: Preserving adjoining inline object and placing caret correctly
|
|
108
|
+
Given the text "(c,{stock-ticker},"
|
|
109
|
+
When the caret is put after "c"
|
|
110
|
+
And ")new" is typed
|
|
111
|
+
Then the text is "©new,{stock-ticker},"
|
|
112
|
+
|
|
113
|
+
Scenario: Preserving adjoining inline object and placing caret correctly
|
|
114
|
+
Given the text "#,{stock-ticker},"
|
|
115
|
+
When the caret is put after "#"
|
|
116
|
+
And " new" is typed
|
|
117
|
+
Then the text is "new,{stock-ticker},"
|
|
118
|
+
|
|
119
|
+
Scenario Outline: H1 rule
|
|
120
|
+
Given the text <text>
|
|
121
|
+
When the caret is put <position>
|
|
122
|
+
And <key> is pressed
|
|
123
|
+
And "# " is inserted
|
|
124
|
+
And "new" is typed
|
|
125
|
+
Then the text is <new text>
|
|
126
|
+
|
|
127
|
+
Examples:
|
|
128
|
+
| text | position | key | new text |
|
|
129
|
+
# Pressing Shift is a noop. It only exists so we can press Backspace in
|
|
130
|
+
# subsequent Scenarios to position to caret at the edge of the inline
|
|
131
|
+
# object.
|
|
132
|
+
| "" | after "" | "{Shift}" | "new" |
|
|
133
|
+
| "foo" | after "foo" | "{Shift}" | "foo# new" |
|
|
134
|
+
| ",{stock-ticker},foo" | after "foo" | "{Shift}" | ",{stock-ticker},foo# new" |
|
|
135
|
+
# This is an edge case we have to live with. There's no way of knowing
|
|
136
|
+
# that the inline object before the caret should prevent the rule from
|
|
137
|
+
# running.
|
|
138
|
+
| ",{stock-ticker},f" | after "f" | "{Backspace}" | "new,{stock-ticker}," |
|
|
139
|
+
| "f,{stock-ticker}," | after "f" | "{Backspace}" | "new,{stock-ticker}," |
|
|
140
|
+
|
|
141
|
+
Scenario Outline: Better H2 rule
|
|
142
|
+
Given the text <text>
|
|
143
|
+
When the caret is put <position>
|
|
144
|
+
And <key> is pressed
|
|
145
|
+
And "## " is inserted
|
|
146
|
+
And "new" is typed
|
|
147
|
+
Then the text is <new text>
|
|
148
|
+
|
|
149
|
+
Examples:
|
|
150
|
+
| text | position | key | new text |
|
|
151
|
+
# Pressing Shift is a noop. It only exists so we can press Backspace in
|
|
152
|
+
# subsequent Scenarios to position to caret at the edge of the inline
|
|
153
|
+
# object.
|
|
154
|
+
| "" | after "" | "{Shift}" | "new" |
|
|
155
|
+
| "foo" | after "foo" | "{Shift}" | "foo## new" |
|
|
156
|
+
| ",{stock-ticker},foo" | after "foo" | "{Shift}" | ",{stock-ticker},foo## new" |
|
|
157
|
+
| ",{stock-ticker},f" | after "f" | "{Backspace}" | ",{stock-ticker},## new" |
|
|
158
|
+
| "f,{stock-ticker}," | after "f" | "{Backspace}" | "new,{stock-ticker}," |
|
package/src/edge-cases.test.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {getPreviousInlineObject} from '@portabletext/editor/selectors'
|
|
1
2
|
import {parameterTypes} from '@portabletext/editor/test'
|
|
2
3
|
import {
|
|
3
4
|
createTestEditor,
|
|
@@ -31,6 +32,26 @@ const multipleGroupsRule = defineTextTransformRule({
|
|
|
31
32
|
transform: () => 'z',
|
|
32
33
|
})
|
|
33
34
|
|
|
35
|
+
const replaceAandCRule = defineTextTransformRule({
|
|
36
|
+
on: /(A).*(C)/,
|
|
37
|
+
transform: ({location}) => {
|
|
38
|
+
return location.text === 'A' ? 'C' : 'A'
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const h1Rule = defineTextTransformRule({
|
|
43
|
+
on: /^(# )/,
|
|
44
|
+
transform: () => '',
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const betterH2Rule = defineTextTransformRule({
|
|
48
|
+
on: /^(## )/,
|
|
49
|
+
guard: ({snapshot}) => {
|
|
50
|
+
return !getPreviousInlineObject(snapshot)
|
|
51
|
+
},
|
|
52
|
+
transform: () => '',
|
|
53
|
+
})
|
|
54
|
+
|
|
34
55
|
Feature({
|
|
35
56
|
hooks: [
|
|
36
57
|
Before(async (context: Context) => {
|
|
@@ -41,11 +62,15 @@ Feature({
|
|
|
41
62
|
<InputRulePlugin rules={[endStringRule]} />
|
|
42
63
|
<InputRulePlugin rules={[nonGlobalRule]} />
|
|
43
64
|
<InputRulePlugin rules={[multipleGroupsRule]} />
|
|
65
|
+
<InputRulePlugin rules={[h1Rule]} />
|
|
66
|
+
<InputRulePlugin rules={[betterH2Rule]} />
|
|
67
|
+
<InputRulePlugin rules={[replaceAandCRule]} />
|
|
44
68
|
</>
|
|
45
69
|
),
|
|
46
70
|
schemaDefinition: defineSchema({
|
|
47
71
|
decorators: [{name: 'strong'}],
|
|
48
72
|
annotations: [{name: 'link'}],
|
|
73
|
+
inlineObjects: [{name: 'stock-ticker'}],
|
|
49
74
|
}),
|
|
50
75
|
})
|
|
51
76
|
|
|
@@ -4,9 +4,17 @@ import type {
|
|
|
4
4
|
EditorSelection,
|
|
5
5
|
EditorSnapshot,
|
|
6
6
|
} from '@portabletext/editor'
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
getNextInlineObjects,
|
|
9
|
+
getPreviousInlineObjects,
|
|
10
|
+
} from '@portabletext/editor/selectors'
|
|
11
|
+
import {blockOffsetToSpanSelectionPoint} from '@portabletext/editor/utils'
|
|
8
12
|
|
|
9
13
|
export type InputRuleMatchLocation = {
|
|
14
|
+
/**
|
|
15
|
+
* The matched text
|
|
16
|
+
*/
|
|
17
|
+
text: string
|
|
10
18
|
/**
|
|
11
19
|
* Estimated selection of where in the original text the match is located.
|
|
12
20
|
* The selection is estimated since the match is found in the text after
|
|
@@ -30,7 +38,7 @@ export function getInputRuleMatchLocation({
|
|
|
30
38
|
focusTextBlock,
|
|
31
39
|
originalTextBefore,
|
|
32
40
|
}: {
|
|
33
|
-
match: [number, number]
|
|
41
|
+
match: [string, number, number]
|
|
34
42
|
adjustIndexBy: number
|
|
35
43
|
snapshot: EditorSnapshot
|
|
36
44
|
focusTextBlock: {
|
|
@@ -38,7 +46,7 @@ export function getInputRuleMatchLocation({
|
|
|
38
46
|
}
|
|
39
47
|
originalTextBefore: string
|
|
40
48
|
}): InputRuleMatchLocation | undefined {
|
|
41
|
-
const [start, end] = match
|
|
49
|
+
const [text, start, end] = match
|
|
42
50
|
const adjustedIndex = start + adjustIndexBy
|
|
43
51
|
|
|
44
52
|
const targetOffsets = {
|
|
@@ -63,17 +71,52 @@ export function getInputRuleMatchLocation({
|
|
|
63
71
|
},
|
|
64
72
|
backward: false,
|
|
65
73
|
}
|
|
66
|
-
|
|
74
|
+
|
|
75
|
+
const anchorBackwards = blockOffsetToSpanSelectionPoint({
|
|
67
76
|
context: snapshot.context,
|
|
68
|
-
|
|
69
|
-
|
|
77
|
+
blockOffset: normalizedOffsets.anchor,
|
|
78
|
+
direction: 'backward',
|
|
79
|
+
})
|
|
80
|
+
const focusForwards = blockOffsetToSpanSelectionPoint({
|
|
81
|
+
context: snapshot.context,
|
|
82
|
+
blockOffset: normalizedOffsets.focus,
|
|
83
|
+
direction: 'forward',
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
if (!anchorBackwards || !focusForwards) {
|
|
87
|
+
return undefined
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const selection = {
|
|
91
|
+
anchor: anchorBackwards,
|
|
92
|
+
focus: focusForwards,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const inlineObjectsAfterMatch = getNextInlineObjects({
|
|
96
|
+
...snapshot,
|
|
97
|
+
context: {
|
|
98
|
+
...snapshot.context,
|
|
99
|
+
selection: {
|
|
100
|
+
anchor: selection.anchor,
|
|
101
|
+
focus: selection.anchor,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
70
104
|
})
|
|
105
|
+
const inlineObjectsBefore = getPreviousInlineObjects(snapshot)
|
|
71
106
|
|
|
72
|
-
if (
|
|
107
|
+
if (
|
|
108
|
+
inlineObjectsAfterMatch.some((inlineObjectAfter) =>
|
|
109
|
+
inlineObjectsBefore.some(
|
|
110
|
+
(inlineObjectBefore) =>
|
|
111
|
+
inlineObjectAfter.node._key === inlineObjectBefore.node._key,
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
) {
|
|
73
115
|
return undefined
|
|
74
116
|
}
|
|
75
117
|
|
|
76
118
|
return {
|
|
119
|
+
text,
|
|
77
120
|
selection,
|
|
78
121
|
targetOffsets,
|
|
79
122
|
}
|
|
@@ -64,7 +64,7 @@ function createInputRuleBehavior(config: {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
const matchLocation = getInputRuleMatchLocation({
|
|
67
|
-
match,
|
|
67
|
+
match: [regExpMatch.at(0) ?? '', ...match],
|
|
68
68
|
adjustIndexBy: originalNewText.length - newText.length,
|
|
69
69
|
snapshot,
|
|
70
70
|
focusTextBlock,
|
|
@@ -101,11 +101,13 @@ function createInputRuleBehavior(config: {
|
|
|
101
101
|
: []
|
|
102
102
|
|
|
103
103
|
const ruleMatch = {
|
|
104
|
+
text: matchLocation.text,
|
|
104
105
|
selection: matchLocation.selection,
|
|
105
106
|
targetOffsets: matchLocation.targetOffsets,
|
|
106
|
-
groupMatches: groupMatches.flatMap((match) => {
|
|
107
|
+
groupMatches: groupMatches.flatMap((match, index) => {
|
|
108
|
+
const text = regExpMatch.at(index + 1) ?? ''
|
|
107
109
|
const groupMatchLocation = getInputRuleMatchLocation({
|
|
108
|
-
match,
|
|
110
|
+
match: [text, ...match],
|
|
109
111
|
adjustIndexBy: originalNewText.length - newText.length,
|
|
110
112
|
snapshot,
|
|
111
113
|
focusTextBlock,
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Feature: Markdown Link Rule
|
|
2
|
+
|
|
3
|
+
Background:
|
|
4
|
+
Given the editor is focused
|
|
5
|
+
And a global keymap
|
|
6
|
+
|
|
7
|
+
Scenario Outline: Transform markdown Link into annotation
|
|
8
|
+
Given the text <text>
|
|
9
|
+
When <inserted text> is inserted
|
|
10
|
+
And "new" is typed
|
|
11
|
+
Then the text is <new text>
|
|
12
|
+
And <annotated> has marks "k4"
|
|
13
|
+
|
|
14
|
+
Examples:
|
|
15
|
+
| text | inserted text | new text | annotated |
|
|
16
|
+
| "[foo](bar" | ")" | "foo,new" | "foo" |
|
|
17
|
+
|
|
18
|
+
Scenario: Preserving decorator in link text
|
|
19
|
+
Given the text "[foo](bar"
|
|
20
|
+
And "strong" around "foo"
|
|
21
|
+
When ")" is inserted
|
|
22
|
+
And "new" is typed
|
|
23
|
+
Then the text is "foo,new"
|
|
24
|
+
And "foo" has marks "strong,k6"
|
|
25
|
+
|
|
26
|
+
Scenario: Preserving decorators in link text
|
|
27
|
+
Given the text "[foo](bar"
|
|
28
|
+
And "strong" around "foo"
|
|
29
|
+
And "em" around "oo"
|
|
30
|
+
When ")" is inserted
|
|
31
|
+
And "new" is typed
|
|
32
|
+
Then the text is "f,oo,new"
|
|
33
|
+
And "f" has marks "strong,k7"
|
|
34
|
+
And "oo" has marks "strong,em,k7"
|
|
35
|
+
|
|
36
|
+
Scenario: Overwriting other links
|
|
37
|
+
Given the text "[foo](bar"
|
|
38
|
+
And a "link" "l1" around "foo"
|
|
39
|
+
When the caret is put after "bar"
|
|
40
|
+
And ")" is inserted
|
|
41
|
+
And "new" is typed
|
|
42
|
+
Then the text is "foo,new"
|
|
43
|
+
And "foo" has an annotation different than "l1"
|
|
44
|
+
|
|
45
|
+
Scenario: Preserving other annotations
|
|
46
|
+
Given the text "[foo](bar"
|
|
47
|
+
And a "link" "l1" around "foo"
|
|
48
|
+
And a "comment" "c1" around "foo"
|
|
49
|
+
When the caret is put after "bar"
|
|
50
|
+
And ")" is inserted
|
|
51
|
+
And "new" is typed
|
|
52
|
+
Then the text is "foo,new"
|
|
53
|
+
And "foo" has an annotation different than "l1"
|
|
54
|
+
And "foo" has marks "c1,k9"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {parameterTypes} from '@portabletext/editor/test'
|
|
2
|
+
import {
|
|
3
|
+
createTestEditor,
|
|
4
|
+
stepDefinitions,
|
|
5
|
+
type Context,
|
|
6
|
+
} from '@portabletext/editor/test/vitest'
|
|
7
|
+
import {defineSchema} from '@portabletext/schema'
|
|
8
|
+
import {Before} from 'racejar'
|
|
9
|
+
import {Feature} from 'racejar/vitest'
|
|
10
|
+
import {InputRulePlugin} from './plugin.input-rule'
|
|
11
|
+
import {createMarkdownLinkRule} from './rule.markdown-link'
|
|
12
|
+
import markdownLinkFeature from './rule.markdown-link.feature?raw'
|
|
13
|
+
|
|
14
|
+
const markdownLinkRule = createMarkdownLinkRule({
|
|
15
|
+
linkObject: (context) => ({
|
|
16
|
+
name: 'link',
|
|
17
|
+
value: {
|
|
18
|
+
href: context.href,
|
|
19
|
+
},
|
|
20
|
+
}),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
Feature({
|
|
24
|
+
hooks: [
|
|
25
|
+
Before(async (context: Context) => {
|
|
26
|
+
const {editor, locator} = await createTestEditor({
|
|
27
|
+
children: (
|
|
28
|
+
<>
|
|
29
|
+
<InputRulePlugin rules={[markdownLinkRule]} />
|
|
30
|
+
</>
|
|
31
|
+
),
|
|
32
|
+
schemaDefinition: defineSchema({
|
|
33
|
+
decorators: [{name: 'strong'}, {name: 'em'}],
|
|
34
|
+
annotations: [{name: 'link'}, {name: 'comment'}],
|
|
35
|
+
inlineObjects: [{name: 'stock-ticker'}],
|
|
36
|
+
}),
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
context.locator = locator
|
|
40
|
+
context.editor = editor
|
|
41
|
+
}),
|
|
42
|
+
],
|
|
43
|
+
featureText: markdownLinkFeature,
|
|
44
|
+
stepDefinitions,
|
|
45
|
+
parameterTypes,
|
|
46
|
+
})
|
|
@@ -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,6 +1,7 @@
|
|
|
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
|
|
@@ -8,7 +9,7 @@ import type {InputRule, InputRuleGuard} from './input-rule'
|
|
|
8
9
|
export type TextTransformRule = {
|
|
9
10
|
on: RegExp
|
|
10
11
|
guard?: InputRuleGuard
|
|
11
|
-
transform: () => string
|
|
12
|
+
transform: ({location}: {location: InputRuleMatchLocation}) => string
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -31,53 +32,58 @@ export function defineTextTransformRule(config: TextTransformRule): InputRule {
|
|
|
31
32
|
guard: config.guard ?? (() => true),
|
|
32
33
|
actions: [
|
|
33
34
|
({snapshot, event}) => {
|
|
34
|
-
const
|
|
35
|
+
const locations = event.matches.flatMap((match) =>
|
|
35
36
|
match.groupMatches.length === 0 ? [match] : match.groupMatches,
|
|
36
37
|
)
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
const newText = event.textBefore + event.textInserted
|
|
39
|
+
|
|
40
|
+
let textLengthDelta = 0
|
|
41
|
+
const actions: Array<BehaviorAction> = []
|
|
42
|
+
|
|
43
|
+
for (const location of locations.reverse()) {
|
|
44
|
+
const text = config.transform({location})
|
|
45
|
+
|
|
46
|
+
textLengthDelta =
|
|
47
|
+
textLengthDelta -
|
|
48
|
+
(text.length -
|
|
49
|
+
(location.targetOffsets.focus.offset -
|
|
50
|
+
location.targetOffsets.anchor.offset))
|
|
51
|
+
|
|
52
|
+
actions.push(raise({type: 'select', at: location.targetOffsets}))
|
|
53
|
+
actions.push(raise({type: 'delete', at: location.targetOffsets}))
|
|
54
|
+
actions.push(
|
|
55
|
+
raise({
|
|
56
|
+
type: 'insert.child',
|
|
57
|
+
child: {
|
|
58
|
+
_type: snapshot.context.schema.span.name,
|
|
59
|
+
text,
|
|
60
|
+
marks:
|
|
61
|
+
getMarkState({
|
|
62
|
+
...snapshot,
|
|
63
|
+
context: {
|
|
64
|
+
...snapshot.context,
|
|
65
|
+
selection: {
|
|
66
|
+
anchor: location.selection.anchor,
|
|
67
|
+
focus: {
|
|
68
|
+
path: location.selection.focus.path,
|
|
69
|
+
offset: Math.min(
|
|
70
|
+
location.selection.focus.offset,
|
|
71
|
+
event.textBefore.length,
|
|
72
|
+
),
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
})?.marks ?? [],
|
|
77
|
+
},
|
|
78
|
+
}),
|
|
43
79
|
)
|
|
44
|
-
}
|
|
80
|
+
}
|
|
45
81
|
|
|
46
|
-
const newText = event.textBefore + event.textInserted
|
|
47
82
|
const endCaretPosition = {
|
|
48
83
|
path: event.focusTextBlock.path,
|
|
49
84
|
offset: newText.length - textLengthDelta,
|
|
50
85
|
}
|
|
51
86
|
|
|
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
87
|
return [
|
|
82
88
|
...actions,
|
|
83
89
|
raise({
|