|  |  | import { EditorState, Plugin, TextSelection } from 'prosemirror-state'
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | import { CommandManager } from './CommandManager'
 | 
						
						
						
							|  |  | import { Editor } from './Editor'
 | 
						
						
						
							|  |  | import { createChainableState } from './helpers/createChainableState'
 | 
						
						
						
							|  |  | import { getTextContentFromNodes } from './helpers/getTextContentFromNodes'
 | 
						
						
						
							|  |  | import {
 | 
						
						
						
							|  |  |   CanCommands,
 | 
						
						
						
							|  |  |   ChainedCommands,
 | 
						
						
						
							|  |  |   ExtendedRegExpMatchArray,
 | 
						
						
						
							|  |  |   Range,
 | 
						
						
						
							|  |  |   SingleCommands,
 | 
						
						
						
							|  |  | } from './types'
 | 
						
						
						
							|  |  | import { isRegExp } from './utilities/isRegExp'
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | export type InputRuleMatch = {
 | 
						
						
						
							|  |  |   index: number,
 | 
						
						
						
							|  |  |   text: string,
 | 
						
						
						
							|  |  |   replaceWith?: string,
 | 
						
						
						
							|  |  |   match?: RegExpMatchArray,
 | 
						
						
						
							|  |  |   data?: Record<string, any>,
 | 
						
						
						
							|  |  | }
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | export type InputRuleFinder =
 | 
						
						
						
							|  |  |   | RegExp
 | 
						
						
						
							|  |  |   | ((text: string) => InputRuleMatch | null)
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | export class InputRule {
 | 
						
						
						
							|  |  |   find: InputRuleFinder
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |   handler: (props: {
 | 
						
						
						
							|  |  |     state: EditorState,
 | 
						
						
						
							|  |  |     range: Range,
 | 
						
						
						
							|  |  |     match: ExtendedRegExpMatchArray,
 | 
						
						
						
							|  |  |     commands: SingleCommands,
 | 
						
						
						
							|  |  |     chain: () => ChainedCommands,
 | 
						
						
						
							|  |  |     can: () => CanCommands,
 | 
						
						
						
							|  |  |   }) => void | null
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |   constructor(config: {
 | 
						
						
						
							|  |  |     find: InputRuleFinder,
 | 
						
						
						
							|  |  |     handler: (props: {
 | 
						
						
						
							|  |  |       state: EditorState,
 | 
						
						
						
							|  |  |       range: Range,
 | 
						
						
						
							|  |  |       match: ExtendedRegExpMatchArray,
 | 
						
						
						
							|  |  |       commands: SingleCommands,
 | 
						
						
						
							|  |  |       chain: () => ChainedCommands,
 | 
						
						
						
							|  |  |       can: () => CanCommands,
 | 
						
						
						
							|  |  |     }) => void | null,
 | 
						
						
						
							|  |  |   }) {
 | 
						
						
						
							|  |  |     this.find = config.find
 | 
						
						
						
							|  |  |     this.handler = config.handler
 | 
						
						
						
							|  |  |   }
 | 
						
						
						
							|  |  | }
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | const inputRuleMatcherHandler = (text: string, find: InputRuleFinder): ExtendedRegExpMatchArray | null => {
 | 
						
						
						
							|  |  |   if (isRegExp(find)) {
 | 
						
						
						
							|  |  |     return find.exec(text)
 | 
						
						
						
							|  |  |   }
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |   const inputRuleMatch = find(text)
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |   if (!inputRuleMatch) {
 | 
						
						
						
							|  |  |     return null
 | 
						
						
						
							|  |  |   }
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |   const result: ExtendedRegExpMatchArray = []
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |   result.push(inputRuleMatch.text)
 | 
						
						
						
							|  |  |   result.index = inputRuleMatch.index
 | 
						
						
						
							|  |  |   result.input = text
 | 
						
						
						
							|  |  |   result.data = inputRuleMatch.data
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |   if (inputRuleMatch.replaceWith) {
 | 
						
						
						
							|  |  |     if (!inputRuleMatch.text.includes(inputRuleMatch.replaceWith)) {
 | 
						
						
						
							|  |  |       console.warn('[tiptap warn]: "inputRuleMatch.replaceWith" must be part of "inputRuleMatch.text".')
 | 
						
						
						
							|  |  |     }
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |     result.push(inputRuleMatch.replaceWith)
 | 
						
						
						
							|  |  |   }
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |   return result
 | 
						
						
						
							|  |  | }
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | function run(config: {
 | 
						
						
						
							|  |  |   editor: Editor,
 | 
						
						
						
							|  |  |   from: number,
 | 
						
						
						
							|  |  |   to: number,
 | 
						
						
						
							|  |  |   text: string,
 | 
						
						
						
							|  |  |   rules: InputRule[],
 | 
						
						
						
							|  |  |   plugin: Plugin,
 | 
						
						
						
							|  |  | }): boolean {
 | 
						
						
						
							|  |  |   const {
 | 
						
						
						
							|  |  |     editor,
 | 
						
						
						
							|  |  |     from,
 | 
						
						
						
							|  |  |     to,
 | 
						
						
						
							|  |  |     text,
 | 
						
						
						
							|  |  |     rules,
 | 
						
						
						
							|  |  |     plugin,
 | 
						
						
						
							|  |  |   } = config
 | 
						
						
						
							|  |  |   const { view } = editor
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |   if (view.composing) {
 | 
						
						
						
							|  |  |     return false
 | 
						
						
						
							|  |  |   }
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |   const $from = view.state.doc.resolve(from)
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |   if (
 | 
						
						
						
							|  |  |     // check for code node
 | 
						
						
						
							|  |  |     $from.parent.type.spec.code
 | 
						
						
						
							|  |  |     // check for code mark
 | 
						
						
						
							|  |  |     || !!($from.nodeBefore || $from.nodeAfter)?.marks.find(mark => mark.type.spec.code)
 | 
						
						
						
							|  |  |   ) {
 | 
						
						
						
							|  |  |     return false
 | 
						
						
						
							|  |  |   }
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |   let matched = false
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |   const textBefore = getTextContentFromNodes($from) + text
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |   rules.forEach(rule => {
 | 
						
						
						
							|  |  |     if (matched) {
 | 
						
						
						
							|  |  |       return
 | 
						
						
						
							|  |  |     }
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |     const match = inputRuleMatcherHandler(textBefore, rule.find)
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |     if (!match) {
 | 
						
						
						
							|  |  |       return
 | 
						
						
						
							|  |  |     }
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |     const tr = view.state.tr
 | 
						
						
						
							|  |  |     const state = createChainableState({
 | 
						
						
						
							|  |  |       state: view.state,
 | 
						
						
						
							|  |  |       transaction: tr,
 | 
						
						
						
							|  |  |     })
 | 
						
						
						
							|  |  |     const range = {
 | 
						
						
						
							|  |  |       from: from - (match[0].length - text.length),
 | 
						
						
						
							|  |  |       to,
 | 
						
						
						
							|  |  |     }
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |     const { commands, chain, can } = new CommandManager({
 | 
						
						
						
							|  |  |       editor,
 | 
						
						
						
							|  |  |       state,
 | 
						
						
						
							|  |  |     })
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |     const handler = rule.handler({
 | 
						
						
						
							|  |  |       state,
 | 
						
						
						
							|  |  |       range,
 | 
						
						
						
							|  |  |       match,
 | 
						
						
						
							|  |  |       commands,
 | 
						
						
						
							|  |  |       chain,
 | 
						
						
						
							|  |  |       can,
 | 
						
						
						
							|  |  |     })
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |     // stop if there are no changes
 | 
						
						
						
							|  |  |     if (handler === null || !tr.steps.length) {
 | 
						
						
						
							|  |  |       return
 | 
						
						
						
							|  |  |     }
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |     // store transform as meta data
 | 
						
						
						
							|  |  |     // so we can undo input rules within the `undoInputRules` command
 | 
						
						
						
							|  |  |     tr.setMeta(plugin, {
 | 
						
						
						
							|  |  |       transform: tr,
 | 
						
						
						
							|  |  |       from,
 | 
						
						
						
							|  |  |       to,
 | 
						
						
						
							|  |  |       text,
 | 
						
						
						
							|  |  |     })
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |     view.dispatch(tr)
 | 
						
						
						
							|  |  |     matched = true
 | 
						
						
						
							|  |  |   })
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |   return matched
 | 
						
						
						
							|  |  | }
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | /**
 | 
						
						
						
							|  |  |  * Create an input rules plugin. When enabled, it will cause text
 | 
						
						
						
							|  |  |  * input that matches any of the given rules to trigger the rule’s
 | 
						
						
						
							|  |  |  * action.
 | 
						
						
						
							|  |  |  */
 | 
						
						
						
							|  |  | export function inputRulesPlugin(props: { editor: Editor, rules: InputRule[] }): Plugin {
 | 
						
						
						
							|  |  |   const { editor, rules } = props
 | 
						
						
						
							|  |  |   const plugin = new Plugin({
 | 
						
						
						
							|  |  |     state: {
 | 
						
						
						
							|  |  |       init() {
 | 
						
						
						
							|  |  |         return null
 | 
						
						
						
							|  |  |       },
 | 
						
						
						
							|  |  |       apply(tr, prev) {
 | 
						
						
						
							|  |  |         const stored = tr.getMeta(plugin)
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |         if (stored) {
 | 
						
						
						
							|  |  |           return stored
 | 
						
						
						
							|  |  |         }
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |         return tr.selectionSet || tr.docChanged
 | 
						
						
						
							|  |  |           ? null
 | 
						
						
						
							|  |  |           : prev
 | 
						
						
						
							|  |  |       },
 | 
						
						
						
							|  |  |     },
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |     props: {
 | 
						
						
						
							|  |  |       handleTextInput(view, from, to, text) {
 | 
						
						
						
							|  |  |         return run({
 | 
						
						
						
							|  |  |           editor,
 | 
						
						
						
							|  |  |           from,
 | 
						
						
						
							|  |  |           to,
 | 
						
						
						
							|  |  |           text,
 | 
						
						
						
							|  |  |           rules,
 | 
						
						
						
							|  |  |           plugin,
 | 
						
						
						
							|  |  |         })
 | 
						
						
						
							|  |  |       },
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |       handleDOMEvents: {
 | 
						
						
						
							|  |  |         compositionend: view => {
 | 
						
						
						
							|  |  |           setTimeout(() => {
 | 
						
						
						
							|  |  |             const { $cursor } = view.state.selection as TextSelection
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |             if ($cursor) {
 | 
						
						
						
							|  |  |               run({
 | 
						
						
						
							|  |  |                 editor,
 | 
						
						
						
							|  |  |                 from: $cursor.pos,
 | 
						
						
						
							|  |  |                 to: $cursor.pos,
 | 
						
						
						
							|  |  |                 text: '',
 | 
						
						
						
							|  |  |                 rules,
 | 
						
						
						
							|  |  |                 plugin,
 | 
						
						
						
							|  |  |               })
 | 
						
						
						
							|  |  |             }
 | 
						
						
						
							|  |  |           })
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |           return false
 | 
						
						
						
							|  |  |         },
 | 
						
						
						
							|  |  |       },
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |       // add support for input rules to trigger on enter
 | 
						
						
						
							|  |  |       // this is useful for example for code blocks
 | 
						
						
						
							|  |  |       handleKeyDown(view, event) {
 | 
						
						
						
							|  |  |         if (event.key !== 'Enter') {
 | 
						
						
						
							|  |  |           return false
 | 
						
						
						
							|  |  |         }
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |         const { $cursor } = view.state.selection as TextSelection
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |         if ($cursor) {
 | 
						
						
						
							|  |  |           return run({
 | 
						
						
						
							|  |  |             editor,
 | 
						
						
						
							|  |  |             from: $cursor.pos,
 | 
						
						
						
							|  |  |             to: $cursor.pos,
 | 
						
						
						
							|  |  |             text: '\n',
 | 
						
						
						
							|  |  |             rules,
 | 
						
						
						
							|  |  |             plugin,
 | 
						
						
						
							|  |  |           })
 | 
						
						
						
							|  |  |         }
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |         return false
 | 
						
						
						
							|  |  |       },
 | 
						
						
						
							|  |  |     },
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |     // @ts-ignore
 | 
						
						
						
							|  |  |     isInputRules: true,
 | 
						
						
						
							|  |  |   }) as Plugin
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  |   return plugin
 | 
						
						
						
							|  |  | }
 |