Files
tiptap/packages/extension-link/src/helpers/autolink.ts
2022-05-16 12:00:25 +02:00

112 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { find, test } from 'linkifyjs'
import { MarkType } from 'prosemirror-model'
import { Plugin, PluginKey } from 'prosemirror-state'
import {
combineTransactionSteps,
findChildrenInRange,
getChangedRanges,
getMarksBetween,
} from '@tiptap/core'
type AutolinkOptions = {
type: MarkType,
validate?: (url: string) => boolean,
}
export function autolink(options: AutolinkOptions): Plugin {
return new Plugin({
key: new PluginKey('autolink'),
appendTransaction: (transactions, oldState, newState) => {
const docChanges = transactions.some(transaction => transaction.docChanged)
&& !oldState.doc.eq(newState.doc)
const preventAutolink = transactions.some(transaction => transaction.getMeta('preventAutolink'))
if (!docChanges || preventAutolink) {
return
}
const { tr } = newState
const transform = combineTransactionSteps(oldState.doc, transactions)
const { mapping } = transform
const changes = getChangedRanges(transform)
changes.forEach(({ oldRange, newRange }) => {
// at first we check if we have to remove links
getMarksBetween(oldRange.from, oldRange.to, oldState.doc)
.filter(item => item.mark.type === options.type)
.forEach(oldMark => {
const newFrom = mapping.map(oldMark.from)
const newTo = mapping.map(oldMark.to)
const newMarks = getMarksBetween(newFrom, newTo, newState.doc)
.filter(item => item.mark.type === options.type)
if (!newMarks.length) {
return
}
const newMark = newMarks[0]
const oldLinkText = oldState.doc.textBetween(oldMark.from, oldMark.to, undefined, ' ')
const newLinkText = newState.doc.textBetween(newMark.from, newMark.to, undefined, ' ')
const wasLink = test(oldLinkText)
const isLink = test(newLinkText)
// remove only the link, if it was a link before too
// because we dont want to remove links that were set manually
if (wasLink && !isLink) {
tr.removeMark(newMark.from, newMark.to, options.type)
}
})
// now lets see if we can add new links
findChildrenInRange(newState.doc, newRange, node => node.isTextblock)
.forEach(textBlock => {
// we need to define a placeholder for leaf nodes
// so that the link position can be calculated correctly
const text = newState.doc.textBetween(
textBlock.pos,
textBlock.pos + textBlock.node.nodeSize,
undefined,
' ',
)
find(text)
.filter(link => link.isLink)
.filter(link => {
if (options.validate) {
return options.validate(link.value)
}
return true
})
// calculate link position
.map(link => ({
...link,
from: textBlock.pos + link.start + 1,
to: textBlock.pos + link.end + 1,
}))
// check if link is within the changed range
.filter(link => {
const fromIsInRange = newRange.from >= link.from && newRange.from <= link.to
const toIsInRange = newRange.to >= link.from && newRange.to <= link.to
return fromIsInRange || toIsInRange
})
// add link mark
.forEach(link => {
tr.addMark(link.from, link.to, options.type.create({
href: link.href,
}))
})
})
})
if (!tr.steps.length) {
return
}
return tr
},
})
}