import { Node as ProseMirrorNode, NodeType } from '@tiptap/pm/model' import { canJoin, findWrapping } from '@tiptap/pm/transform' import { Editor } from '../Editor.js' import { InputRule, InputRuleFinder } from '../InputRule.js' import { ExtendedRegExpMatchArray } from '../types.js' import { callOrReturn } from '../utilities/callOrReturn.js' /** * Build an input rule for automatically wrapping a textblock when a * given string is typed. When using a regular expresion you’ll * probably want the regexp to start with `^`, so that the pattern can * only occur at the start of a textblock. * * `type` is the type of node to wrap in. * * By default, if there’s a node with the same type above the newly * wrapped node, the rule will try to join those * two nodes. You can pass a join predicate, which takes a regular * expression match and the node before the wrapped node, and can * return a boolean to indicate whether a join should happen. */ export function wrappingInputRule(config: { find: InputRuleFinder, type: NodeType, keepMarks?: boolean, keepAttributes?: boolean, editor?: Editor getAttributes?: | Record | ((match: ExtendedRegExpMatchArray) => Record) | false | null , joinPredicate?: (match: ExtendedRegExpMatchArray, node: ProseMirrorNode) => boolean, }) { return new InputRule({ find: config.find, handler: ({ state, range, match, chain, }) => { const attributes = callOrReturn(config.getAttributes, undefined, match) || {} const tr = state.tr.delete(range.from, range.to) const $start = tr.doc.resolve(range.from) const blockRange = $start.blockRange() const wrapping = blockRange && findWrapping(blockRange, config.type, attributes) if (!wrapping) { return null } tr.wrap(blockRange, wrapping) if (config.keepMarks && config.editor) { const { selection, storedMarks } = state const { splittableMarks } = config.editor.extensionManager const marks = storedMarks || (selection.$to.parentOffset && selection.$from.marks()) if (marks) { const filteredMarks = marks.filter(mark => splittableMarks.includes(mark.type.name)) tr.ensureMarks(filteredMarks) } } if (config.keepAttributes) { /** If the nodeType is `bulletList` or `orderedList` set the `nodeType` as `listItem` */ const nodeType = config.type.name === 'bulletList' || config.type.name === 'orderedList' ? 'listItem' : 'taskList' chain().updateAttributes(nodeType, attributes).run() } const before = tr.doc.resolve(range.from - 1).nodeBefore if ( before && before.type === config.type && canJoin(tr.doc, range.from - 1) && (!config.joinPredicate || config.joinPredicate(match, before)) ) { tr.join(range.from - 1) } }, }) }