133 lines
3.3 KiB
TypeScript
133 lines
3.3 KiB
TypeScript
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<typeof useCreateBlockNote>['_tiptapEditor'] | undefined,
|
|
) {
|
|
return Boolean(tiptap && typeof tiptap.on === 'function' && typeof tiptap.off === 'function')
|
|
}
|
|
|
|
function collectAccidentalFilenameLinkRanges(editor: ReturnType<typeof useCreateBlockNote>) {
|
|
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<typeof useCreateBlockNote>,
|
|
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<typeof useCreateBlockNote>) {
|
|
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])
|
|
}
|