import { useEffect } from 'react' import type { useCreateBlockNote } from '@blocknote/react' import { shouldStripAutoLinkedLocalFileMark } from '../utils/editorLinkAutolink' const FILENAME_AUTOLINK_GUARD_META = 'tolaria-filename-autolink-guard' type TiptapLinkMark = { attrs?: { href?: string } type: unknown } type TiptapTextNode = { isText?: boolean marks?: TiptapLinkMark[] nodeSize: number text?: string | null } function hasTextContent(node: TiptapTextNode) { return Boolean(node.isText && node.text && node.marks?.length) } function isAccidentalFilenameLinkMark( mark: TiptapLinkMark, linkMarkType: unknown, text: string, ) { if (mark.type !== linkMarkType) { return false } if (typeof mark.attrs?.href !== 'string') { return false } return shouldStripAutoLinkedLocalFileMark({ href: { raw: mark.attrs.href }, text: { raw: text }, }) } function hasEditorEventApi( tiptap: ReturnType['_tiptapEditor'] | undefined, ) { return Boolean(tiptap && typeof tiptap.on === 'function' && typeof tiptap.off === 'function') } function collectAccidentalFilenameLinkRanges(editor: ReturnType) { const linkMarkType = editor._tiptapEditor?.schema?.marks?.link if (!linkMarkType) { return [] } const ranges: Array<{ from: number; to: number }> = [] editor._tiptapEditor.state.doc.descendants((node: TiptapTextNode, pos: number) => { if (!hasTextContent(node)) { return } const hasAccidentalFilenameLink = node.marks?.some((mark) => ( isAccidentalFilenameLinkMark(mark, linkMarkType, node.text ?? '') )) ?? false if (hasAccidentalFilenameLink) { ranges.push({ from: pos, to: pos + node.nodeSize }) } }) return ranges } function shouldSkipGuardRun( applyingGuard: boolean, transaction: { docChanged?: boolean; getMeta: (key: string) => unknown }, ) { return ( applyingGuard || !transaction.docChanged || transaction.getMeta(FILENAME_AUTOLINK_GUARD_META) ) } function removeLinkRanges( editor: ReturnType, ranges: Array<{ from: number; to: number }>, ) { const tr = editor._tiptapEditor.state.tr for (const range of ranges) { tr.removeMark(range.from, range.to, editor._tiptapEditor.schema.marks.link) } return tr } export function useFilenameAutolinkGuard(editor: ReturnType) { useEffect(() => { const tiptap = editor._tiptapEditor if (!hasEditorEventApi(tiptap)) { return } let applyingGuard = false const stripAccidentalFilenameAutolinks = ({ transaction }: { transaction: { docChanged?: boolean; getMeta: (key: string) => unknown } }) => { if (shouldSkipGuardRun(applyingGuard, transaction)) { return } const ranges = collectAccidentalFilenameLinkRanges(editor) if (ranges.length === 0) { return } const tr = removeLinkRanges(editor, ranges) if (!tr.docChanged) { return } tr.setMeta(FILENAME_AUTOLINK_GUARD_META, true) applyingGuard = true tiptap.view.dispatch(tr) applyingGuard = false } tiptap.on('update', stripAccidentalFilenameAutolinks) return () => { tiptap.off('update', stripAccidentalFilenameAutolinks) } }, [editor]) }