mirror of
https://github.com/ueberdosis/tiptap.git
synced 2024-12-21 07:18:05 +08:00
723b955cec
* refactoring * improve link regex * WIP: add new markPasteRule und linkify to image mark * move copy of inputrule to core * trigger codeblock inputrule on enter * refactoring * add regex match to markpasterulematch * refactoring * improve link regex * WIP: add new markPasteRule und linkify to image mark * move copy of inputrule to core * trigger codeblock inputrule on enter * refactoring * add regex match to markpasterulematch * update linkify * wip * wip * log * wip * remove debug code * wip * wip * wip * wip * wip * wip * wip * wip * rename matcher * add data to ExtendedRegExpMatchArray * remove logging * add code option to marks, prevent inputrules in code mark * remove link regex * fix codeblock inputrule on enter * refactoring * refactoring * refactoring * refactoring * fix position bug * add test * export InputRule and PasteRule * clean up link demo * fix types
178 lines
3.9 KiB
TypeScript
178 lines
3.9 KiB
TypeScript
import { EditorState, Plugin } from 'prosemirror-state'
|
||
import createChainableState from './helpers/createChainableState'
|
||
import isRegExp from './utilities/isRegExp'
|
||
import { Range, ExtendedRegExpMatchArray } from './types'
|
||
|
||
export type PasteRuleMatch = {
|
||
index: number,
|
||
text: string,
|
||
replaceWith?: string,
|
||
match?: RegExpMatchArray,
|
||
data?: Record<string, any>,
|
||
}
|
||
|
||
export type PasteRuleFinder =
|
||
| RegExp
|
||
| ((text: string) => PasteRuleMatch[] | null | undefined)
|
||
|
||
export class PasteRule {
|
||
find: PasteRuleFinder
|
||
|
||
handler: (props: {
|
||
state: EditorState,
|
||
range: Range,
|
||
match: ExtendedRegExpMatchArray,
|
||
}) => void
|
||
|
||
constructor(config: {
|
||
find: PasteRuleFinder,
|
||
handler: (props: {
|
||
state: EditorState,
|
||
range: Range,
|
||
match: ExtendedRegExpMatchArray,
|
||
}) => void,
|
||
}) {
|
||
this.find = config.find
|
||
this.handler = config.handler
|
||
}
|
||
}
|
||
|
||
const pasteRuleMatcherHandler = (text: string, find: PasteRuleFinder): ExtendedRegExpMatchArray[] => {
|
||
if (isRegExp(find)) {
|
||
return [...text.matchAll(find)]
|
||
}
|
||
|
||
const matches = find(text)
|
||
|
||
if (!matches) {
|
||
return []
|
||
}
|
||
|
||
return matches.map(pasteRuleMatch => {
|
||
const result: ExtendedRegExpMatchArray = []
|
||
|
||
result.push(pasteRuleMatch.text)
|
||
result.index = pasteRuleMatch.index
|
||
result.input = text
|
||
result.data = pasteRuleMatch.data
|
||
|
||
if (pasteRuleMatch.replaceWith) {
|
||
if (!pasteRuleMatch.text.includes(pasteRuleMatch.replaceWith)) {
|
||
console.warn('[tiptap warn]: "pasteRuleMatch.replaceWith" must be part of "pasteRuleMatch.text".')
|
||
}
|
||
|
||
result.push(pasteRuleMatch.replaceWith)
|
||
}
|
||
|
||
return result
|
||
})
|
||
}
|
||
|
||
function run(config: {
|
||
state: EditorState,
|
||
from: number,
|
||
to: number,
|
||
rules: PasteRule[],
|
||
plugin: Plugin,
|
||
}): any {
|
||
const {
|
||
state,
|
||
from,
|
||
to,
|
||
rules,
|
||
} = config
|
||
|
||
state.doc.nodesBetween(from, to, (node, pos) => {
|
||
if (!node.isTextblock || node.type.spec.code) {
|
||
return
|
||
}
|
||
|
||
const resolvedFrom = Math.max(from, pos)
|
||
const resolvedTo = Math.min(to, pos + node.content.size)
|
||
const textToMatch = node.textBetween(
|
||
resolvedFrom - pos,
|
||
resolvedTo - pos,
|
||
undefined,
|
||
'\ufffc',
|
||
)
|
||
|
||
rules.forEach(rule => {
|
||
const matches = pasteRuleMatcherHandler(textToMatch, rule.find)
|
||
|
||
matches.forEach(match => {
|
||
if (match.index === undefined) {
|
||
return
|
||
}
|
||
|
||
const start = resolvedFrom + match.index
|
||
const end = start + match[0].length
|
||
const range = {
|
||
from: state.tr.mapping.map(start),
|
||
to: state.tr.mapping.map(end),
|
||
}
|
||
|
||
rule.handler({
|
||
state,
|
||
range,
|
||
match,
|
||
})
|
||
})
|
||
})
|
||
}, from)
|
||
}
|
||
|
||
/**
|
||
* Create an paste rules plugin. When enabled, it will cause pasted
|
||
* text that matches any of the given rules to trigger the rule’s
|
||
* action.
|
||
*/
|
||
export function pasteRulesPlugin(rules: PasteRule[]): Plugin {
|
||
const plugin = new Plugin({
|
||
appendTransaction: (transactions, oldState, state) => {
|
||
const transaction = transactions[0]
|
||
|
||
// stop if there is not a paste event
|
||
if (!transaction.getMeta('paste')) {
|
||
return
|
||
}
|
||
|
||
// stop if there is no changed range
|
||
const { doc, before } = transaction
|
||
const from = before.content.findDiffStart(doc.content)
|
||
const to = before.content.findDiffEnd(doc.content)
|
||
|
||
if (!from || !to) {
|
||
return
|
||
}
|
||
|
||
// build a chainable state
|
||
// so we can use a single transaction for all paste rules
|
||
const tr = state.tr
|
||
const chainableState = createChainableState({
|
||
state,
|
||
transaction: tr,
|
||
})
|
||
|
||
run({
|
||
state: chainableState,
|
||
from,
|
||
to: to.b,
|
||
rules,
|
||
plugin,
|
||
})
|
||
|
||
// stop if there are no changes
|
||
if (!tr.steps.length) {
|
||
return
|
||
}
|
||
|
||
return tr
|
||
},
|
||
|
||
// @ts-ignore
|
||
isPasteRules: true,
|
||
})
|
||
|
||
return plugin
|
||
}
|