mirror of
https://github.com/ueberdosis/tiptap.git
synced 2024-11-27 23:15:15 +08:00
autolink improvement
This commit is contained in:
parent
43a6a865c8
commit
d9cdf1e1c1
@ -3,15 +3,23 @@ import {
|
|||||||
findChildrenInRange,
|
findChildrenInRange,
|
||||||
getChangedRanges,
|
getChangedRanges,
|
||||||
getMarksBetween,
|
getMarksBetween,
|
||||||
NodeWithPos,
|
|
||||||
} from '@tiptap/core'
|
} from '@tiptap/core'
|
||||||
import { MarkType } from '@tiptap/pm/model'
|
import { MarkType } from '@tiptap/pm/model'
|
||||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||||
import { find } from 'linkifyjs'
|
import { find } from 'linkifyjs'
|
||||||
|
|
||||||
type AutolinkOptions = {
|
type AutolinkOptions = {
|
||||||
type: MarkType
|
type: MarkType;
|
||||||
validate?: (url: string) => boolean
|
validate?: (url: string) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type LinkType = {
|
||||||
|
type: string;
|
||||||
|
value: string;
|
||||||
|
isLink: boolean;
|
||||||
|
href: string;
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function autolink(options: AutolinkOptions): Plugin {
|
export function autolink(options: AutolinkOptions): Plugin {
|
||||||
@ -37,40 +45,34 @@ export function autolink(options: AutolinkOptions): Plugin {
|
|||||||
node => node.isTextblock,
|
node => node.isTextblock,
|
||||||
)
|
)
|
||||||
|
|
||||||
let textBlock: NodeWithPos | undefined
|
// Grab the first node within the changed ranges (ex. the first of two paragraphs when hitting enter).
|
||||||
let textBeforeWhitespace: string | undefined
|
const textBlock = nodesInChangedRanges[0]
|
||||||
|
|
||||||
if (nodesInChangedRanges.length > 1) {
|
if (!textBlock) {
|
||||||
// Grab the first node within the changed ranges (ex. the first of two paragraphs when hitting enter).
|
return
|
||||||
textBlock = nodesInChangedRanges[0]
|
|
||||||
textBeforeWhitespace = newState.doc.textBetween(
|
|
||||||
textBlock.pos,
|
|
||||||
textBlock.pos + textBlock.node.nodeSize,
|
|
||||||
undefined,
|
|
||||||
' ',
|
|
||||||
)
|
|
||||||
} else if (
|
|
||||||
nodesInChangedRanges.length
|
|
||||||
// We want to make sure to include the block seperator argument to treat hard breaks like spaces.
|
|
||||||
&& newState.doc.textBetween(newRange.from, newRange.to, ' ', ' ').endsWith(' ')
|
|
||||||
) {
|
|
||||||
textBlock = nodesInChangedRanges[0]
|
|
||||||
textBeforeWhitespace = newState.doc.textBetween(
|
|
||||||
textBlock.pos,
|
|
||||||
newRange.to,
|
|
||||||
undefined,
|
|
||||||
' ',
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (textBlock && textBeforeWhitespace) {
|
const textBeforeWhitespace = newState.doc.textBetween(
|
||||||
const wordsBeforeWhitespace = textBeforeWhitespace.split(' ').filter(s => s !== '')
|
textBlock.pos,
|
||||||
|
newRange.to,
|
||||||
|
' ',
|
||||||
|
' ',
|
||||||
|
)
|
||||||
|
|
||||||
if (wordsBeforeWhitespace.length <= 0) {
|
if (textBeforeWhitespace) {
|
||||||
return false
|
|
||||||
|
if (!/\s*\S+\s+$/gm.test(textBeforeWhitespace)) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastWordBeforeSpace = wordsBeforeWhitespace[wordsBeforeWhitespace.length - 1]
|
const lastWordBeforeSpaceMatch = textBeforeWhitespace.matchAll(/\s*(\S+)\s+$/gm)
|
||||||
|
const lastMatch = Array.from(lastWordBeforeSpaceMatch).at(-1)
|
||||||
|
|
||||||
|
if (lastMatch === undefined || lastMatch.length < 2) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastWordBeforeSpace = lastMatch[1]
|
||||||
const lastWordAndBlockOffset = textBlock.pos + textBeforeWhitespace.lastIndexOf(lastWordBeforeSpace)
|
const lastWordAndBlockOffset = textBlock.pos + textBeforeWhitespace.lastIndexOf(lastWordBeforeSpace)
|
||||||
|
|
||||||
if (!lastWordBeforeSpace) {
|
if (!lastWordBeforeSpace) {
|
||||||
@ -78,22 +80,22 @@ export function autolink(options: AutolinkOptions): Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
find(lastWordBeforeSpace)
|
find(lastWordBeforeSpace)
|
||||||
.filter(link => link.isLink)
|
.filter((link: LinkType) => link.isLink)
|
||||||
// Calculate link position.
|
// Calculate link position.
|
||||||
.map(link => ({
|
.map((link: LinkType) => ({
|
||||||
...link,
|
...link,
|
||||||
from: lastWordAndBlockOffset + link.start + 1,
|
start: lastWordAndBlockOffset + link.start + 1,
|
||||||
to: lastWordAndBlockOffset + link.end + 1,
|
end: lastWordAndBlockOffset + link.end + 1,
|
||||||
}))
|
}))
|
||||||
// ignore link inside code mark
|
// ignore link inside code mark
|
||||||
.filter(link => {
|
.filter((link: LinkType) => {
|
||||||
if (!newState.schema.marks.code) {
|
if (!newState.schema.marks.code) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return !newState.doc.rangeHasMark(
|
return !newState.doc.rangeHasMark(
|
||||||
link.from,
|
link.start,
|
||||||
link.to,
|
link.end,
|
||||||
newState.schema.marks.code,
|
newState.schema.marks.code,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -106,13 +108,13 @@ export function autolink(options: AutolinkOptions): Plugin {
|
|||||||
})
|
})
|
||||||
// Add link mark.
|
// Add link mark.
|
||||||
.forEach(link => {
|
.forEach(link => {
|
||||||
if (getMarksBetween(link.from, link.to, newState.doc).some(item => item.mark.type === options.type)) {
|
if (getMarksBetween(link.start, link.end, newState.doc).some(item => item.mark.type === options.type)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tr.addMark(
|
tr.addMark(
|
||||||
link.from,
|
link.start,
|
||||||
link.to,
|
link.end,
|
||||||
options.type.create({
|
options.type.create({
|
||||||
href: link.href,
|
href: link.href,
|
||||||
}),
|
}),
|
||||||
|
Loading…
Reference in New Issue
Block a user