@portabletext/plugin-input-rule 0.1.3 → 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 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
- backward: !1
34
- }, selection = utils.blockOffsetsToSelection({
32
+ }
33
+ }, anchorBackwards = utils.blockOffsetToSpanSelectionPoint({
35
34
  context: snapshot.context,
36
- offsets: normalizedOffsets,
37
- backward: !1
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 (selection)
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) => getInputRuleMatchLocation({
88
- match: match2,
89
- adjustIndexBy: originalNewText.length - newText.length,
90
- snapshot,
91
- focusTextBlock,
92
- originalTextBefore
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 matches = event.matches.flatMap((match) => match.groupMatches.length === 0 ? [match] : match.groupMatches), textLengthDelta = matches.reduce((length, match) => length - (config.transform().length - (match.targetOffsets.focus.offset - match.targetOffsets.anchor.offset)), 0), newText = event.textBefore + event.textInserted, endCaretPosition = {
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 [...matches.reverse().flatMap((match) => [behaviors.raise({
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,
@@ -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 { blockOffsetsToSelection } from "@portabletext/editor/utils";
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
- backward: !1
38
- }, selection = blockOffsetsToSelection({
36
+ }
37
+ }, anchorBackwards = blockOffsetToSpanSelectionPoint({
39
38
  context: snapshot.context,
40
- offsets: normalizedOffsets,
41
- backward: !1
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 (selection)
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) => getInputRuleMatchLocation({
92
- match: match2,
93
- adjustIndexBy: originalNewText.length - newText.length,
94
- snapshot,
95
- focusTextBlock,
96
- originalTextBefore
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 matches = event.matches.flatMap((match) => match.groupMatches.length === 0 ? [match] : match.groupMatches), textLengthDelta = matches.reduce((length, match) => length - (config.transform().length - (match.targetOffsets.focus.offset - match.targetOffsets.anchor.offset)), 0), newText = event.textBefore + event.textInserted, endCaretPosition = {
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 [...matches.reverse().flatMap((match) => [raise({
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":";;;;;;;AA0DO,SAASA,gBAAgBC,QAA8B;AAC5D,SAAOA;AACT;ACnCO,SAASC,0BAA0B;AAAA,EACxCC;AAAAA,EACAC;AAAAA,EACAC;AAAAA,EACAC;AAAAA,EACAC;AASF,GAAuC;AACrC,QAAM,CAACC,OAAOC,GAAG,IAAIN,OACfO,gBAAgBF,QAAQJ,eAExBO,gBAAgB;AAAA,IACpBC,QAAQ;AAAA,MACNC,MAAMP,eAAeO;AAAAA,MACrBC,QAAQJ;AAAAA,IAAAA;AAAAA,IAEVK,OAAO;AAAA,MACLF,MAAMP,eAAeO;AAAAA,MACrBC,QAAQJ,gBAAgBD,MAAMD;AAAAA,IAAAA;AAAAA,IAEhCQ,UAAU;AAAA,EAAA,GAENC,oBAAoB;AAAA,IACxBL,QAAQ;AAAA,MACNC,MAAMP,eAAeO;AAAAA,MACrBC,QAAQI,KAAKC,IAAIR,cAAcC,OAAOE,QAAQP,mBAAmBa,MAAM;AAAA,IAAA;AAAA,IAEzEL,OAAO;AAAA,MACLF,MAAMP,eAAeO;AAAAA,MACrBC,QAAQI,KAAKC,IAAIR,cAAcI,MAAMD,QAAQP,mBAAmBa,MAAM;AAAA,IAAA;AAAA,IAExEJ,UAAU;AAAA,EAAA,GAENK,YAAYC,wBAAwB;AAAA,IACxCC,SAASlB,SAASkB;AAAAA,IAClBC,SAASP;AAAAA,IACTD,UAAU;AAAA,EAAA,CACX;AAED,MAAKK;AAIL,WAAO;AAAA,MACLA;AAAAA,MACAV;AAAAA,IAAAA;AAEJ;ACxDA,SAASc,wBAAwBxB,QAO9B;AACD,SAAOyB,eAAe;AAAA,IACpBC,IAAI;AAAA,IACJC,OAAOA,CAAC;AAAA,MAACvB;AAAAA,MAAUwB;AAAAA,MAAOC;AAAAA,IAAAA,MAAS;AACjC,YAAMxB,iBAAiByB,kBAAkB1B,QAAQ;AAEjD,UAAI,CAACC;AACH,eAAO;AAGT,YAAMC,qBAAqByB,mBAAmB3B,QAAQ;AACtD,UAAI4B,aAAa1B;AACjB,YAAM2B,kBAAkBD,aAAaJ,MAAMM;AAC3C,UAAIC,UAAUF;AAEd,YAAMG,eAA8D,IAC9DC,eAAsC,CAAA;AAE5C,iBAAWC,QAAQtC,OAAOuC,OAAO;AAC/B,cAAMC,UAAU,IAAIC,OAAOH,KAAKZ,GAAGgB,QAAQ,IAAI;AAE/C,mBAAa;AAEX,gBAAMC,cAAc,CAAC,GAAGR,QAAQS,SAASJ,OAAO,CAAC,EAAEK,QAChDC,CAAAA,gBAAgB;AACf,gBAAIA,YAAYC,YAAYC;AAC1B,qBAAO,CAAA;AAGT,kBAAM9C,QAAQ4C,YAAYC,QAAQE,GAAG,CAAC;AAEtC,gBAAI,CAAC/C;AACH,qBAAO,CAAA;AAGT,kBAAMgD,gBAAgBjD,0BAA0B;AAAA,cAC9CC;AAAAA,cACAC,eAAe8B,gBAAgBd,SAASgB,QAAQhB;AAAAA,cAChDf;AAAAA,cACAC;AAAAA,cACAC;AAAAA,YAAAA,CACD;AAED,gBAAI,CAAC4C;AACH,qBAAO,CAAA;AAQT,gBAJEA,cAAcxC,cAAcI,MAAMD,UAClCP,mBAAmBa;AAInB,qBAAO,CAAA;AAUT,gBAPqBiB,aAAae,KAC/BC,CAAAA,eACCA,WAAW1C,cAAcC,OAAOE,WAChCqC,cAAcxC,cAAcC,OAAOE,MACvC;AAIE,qBAAO,CAAA;AAGT,kBAAMwC,eACJP,YAAYC,QAAQ5B,SAAS,IACzB2B,YAAYC,QAAQO,MAAM,CAAC,IAC3B,CAAA;AAsBN,mBAAO,CApBW;AAAA,cAChBlC,WAAW8B,cAAc9B;AAAAA,cACzBV,eAAewC,cAAcxC;AAAAA,cAC7B2C,cAAcA,aAAaR,QAAS3C,CAAAA,WACPD,0BAA0B;AAAA,gBACnDC,OAAAA;AAAAA,gBACAC,eAAe8B,gBAAgBd,SAASgB,QAAQhB;AAAAA,gBAChDf;AAAAA,gBACAC;AAAAA,gBACAC;AAAAA,cAAAA,CACD,KAGQ,CAAA,CAIV;AAAA,YAAA,CAGc;AAAA,UACnB,CACF;AAEA,cAAIqC,YAAYxB,SAAS,GAAG;AAC1B,kBAAMoC,cACJjB,KAAKX,QAAQ;AAAA,cACXvB;AAAAA,cACAwB,OAAO;AAAA,gBACL4B,MAAM;AAAA,gBACNC,SAASd;AAAAA,gBACTtC;AAAAA,gBACA2B,YAAY1B;AAAAA,gBACZoD,cAAc9B,MAAMM;AAAAA,cAAAA;AAAAA,cAEtBL;AAAAA,YAAAA,CACD,KAAK;AAER,gBAAI,CAAC0B;AACH;AAGF,kBAAMI,aAAarB,KAAKsB,QAAQC,IAAKC,YACnCA,OACE;AAAA,cACE1D;AAAAA,cACAwB,OAAO;AAAA,gBACL4B,MAAM;AAAA,gBACNC,SAASd;AAAAA,gBACTtC;AAAAA,gBACA2B,YAAY1B;AAAAA,gBACZoD,cAAc9B,MAAMM;AAAAA,cAAAA;AAAAA,cAEtBL;AAAAA,YAAAA,GAEF0B,WACF,CACF;AAEA,uBAAWQ,aAAaJ;AACtB,yBAAWG,UAAUC;AACnB1B,6BAAa2B,KAAKF,MAAM;AAI5B,kBAAML,UAAUd,YAAYE,QAAS3C,CAAAA,UACnCA,MAAMmD,aAAalC,WAAW,IAAI,CAACjB,KAAK,IAAIA,MAAMmD,YACpD;AAEA,uBAAWnD,SAASuD;AAGlBrB,2BAAa4B,KAAK9D,KAAK,GACvB8B,aAAaG,QAAQmB,MACnB,GACApD,MAAMQ,cAAcI,MAAMD,UAAU,CACtC,GACAsB,UAAUF,gBAAgBqB,MACxBpD,MAAMQ,cAAcI,MAAMD,UAAU,CACtC;AAAA,UAEJ;AAGE;AAAA,QAEJ;AAAA,MACF;AAEA,aAAIwB,aAAalB,WAAW,IACnB,KAGF;AAAA,QAACyC,SAASvB;AAAAA,MAAAA;AAAAA,IACnB;AAAA,IACAuB,SAAS,CACP,CAAC;AAAA,MAAChC;AAAAA,IAAAA,MAAW,CAACqC,QAAQrC,KAAK,CAAC,GAC5B,CAACsC,GAAG;AAAA,MAACN;AAAAA,IAAAA,MAAaA,SAClB,CAAC;AAAA,MAACxD;AAAAA,IAAAA,MAAc,CACd+D,OAAO,MAAM;AACX,YAAMC,eAAeC,gBAAgBjE,QAAQ;AAE7CJ,aAAOsE,QAAQ;AAAA,QAACC,YAAYH;AAAAA,MAAAA,CAAa;AAAA,IAC3C,CAAC,CAAC,CACH;AAAA,EAAA,CAEJ;AACH;AAoBO,SAAAI,gBAAAC,OAAA;AAAA,QAAAC,IAAAC,EAAA,CAAA,GACLC,SAAeC,UAAAA;AAAW,MAAAC;AAAA,SAAAJ,SAAAE,UAAAF,EAAA,CAAA,MAAAD,MAAAlC,SAEIuC,KAAA;AAAA,IAAAC,OAAA;AAAA,MAAAH;AAAAA,MAAArC,OACLkC,MAAKlC;AAAAA,IAAAA;AAAAA,EAAA,GAC7BmC,OAAAE,QAAAF,EAAA,CAAA,IAAAD,MAAAlC,OAAAmC,OAAAI,MAAAA,KAAAJ,EAAA,CAAA,GAFDM,YAAAC,kBAA8BH,EAE7B,GAAC;AAAA;AAgBJ,MAAMI,4BAOFA,CAAC;AAAA,EAACH;AAAAA,EAAOI;AAAQ,MAAM;AACzB,QAAMC,aAAaL,MAAMH,OAAOS,iBAAiB;AAAA,IAC/CC,UAAU9D,wBAAwB;AAAA,MAChCe,OAAOwC,MAAMxC;AAAAA,MACb+B,SAASA,CAAC;AAAA,QAACC;AAAAA,MAAAA,MAAgB;AACzBY,iBAAS;AAAA,UAAC3B,MAAM;AAAA,UAAqBe;AAAAA,QAAAA,CAAW;AAAA,MAClD;AAAA,IAAA,CACD;AAAA,EAAA,CACF;AAED,SAAO,MAAM;AACXa,eAAAA;AAAAA,EACF;AACF,GAEMG,iCAIFA,CAAC;AAAA,EAACR;AAAAA,EAAOI;AAAQ,MACZJ,MAAMH,OAAOS,iBAAiB;AAAA,EACnCC,UAAU7D,eAAe;AAAA,IACvBC,IAAI;AAAA,IACJkC,SAAS,CACP,MAAM,CACJ4B,MAAM;AAAA,MAAChC,MAAM;AAAA,IAAA,CAAe,GAC5BW,OAAO,MAAM;AACXgB,eAAS;AAAA,QAAC3B,MAAM;AAAA,MAAA,CAAsB;AAAA,IACxC,CAAC,CAAC,CACH;AAAA,EAAA,CAEJ;AACH,CAAC,GAGGiC,4BAIFA,CAAC;AAAA,EAACN;AAAAA,EAAUJ;AAAK,MACAA,MAAMH,OAAOS,iBAAiB;AAAA,EAC/CC,UAAU7D,eAAe;AAAA,IACvBC,IAAI;AAAA,IACJC,OAAOA,CAAC;AAAA,MAACvB;AAAAA,MAAUwB;AAAAA,IAAAA,OASV;AAAA,MAACwC,cARaC,gBAAgB;AAAA,QACnC,GAAGjE;AAAAA,QACHkB,SAAS;AAAA,UACP,GAAGlB,SAASkB;AAAAA,UACZF,WAAWQ,MAAMqB;AAAAA,QAAAA;AAAAA,MACnB,CACD;AAAA,IAAA;AAAA,IAIHW,SAAS,CACP,CAAC;AAAA,MAAChC;AAAAA,IAAAA,GAAQ;AAAA,MAACwC;AAAAA,IAAAA,MAAkB,CAC3BD,OAAO,MAAM;AACXgB,eAAS;AAAA,QAAC3B,MAAM;AAAA,QAAqBY;AAAAA,MAAAA,CAAa;AAAA,IACpD,CAAC,GACDH,QAAQrC,KAAK,CAAC,CACf;AAAA,EAAA,CAEJ;AACH,CAAC,GAKG8D,iBAAiBC,MAAM;AAAA,EAC3BC,OAAO;AAAA,IACLtE,SAAS,CAAA;AAAA,IAKTyD,OAAO,CAAA;AAAA,IAIPc,QAAQ,CAAA;AAAA,EAAC;AAAA,EAEXC,QAAQ;AAAA,IACN,4BAA4BC,aAAaR,8BAA8B;AAAA,IACvE,uBAAuBQ,aAAab,yBAAyB;AAAA,IAC7D,sBAAsBa,aAAaN,yBAAyB;AAAA,EAAA;AAAA,EAE9DO,QAAQ;AAAA,IACN,wBAAwBC,CAAC;AAAA,MAAC3E;AAAAA,MAASM;AAAAA,IAAAA,MAAW;AAC5C,UAAIA,MAAM4B,SAAS;AACjB,eAAO;AAGT,UAAI,CAAC5B,MAAMwC,gBAAgB,CAAC9C,QAAQiD;AAClC,eAAO;AAGT,YAAM2B,eACJ5E,QAAQiD,WAAWhE,MAAMK,KAAK,CAAC,EAAEuF,SAC/BvE,MAAMwC,aAAa7D,MAAMK,KAAK,CAAC,EAAEuF,QACnC7E,QAAQiD,WAAWhE,MAAMM,WAAWe,MAAMwC,aAAa7D,MAAMM,QACzDuF,aACJ9E,QAAQiD,WAAW/D,IAAII,KAAK,CAAC,EAAEuF,SAC7BvE,MAAMwC,aAAa5D,IAAII,KAAK,CAAC,EAAEuF,QACjC7E,QAAQiD,WAAW/D,IAAIK,WAAWe,MAAMwC,aAAa5D,IAAIK;AAE3D,aAAOqF,gBAAgBE;AAAAA,IACzB;AAAA,EAAA;AAEJ,CAAC,GAEKC,mBAAmBX,eAAeY,OAAO;AAAA,EAC7C/B,YAAYA,CAAC;AAAA,IAACjD;AAAAA,IAASM;AAAAA,EAAAA,MACrBA,MAAM4B,SAAS,sBAAsB5B,MAAM2C,aAAajD,QAAQiD;AACpE,CAAC,GAEKU,mBAAmBS,eAAea,cAAc;AAAA,EACpDC,IAAI;AAAA,EACJlF,SAASA,CAAC;AAAA,IAACyD;AAAAA,EAAAA,OAAY;AAAA,IACrBH,QAAQG,MAAMH;AAAAA,IACdrC,OAAOwC,MAAMxC;AAAAA,IACbgC,YAAYvB;AAAAA,EAAAA;AAAAA,EAEdyD,SAAS;AAAA,EACTC,QAAQ;AAAA,IACNC,KAAK;AAAA,IACL5B,OAAOA,CAAC;AAAA,MAACzD;AAAAA,IAAAA,OAAc;AAAA,MACrBsD,QAAQtD,QAAQsD;AAAAA,MAChBrC,OAAOjB,QAAQiB;AAAAA,IAAAA;AAAAA,EACjB;AAAA,EAEFb,IAAI;AAAA,IACF,qBAAqB;AAAA,MACnBkF,QAAQ;AAAA,MACRhD,SAASyC;AAAAA,IAAAA;AAAAA,EACX;AAAA,EAEFQ,QAAQ;AAAA,IACN,MAAQ,CAAA;AAAA,IACR,sBAAsB;AAAA,MACpBH,QAAQ,CACN;AAAA,QACEC,KAAK;AAAA,QACL5B,OAAOA,CAAC;AAAA,UAACzD;AAAAA,QAAAA,OAAc;AAAA,UAACsD,QAAQtD,QAAQsD;AAAAA,QAAAA;AAAAA,MAAM,GAEhD;AAAA,QACE+B,KAAK;AAAA,QACL5B,OAAOA,CAAC;AAAA,UAACzD;AAAAA,QAAAA,OAAc;AAAA,UAACsD,QAAQtD,QAAQsD;AAAAA,QAAAA;AAAAA,MAAM,CAC/C;AAAA,MAEHlD,IAAI;AAAA,QACF,qBAAqB;AAAA,UACnBkF,QAAQ;AAAA,UACRjF,OAAO;AAAA,QAAA;AAAA,QAET,uBAAuB;AAAA,UACrBiF,QAAQ;AAAA,QAAA;AAAA,MACV;AAAA,IACF;AAAA,EACF;AAEJ,CAAC;ACtYM,SAASE,wBAAwB9G,QAAsC;AAC5E,SAAO;AAAA,IACL0B,IAAI1B,OAAO0B;AAAAA,IACXC,OAAO3B,OAAO2B,UAAU,MAAM;AAAA,IAC9BiC,SAAS,CACP,CAAC;AAAA,MAACxD;AAAAA,MAAUwB;AAAAA,IAAAA,MAAW;AACrB,YAAM6B,UAAU7B,MAAM6B,QAAQZ,QAAS3C,CAAAA,UACrCA,MAAMmD,aAAalC,WAAW,IAAI,CAACjB,KAAK,IAAIA,MAAMmD,YACpD,GACM0D,kBAAkBtD,QAAQuD,OAAO,CAAC7F,QAAQjB,UAE5CiB,UACCnB,OAAOiH,UAAAA,EAAY9F,UACjBjB,MAAMQ,cAAcI,MAAMD,SACzBX,MAAMQ,cAAcC,OAAOE,UAEhC,CAAC,GAEEsB,UAAUP,MAAMI,aAAaJ,MAAM8B,cACnCwD,mBAAmB;AAAA,QACvBtG,MAAMgB,MAAMvB,eAAeO;AAAAA,QAC3BC,QAAQsB,QAAQhB,SAAS4F;AAAAA,MAAAA;AAgC3B,aAAO,CACL,GA9BctD,QAAQ0D,QAAAA,EAAUtE,QAAS3C,CAAAA,UAAU,CACnDsF,MAAM;AAAA,QAAChC,MAAM;AAAA,QAAUP,IAAI/C,MAAMQ;AAAAA,MAAAA,CAAc,GAC/C8E,MAAM;AAAA,QAAChC,MAAM;AAAA,QAAUP,IAAI/C,MAAMQ;AAAAA,MAAAA,CAAc,GAC/C8E,MAAM;AAAA,QACJhC,MAAM;AAAA,QACN4D,OAAO;AAAA,UACLC,OAAOjH,SAASkB,QAAQgG,OAAOC,KAAKC;AAAAA,UACpCtF,MAAMlC,OAAOiH,UAAAA;AAAAA,UACbQ,OACEC,aAAa;AAAA,YACX,GAAGtH;AAAAA,YACHkB,SAAS;AAAA,cACP,GAAGlB,SAASkB;AAAAA,cACZF,WAAW;AAAA,gBACTT,QAAQT,MAAMkB,UAAUT;AAAAA,gBACxBG,OAAO;AAAA,kBACLF,MAAMV,MAAMkB,UAAUN,MAAMF;AAAAA,kBAC5BC,QAAQI,KAAKC,IACXhB,MAAMkB,UAAUN,MAAMD,QACtBe,MAAMI,WAAWb,MACnB;AAAA,gBAAA;AAAA,cACF;AAAA,YACF;AAAA,UACF,CACD,GAAGsG,SAAS,CAAA;AAAA,QAAA;AAAA,MACjB,CACD,CAAC,CACH,GAICjC,MAAM;AAAA,QACJhC,MAAM;AAAA,QACNP,IAAI;AAAA,UACFtC,QAAQuG;AAAAA,UACRpG,OAAOoG;AAAAA,QAAAA;AAAAA,MACT,CACD,CAAC;AAAA,IAEN,CAAC;AAAA,EAAA;AAGP;"}
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":";;;;;;;AA0DO,SAASA,gBAAgBC,QAA8B;AAC5D,SAAOA;AACT;AC3BO,SAASC,0BAA0B;AAAA,EACxCC;AAAAA,EACAC;AAAAA,EACAC;AAAAA,EACAC;AAAAA,EACAC;AASF,GAAuC;AACrC,QAAM,CAACC,MAAMC,OAAOC,GAAG,IAAIP,OACrBQ,gBAAgBF,QAAQL,eAExBQ,gBAAgB;AAAA,IACpBC,QAAQ;AAAA,MACNC,MAAMR,eAAeQ;AAAAA,MACrBC,QAAQJ;AAAAA,IAAAA;AAAAA,IAEVK,OAAO;AAAA,MACLF,MAAMR,eAAeQ;AAAAA,MACrBC,QAAQJ,gBAAgBD,MAAMD;AAAAA,IAAAA;AAAAA,IAEhCQ,UAAU;AAAA,EAAA,GAENC,oBAAoB;AAAA,IACxBL,QAAQ;AAAA,MACNC,MAAMR,eAAeQ;AAAAA,MACrBC,QAAQI,KAAKC,IAAIR,cAAcC,OAAOE,QAAQR,mBAAmBc,MAAM;AAAA,IAAA;AAAA,IAEzEL,OAAO;AAAA,MACLF,MAAMR,eAAeQ;AAAAA,MACrBC,QAAQI,KAAKC,IAAIR,cAAcI,MAAMD,QAAQR,mBAAmBc,MAAM;AAAA,IAAA;AAAA,EAG1E,GAEMC,kBAAkBC,gCAAgC;AAAA,IACtDC,SAASnB,SAASmB;AAAAA,IAClBC,aAAaP,kBAAkBL;AAAAA,IAC/Ba,WAAW;AAAA,EAAA,CACZ,GACKC,gBAAgBJ,gCAAgC;AAAA,IACpDC,SAASnB,SAASmB;AAAAA,IAClBC,aAAaP,kBAAkBF;AAAAA,IAC/BU,WAAW;AAAA,EAAA,CACZ;AAED,MAAI,CAACJ,mBAAmB,CAACK;AACvB;AAGF,QAAMC,YAAY;AAAA,IAChBf,QAAQS;AAAAA,IACRN,OAAOW;AAAAA,EAAAA,GAGHE,0BAA0BC,qBAAqB;AAAA,IACnD,GAAGzB;AAAAA,IACHmB,SAAS;AAAA,MACP,GAAGnB,SAASmB;AAAAA,MACZI,WAAW;AAAA,QACTf,QAAQe,UAAUf;AAAAA,QAClBG,OAAOY,UAAUf;AAAAA,MAAAA;AAAAA,IACnB;AAAA,EACF,CACD,GACKkB,sBAAsBC,yBAAyB3B,QAAQ;AAE7D,MACEwB,CAAAA,wBAAwBI,KAAMC,CAAAA,sBAC5BH,oBAAoBE,KACjBE,CAAAA,uBACCD,kBAAkBE,KAAKC,SAASF,mBAAmBC,KAAKC,IAC5D,CACF;AAKF,WAAO;AAAA,MACL7B;AAAAA,MACAoB;AAAAA,MACAhB;AAAAA,IAAAA;AAEJ;ACnGA,SAAS0B,wBAAwBrC,QAO9B;AACD,SAAOsC,eAAe;AAAA,IACpBC,IAAI;AAAA,IACJC,OAAOA,CAAC;AAAA,MAACpC;AAAAA,MAAUqC;AAAAA,MAAOC;AAAAA,IAAAA,MAAS;AACjC,YAAMrC,iBAAiBsC,kBAAkBvC,QAAQ;AAEjD,UAAI,CAACC;AACH,eAAO;AAGT,YAAMC,qBAAqBsC,mBAAmBxC,QAAQ;AACtD,UAAIyC,aAAavC;AACjB,YAAMwC,kBAAkBD,aAAaJ,MAAMlC;AAC3C,UAAIwC,UAAUD;AAEd,YAAME,eAA8D,IAC9DC,eAAsC,CAAA;AAE5C,iBAAWC,QAAQlD,OAAOmD,OAAO;AAC/B,cAAMC,UAAU,IAAIC,OAAOH,KAAKX,GAAGe,QAAQ,IAAI;AAE/C,mBAAa;AAEX,gBAAMC,cAAc,CAAC,GAAGR,QAAQS,SAASJ,OAAO,CAAC,EAAEK,QAChDC,CAAAA,gBAAgB;AACf,gBAAIA,YAAYC,YAAYC;AAC1B,qBAAO,CAAA;AAGT,kBAAM1D,QAAQwD,YAAYC,QAAQE,GAAG,CAAC;AAEtC,gBAAI,CAAC3D;AACH,qBAAO,CAAA;AAGT,kBAAM4D,gBAAgB7D,0BAA0B;AAAA,cAC9CC,OAAO,CAACwD,YAAYG,GAAG,CAAC,KAAK,IAAI,GAAG3D,KAAK;AAAA,cACzCC,eAAe2C,gBAAgB1B,SAAS2B,QAAQ3B;AAAAA,cAChDhB;AAAAA,cACAC;AAAAA,cACAC;AAAAA,YAAAA,CACD;AAED,gBAAI,CAACwD;AACH,qBAAO,CAAA;AAQT,gBAJEA,cAAcnD,cAAcI,MAAMD,UAClCR,mBAAmBc;AAInB,qBAAO,CAAA;AAUT,gBAPqB4B,aAAahB,KAC/B+B,CAAAA,eACCA,WAAWpD,cAAcC,OAAOE,WAChCgD,cAAcnD,cAAcC,OAAOE,MACvC;AAIE,qBAAO,CAAA;AAGT,kBAAMkD,eACJN,YAAYC,QAAQvC,SAAS,IACzBsC,YAAYC,QAAQM,MAAM,CAAC,IAC3B,CAAA;AAwBN,mBAAO,CAtBW;AAAA,cAChB1D,MAAMuD,cAAcvD;AAAAA,cACpBoB,WAAWmC,cAAcnC;AAAAA,cACzBhB,eAAemD,cAAcnD;AAAAA,cAC7BqD,cAAcA,aAAaP,QAAQ,CAACvD,QAAOgE,UAAU;AACnD,sBAAM3D,OAAOmD,YAAYG,GAAGK,QAAQ,CAAC,KAAK;AAS1C,uBAR2BjE,0BAA0B;AAAA,kBACnDC,OAAO,CAACK,MAAM,GAAGL,MAAK;AAAA,kBACtBC,eAAe2C,gBAAgB1B,SAAS2B,QAAQ3B;AAAAA,kBAChDhB;AAAAA,kBACAC;AAAAA,kBACAC;AAAAA,gBAAAA,CACD,KAGQ,CAAA;AAAA,cAIX,CAAC;AAAA,YAAA,CAGc;AAAA,UACnB,CACF;AAEA,cAAIiD,YAAYnC,SAAS,GAAG;AAC1B,kBAAM+C,cACJjB,KAAKV,QAAQ;AAAA,cACXpC;AAAAA,cACAqC,OAAO;AAAA,gBACL2B,MAAM;AAAA,gBACNC,SAASd;AAAAA,gBACTlD;AAAAA,gBACAwC,YAAYvC;AAAAA,gBACZgE,cAAc7B,MAAMlC;AAAAA,cAAAA;AAAAA,cAEtBmC;AAAAA,YAAAA,CACD,KAAK;AAER,gBAAI,CAACyB;AACH;AAGF,kBAAMI,aAAarB,KAAKsB,QAAQC,IAAKC,YACnCA,OACE;AAAA,cACEtE;AAAAA,cACAqC,OAAO;AAAA,gBACL2B,MAAM;AAAA,gBACNC,SAASd;AAAAA,gBACTlD;AAAAA,gBACAwC,YAAYvC;AAAAA,gBACZgE,cAAc7B,MAAMlC;AAAAA,cAAAA;AAAAA,cAEtBmC;AAAAA,YAAAA,GAEFyB,WACF,CACF;AAEA,uBAAWQ,aAAaJ;AACtB,yBAAWG,UAAUC;AACnB1B,6BAAa2B,KAAKF,MAAM;AAI5B,kBAAML,UAAUd,YAAYE,QAASvD,CAAAA,UACnCA,MAAM8D,aAAa5C,WAAW,IAAI,CAAClB,KAAK,IAAIA,MAAM8D,YACpD;AAEA,uBAAW9D,SAASmE;AAGlBrB,2BAAa4B,KAAK1E,KAAK,GACvB2C,aAAaE,QAAQkB,MACnB,GACA/D,MAAMS,cAAcI,MAAMD,UAAU,CACtC,GACAiC,UAAUD,gBAAgBmB,MACxB/D,MAAMS,cAAcI,MAAMD,UAAU,CACtC;AAAA,UAEJ;AAGE;AAAA,QAEJ;AAAA,MACF;AAEA,aAAImC,aAAa7B,WAAW,IACnB,KAGF;AAAA,QAACoD,SAASvB;AAAAA,MAAAA;AAAAA,IACnB;AAAA,IACAuB,SAAS,CACP,CAAC;AAAA,MAAC/B;AAAAA,IAAAA,MAAW,CAACoC,QAAQpC,KAAK,CAAC,GAC5B,CAACqC,GAAG;AAAA,MAACN;AAAAA,IAAAA,MAAaA,SAClB,CAAC;AAAA,MAACpE;AAAAA,IAAAA,MAAc,CACd2E,OAAO,MAAM;AACX,YAAMC,eAAeC,gBAAgB7E,QAAQ;AAE7CJ,aAAOkF,QAAQ;AAAA,QAACC,YAAYH;AAAAA,MAAAA,CAAa;AAAA,IAC3C,CAAC,CAAC,CACH;AAAA,EAAA,CAEJ;AACH;AAoBO,SAAAI,gBAAAC,OAAA;AAAA,QAAAC,IAAAC,EAAA,CAAA,GACLC,SAAeC,UAAAA;AAAW,MAAAC;AAAA,SAAAJ,SAAAE,UAAAF,EAAA,CAAA,MAAAD,MAAAlC,SAEIuC,KAAA;AAAA,IAAAC,OAAA;AAAA,MAAAH;AAAAA,MAAArC,OACLkC,MAAKlC;AAAAA,IAAAA;AAAAA,EAAA,GAC7BmC,OAAAE,QAAAF,EAAA,CAAA,IAAAD,MAAAlC,OAAAmC,OAAAI,MAAAA,KAAAJ,EAAA,CAAA,GAFDM,YAAAC,kBAA8BH,EAE7B,GAAC;AAAA;AAgBJ,MAAMI,4BAOFA,CAAC;AAAA,EAACH;AAAAA,EAAOI;AAAQ,MAAM;AACzB,QAAMC,aAAaL,MAAMH,OAAOS,iBAAiB;AAAA,IAC/CC,UAAU7D,wBAAwB;AAAA,MAChCc,OAAOwC,MAAMxC;AAAAA,MACb+B,SAASA,CAAC;AAAA,QAACC;AAAAA,MAAAA,MAAgB;AACzBY,iBAAS;AAAA,UAAC3B,MAAM;AAAA,UAAqBe;AAAAA,QAAAA,CAAW;AAAA,MAClD;AAAA,IAAA,CACD;AAAA,EAAA,CACF;AAED,SAAO,MAAM;AACXa,eAAAA;AAAAA,EACF;AACF,GAEMG,iCAIFA,CAAC;AAAA,EAACR;AAAAA,EAAOI;AAAQ,MACZJ,MAAMH,OAAOS,iBAAiB;AAAA,EACnCC,UAAU5D,eAAe;AAAA,IACvBC,IAAI;AAAA,IACJiC,SAAS,CACP,MAAM,CACJ4B,MAAM;AAAA,MAAChC,MAAM;AAAA,IAAA,CAAe,GAC5BW,OAAO,MAAM;AACXgB,eAAS;AAAA,QAAC3B,MAAM;AAAA,MAAA,CAAsB;AAAA,IACxC,CAAC,CAAC,CACH;AAAA,EAAA,CAEJ;AACH,CAAC,GAGGiC,4BAIFA,CAAC;AAAA,EAACN;AAAAA,EAAUJ;AAAK,MACAA,MAAMH,OAAOS,iBAAiB;AAAA,EAC/CC,UAAU5D,eAAe;AAAA,IACvBC,IAAI;AAAA,IACJC,OAAOA,CAAC;AAAA,MAACpC;AAAAA,MAAUqC;AAAAA,IAAAA,OASV;AAAA,MAACuC,cARaC,gBAAgB;AAAA,QACnC,GAAG7E;AAAAA,QACHmB,SAAS;AAAA,UACP,GAAGnB,SAASmB;AAAAA,UACZI,WAAWc,MAAMoB;AAAAA,QAAAA;AAAAA,MACnB,CACD;AAAA,IAAA;AAAA,IAIHW,SAAS,CACP,CAAC;AAAA,MAAC/B;AAAAA,IAAAA,GAAQ;AAAA,MAACuC;AAAAA,IAAAA,MAAkB,CAC3BD,OAAO,MAAM;AACXgB,eAAS;AAAA,QAAC3B,MAAM;AAAA,QAAqBY;AAAAA,MAAAA,CAAa;AAAA,IACpD,CAAC,GACDH,QAAQpC,KAAK,CAAC,CACf;AAAA,EAAA,CAEJ;AACH,CAAC,GAKG6D,iBAAiBC,MAAM;AAAA,EAC3BC,OAAO;AAAA,IACLjF,SAAS,CAAA;AAAA,IAKToE,OAAO,CAAA;AAAA,IAIPc,QAAQ,CAAA;AAAA,EAAC;AAAA,EAEXC,QAAQ;AAAA,IACN,4BAA4BC,aAAaR,8BAA8B;AAAA,IACvE,uBAAuBQ,aAAab,yBAAyB;AAAA,IAC7D,sBAAsBa,aAAaN,yBAAyB;AAAA,EAAA;AAAA,EAE9DO,QAAQ;AAAA,IACN,wBAAwBC,CAAC;AAAA,MAACtF;AAAAA,MAASkB;AAAAA,IAAAA,MAAW;AAC5C,UAAIA,MAAM2B,SAAS;AACjB,eAAO;AAGT,UAAI,CAAC3B,MAAMuC,gBAAgB,CAACzD,QAAQ4D;AAClC,eAAO;AAGT,YAAM2B,eACJvF,QAAQ4D,WAAW3E,MAAMK,KAAK,CAAC,EAAEuB,SAC/BK,MAAMuC,aAAaxE,MAAMK,KAAK,CAAC,EAAEuB,QACnCb,QAAQ4D,WAAW3E,MAAMM,WAAW2B,MAAMuC,aAAaxE,MAAMM,QACzDiG,aACJxF,QAAQ4D,WAAW1E,IAAII,KAAK,CAAC,EAAEuB,SAC7BK,MAAMuC,aAAavE,IAAII,KAAK,CAAC,EAAEuB,QACjCb,QAAQ4D,WAAW1E,IAAIK,WAAW2B,MAAMuC,aAAavE,IAAIK;AAE3D,aAAOgG,gBAAgBC;AAAAA,IACzB;AAAA,EAAA;AAEJ,CAAC,GAEKC,mBAAmBV,eAAeW,OAAO;AAAA,EAC7C9B,YAAYA,CAAC;AAAA,IAAC5D;AAAAA,IAASkB;AAAAA,EAAAA,MACrBA,MAAM2B,SAAS,sBAAsB3B,MAAM0C,aAAa5D,QAAQ4D;AACpE,CAAC,GAEKU,mBAAmBS,eAAeY,cAAc;AAAA,EACpDC,IAAI;AAAA,EACJ5F,SAASA,CAAC;AAAA,IAACoE;AAAAA,EAAAA,OAAY;AAAA,IACrBH,QAAQG,MAAMH;AAAAA,IACdrC,OAAOwC,MAAMxC;AAAAA,IACbgC,YAAYvB;AAAAA,EAAAA;AAAAA,EAEdwD,SAAS;AAAA,EACTC,QAAQ;AAAA,IACNC,KAAK;AAAA,IACL3B,OAAOA,CAAC;AAAA,MAACpE;AAAAA,IAAAA,OAAc;AAAA,MACrBiE,QAAQjE,QAAQiE;AAAAA,MAChBrC,OAAO5B,QAAQ4B;AAAAA,IAAAA;AAAAA,EACjB;AAAA,EAEFZ,IAAI;AAAA,IACF,qBAAqB;AAAA,MACnBgF,QAAQ;AAAA,MACR/C,SAASwC;AAAAA,IAAAA;AAAAA,EACX;AAAA,EAEFQ,QAAQ;AAAA,IACN,MAAQ,CAAA;AAAA,IACR,sBAAsB;AAAA,MACpBH,QAAQ,CACN;AAAA,QACEC,KAAK;AAAA,QACL3B,OAAOA,CAAC;AAAA,UAACpE;AAAAA,QAAAA,OAAc;AAAA,UAACiE,QAAQjE,QAAQiE;AAAAA,QAAAA;AAAAA,MAAM,GAEhD;AAAA,QACE8B,KAAK;AAAA,QACL3B,OAAOA,CAAC;AAAA,UAACpE;AAAAA,QAAAA,OAAc;AAAA,UAACiE,QAAQjE,QAAQiE;AAAAA,QAAAA;AAAAA,MAAM,CAC/C;AAAA,MAEHjD,IAAI;AAAA,QACF,qBAAqB;AAAA,UACnBgF,QAAQ;AAAA,UACR/E,OAAO;AAAA,QAAA;AAAA,QAET,uBAAuB;AAAA,UACrB+E,QAAQ;AAAA,QAAA;AAAA,MACV;AAAA,IACF;AAAA,EACF;AAEJ,CAAC;ACvYM,SAASE,wBAAwBzH,QAAsC;AAC5E,SAAO;AAAA,IACLuC,IAAIvC,OAAOuC;AAAAA,IACXC,OAAOxC,OAAOwC,UAAU,MAAM;AAAA,IAC9BgC,SAAS,CACP,CAAC;AAAA,MAACpE;AAAAA,MAAUqC;AAAAA,IAAAA,MAAW;AACrB,YAAMiF,YAAYjF,MAAM4B,QAAQZ,QAASvD,CAAAA,UACvCA,MAAM8D,aAAa5C,WAAW,IAAI,CAAClB,KAAK,IAAIA,MAAM8D,YACpD,GACMjB,UAAUN,MAAMI,aAAaJ,MAAM6B;AAEzC,UAAIqD,kBAAkB;AACtB,YAAMnD,UAAiC,CAAA;AAEvC,iBAAWoD,YAAYF,UAAUG,WAAW;AAC1C,cAAMtH,OAAOP,OAAO8H,UAAU;AAAA,UAACF;AAAAA,QAAAA,CAAS;AAExCD,0BACEA,mBACCpH,KAAKa,UACHwG,SAASjH,cAAcI,MAAMD,SAC5B8G,SAASjH,cAAcC,OAAOE,UAEpC0D,QAAQI,KAAKwB,MAAM;AAAA,UAAChC,MAAM;AAAA,UAAUP,IAAI+D,SAASjH;AAAAA,QAAAA,CAAc,CAAC,GAChE6D,QAAQI,KAAKwB,MAAM;AAAA,UAAChC,MAAM;AAAA,UAAUP,IAAI+D,SAASjH;AAAAA,QAAAA,CAAc,CAAC,GAChE6D,QAAQI,KACNwB,MAAM;AAAA,UACJhC,MAAM;AAAA,UACN2D,OAAO;AAAA,YACLC,OAAO5H,SAASmB,QAAQ0G,OAAOC,KAAKC;AAAAA,YACpC5H;AAAAA,YACA6H,OACEC,aAAa;AAAA,cACX,GAAGjI;AAAAA,cACHmB,SAAS;AAAA,gBACP,GAAGnB,SAASmB;AAAAA,gBACZI,WAAW;AAAA,kBACTf,QAAQgH,SAASjG,UAAUf;AAAAA,kBAC3BG,OAAO;AAAA,oBACLF,MAAM+G,SAASjG,UAAUZ,MAAMF;AAAAA,oBAC/BC,QAAQI,KAAKC,IACXyG,SAASjG,UAAUZ,MAAMD,QACzB2B,MAAMI,WAAWzB,MACnB;AAAA,kBAAA;AAAA,gBACF;AAAA,cACF;AAAA,YACF,CACD,GAAGgH,SAAS,CAAA;AAAA,UAAA;AAAA,QACjB,CACD,CACH;AAAA,MACF;AAEA,YAAME,mBAAmB;AAAA,QACvBzH,MAAM4B,MAAMpC,eAAeQ;AAAAA,QAC3BC,QAAQiC,QAAQ3B,SAASuG;AAAAA,MAAAA;AAG3B,aAAO,CACL,GAAGnD,SACH4B,MAAM;AAAA,QACJhC,MAAM;AAAA,QACNP,IAAI;AAAA,UACFjD,QAAQ0H;AAAAA,UACRvH,OAAOuH;AAAAA,QAAAA;AAAAA,MACT,CACD,CAAC;AAAA,IAEN,CAAC;AAAA,EAAA;AAGP;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portabletext/plugin-input-rule",
3
- "version": "0.1.3",
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.22.1"
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
- "racejar": "1.3.1",
56
+ "@portabletext/editor": "2.14.0",
57
57
  "@portabletext/schema": "1.2.0",
58
- "@portabletext/editor": "2.13.7"
58
+ "racejar": "1.3.1"
59
59
  },
60
60
  "peerDependencies": {
61
- "@portabletext/editor": "^2.13.7",
61
+ "@portabletext/editor": "^2.14.0",
62
62
  "react": "^19.1.1"
63
63
  },
64
64
  "publishConfig": {
@@ -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}," |
@@ -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 {blockOffsetsToSelection} from '@portabletext/editor/utils'
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
- const selection = blockOffsetsToSelection({
74
+
75
+ const anchorBackwards = blockOffsetToSpanSelectionPoint({
67
76
  context: snapshot.context,
68
- offsets: normalizedOffsets,
69
- backward: false,
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 (!selection) {
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 matches = event.matches.flatMap((match) =>
35
+ const locations = event.matches.flatMap((match) =>
35
36
  match.groupMatches.length === 0 ? [match] : match.groupMatches,
36
37
  )
37
- const textLengthDelta = matches.reduce((length, match) => {
38
- return (
39
- length -
40
- (config.transform().length -
41
- (match.targetOffsets.focus.offset -
42
- match.targetOffsets.anchor.offset))
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
- }, 0)
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({