2022-09-09 11:53:35 +00:00
|
|
|
import { Plugin, PluginKey, TextSelection, Transaction } from 'prosemirror-state'
|
|
|
|
import type { EditorView } from 'prosemirror-view'
|
2022-10-09 00:00:13 +00:00
|
|
|
import type { Mark, Node, Schema } from 'prosemirror-model'
|
|
|
|
import type { ProseMirrorExtension } from '../helpers'
|
2022-09-09 11:53:35 +00:00
|
|
|
|
|
|
|
const REGEX = /(^|\s)\[(.+)]\(([^ ]+)(?: "(.+)")?\)/
|
|
|
|
|
|
|
|
const findMarkPosition = (mark: Mark, doc: Node, from: number, to: number) => {
|
|
|
|
let markPos = { from: -1, to: -1 }
|
|
|
|
doc.nodesBetween(from, to, (node, pos) => {
|
|
|
|
if (markPos.from > -1) return false
|
|
|
|
if (markPos.from === -1 && mark.isInSet(node.marks)) {
|
|
|
|
markPos = { from: pos, to: pos + Math.max(node.textContent.length, 1) }
|
|
|
|
}
|
|
|
|
})
|
2022-10-09 00:00:13 +00:00
|
|
|
|
2022-09-09 11:53:35 +00:00
|
|
|
return markPos
|
|
|
|
}
|
|
|
|
|
|
|
|
const pluginKey = new PluginKey('markdown-links')
|
|
|
|
|
2022-10-09 00:00:13 +00:00
|
|
|
const markdownLinks = (schema: Schema) =>
|
|
|
|
new Plugin({
|
|
|
|
key: pluginKey,
|
|
|
|
state: {
|
|
|
|
init() {
|
|
|
|
return { schema }
|
|
|
|
},
|
|
|
|
apply(tr, state) {
|
|
|
|
const action = tr.getMeta(this)
|
|
|
|
if (action?.pos) {
|
|
|
|
(state as any).pos = action.pos
|
|
|
|
}
|
|
|
|
return state
|
|
|
|
}
|
|
|
|
},
|
|
|
|
props: {
|
|
|
|
handleDOMEvents: {
|
|
|
|
keyup: (view) => {
|
|
|
|
return handleMove(view)
|
|
|
|
},
|
|
|
|
click: (view, e) => {
|
|
|
|
if (handleMove(view)) {
|
|
|
|
e.preventDefault()
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
const resolvePos = (view: EditorView, pos: number) => {
|
|
|
|
try {
|
|
|
|
return view.state.doc.resolve(pos)
|
|
|
|
} catch {
|
|
|
|
// ignore
|
|
|
|
}
|
|
|
|
}
|
2022-09-09 11:53:35 +00:00
|
|
|
|
|
|
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
|
|
const toLink = (view: EditorView, tr: Transaction) => {
|
|
|
|
const sel = view.state.selection
|
|
|
|
const state = pluginKey.getState(view.state)
|
|
|
|
const lastPos = state.pos
|
|
|
|
|
|
|
|
if (lastPos !== undefined) {
|
|
|
|
const $from = resolvePos(view, lastPos)
|
|
|
|
if (!$from || $from.depth === 0 || $from.parent.type.spec.code) {
|
|
|
|
return false
|
|
|
|
}
|
2022-10-09 00:00:13 +00:00
|
|
|
|
2022-09-09 11:53:35 +00:00
|
|
|
const lineFrom = $from.before()
|
|
|
|
const lineTo = $from.after()
|
2022-10-09 00:00:13 +00:00
|
|
|
|
2022-09-09 11:53:35 +00:00
|
|
|
const line = view.state.doc.textBetween(lineFrom, lineTo, '\0', '\0')
|
|
|
|
const match = REGEX.exec(line)
|
2022-10-09 00:00:13 +00:00
|
|
|
|
2022-09-09 11:53:35 +00:00
|
|
|
if (match) {
|
|
|
|
const [full, , text, href] = match
|
|
|
|
const spaceLeft = full.indexOf(text) - 1
|
|
|
|
const spaceRight = full.length - text.length - href.length - spaceLeft - 4
|
|
|
|
const start = match.index + $from.start() + spaceLeft
|
|
|
|
const end = start + full.length - spaceLeft - spaceRight
|
2022-10-09 00:00:13 +00:00
|
|
|
|
2022-09-09 11:53:35 +00:00
|
|
|
if (sel.$from.pos >= start && sel.$from.pos <= end) {
|
|
|
|
return false
|
|
|
|
}
|
2022-10-09 00:00:13 +00:00
|
|
|
|
2022-09-09 11:53:35 +00:00
|
|
|
// Do not convert md links if content has marks
|
|
|
|
const $startPos = resolvePos(view, start)
|
2022-10-09 00:00:13 +00:00
|
|
|
if ($startPos.marks().length > 0) {
|
2022-09-09 11:53:35 +00:00
|
|
|
return false
|
|
|
|
}
|
2022-10-09 00:00:13 +00:00
|
|
|
|
2022-09-09 11:53:35 +00:00
|
|
|
const textStart = start + 1
|
|
|
|
const textEnd = textStart + text.length
|
2022-10-09 00:00:13 +00:00
|
|
|
|
2022-09-09 11:53:35 +00:00
|
|
|
if (textEnd < end) tr.delete(textEnd, end)
|
|
|
|
if (textStart > start) tr.delete(start, textStart)
|
2022-10-09 00:00:13 +00:00
|
|
|
|
2022-09-09 11:53:35 +00:00
|
|
|
const to = start + text.length
|
|
|
|
tr.addMark(start, to, state.schema.marks.link.create({ href }))
|
2022-10-09 00:00:13 +00:00
|
|
|
|
2022-09-09 11:53:35 +00:00
|
|
|
const sub = end - textEnd + textStart - start
|
|
|
|
tr.setMeta(pluginKey, { pos: sel.$head.pos - sub })
|
2022-10-09 00:00:13 +00:00
|
|
|
|
2022-09-09 11:53:35 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
const toMarkdown = (view: EditorView, tr: Transaction) => {
|
|
|
|
const { schema } = pluginKey.getState(view.state)
|
|
|
|
const sel = view.state.selection
|
2022-10-09 00:00:13 +00:00
|
|
|
if (sel.$head.depth === 0 || sel.$head.parent.type.spec.code) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2022-09-09 11:53:35 +00:00
|
|
|
const mark = schema.marks.link.isInSet(sel.$head.marks())
|
|
|
|
const textFrom = sel.$head.pos - sel.$head.textOffset
|
|
|
|
const textTo = sel.$head.after()
|
|
|
|
|
|
|
|
if (mark) {
|
|
|
|
const { href } = mark.attrs
|
|
|
|
const range = findMarkPosition(mark, view.state.doc, textFrom, textTo)
|
|
|
|
const text = view.state.doc.textBetween(range.from, range.to, '\0', '\0')
|
|
|
|
tr.replaceRangeWith(range.from, range.to, view.state.schema.text(`[${text}](${href})`))
|
|
|
|
tr.setSelection(new TextSelection(tr.doc.resolve(sel.$head.pos + 1)))
|
|
|
|
tr.setMeta(pluginKey, { pos: sel.$head.pos })
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
const handleMove = (view: EditorView) => {
|
|
|
|
const sel = view.state.selection
|
|
|
|
if (!sel.empty || !sel.$head) return false
|
|
|
|
const pos = sel.$head.pos
|
|
|
|
const tr = view.state.tr
|
2022-10-09 00:00:13 +00:00
|
|
|
|
2022-09-09 11:53:35 +00:00
|
|
|
if (toLink(view, tr)) {
|
|
|
|
view.dispatch(tr)
|
|
|
|
return true
|
|
|
|
}
|
2022-10-09 00:00:13 +00:00
|
|
|
|
2022-09-09 11:53:35 +00:00
|
|
|
if (toMarkdown(view, tr)) {
|
|
|
|
view.dispatch(tr)
|
|
|
|
return true
|
|
|
|
}
|
2022-10-09 00:00:13 +00:00
|
|
|
|
2022-09-09 11:53:35 +00:00
|
|
|
tr.setMeta(pluginKey, { pos })
|
|
|
|
view.dispatch(tr)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
export default (): ProseMirrorExtension => ({
|
|
|
|
plugins: (prev, schema) => [...prev, markdownLinks(schema)]
|
|
|
|
})
|