gdpr audit implemented, email log, vollmachten, pdf delete cancel data privacy and vollmachten, removed message no id card in engergy car, and other contracts that are not telecom contracts, added insert counter for engery
This commit is contained in:
+159
@@ -0,0 +1,159 @@
|
||||
import type { NodeWithPos } from '@tiptap/core'
|
||||
import { combineTransactionSteps, findChildrenInRange, getChangedRanges, getMarksBetween } from '@tiptap/core'
|
||||
import type { MarkType } from '@tiptap/pm/model'
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
import type { MultiToken } from 'linkifyjs'
|
||||
import { tokenize } from 'linkifyjs'
|
||||
|
||||
import { UNICODE_WHITESPACE_REGEX, UNICODE_WHITESPACE_REGEX_END } from './whitespace.js'
|
||||
|
||||
/**
|
||||
* Check if the provided tokens form a valid link structure, which can either be a single link token
|
||||
* or a link token surrounded by parentheses or square brackets.
|
||||
*
|
||||
* This ensures that only complete and valid text is hyperlinked, preventing cases where a valid
|
||||
* top-level domain (TLD) is immediately followed by an invalid character, like a number. For
|
||||
* example, with the `find` method from Linkify, entering `example.com1` would result in
|
||||
* `example.com` being linked and the trailing `1` left as plain text. By using the `tokenize`
|
||||
* method, we can perform more comprehensive validation on the input text.
|
||||
*/
|
||||
function isValidLinkStructure(tokens: Array<ReturnType<MultiToken['toObject']>>) {
|
||||
if (tokens.length === 1) {
|
||||
return tokens[0].isLink
|
||||
}
|
||||
|
||||
if (tokens.length === 3 && tokens[1].isLink) {
|
||||
return ['()', '[]'].includes(tokens[0].value + tokens[2].value)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type AutolinkOptions = {
|
||||
type: MarkType
|
||||
defaultProtocol: string
|
||||
validate: (url: string) => boolean
|
||||
shouldAutoLink: (url: string) => boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* This plugin allows you to automatically add links to your editor.
|
||||
* @param options The plugin options
|
||||
* @returns The plugin instance
|
||||
*/
|
||||
export function autolink(options: AutolinkOptions): Plugin {
|
||||
return new Plugin({
|
||||
key: new PluginKey('autolink'),
|
||||
appendTransaction: (transactions, oldState, newState) => {
|
||||
/**
|
||||
* Does the transaction change the document?
|
||||
*/
|
||||
const docChanges = transactions.some(transaction => transaction.docChanged) && !oldState.doc.eq(newState.doc)
|
||||
|
||||
/**
|
||||
* Prevent autolink if the transaction is not a document change or if the transaction has the meta `preventAutolink`.
|
||||
*/
|
||||
const preventAutolink = transactions.some(transaction => transaction.getMeta('preventAutolink'))
|
||||
|
||||
/**
|
||||
* Prevent autolink if the transaction is not a document change
|
||||
* or if the transaction has the meta `preventAutolink`.
|
||||
*/
|
||||
if (!docChanges || preventAutolink) {
|
||||
return
|
||||
}
|
||||
|
||||
const { tr } = newState
|
||||
const transform = combineTransactionSteps(oldState.doc, [...transactions])
|
||||
const changes = getChangedRanges(transform)
|
||||
|
||||
changes.forEach(({ newRange }) => {
|
||||
// Now let’s see if we can add new links.
|
||||
const nodesInChangedRanges = findChildrenInRange(newState.doc, newRange, node => node.isTextblock)
|
||||
|
||||
let textBlock: NodeWithPos | undefined
|
||||
let textBeforeWhitespace: string | undefined
|
||||
|
||||
if (nodesInChangedRanges.length > 1) {
|
||||
// Grab the first node within the changed ranges (ex. the first of two paragraphs when hitting enter).
|
||||
textBlock = nodesInChangedRanges[0]
|
||||
textBeforeWhitespace = newState.doc.textBetween(
|
||||
textBlock.pos,
|
||||
textBlock.pos + textBlock.node.nodeSize,
|
||||
undefined,
|
||||
' ',
|
||||
)
|
||||
} else if (nodesInChangedRanges.length) {
|
||||
const endText = newState.doc.textBetween(newRange.from, newRange.to, ' ', ' ')
|
||||
if (!UNICODE_WHITESPACE_REGEX_END.test(endText)) {
|
||||
return
|
||||
}
|
||||
textBlock = nodesInChangedRanges[0]
|
||||
textBeforeWhitespace = newState.doc.textBetween(textBlock.pos, newRange.to, undefined, ' ')
|
||||
}
|
||||
|
||||
if (textBlock && textBeforeWhitespace) {
|
||||
const wordsBeforeWhitespace = textBeforeWhitespace.split(UNICODE_WHITESPACE_REGEX).filter(Boolean)
|
||||
|
||||
if (wordsBeforeWhitespace.length <= 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
const lastWordBeforeSpace = wordsBeforeWhitespace[wordsBeforeWhitespace.length - 1]
|
||||
const lastWordAndBlockOffset = textBlock.pos + textBeforeWhitespace.lastIndexOf(lastWordBeforeSpace)
|
||||
|
||||
if (!lastWordBeforeSpace) {
|
||||
return false
|
||||
}
|
||||
|
||||
const linksBeforeSpace = tokenize(lastWordBeforeSpace).map(t => t.toObject(options.defaultProtocol))
|
||||
|
||||
if (!isValidLinkStructure(linksBeforeSpace)) {
|
||||
return false
|
||||
}
|
||||
|
||||
linksBeforeSpace
|
||||
.filter(link => link.isLink)
|
||||
// Calculate link position.
|
||||
.map(link => ({
|
||||
...link,
|
||||
from: lastWordAndBlockOffset + link.start + 1,
|
||||
to: lastWordAndBlockOffset + link.end + 1,
|
||||
}))
|
||||
// ignore link inside code mark
|
||||
.filter(link => {
|
||||
if (!newState.schema.marks.code) {
|
||||
return true
|
||||
}
|
||||
|
||||
return !newState.doc.rangeHasMark(link.from, link.to, newState.schema.marks.code)
|
||||
})
|
||||
// validate link
|
||||
.filter(link => options.validate(link.value))
|
||||
// check whether should autolink
|
||||
.filter(link => options.shouldAutoLink(link.value))
|
||||
// Add link mark.
|
||||
.forEach(link => {
|
||||
if (getMarksBetween(link.from, link.to, newState.doc).some(item => item.mark.type === options.type)) {
|
||||
return
|
||||
}
|
||||
|
||||
tr.addMark(
|
||||
link.from,
|
||||
link.to,
|
||||
options.type.create({
|
||||
href: link.href,
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if (!tr.steps.length) {
|
||||
return
|
||||
}
|
||||
|
||||
return tr
|
||||
},
|
||||
})
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import type { Editor } from '@tiptap/core'
|
||||
import { getAttributes } from '@tiptap/core'
|
||||
import type { MarkType } from '@tiptap/pm/model'
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
|
||||
type ClickHandlerOptions = {
|
||||
type: MarkType
|
||||
editor: Editor
|
||||
openOnClick?: boolean
|
||||
enableClickSelection?: boolean
|
||||
}
|
||||
|
||||
export function clickHandler(options: ClickHandlerOptions): Plugin {
|
||||
return new Plugin({
|
||||
key: new PluginKey('handleClickLink'),
|
||||
props: {
|
||||
handleClick: (view, pos, event) => {
|
||||
if (event.button !== 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!view.editable) {
|
||||
return false
|
||||
}
|
||||
|
||||
let link: HTMLAnchorElement | null = null
|
||||
|
||||
if (event.target instanceof HTMLAnchorElement) {
|
||||
link = event.target
|
||||
} else {
|
||||
const target = event.target as HTMLElement | null
|
||||
if (!target) {
|
||||
return false
|
||||
}
|
||||
|
||||
const root = options.editor.view.dom
|
||||
|
||||
// Tntentionally limit the lookup to the editor root.
|
||||
// Using tag names like DIV as boundaries breaks with custom NodeViews,
|
||||
link = target.closest<HTMLAnchorElement>('a')
|
||||
|
||||
if (link && !root.contains(link)) {
|
||||
link = null
|
||||
}
|
||||
}
|
||||
|
||||
if (!link) {
|
||||
return false
|
||||
}
|
||||
|
||||
let handled = false
|
||||
|
||||
if (options.enableClickSelection) {
|
||||
const commandResult = options.editor.commands.extendMarkRange(options.type.name)
|
||||
handled = commandResult
|
||||
}
|
||||
|
||||
if (options.openOnClick) {
|
||||
const attrs = getAttributes(view.state, options.type.name)
|
||||
const href = link.href ?? attrs.href
|
||||
const target = link.target ?? attrs.target
|
||||
|
||||
if (href) {
|
||||
window.open(href, target)
|
||||
handled = true
|
||||
}
|
||||
}
|
||||
|
||||
return handled
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import type { Editor } from '@tiptap/core'
|
||||
import type { MarkType } from '@tiptap/pm/model'
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
import { find } from 'linkifyjs'
|
||||
|
||||
import type { LinkOptions } from '../link.js'
|
||||
|
||||
type PasteHandlerOptions = {
|
||||
editor: Editor
|
||||
defaultProtocol: string
|
||||
type: MarkType
|
||||
shouldAutoLink?: LinkOptions['shouldAutoLink']
|
||||
}
|
||||
|
||||
export function pasteHandler(options: PasteHandlerOptions): Plugin {
|
||||
return new Plugin({
|
||||
key: new PluginKey('handlePasteLink'),
|
||||
props: {
|
||||
handlePaste: (view, _event, slice) => {
|
||||
const { shouldAutoLink } = options
|
||||
const { state } = view
|
||||
const { selection } = state
|
||||
const { empty } = selection
|
||||
|
||||
if (empty) {
|
||||
return false
|
||||
}
|
||||
|
||||
let textContent = ''
|
||||
|
||||
slice.content.forEach(node => {
|
||||
textContent += node.textContent
|
||||
})
|
||||
|
||||
const link = find(textContent, { defaultProtocol: options.defaultProtocol }).find(
|
||||
item => item.isLink && item.value === textContent,
|
||||
)
|
||||
|
||||
if (!textContent || !link || (shouldAutoLink !== undefined && !shouldAutoLink(link.value))) {
|
||||
return false
|
||||
}
|
||||
|
||||
return options.editor.commands.setMark(options.type, {
|
||||
href: link.href,
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// From DOMPurify
|
||||
// https://github.com/cure53/DOMPurify/blob/main/src/regexp.ts
|
||||
export const UNICODE_WHITESPACE_PATTERN = '[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]'
|
||||
|
||||
export const UNICODE_WHITESPACE_REGEX = new RegExp(UNICODE_WHITESPACE_PATTERN)
|
||||
export const UNICODE_WHITESPACE_REGEX_END = new RegExp(`${UNICODE_WHITESPACE_PATTERN}$`)
|
||||
export const UNICODE_WHITESPACE_REGEX_GLOBAL = new RegExp(UNICODE_WHITESPACE_PATTERN, 'g')
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { Link } from './link.js'
|
||||
|
||||
export * from './link.js'
|
||||
|
||||
export default Link
|
||||
+489
@@ -0,0 +1,489 @@
|
||||
import type { PasteRuleMatch } from '@tiptap/core'
|
||||
import { Mark, markPasteRule, mergeAttributes } from '@tiptap/core'
|
||||
import type { Plugin } from '@tiptap/pm/state'
|
||||
import { find, registerCustomProtocol, reset } from 'linkifyjs'
|
||||
|
||||
import { autolink } from './helpers/autolink.js'
|
||||
import { clickHandler } from './helpers/clickHandler.js'
|
||||
import { pasteHandler } from './helpers/pasteHandler.js'
|
||||
import { UNICODE_WHITESPACE_REGEX_GLOBAL } from './helpers/whitespace.js'
|
||||
|
||||
export interface LinkProtocolOptions {
|
||||
/**
|
||||
* The protocol scheme to be registered.
|
||||
* @default '''
|
||||
* @example 'ftp'
|
||||
* @example 'git'
|
||||
*/
|
||||
scheme: string
|
||||
|
||||
/**
|
||||
* If enabled, it allows optional slashes after the protocol.
|
||||
* @default false
|
||||
* @example true
|
||||
*/
|
||||
optionalSlashes?: boolean
|
||||
}
|
||||
|
||||
export const pasteRegex =
|
||||
/https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z]{2,}\b(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)/gi
|
||||
|
||||
/**
|
||||
* @deprecated The default behavior is now to open links when the editor is not editable.
|
||||
*/
|
||||
type DeprecatedOpenWhenNotEditable = 'whenNotEditable'
|
||||
|
||||
export interface LinkOptions {
|
||||
/**
|
||||
* If enabled, the extension will automatically add links as you type.
|
||||
* @default true
|
||||
* @example false
|
||||
*/
|
||||
autolink: boolean
|
||||
|
||||
/**
|
||||
* An array of custom protocols to be registered with linkifyjs.
|
||||
* @default []
|
||||
* @example ['ftp', 'git']
|
||||
*/
|
||||
protocols: Array<LinkProtocolOptions | string>
|
||||
|
||||
/**
|
||||
* Default protocol to use when no protocol is specified.
|
||||
* @default 'http'
|
||||
*/
|
||||
defaultProtocol: string
|
||||
/**
|
||||
* If enabled, links will be opened on click.
|
||||
* @default true
|
||||
* @example false
|
||||
*/
|
||||
openOnClick: boolean | DeprecatedOpenWhenNotEditable
|
||||
/**
|
||||
* If enabled, the link will be selected when clicked.
|
||||
* @default false
|
||||
* @example true
|
||||
*/
|
||||
enableClickSelection: boolean
|
||||
/**
|
||||
* Adds a link to the current selection if the pasted content only contains an url.
|
||||
* @default true
|
||||
* @example false
|
||||
*/
|
||||
linkOnPaste: boolean
|
||||
|
||||
/**
|
||||
* HTML attributes to add to the link element.
|
||||
* @default {}
|
||||
* @example { class: 'foo' }
|
||||
*/
|
||||
HTMLAttributes: Record<string, any>
|
||||
|
||||
/**
|
||||
* @deprecated Use the `shouldAutoLink` option instead.
|
||||
* A validation function that modifies link verification for the auto linker.
|
||||
* @param url - The url to be validated.
|
||||
* @returns - True if the url is valid, false otherwise.
|
||||
*/
|
||||
validate: (url: string) => boolean
|
||||
|
||||
/**
|
||||
* A validation function which is used for configuring link verification for preventing XSS attacks.
|
||||
* Only modify this if you know what you're doing.
|
||||
*
|
||||
* @returns {boolean} `true` if the URL is valid, `false` otherwise.
|
||||
*
|
||||
* @example
|
||||
* isAllowedUri: (url, { defaultValidate, protocols, defaultProtocol }) => {
|
||||
* return url.startsWith('./') || defaultValidate(url)
|
||||
* }
|
||||
*/
|
||||
isAllowedUri: (
|
||||
/**
|
||||
* The URL to be validated.
|
||||
*/
|
||||
url: string,
|
||||
ctx: {
|
||||
/**
|
||||
* The default validation function.
|
||||
*/
|
||||
defaultValidate: (url: string) => boolean
|
||||
/**
|
||||
* An array of allowed protocols for the URL (e.g., "http", "https"). As defined in the `protocols` option.
|
||||
*/
|
||||
protocols: Array<LinkProtocolOptions | string>
|
||||
/**
|
||||
* A string that represents the default protocol (e.g., 'http'). As defined in the `defaultProtocol` option.
|
||||
*/
|
||||
defaultProtocol: string
|
||||
},
|
||||
) => boolean
|
||||
|
||||
/**
|
||||
* Determines whether a valid link should be automatically linked in the content.
|
||||
*
|
||||
* @param {string} url - The URL that has already been validated.
|
||||
* @returns {boolean} - True if the link should be auto-linked; false if it should not be auto-linked.
|
||||
*/
|
||||
shouldAutoLink: (url: string) => boolean
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
link: {
|
||||
/**
|
||||
* Set a link mark
|
||||
* @param attributes The link attributes
|
||||
* @example editor.commands.setLink({ href: 'https://tiptap.dev' })
|
||||
*/
|
||||
setLink: (attributes: {
|
||||
href: string
|
||||
target?: string | null
|
||||
rel?: string | null
|
||||
class?: string | null
|
||||
title?: string | null
|
||||
}) => ReturnType
|
||||
/**
|
||||
* Toggle a link mark
|
||||
* @param attributes The link attributes
|
||||
* @example editor.commands.toggleLink({ href: 'https://tiptap.dev' })
|
||||
*/
|
||||
toggleLink: (attributes?: {
|
||||
href: string
|
||||
target?: string | null
|
||||
rel?: string | null
|
||||
class?: string | null
|
||||
title?: string | null
|
||||
}) => ReturnType
|
||||
/**
|
||||
* Unset a link mark
|
||||
* @example editor.commands.unsetLink()
|
||||
*/
|
||||
unsetLink: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isAllowedUri(uri: string | undefined, protocols?: LinkOptions['protocols']) {
|
||||
const allowedProtocols: string[] = ['http', 'https', 'ftp', 'ftps', 'mailto', 'tel', 'callto', 'sms', 'cid', 'xmpp']
|
||||
|
||||
if (protocols) {
|
||||
protocols.forEach(protocol => {
|
||||
const nextProtocol = typeof protocol === 'string' ? protocol : protocol.scheme
|
||||
|
||||
if (nextProtocol) {
|
||||
allowedProtocols.push(nextProtocol)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
!uri ||
|
||||
uri.replace(UNICODE_WHITESPACE_REGEX_GLOBAL, '').match(
|
||||
new RegExp(
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
`^(?:(?:${allowedProtocols.join('|')}):|[^a-z]|[a-z0-9+.\-]+(?:[^a-z+.\-:]|$))`,
|
||||
'i',
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* This extension allows you to create links.
|
||||
* @see https://www.tiptap.dev/api/marks/link
|
||||
*/
|
||||
export const Link = Mark.create<LinkOptions>({
|
||||
name: 'link',
|
||||
|
||||
priority: 1000,
|
||||
|
||||
keepOnSplit: false,
|
||||
|
||||
exitable: true,
|
||||
|
||||
onCreate() {
|
||||
// TODO: v4 - remove validate option
|
||||
if (this.options.validate && !this.options.shouldAutoLink) {
|
||||
// Copy the validate function to the shouldAutoLink option
|
||||
this.options.shouldAutoLink = this.options.validate
|
||||
console.warn('The `validate` option is deprecated. Rename to the `shouldAutoLink` option instead.')
|
||||
}
|
||||
this.options.protocols.forEach(protocol => {
|
||||
if (typeof protocol === 'string') {
|
||||
registerCustomProtocol(protocol)
|
||||
return
|
||||
}
|
||||
registerCustomProtocol(protocol.scheme, protocol.optionalSlashes)
|
||||
})
|
||||
},
|
||||
|
||||
onDestroy() {
|
||||
reset()
|
||||
},
|
||||
|
||||
inclusive() {
|
||||
return this.options.autolink
|
||||
},
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
openOnClick: true,
|
||||
enableClickSelection: false,
|
||||
linkOnPaste: true,
|
||||
autolink: true,
|
||||
protocols: [],
|
||||
defaultProtocol: 'http',
|
||||
HTMLAttributes: {
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer nofollow',
|
||||
class: null,
|
||||
},
|
||||
isAllowedUri: (url, ctx) => !!isAllowedUri(url, ctx.protocols),
|
||||
validate: url => !!url,
|
||||
shouldAutoLink: url => {
|
||||
// URLs with explicit protocols (e.g., https://) should be auto-linked
|
||||
// But not if @ appears before :// (that would be userinfo like user:pass@host)
|
||||
const hasProtocol = /^[a-z][a-z0-9+.-]*:\/\//i.test(url)
|
||||
const hasMaybeProtocol = /^[a-z][a-z0-9+.-]*:/i.test(url)
|
||||
|
||||
if (hasProtocol || (hasMaybeProtocol && !url.includes('@'))) {
|
||||
return true
|
||||
}
|
||||
// Strip userinfo (user:pass@) if present, then extract hostname
|
||||
const urlWithoutUserinfo = url.includes('@') ? url.split('@').pop()! : url
|
||||
const hostname = urlWithoutUserinfo.split(/[/?#:]/)[0]
|
||||
|
||||
// Don't auto-link IP addresses without protocol
|
||||
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) {
|
||||
return false
|
||||
}
|
||||
// Don't auto-link single-word hostnames without TLD (e.g., "localhost")
|
||||
if (!/\./.test(hostname)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
href: {
|
||||
default: null,
|
||||
parseHTML(element) {
|
||||
return element.getAttribute('href')
|
||||
},
|
||||
},
|
||||
target: {
|
||||
default: this.options.HTMLAttributes.target,
|
||||
},
|
||||
rel: {
|
||||
default: this.options.HTMLAttributes.rel,
|
||||
},
|
||||
class: {
|
||||
default: this.options.HTMLAttributes.class,
|
||||
},
|
||||
title: {
|
||||
default: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'a[href]',
|
||||
getAttrs: dom => {
|
||||
const href = (dom as HTMLElement).getAttribute('href')
|
||||
|
||||
// prevent XSS attacks
|
||||
if (
|
||||
!href ||
|
||||
!this.options.isAllowedUri(href, {
|
||||
defaultValidate: url => !!isAllowedUri(url, this.options.protocols),
|
||||
protocols: this.options.protocols,
|
||||
defaultProtocol: this.options.defaultProtocol,
|
||||
})
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return null
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
// prevent XSS attacks
|
||||
if (
|
||||
!this.options.isAllowedUri(HTMLAttributes.href, {
|
||||
defaultValidate: href => !!isAllowedUri(href, this.options.protocols),
|
||||
protocols: this.options.protocols,
|
||||
defaultProtocol: this.options.defaultProtocol,
|
||||
})
|
||||
) {
|
||||
// strip out the href
|
||||
return ['a', mergeAttributes(this.options.HTMLAttributes, { ...HTMLAttributes, href: '' }), 0]
|
||||
}
|
||||
|
||||
return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
markdownTokenName: 'link',
|
||||
|
||||
parseMarkdown: (token, helpers) => {
|
||||
return helpers.applyMark('link', helpers.parseInline(token.tokens || []), {
|
||||
href: token.href,
|
||||
title: token.title || null,
|
||||
})
|
||||
},
|
||||
|
||||
renderMarkdown: (node, h) => {
|
||||
const href = node.attrs?.href ?? ''
|
||||
const title = node.attrs?.title ?? ''
|
||||
const text = h.renderChildren(node)
|
||||
|
||||
return title ? `[${text}](${href} "${title}")` : `[${text}](${href})`
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setLink:
|
||||
attributes =>
|
||||
({ chain }) => {
|
||||
const { href } = attributes
|
||||
|
||||
if (
|
||||
!this.options.isAllowedUri(href, {
|
||||
defaultValidate: url => !!isAllowedUri(url, this.options.protocols),
|
||||
protocols: this.options.protocols,
|
||||
defaultProtocol: this.options.defaultProtocol,
|
||||
})
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return chain().setMark(this.name, attributes).setMeta('preventAutolink', true).run()
|
||||
},
|
||||
|
||||
toggleLink:
|
||||
attributes =>
|
||||
({ chain }) => {
|
||||
const { href } = attributes || {}
|
||||
|
||||
if (
|
||||
href &&
|
||||
!this.options.isAllowedUri(href, {
|
||||
defaultValidate: url => !!isAllowedUri(url, this.options.protocols),
|
||||
protocols: this.options.protocols,
|
||||
defaultProtocol: this.options.defaultProtocol,
|
||||
})
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return chain()
|
||||
.toggleMark(this.name, attributes, { extendEmptyMarkRange: true })
|
||||
.setMeta('preventAutolink', true)
|
||||
.run()
|
||||
},
|
||||
|
||||
unsetLink:
|
||||
() =>
|
||||
({ chain }) => {
|
||||
return chain().unsetMark(this.name, { extendEmptyMarkRange: true }).setMeta('preventAutolink', true).run()
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
addPasteRules() {
|
||||
return [
|
||||
markPasteRule({
|
||||
find: text => {
|
||||
const foundLinks: PasteRuleMatch[] = []
|
||||
|
||||
if (text) {
|
||||
const { protocols, defaultProtocol } = this.options
|
||||
const links = find(text).filter(
|
||||
item =>
|
||||
item.isLink &&
|
||||
this.options.isAllowedUri(item.value, {
|
||||
defaultValidate: href => !!isAllowedUri(href, protocols),
|
||||
protocols,
|
||||
defaultProtocol,
|
||||
}),
|
||||
)
|
||||
|
||||
if (links.length) {
|
||||
links.forEach(link => {
|
||||
if (!this.options.shouldAutoLink(link.value)) {
|
||||
return
|
||||
}
|
||||
|
||||
foundLinks.push({
|
||||
text: link.value,
|
||||
data: {
|
||||
href: link.href,
|
||||
},
|
||||
index: link.start,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return foundLinks
|
||||
},
|
||||
type: this.type,
|
||||
getAttributes: match => {
|
||||
return {
|
||||
href: match.data?.href,
|
||||
}
|
||||
},
|
||||
}),
|
||||
]
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const plugins: Plugin[] = []
|
||||
const { protocols, defaultProtocol } = this.options
|
||||
|
||||
if (this.options.autolink) {
|
||||
plugins.push(
|
||||
autolink({
|
||||
type: this.type,
|
||||
defaultProtocol: this.options.defaultProtocol,
|
||||
validate: url =>
|
||||
this.options.isAllowedUri(url, {
|
||||
defaultValidate: href => !!isAllowedUri(href, protocols),
|
||||
protocols,
|
||||
defaultProtocol,
|
||||
}),
|
||||
shouldAutoLink: this.options.shouldAutoLink,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
plugins.push(
|
||||
clickHandler({
|
||||
type: this.type,
|
||||
editor: this.editor,
|
||||
openOnClick: this.options.openOnClick === 'whenNotEditable' ? true : this.options.openOnClick,
|
||||
enableClickSelection: this.options.enableClickSelection,
|
||||
}),
|
||||
)
|
||||
|
||||
if (this.options.linkOnPaste) {
|
||||
plugins.push(
|
||||
pasteHandler({
|
||||
editor: this.editor,
|
||||
defaultProtocol: this.options.defaultProtocol,
|
||||
type: this.type,
|
||||
shouldAutoLink: this.options.shouldAutoLink,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return plugins
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user