import { resolveArrowLigatureInput } from '../utils/arrowLigatures' import { createRichEditorInputTransformExtension, type RichEditorInputTransform, } from './richEditorInputTransform' const PREFIX_CONTEXT_LENGTH = 2 interface CodeContextSelection { from?: unknown to?: unknown $from?: { depth: number node: (depth?: number) => { type?: { name?: string spec?: { code?: boolean } } } } } interface ArrowLigatureTransactionArgs { event: InputEvent & { data: string } literalAsciiCursor: number | null view: { state: { doc: { textBetween: (from: number, to: number, blockSeparator: string, leafText: string) => string } selection: CodeContextSelection tr: { insertText: (text: string, from: number, to: number) => Transaction } } } } interface ArrowLigatureTransactionResult { nextLiteralAsciiCursor: number | null transaction: Transaction | null } function isInsertedCharacter(event: InputEvent): event is InputEvent & { data: string } { return event.inputType === 'insertText' && typeof event.data === 'string' } function isCodeContext(selection: CodeContextSelection): boolean { const position = selection.$from if (!position) return false for (let depth = position.depth; depth >= 0; depth--) { const type = position.node(depth).type if (type?.spec?.code || type?.name === 'codeBlock') return true } return false } function getWritableCursor(selection: CodeContextSelection): number | null { const { from, to } = selection if (typeof from !== 'number' || typeof to !== 'number') return null if (!Number.isFinite(from) || !Number.isFinite(to)) return null return from === to ? from : null } function withoutTransaction( nextLiteralAsciiCursor: number | null, ): ArrowLigatureTransactionResult { return { nextLiteralAsciiCursor, transaction: null } } function buildArrowLigatureTransaction({ event, literalAsciiCursor, view, }: ArrowLigatureTransactionArgs): ArrowLigatureTransactionResult { try { const { state } = view const { selection } = state const from = getWritableCursor(selection) if (from === null) return withoutTransaction(literalAsciiCursor) if (isCodeContext(selection)) return withoutTransaction(null) const beforeText = state.doc.textBetween( Math.max(0, from - PREFIX_CONTEXT_LENGTH), from, '', '', ) const resolution = resolveArrowLigatureInput({ beforeText, cursor: from, inputText: event.data, literalAsciiCursor, }) if (!resolution.change) return withoutTransaction(resolution.nextLiteralAsciiCursor) return { nextLiteralAsciiCursor: resolution.nextLiteralAsciiCursor, transaction: state.tr.insertText( resolution.change.insert, resolution.change.from, resolution.change.to, ), } } catch { return withoutTransaction(null) } } export function createArrowLigatureInputTransform(): RichEditorInputTransform { let literalAsciiCursor: number | null = null return { handleBeforeInput(event, { view }) { if (!isInsertedCharacter(event)) return null const result = buildArrowLigatureTransaction({ event, literalAsciiCursor, view }) literalAsciiCursor = result.nextLiteralAsciiCursor if (result.transaction === null) return null return { ignoreDispatchError: true, onDispatchError: () => { literalAsciiCursor = null }, preventDefault: true, transaction: result.transaction, } }, reset() { literalAsciiCursor = null }, } } export const createArrowLigaturesExtension = createRichEditorInputTransformExtension({ createTransforms: () => [createArrowLigatureInputTransform()], key: 'arrowLigatures', })