mirror of
https://github.com/ueberdosis/tiptap.git
synced 2025-06-07 17:43:49 +08:00
feat: Integrate input rules and paste rules into the core (#1997)
* 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
This commit is contained in:
parent
ace4964d97
commit
723b955cec
@ -5,7 +5,6 @@ prosemirror-commands
|
|||||||
prosemirror-dropcursor
|
prosemirror-dropcursor
|
||||||
prosemirror-gapcursor
|
prosemirror-gapcursor
|
||||||
prosemirror-history
|
prosemirror-history
|
||||||
prosemirror-inputrules
|
|
||||||
prosemirror-keymap
|
prosemirror-keymap
|
||||||
prosemirror-model
|
prosemirror-model
|
||||||
prosemirror-schema-list
|
prosemirror-schema-list
|
||||||
|
@ -16,7 +16,6 @@ import Document from '@tiptap/extension-document'
|
|||||||
import Paragraph from '@tiptap/extension-paragraph'
|
import Paragraph from '@tiptap/extension-paragraph'
|
||||||
import Text from '@tiptap/extension-text'
|
import Text from '@tiptap/extension-text'
|
||||||
import Link from '@tiptap/extension-link'
|
import Link from '@tiptap/extension-link'
|
||||||
import Bold from '@tiptap/extension-bold'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -35,14 +34,13 @@ export default {
|
|||||||
Document,
|
Document,
|
||||||
Paragraph,
|
Paragraph,
|
||||||
Text,
|
Text,
|
||||||
Bold,
|
|
||||||
Link.configure({
|
Link.configure({
|
||||||
openOnClick: false,
|
openOnClick: false,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
content: `
|
content: `
|
||||||
<p>
|
<p>
|
||||||
Wow, this editor has support for links to the whole <strong><a href="https://en.wikipedia.org/wiki/World_Wide_Web">world wide web</a></strong>. We tested a lot of URLs and I think you can add *every URL* you want. Isn’t that cool? Let’s try <a href="https://statamic.com/">another one!</a> Yep, seems to work.
|
Wow, this editor has support for links to the whole <a href="https://en.wikipedia.org/wiki/World_Wide_Web">world wide web</a>. We tested a lot of URLs and I think you can add *every URL* you want. Isn’t that cool? Let’s try <a href="https://statamic.com/">another one!</a> Yep, seems to work.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
By default every link will get a \`rel="noopener noreferrer nofollow"\` attribute. It’s configurable though.
|
By default every link will get a \`rel="noopener noreferrer nofollow"\` attribute. It’s configurable though.
|
||||||
|
@ -126,6 +126,17 @@ context('/src/Nodes/CodeBlock/Vue/', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should make a code block from backtick markdown shortcuts followed by enter', () => {
|
||||||
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
|
editor.commands.clearContent()
|
||||||
|
|
||||||
|
cy.get('.ProseMirror')
|
||||||
|
.type('```{enter}Code')
|
||||||
|
.find('pre>code')
|
||||||
|
.should('contain', 'Code')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('reverts the markdown shortcut when pressing backspace', () => {
|
it('reverts the markdown shortcut when pressing backspace', () => {
|
||||||
cy.get('.ProseMirror').then(([{ editor }]) => {
|
cy.get('.ProseMirror').then(([{ editor }]) => {
|
||||||
editor.commands.clearContent()
|
editor.commands.clearContent()
|
||||||
|
@ -25,7 +25,6 @@
|
|||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prosemirror-commands": "^1.0.4",
|
"@types/prosemirror-commands": "^1.0.4",
|
||||||
"@types/prosemirror-inputrules": "^1.0.4",
|
|
||||||
"@types/prosemirror-keymap": "^1.0.4",
|
"@types/prosemirror-keymap": "^1.0.4",
|
||||||
"@types/prosemirror-model": "^1.13.2",
|
"@types/prosemirror-model": "^1.13.2",
|
||||||
"@types/prosemirror-schema-list": "^1.0.3",
|
"@types/prosemirror-schema-list": "^1.0.3",
|
||||||
@ -33,7 +32,6 @@
|
|||||||
"@types/prosemirror-transform": "^1.1.4",
|
"@types/prosemirror-transform": "^1.1.4",
|
||||||
"@types/prosemirror-view": "^1.19.1",
|
"@types/prosemirror-view": "^1.19.1",
|
||||||
"prosemirror-commands": "^1.1.11",
|
"prosemirror-commands": "^1.1.11",
|
||||||
"prosemirror-inputrules": "^1.1.3",
|
|
||||||
"prosemirror-keymap": "^1.1.3",
|
"prosemirror-keymap": "^1.1.3",
|
||||||
"prosemirror-model": "^1.14.3",
|
"prosemirror-model": "^1.14.3",
|
||||||
"prosemirror-schema-list": "^1.1.6",
|
"prosemirror-schema-list": "^1.1.6",
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { EditorState, Transaction } from 'prosemirror-state'
|
import { Transaction } from 'prosemirror-state'
|
||||||
import { Editor } from './Editor'
|
import { Editor } from './Editor'
|
||||||
|
import createChainableState from './helpers/createChainableState'
|
||||||
import {
|
import {
|
||||||
SingleCommands,
|
SingleCommands,
|
||||||
ChainedCommands,
|
ChainedCommands,
|
||||||
@ -106,7 +107,10 @@ export default class CommandManager {
|
|||||||
tr,
|
tr,
|
||||||
editor,
|
editor,
|
||||||
view,
|
view,
|
||||||
state: this.chainableState(tr, state),
|
state: createChainableState({
|
||||||
|
state,
|
||||||
|
transaction: tr,
|
||||||
|
}),
|
||||||
dispatch: shouldDispatch
|
dispatch: shouldDispatch
|
||||||
? () => undefined
|
? () => undefined
|
||||||
: undefined,
|
: undefined,
|
||||||
@ -124,36 +128,4 @@ export default class CommandManager {
|
|||||||
return props
|
return props
|
||||||
}
|
}
|
||||||
|
|
||||||
public chainableState(tr: Transaction, state: EditorState): EditorState {
|
|
||||||
let { selection } = tr
|
|
||||||
let { doc } = tr
|
|
||||||
let { storedMarks } = tr
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
schema: state.schema,
|
|
||||||
plugins: state.plugins,
|
|
||||||
apply: state.apply.bind(state),
|
|
||||||
applyTransaction: state.applyTransaction.bind(state),
|
|
||||||
reconfigure: state.reconfigure.bind(state),
|
|
||||||
toJSON: state.toJSON.bind(state),
|
|
||||||
get storedMarks() {
|
|
||||||
return storedMarks
|
|
||||||
},
|
|
||||||
get selection() {
|
|
||||||
return selection
|
|
||||||
},
|
|
||||||
get doc() {
|
|
||||||
return doc
|
|
||||||
},
|
|
||||||
get tr() {
|
|
||||||
selection = tr.selection
|
|
||||||
doc = tr.doc
|
|
||||||
storedMarks = tr.storedMarks
|
|
||||||
|
|
||||||
return tr
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ import getText from './helpers/getText'
|
|||||||
import isNodeEmpty from './helpers/isNodeEmpty'
|
import isNodeEmpty from './helpers/isNodeEmpty'
|
||||||
import getTextSeralizersFromSchema from './helpers/getTextSeralizersFromSchema'
|
import getTextSeralizersFromSchema from './helpers/getTextSeralizersFromSchema'
|
||||||
import createStyleTag from './utilities/createStyleTag'
|
import createStyleTag from './utilities/createStyleTag'
|
||||||
|
import isFunction from './utilities/isFunction'
|
||||||
import CommandManager from './CommandManager'
|
import CommandManager from './CommandManager'
|
||||||
import ExtensionManager from './ExtensionManager'
|
import ExtensionManager from './ExtensionManager'
|
||||||
import EventEmitter from './EventEmitter'
|
import EventEmitter from './EventEmitter'
|
||||||
@ -184,7 +185,7 @@ export class Editor extends EventEmitter<EditorEvents> {
|
|||||||
* @param handlePlugins Control how to merge the plugin into the existing plugins.
|
* @param handlePlugins Control how to merge the plugin into the existing plugins.
|
||||||
*/
|
*/
|
||||||
public registerPlugin(plugin: Plugin, handlePlugins?: (newPlugin: Plugin, plugins: Plugin[]) => Plugin[]): void {
|
public registerPlugin(plugin: Plugin, handlePlugins?: (newPlugin: Plugin, plugins: Plugin[]) => Plugin[]): void {
|
||||||
const plugins = typeof handlePlugins === 'function'
|
const plugins = isFunction(handlePlugins)
|
||||||
? handlePlugins(plugin, this.state.plugins)
|
? handlePlugins(plugin, this.state.plugins)
|
||||||
: [...this.state.plugins, plugin]
|
: [...this.state.plugins, plugin]
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Plugin, Transaction } from 'prosemirror-state'
|
import { Plugin, Transaction } from 'prosemirror-state'
|
||||||
import { InputRule } from 'prosemirror-inputrules'
|
import { InputRule } from './InputRule'
|
||||||
|
import { PasteRule } from './PasteRule'
|
||||||
import { Editor } from './Editor'
|
import { Editor } from './Editor'
|
||||||
import { Node } from './Node'
|
import { Node } from './Node'
|
||||||
import { Mark } from './Mark'
|
import { Mark } from './Mark'
|
||||||
@ -81,7 +82,7 @@ declare module '@tiptap/core' {
|
|||||||
options: Options,
|
options: Options,
|
||||||
editor: Editor,
|
editor: Editor,
|
||||||
parent: ParentConfig<ExtensionConfig<Options>>['addPasteRules'],
|
parent: ParentConfig<ExtensionConfig<Options>>['addPasteRules'],
|
||||||
}) => Plugin[],
|
}) => PasteRule[],
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ProseMirror plugins
|
* ProseMirror plugins
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { keymap } from 'prosemirror-keymap'
|
import { keymap } from 'prosemirror-keymap'
|
||||||
import { Schema, Node as ProsemirrorNode } from 'prosemirror-model'
|
import { Schema, Node as ProsemirrorNode } from 'prosemirror-model'
|
||||||
import { inputRules as inputRulesPlugin } from 'prosemirror-inputrules'
|
import { inputRulesPlugin } from './InputRule'
|
||||||
|
import { pasteRulesPlugin } from './PasteRule'
|
||||||
import { EditorView, Decoration } from 'prosemirror-view'
|
import { EditorView, Decoration } from 'prosemirror-view'
|
||||||
import { Plugin } from 'prosemirror-state'
|
import { Plugin } from 'prosemirror-state'
|
||||||
import { Editor } from './Editor'
|
import { Editor } from './Editor'
|
||||||
@ -210,7 +211,12 @@ export default class ExtensionManager {
|
|||||||
// so it feels more natural to run plugins at the end of an array first.
|
// so it feels more natural to run plugins at the end of an array first.
|
||||||
// That’s why we have to reverse the `extensions` array and sort again
|
// That’s why we have to reverse the `extensions` array and sort again
|
||||||
// based on the `priority` option.
|
// based on the `priority` option.
|
||||||
return ExtensionManager.sort([...this.extensions].reverse())
|
const extensions = ExtensionManager.sort([...this.extensions].reverse())
|
||||||
|
|
||||||
|
const inputRules: any[] = []
|
||||||
|
const pasteRules: any[] = []
|
||||||
|
|
||||||
|
const allPlugins = extensions
|
||||||
.map(extension => {
|
.map(extension => {
|
||||||
const context = {
|
const context = {
|
||||||
name: extension.name,
|
name: extension.name,
|
||||||
@ -248,12 +254,7 @@ export default class ExtensionManager {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (this.editor.options.enableInputRules && addInputRules) {
|
if (this.editor.options.enableInputRules && addInputRules) {
|
||||||
const inputRules = addInputRules()
|
inputRules.push(...addInputRules())
|
||||||
const inputRulePlugins = inputRules.length
|
|
||||||
? [inputRulesPlugin({ rules: inputRules })]
|
|
||||||
: []
|
|
||||||
|
|
||||||
plugins.push(...inputRulePlugins)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const addPasteRules = getExtensionField<AnyConfig['addPasteRules']>(
|
const addPasteRules = getExtensionField<AnyConfig['addPasteRules']>(
|
||||||
@ -263,9 +264,7 @@ export default class ExtensionManager {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (this.editor.options.enablePasteRules && addPasteRules) {
|
if (this.editor.options.enablePasteRules && addPasteRules) {
|
||||||
const pasteRulePlugins = addPasteRules()
|
pasteRules.push(...addPasteRules())
|
||||||
|
|
||||||
plugins.push(...pasteRulePlugins)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const addProseMirrorPlugins = getExtensionField<AnyConfig['addProseMirrorPlugins']>(
|
const addProseMirrorPlugins = getExtensionField<AnyConfig['addProseMirrorPlugins']>(
|
||||||
@ -283,6 +282,12 @@ export default class ExtensionManager {
|
|||||||
return plugins
|
return plugins
|
||||||
})
|
})
|
||||||
.flat()
|
.flat()
|
||||||
|
|
||||||
|
return [
|
||||||
|
inputRulesPlugin(inputRules),
|
||||||
|
pasteRulesPlugin(pasteRules),
|
||||||
|
...allPlugins,
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
get attributes() {
|
get attributes() {
|
||||||
|
245
packages/core/src/InputRule.ts
Normal file
245
packages/core/src/InputRule.ts
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
import { EditorView } from 'prosemirror-view'
|
||||||
|
import { EditorState, Plugin, TextSelection } from 'prosemirror-state'
|
||||||
|
import createChainableState from './helpers/createChainableState'
|
||||||
|
import isRegExp from './utilities/isRegExp'
|
||||||
|
import { Range, ExtendedRegExpMatchArray } from './types'
|
||||||
|
|
||||||
|
export type InputRuleMatch = {
|
||||||
|
index: number,
|
||||||
|
text: string,
|
||||||
|
replaceWith?: string,
|
||||||
|
match?: RegExpMatchArray,
|
||||||
|
data?: Record<string, any>,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InputRuleFinder =
|
||||||
|
| RegExp
|
||||||
|
| ((text: string) => InputRuleMatch | null)
|
||||||
|
|
||||||
|
export class InputRule {
|
||||||
|
find: InputRuleFinder
|
||||||
|
|
||||||
|
handler: (props: {
|
||||||
|
state: EditorState,
|
||||||
|
range: Range,
|
||||||
|
match: ExtendedRegExpMatchArray,
|
||||||
|
}) => void
|
||||||
|
|
||||||
|
constructor(config: {
|
||||||
|
find: InputRuleFinder,
|
||||||
|
handler: (props: {
|
||||||
|
state: EditorState,
|
||||||
|
range: Range,
|
||||||
|
match: ExtendedRegExpMatchArray,
|
||||||
|
}) => void,
|
||||||
|
}) {
|
||||||
|
this.find = config.find
|
||||||
|
this.handler = config.handler
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputRuleMatcherHandler = (text: string, find: InputRuleFinder): ExtendedRegExpMatchArray | null => {
|
||||||
|
if (isRegExp(find)) {
|
||||||
|
return find.exec(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputRuleMatch = find(text)
|
||||||
|
|
||||||
|
if (!inputRuleMatch) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: ExtendedRegExpMatchArray = []
|
||||||
|
|
||||||
|
result.push(inputRuleMatch.text)
|
||||||
|
result.index = inputRuleMatch.index
|
||||||
|
result.input = text
|
||||||
|
result.data = inputRuleMatch.data
|
||||||
|
|
||||||
|
if (inputRuleMatch.replaceWith) {
|
||||||
|
if (!inputRuleMatch.text.includes(inputRuleMatch.replaceWith)) {
|
||||||
|
console.warn('[tiptap warn]: "inputRuleMatch.replaceWith" must be part of "inputRuleMatch.text".')
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(inputRuleMatch.replaceWith)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(config: {
|
||||||
|
view: EditorView,
|
||||||
|
from: number,
|
||||||
|
to: number,
|
||||||
|
text: string,
|
||||||
|
rules: InputRule[],
|
||||||
|
plugin: Plugin,
|
||||||
|
}): any {
|
||||||
|
const {
|
||||||
|
view,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
text,
|
||||||
|
rules,
|
||||||
|
plugin,
|
||||||
|
} = config
|
||||||
|
|
||||||
|
if (view.composing) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const $from = view.state.doc.resolve(from)
|
||||||
|
|
||||||
|
if (
|
||||||
|
// check for code node
|
||||||
|
$from.parent.type.spec.code
|
||||||
|
// check for code mark
|
||||||
|
|| !!($from.nodeBefore || $from.nodeAfter)?.marks.find(mark => mark.type.spec.code)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let matched = false
|
||||||
|
const maxMatch = 500
|
||||||
|
const textBefore = $from.parent.textBetween(
|
||||||
|
Math.max(0, $from.parentOffset - maxMatch),
|
||||||
|
$from.parentOffset,
|
||||||
|
undefined,
|
||||||
|
'\ufffc',
|
||||||
|
) + text
|
||||||
|
|
||||||
|
rules.forEach(rule => {
|
||||||
|
if (matched) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = inputRuleMatcherHandler(textBefore, rule.find)
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const tr = view.state.tr
|
||||||
|
const state = createChainableState({
|
||||||
|
state: view.state,
|
||||||
|
transaction: tr,
|
||||||
|
})
|
||||||
|
const range = {
|
||||||
|
from: from - (match[0].length - text.length),
|
||||||
|
to,
|
||||||
|
}
|
||||||
|
|
||||||
|
rule.handler({
|
||||||
|
state,
|
||||||
|
range,
|
||||||
|
match,
|
||||||
|
})
|
||||||
|
|
||||||
|
// stop if there are no changes
|
||||||
|
if (!tr.steps.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// store transform as meta data
|
||||||
|
// so we can undo input rules within the `undoInputRules` command
|
||||||
|
tr.setMeta(plugin, {
|
||||||
|
transform: tr,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
text,
|
||||||
|
})
|
||||||
|
|
||||||
|
view.dispatch(tr)
|
||||||
|
matched = true
|
||||||
|
})
|
||||||
|
|
||||||
|
return matched
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an input rules plugin. When enabled, it will cause text
|
||||||
|
* input that matches any of the given rules to trigger the rule’s
|
||||||
|
* action.
|
||||||
|
*/
|
||||||
|
export function inputRulesPlugin(rules: InputRule[]): Plugin {
|
||||||
|
const plugin = new Plugin({
|
||||||
|
state: {
|
||||||
|
init() {
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
apply(tr, prev) {
|
||||||
|
const stored = tr.getMeta(this)
|
||||||
|
|
||||||
|
if (stored) {
|
||||||
|
return stored
|
||||||
|
}
|
||||||
|
|
||||||
|
return tr.selectionSet || tr.docChanged
|
||||||
|
? null
|
||||||
|
: prev
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
handleTextInput(view, from, to, text) {
|
||||||
|
return run({
|
||||||
|
view,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
text,
|
||||||
|
rules,
|
||||||
|
plugin,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
handleDOMEvents: {
|
||||||
|
compositionend: view => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const { $cursor } = view.state.selection as TextSelection
|
||||||
|
|
||||||
|
if ($cursor) {
|
||||||
|
run({
|
||||||
|
view,
|
||||||
|
from: $cursor.pos,
|
||||||
|
to: $cursor.pos,
|
||||||
|
text: '',
|
||||||
|
rules,
|
||||||
|
plugin,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// add support for input rules to trigger on enter
|
||||||
|
// this is useful for example for code blocks
|
||||||
|
handleKeyDown(view, event) {
|
||||||
|
if (event.key !== 'Enter') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const { $cursor } = view.state.selection as TextSelection
|
||||||
|
|
||||||
|
if ($cursor) {
|
||||||
|
return run({
|
||||||
|
view,
|
||||||
|
from: $cursor.pos,
|
||||||
|
to: $cursor.pos,
|
||||||
|
text: '\n',
|
||||||
|
rules,
|
||||||
|
plugin,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
isInputRules: true,
|
||||||
|
}) as Plugin
|
||||||
|
|
||||||
|
return plugin
|
||||||
|
}
|
@ -5,7 +5,8 @@ import {
|
|||||||
MarkType,
|
MarkType,
|
||||||
} from 'prosemirror-model'
|
} from 'prosemirror-model'
|
||||||
import { Plugin, Transaction } from 'prosemirror-state'
|
import { Plugin, Transaction } from 'prosemirror-state'
|
||||||
import { InputRule } from 'prosemirror-inputrules'
|
import { InputRule } from './InputRule'
|
||||||
|
import { PasteRule } from './PasteRule'
|
||||||
import mergeDeep from './utilities/mergeDeep'
|
import mergeDeep from './utilities/mergeDeep'
|
||||||
import {
|
import {
|
||||||
Extensions,
|
Extensions,
|
||||||
@ -91,7 +92,7 @@ declare module '@tiptap/core' {
|
|||||||
editor: Editor,
|
editor: Editor,
|
||||||
type: MarkType,
|
type: MarkType,
|
||||||
parent: ParentConfig<MarkConfig<Options>>['addPasteRules'],
|
parent: ParentConfig<MarkConfig<Options>>['addPasteRules'],
|
||||||
}) => Plugin[],
|
}) => PasteRule[],
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ProseMirror plugins
|
* ProseMirror plugins
|
||||||
@ -281,6 +282,15 @@ declare module '@tiptap/core' {
|
|||||||
parent: ParentConfig<MarkConfig<Options>>['spanning'],
|
parent: ParentConfig<MarkConfig<Options>>['spanning'],
|
||||||
}) => MarkSpec['spanning']),
|
}) => MarkSpec['spanning']),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Code
|
||||||
|
*/
|
||||||
|
code?: boolean | ((this: {
|
||||||
|
name: string,
|
||||||
|
options: Options,
|
||||||
|
parent: ParentConfig<MarkConfig<Options>>['code'],
|
||||||
|
}) => boolean),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse HTML
|
* Parse HTML
|
||||||
*/
|
*/
|
||||||
|
@ -5,7 +5,8 @@ import {
|
|||||||
NodeType,
|
NodeType,
|
||||||
} from 'prosemirror-model'
|
} from 'prosemirror-model'
|
||||||
import { Plugin, Transaction } from 'prosemirror-state'
|
import { Plugin, Transaction } from 'prosemirror-state'
|
||||||
import { InputRule } from 'prosemirror-inputrules'
|
import { InputRule } from './InputRule'
|
||||||
|
import { PasteRule } from './PasteRule'
|
||||||
import mergeDeep from './utilities/mergeDeep'
|
import mergeDeep from './utilities/mergeDeep'
|
||||||
import {
|
import {
|
||||||
Extensions,
|
Extensions,
|
||||||
@ -91,7 +92,7 @@ declare module '@tiptap/core' {
|
|||||||
editor: Editor,
|
editor: Editor,
|
||||||
type: NodeType,
|
type: NodeType,
|
||||||
parent: ParentConfig<NodeConfig<Options>>['addPasteRules'],
|
parent: ParentConfig<NodeConfig<Options>>['addPasteRules'],
|
||||||
}) => Plugin[],
|
}) => PasteRule[],
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ProseMirror plugins
|
* ProseMirror plugins
|
||||||
|
177
packages/core/src/PasteRule.ts
Normal file
177
packages/core/src/PasteRule.ts
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
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
|
||||||
|
}
|
@ -1,4 +1,3 @@
|
|||||||
import { undoInputRule as originalUndoInputRule } from 'prosemirror-inputrules'
|
|
||||||
import { RawCommands } from '../types'
|
import { RawCommands } from '../types'
|
||||||
|
|
||||||
declare module '@tiptap/core' {
|
declare module '@tiptap/core' {
|
||||||
@ -13,5 +12,34 @@ declare module '@tiptap/core' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const undoInputRule: RawCommands['undoInputRule'] = () => ({ state, dispatch }) => {
|
export const undoInputRule: RawCommands['undoInputRule'] = () => ({ state, dispatch }) => {
|
||||||
return originalUndoInputRule(state, dispatch)
|
const plugins = state.plugins
|
||||||
|
|
||||||
|
for (let i = 0; i < plugins.length; i += 1) {
|
||||||
|
const plugin = plugins[i]
|
||||||
|
let undoable
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
// eslint-disable-next-line
|
||||||
|
if (plugin.spec.isInputRules && (undoable = plugin.getState(state))) {
|
||||||
|
if (dispatch) {
|
||||||
|
const tr = state.tr
|
||||||
|
const toUndo = undoable.transform
|
||||||
|
|
||||||
|
for (let j = toUndo.steps.length - 1; j >= 0; j -= 1) {
|
||||||
|
tr.step(toUndo.steps[j].invert(toUndo.docs[j]))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (undoable.text) {
|
||||||
|
const marks = tr.doc.resolve(undoable.from).marks()
|
||||||
|
tr.replaceWith(undoable.from, undoable.to, state.schema.text(undoable.text, marks))
|
||||||
|
} else {
|
||||||
|
tr.delete(undoable.from, undoable.to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
37
packages/core/src/helpers/createChainableState.ts
Normal file
37
packages/core/src/helpers/createChainableState.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { EditorState, Transaction } from 'prosemirror-state'
|
||||||
|
|
||||||
|
export default function createChainableState(config: {
|
||||||
|
transaction: Transaction,
|
||||||
|
state: EditorState,
|
||||||
|
}): EditorState {
|
||||||
|
const { state, transaction } = config
|
||||||
|
let { selection } = transaction
|
||||||
|
let { doc } = transaction
|
||||||
|
let { storedMarks } = transaction
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
schema: state.schema,
|
||||||
|
plugins: state.plugins,
|
||||||
|
apply: state.apply.bind(state),
|
||||||
|
applyTransaction: state.applyTransaction.bind(state),
|
||||||
|
reconfigure: state.reconfigure.bind(state),
|
||||||
|
toJSON: state.toJSON.bind(state),
|
||||||
|
get storedMarks() {
|
||||||
|
return storedMarks
|
||||||
|
},
|
||||||
|
get selection() {
|
||||||
|
return selection
|
||||||
|
},
|
||||||
|
get doc() {
|
||||||
|
return doc
|
||||||
|
},
|
||||||
|
get tr() {
|
||||||
|
selection = transaction.selection
|
||||||
|
doc = transaction.doc
|
||||||
|
storedMarks = transaction.storedMarks
|
||||||
|
|
||||||
|
return transaction
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
@ -108,10 +108,11 @@ export default function getSchemaByResolvedExtensions(extensions: Extensions): S
|
|||||||
|
|
||||||
const schema: MarkSpec = cleanUpSchemaItem({
|
const schema: MarkSpec = cleanUpSchemaItem({
|
||||||
...extraMarkFields,
|
...extraMarkFields,
|
||||||
inclusive: callOrReturn(getExtensionField<NodeConfig['inclusive']>(extension, 'inclusive', context)),
|
inclusive: callOrReturn(getExtensionField<MarkConfig['inclusive']>(extension, 'inclusive', context)),
|
||||||
excludes: callOrReturn(getExtensionField<NodeConfig['excludes']>(extension, 'excludes', context)),
|
excludes: callOrReturn(getExtensionField<MarkConfig['excludes']>(extension, 'excludes', context)),
|
||||||
group: callOrReturn(getExtensionField<NodeConfig['group']>(extension, 'group', context)),
|
group: callOrReturn(getExtensionField<MarkConfig['group']>(extension, 'group', context)),
|
||||||
spanning: callOrReturn(getExtensionField<NodeConfig['spanning']>(extension, 'spanning', context)),
|
spanning: callOrReturn(getExtensionField<MarkConfig['spanning']>(extension, 'spanning', context)),
|
||||||
|
code: callOrReturn(getExtensionField<MarkConfig['code']>(extension, 'code', context)),
|
||||||
attrs: Object.fromEntries(extensionAttributes.map(extensionAttribute => {
|
attrs: Object.fromEntries(extensionAttributes.map(extensionAttribute => {
|
||||||
return [extensionAttribute.name, { default: extensionAttribute?.attribute?.default }]
|
return [extensionAttribute.name, { default: extensionAttribute?.attribute?.default }]
|
||||||
})),
|
})),
|
||||||
|
@ -7,11 +7,17 @@ export * from './Node'
|
|||||||
export * from './Mark'
|
export * from './Mark'
|
||||||
export * from './NodeView'
|
export * from './NodeView'
|
||||||
export * from './Tracker'
|
export * from './Tracker'
|
||||||
|
export * from './InputRule'
|
||||||
|
export * from './PasteRule'
|
||||||
export * from './types'
|
export * from './types'
|
||||||
|
|
||||||
export { default as nodeInputRule } from './inputRules/nodeInputRule'
|
export { default as nodeInputRule } from './inputRules/nodeInputRule'
|
||||||
export { default as markInputRule } from './inputRules/markInputRule'
|
export { default as markInputRule } from './inputRules/markInputRule'
|
||||||
|
export { default as textblockTypeInputRule } from './inputRules/textblockTypeInputRule'
|
||||||
|
export { default as textInputRule } from './inputRules/textInputRule'
|
||||||
|
export { default as wrappingInputRule } from './inputRules/wrappingInputRule'
|
||||||
export { default as markPasteRule } from './pasteRules/markPasteRule'
|
export { default as markPasteRule } from './pasteRules/markPasteRule'
|
||||||
|
export { default as textPasteRule } from './pasteRules/textPasteRule'
|
||||||
|
|
||||||
export { default as callOrReturn } from './utilities/callOrReturn'
|
export { default as callOrReturn } from './utilities/callOrReturn'
|
||||||
export { default as mergeAttributes } from './utilities/mergeAttributes'
|
export { default as mergeAttributes } from './utilities/mergeAttributes'
|
||||||
|
@ -1,28 +1,49 @@
|
|||||||
import { InputRule } from 'prosemirror-inputrules'
|
import { InputRule, InputRuleFinder } from '../InputRule'
|
||||||
import { MarkType } from 'prosemirror-model'
|
import { MarkType } from 'prosemirror-model'
|
||||||
import getMarksBetween from '../helpers/getMarksBetween'
|
import getMarksBetween from '../helpers/getMarksBetween'
|
||||||
|
import callOrReturn from '../utilities/callOrReturn'
|
||||||
|
import { ExtendedRegExpMatchArray } from '../types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an input rule that adds a mark when the
|
||||||
|
* matched text is typed into it.
|
||||||
|
*/
|
||||||
|
export default function markInputRule(config: {
|
||||||
|
find: InputRuleFinder,
|
||||||
|
type: MarkType,
|
||||||
|
getAttributes?:
|
||||||
|
| Record<string, any>
|
||||||
|
| ((match: ExtendedRegExpMatchArray) => Record<string, any>)
|
||||||
|
| false
|
||||||
|
| null
|
||||||
|
,
|
||||||
|
}) {
|
||||||
|
return new InputRule({
|
||||||
|
find: config.find,
|
||||||
|
handler: ({ state, range, match }) => {
|
||||||
|
const attributes = callOrReturn(config.getAttributes, undefined, match)
|
||||||
|
|
||||||
|
if (attributes === false || attributes === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
export default function (regexp: RegExp, markType: MarkType, getAttributes?: Function): InputRule {
|
|
||||||
return new InputRule(regexp, (state, match, start, end) => {
|
|
||||||
const attributes = getAttributes instanceof Function
|
|
||||||
? getAttributes(match)
|
|
||||||
: getAttributes
|
|
||||||
const { tr } = state
|
const { tr } = state
|
||||||
const captureGroup = match[match.length - 1]
|
const captureGroup = match[match.length - 1]
|
||||||
const fullMatch = match[0]
|
const fullMatch = match[0]
|
||||||
let markEnd = end
|
let markEnd = range.to
|
||||||
|
|
||||||
if (captureGroup) {
|
if (captureGroup) {
|
||||||
const startSpaces = fullMatch.search(/\S/)
|
const startSpaces = fullMatch.search(/\S/)
|
||||||
const textStart = start + fullMatch.indexOf(captureGroup)
|
const textStart = range.from + fullMatch.indexOf(captureGroup)
|
||||||
const textEnd = textStart + captureGroup.length
|
const textEnd = textStart + captureGroup.length
|
||||||
|
|
||||||
const excludedMarks = getMarksBetween(start, end, state)
|
const excludedMarks = getMarksBetween(range.from, range.to, state)
|
||||||
.filter(item => {
|
.filter(item => {
|
||||||
// TODO: PR to add excluded to MarkType
|
// TODO: PR to add excluded to MarkType
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const { excluded } = item.mark.type
|
const { excluded } = item.mark.type
|
||||||
return excluded.find((type: MarkType) => type.name === markType.name)
|
|
||||||
|
return excluded.find((type: MarkType) => type.name === config.type.name)
|
||||||
})
|
})
|
||||||
.filter(item => item.to > textStart)
|
.filter(item => item.to > textStart)
|
||||||
|
|
||||||
@ -30,21 +51,20 @@ export default function (regexp: RegExp, markType: MarkType, getAttributes?: Fun
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (textEnd < end) {
|
if (textEnd < range.to) {
|
||||||
tr.delete(textEnd, end)
|
tr.delete(textEnd, range.to)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (textStart > start) {
|
if (textStart > range.from) {
|
||||||
tr.delete(start + startSpaces, textStart)
|
tr.delete(range.from + startSpaces, textStart)
|
||||||
}
|
}
|
||||||
|
|
||||||
markEnd = start + startSpaces + captureGroup.length
|
markEnd = range.from + startSpaces + captureGroup.length
|
||||||
|
|
||||||
tr.addMark(start + startSpaces, markEnd, markType.create(attributes))
|
tr.addMark(range.from + startSpaces, markEnd, config.type.create(attributes || {}))
|
||||||
|
|
||||||
tr.removeStoredMark(markType)
|
tr.removeStoredMark(config.type)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
return tr
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,34 @@
|
|||||||
import { InputRule } from 'prosemirror-inputrules'
|
|
||||||
import { NodeType } from 'prosemirror-model'
|
import { NodeType } from 'prosemirror-model'
|
||||||
|
import { InputRule, InputRuleFinder } from '../InputRule'
|
||||||
|
import { ExtendedRegExpMatchArray } from '../types'
|
||||||
|
import callOrReturn from '../utilities/callOrReturn'
|
||||||
|
|
||||||
export default function (regexp: RegExp, type: NodeType, getAttributes?: (match: any) => any): InputRule {
|
/**
|
||||||
return new InputRule(regexp, (state, match, start, end) => {
|
* Build an input rule that adds a node when the
|
||||||
const attributes = getAttributes instanceof Function
|
* matched text is typed into it.
|
||||||
? getAttributes(match)
|
*/
|
||||||
: getAttributes
|
export default function nodeInputRule(config: {
|
||||||
|
find: InputRuleFinder,
|
||||||
|
type: NodeType,
|
||||||
|
getAttributes?:
|
||||||
|
| Record<string, any>
|
||||||
|
| ((match: ExtendedRegExpMatchArray) => Record<string, any>)
|
||||||
|
| false
|
||||||
|
| null
|
||||||
|
,
|
||||||
|
}) {
|
||||||
|
return new InputRule({
|
||||||
|
find: config.find,
|
||||||
|
handler: ({ state, range, match }) => {
|
||||||
|
const attributes = callOrReturn(config.getAttributes, undefined, match) || {}
|
||||||
const { tr } = state
|
const { tr } = state
|
||||||
|
const start = range.from
|
||||||
|
let end = range.to
|
||||||
|
|
||||||
if (match[1]) {
|
if (match[1]) {
|
||||||
const offset = match[0].lastIndexOf(match[1])
|
const offset = match[0].lastIndexOf(match[1])
|
||||||
let matchStart = start + offset
|
let matchStart = start + offset
|
||||||
|
|
||||||
if (matchStart > end) {
|
if (matchStart > end) {
|
||||||
matchStart = end
|
matchStart = end
|
||||||
} else {
|
} else {
|
||||||
@ -22,11 +40,10 @@ export default function (regexp: RegExp, type: NodeType, getAttributes?: (match:
|
|||||||
tr.insertText(lastChar, start + match[0].length - 1)
|
tr.insertText(lastChar, start + match[0].length - 1)
|
||||||
|
|
||||||
// insert node from input rule
|
// insert node from input rule
|
||||||
tr.replaceWith(matchStart, end, type.create(attributes))
|
tr.replaceWith(matchStart, end, config.type.create(attributes))
|
||||||
} else if (match[0]) {
|
} else if (match[0]) {
|
||||||
tr.replaceWith(start, end, type.create(attributes))
|
tr.replaceWith(start, end, config.type.create(attributes))
|
||||||
}
|
}
|
||||||
|
},
|
||||||
return tr
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
35
packages/core/src/inputRules/textInputRule.ts
Normal file
35
packages/core/src/inputRules/textInputRule.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { InputRule, InputRuleFinder } from '../InputRule'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an input rule that replaces text when the
|
||||||
|
* matched text is typed into it.
|
||||||
|
*/
|
||||||
|
export default function textInputRule(config: {
|
||||||
|
find: InputRuleFinder,
|
||||||
|
replace: string,
|
||||||
|
}) {
|
||||||
|
return new InputRule({
|
||||||
|
find: config.find,
|
||||||
|
handler: ({ state, range, match }) => {
|
||||||
|
let insert = config.replace
|
||||||
|
let start = range.from
|
||||||
|
const end = range.to
|
||||||
|
|
||||||
|
if (match[1]) {
|
||||||
|
const offset = match[0].lastIndexOf(match[1])
|
||||||
|
|
||||||
|
insert += match[0].slice(offset + match[1].length)
|
||||||
|
start += offset
|
||||||
|
|
||||||
|
const cutOff = start - end
|
||||||
|
|
||||||
|
if (cutOff > 0) {
|
||||||
|
insert = match[0].slice(offset - cutOff, offset) + insert
|
||||||
|
start = end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.tr.insertText(insert, start, end)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
37
packages/core/src/inputRules/textblockTypeInputRule.ts
Normal file
37
packages/core/src/inputRules/textblockTypeInputRule.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { InputRule, InputRuleFinder } from '../InputRule'
|
||||||
|
import { NodeType } from 'prosemirror-model'
|
||||||
|
import { ExtendedRegExpMatchArray } from '../types'
|
||||||
|
import callOrReturn from '../utilities/callOrReturn'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an input rule that changes the type of a textblock when the
|
||||||
|
* matched text is typed into it. When using a regular expresion you’ll
|
||||||
|
* probably want the regexp to start with `^`, so that the pattern can
|
||||||
|
* only occur at the start of a textblock.
|
||||||
|
*/
|
||||||
|
export default function textblockTypeInputRule(config: {
|
||||||
|
find: InputRuleFinder,
|
||||||
|
type: NodeType,
|
||||||
|
getAttributes?:
|
||||||
|
| Record<string, any>
|
||||||
|
| ((match: ExtendedRegExpMatchArray) => Record<string, any>)
|
||||||
|
| false
|
||||||
|
| null
|
||||||
|
,
|
||||||
|
}) {
|
||||||
|
return new InputRule({
|
||||||
|
find: config.find,
|
||||||
|
handler: ({ state, range, match }) => {
|
||||||
|
const $start = state.doc.resolve(range.from)
|
||||||
|
const attributes = callOrReturn(config.getAttributes, undefined, match) || {}
|
||||||
|
|
||||||
|
if (!$start.node(-1).canReplaceWith($start.index(-1), $start.indexAfter(-1), config.type)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
state.tr
|
||||||
|
.delete(range.from, range.to)
|
||||||
|
.setBlockType(range.from, range.from, config.type, attributes)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
59
packages/core/src/inputRules/wrappingInputRule.ts
Normal file
59
packages/core/src/inputRules/wrappingInputRule.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { InputRule, InputRuleFinder } from '../InputRule'
|
||||||
|
import { NodeType, Node as ProseMirrorNode } from 'prosemirror-model'
|
||||||
|
import { findWrapping, canJoin } from 'prosemirror-transform'
|
||||||
|
import { ExtendedRegExpMatchArray } from '../types'
|
||||||
|
import callOrReturn from '../utilities/callOrReturn'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an input rule for automatically wrapping a textblock when a
|
||||||
|
* given string is typed. When using a regular expresion you’ll
|
||||||
|
* probably want the regexp to start with `^`, so that the pattern can
|
||||||
|
* only occur at the start of a textblock.
|
||||||
|
*
|
||||||
|
* `type` is the type of node to wrap in.
|
||||||
|
*
|
||||||
|
* By default, if there’s a node with the same type above the newly
|
||||||
|
* wrapped node, the rule will try to join those
|
||||||
|
* two nodes. You can pass a join predicate, which takes a regular
|
||||||
|
* expression match and the node before the wrapped node, and can
|
||||||
|
* return a boolean to indicate whether a join should happen.
|
||||||
|
*/
|
||||||
|
export default function wrappingInputRule(config: {
|
||||||
|
find: InputRuleFinder,
|
||||||
|
type: NodeType,
|
||||||
|
getAttributes?:
|
||||||
|
| Record<string, any>
|
||||||
|
| ((match: ExtendedRegExpMatchArray) => Record<string, any>)
|
||||||
|
| false
|
||||||
|
| null
|
||||||
|
,
|
||||||
|
joinPredicate?: (match: ExtendedRegExpMatchArray, node: ProseMirrorNode) => boolean,
|
||||||
|
}) {
|
||||||
|
return new InputRule({
|
||||||
|
find: config.find,
|
||||||
|
handler: ({ state, range, match }) => {
|
||||||
|
const attributes = callOrReturn(config.getAttributes, undefined, match) || {}
|
||||||
|
const tr = state.tr.delete(range.from, range.to)
|
||||||
|
const $start = tr.doc.resolve(range.from)
|
||||||
|
const blockRange = $start.blockRange()
|
||||||
|
const wrapping = blockRange && findWrapping(blockRange, config.type, attributes)
|
||||||
|
|
||||||
|
if (!wrapping) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.wrap(blockRange, wrapping)
|
||||||
|
|
||||||
|
const before = tr.doc.resolve(range.from - 1).nodeBefore
|
||||||
|
|
||||||
|
if (
|
||||||
|
before
|
||||||
|
&& before.type === config.type
|
||||||
|
&& canJoin(tr.doc, range.from - 1)
|
||||||
|
&& (!config.joinPredicate || config.joinPredicate(match, before))
|
||||||
|
) {
|
||||||
|
tr.join(range.from - 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
@ -1,76 +1,70 @@
|
|||||||
import { Plugin, PluginKey } from 'prosemirror-state'
|
import { PasteRule, PasteRuleFinder } from '../PasteRule'
|
||||||
import { Slice, Fragment, MarkType } from 'prosemirror-model'
|
import { MarkType } from 'prosemirror-model'
|
||||||
|
import getMarksBetween from '../helpers/getMarksBetween'
|
||||||
|
import callOrReturn from '../utilities/callOrReturn'
|
||||||
|
import { ExtendedRegExpMatchArray } from '../types'
|
||||||
|
|
||||||
export default function (
|
/**
|
||||||
regexp: RegExp,
|
* Build an paste rule that adds a mark when the
|
||||||
|
* matched text is pasted into it.
|
||||||
|
*/
|
||||||
|
export default function markPasteRule(config: {
|
||||||
|
find: PasteRuleFinder,
|
||||||
type: MarkType,
|
type: MarkType,
|
||||||
getAttributes?:
|
getAttributes?:
|
||||||
| Record<string, any>
|
| Record<string, any>
|
||||||
| ((match: RegExpExecArray) => Record<string, any>)
|
| ((match: ExtendedRegExpMatchArray) => Record<string, any>)
|
||||||
| false
|
| false
|
||||||
| null
|
| null
|
||||||
,
|
,
|
||||||
): Plugin {
|
}) {
|
||||||
const handler = (fragment: Fragment, parent?: any) => {
|
return new PasteRule({
|
||||||
const nodes: any[] = []
|
find: config.find,
|
||||||
|
handler: ({ state, range, match }) => {
|
||||||
|
const attributes = callOrReturn(config.getAttributes, undefined, match)
|
||||||
|
|
||||||
fragment.forEach(child => {
|
if (attributes === false || attributes === null) {
|
||||||
if (child.isText && child.text) {
|
return
|
||||||
const { text } = child
|
|
||||||
let pos = 0
|
|
||||||
let match
|
|
||||||
|
|
||||||
// eslint-disable-next-line
|
|
||||||
while ((match = regexp.exec(text)) !== null) {
|
|
||||||
const outerMatch = Math.max(match.length - 2, 0)
|
|
||||||
const innerMatch = Math.max(match.length - 1, 0)
|
|
||||||
|
|
||||||
if (parent?.type.allowsMarkType(type)) {
|
|
||||||
const start = match.index
|
|
||||||
const matchStart = start + match[0].indexOf(match[outerMatch])
|
|
||||||
const matchEnd = matchStart + match[outerMatch].length
|
|
||||||
const textStart = matchStart + match[outerMatch].lastIndexOf(match[innerMatch])
|
|
||||||
const textEnd = textStart + match[innerMatch].length
|
|
||||||
const attrs = getAttributes instanceof Function
|
|
||||||
? getAttributes(match)
|
|
||||||
: getAttributes
|
|
||||||
|
|
||||||
if (!attrs && attrs !== undefined) {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// adding text before markdown to nodes
|
const { tr } = state
|
||||||
if (matchStart > 0) {
|
const captureGroup = match[match.length - 1]
|
||||||
nodes.push(child.cut(pos, matchStart))
|
const fullMatch = match[0]
|
||||||
}
|
let markEnd = range.to
|
||||||
|
|
||||||
// adding the markdown part to nodes
|
if (captureGroup) {
|
||||||
nodes.push(child
|
const startSpaces = fullMatch.search(/\S/)
|
||||||
.cut(textStart, textEnd)
|
const textStart = range.from + fullMatch.indexOf(captureGroup)
|
||||||
.mark(type.create(attrs).addToSet(child.marks)))
|
const textEnd = textStart + captureGroup.length
|
||||||
|
|
||||||
pos = matchEnd
|
const excludedMarks = getMarksBetween(range.from, range.to, state)
|
||||||
}
|
.filter(item => {
|
||||||
}
|
// TODO: PR to add excluded to MarkType
|
||||||
|
// @ts-ignore
|
||||||
|
const { excluded } = item.mark.type
|
||||||
|
|
||||||
// adding rest of text to nodes
|
return excluded.find((type: MarkType) => type.name === config.type.name)
|
||||||
if (pos < text.length) {
|
|
||||||
nodes.push(child.cut(pos))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
nodes.push(child.copy(handler(child.content, child)))
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
.filter(item => item.to > textStart)
|
||||||
|
|
||||||
return Fragment.fromArray(nodes)
|
if (excludedMarks.length) {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Plugin({
|
if (textEnd < range.to) {
|
||||||
key: new PluginKey('markPasteRule'),
|
tr.delete(textEnd, range.to)
|
||||||
props: {
|
}
|
||||||
transformPasted: slice => {
|
|
||||||
return new Slice(handler(slice.content), slice.openStart, slice.openEnd)
|
if (textStart > range.from) {
|
||||||
},
|
tr.delete(range.from + startSpaces, textStart)
|
||||||
|
}
|
||||||
|
|
||||||
|
markEnd = range.from + startSpaces + captureGroup.length
|
||||||
|
|
||||||
|
tr.addMark(range.from + startSpaces, markEnd, config.type.create(attributes || {}))
|
||||||
|
|
||||||
|
tr.removeStoredMark(config.type)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
35
packages/core/src/pasteRules/textPasteRule.ts
Normal file
35
packages/core/src/pasteRules/textPasteRule.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { PasteRule, PasteRuleFinder } from '../PasteRule'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an paste rule that replaces text when the
|
||||||
|
* matched text is pasted into it.
|
||||||
|
*/
|
||||||
|
export default function textPasteRule(config: {
|
||||||
|
find: PasteRuleFinder,
|
||||||
|
replace: string,
|
||||||
|
}) {
|
||||||
|
return new PasteRule({
|
||||||
|
find: config.find,
|
||||||
|
handler: ({ state, range, match }) => {
|
||||||
|
let insert = config.replace
|
||||||
|
let start = range.from
|
||||||
|
const end = range.to
|
||||||
|
|
||||||
|
if (match[1]) {
|
||||||
|
const offset = match[0].lastIndexOf(match[1])
|
||||||
|
|
||||||
|
insert += match[0].slice(offset + match[1].length)
|
||||||
|
start += offset
|
||||||
|
|
||||||
|
const cutOff = start - end
|
||||||
|
|
||||||
|
if (cutOff > 0) {
|
||||||
|
insert = match[0].slice(offset - cutOff, offset) + insert
|
||||||
|
start = end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.tr.insertText(insert, start, end)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
@ -229,3 +229,7 @@ export type TextSerializer = (props: {
|
|||||||
parent: ProseMirrorNode,
|
parent: ProseMirrorNode,
|
||||||
index: number,
|
index: number,
|
||||||
}) => string
|
}) => string
|
||||||
|
|
||||||
|
export type ExtendedRegExpMatchArray = RegExpMatchArray & {
|
||||||
|
data?: Record<string, any>,
|
||||||
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { MaybeReturnType } from '../types'
|
import { MaybeReturnType } from '../types'
|
||||||
|
import isFunction from './isFunction'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optionally calls `value` as a function.
|
* Optionally calls `value` as a function.
|
||||||
@ -8,7 +9,7 @@ import { MaybeReturnType } from '../types'
|
|||||||
* @param props Optional props to pass to function.
|
* @param props Optional props to pass to function.
|
||||||
*/
|
*/
|
||||||
export default function callOrReturn<T>(value: T, context: any = undefined, ...props: any[]): MaybeReturnType<T> {
|
export default function callOrReturn<T>(value: T, context: any = undefined, ...props: any[]): MaybeReturnType<T> {
|
||||||
if (typeof value === 'function') {
|
if (isFunction(value)) {
|
||||||
if (context) {
|
if (context) {
|
||||||
return value.bind(context)(...props)
|
return value.bind(context)(...props)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
export default function isClass(item: any): boolean {
|
export default function isClass(value: any): boolean {
|
||||||
if (item.constructor?.toString().substring(0, 5) !== 'class') {
|
if (value.constructor?.toString().substring(0, 5) !== 'class') {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
export default function isEmptyObject(object = {}): boolean {
|
export default function isEmptyObject(value = {}): boolean {
|
||||||
return Object.keys(object).length === 0 && object.constructor === Object
|
return Object.keys(value).length === 0 && value.constructor === Object
|
||||||
}
|
}
|
||||||
|
3
packages/core/src/utilities/isFunction.ts
Normal file
3
packages/core/src/utilities/isFunction.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default function isObject(value: any): value is Function {
|
||||||
|
return typeof value === 'function'
|
||||||
|
}
|
@ -1,10 +1,10 @@
|
|||||||
import isClass from './isClass'
|
import isClass from './isClass'
|
||||||
|
|
||||||
export default function isObject(item: any): boolean {
|
export default function isObject(value: any): boolean {
|
||||||
return (
|
return (
|
||||||
item
|
value
|
||||||
&& typeof item === 'object'
|
&& typeof value === 'object'
|
||||||
&& !Array.isArray(item)
|
&& !Array.isArray(value)
|
||||||
&& !isClass(item)
|
&& !isClass(value)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
// see: https://github.com/mesqueeb/is-what/blob/88d6e4ca92fb2baab6003c54e02eedf4e729e5ab/src/index.ts
|
// see: https://github.com/mesqueeb/is-what/blob/88d6e4ca92fb2baab6003c54e02eedf4e729e5ab/src/index.ts
|
||||||
|
|
||||||
function getType(payload: any): string {
|
function getType(value: any): string {
|
||||||
return Object.prototype.toString.call(payload).slice(8, -1)
|
return Object.prototype.toString.call(value).slice(8, -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function isPlainObject(payload: any): payload is Record<string, any> {
|
export default function isPlainObject(value: any): value is Record<string, any> {
|
||||||
if (getType(payload) !== 'Object') return false
|
if (getType(value) !== 'Object') return false
|
||||||
return payload.constructor === Object && Object.getPrototypeOf(payload) === Object.prototype
|
return value.constructor === Object && Object.getPrototypeOf(value) === Object.prototype
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
export default function isRegExp(value: any): boolean {
|
export default function isRegExp(value: any): value is RegExp {
|
||||||
return Object.prototype.toString.call(value) === '[object RegExp]'
|
return Object.prototype.toString.call(value) === '[object RegExp]'
|
||||||
}
|
}
|
||||||
|
3
packages/core/src/utilities/isString.ts
Normal file
3
packages/core/src/utilities/isString.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default function isString(value: any): value is string {
|
||||||
|
return typeof value === 'string'
|
||||||
|
}
|
@ -23,9 +23,6 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tiptap/core": "^2.0.0-beta.1"
|
"@tiptap/core": "^2.0.0-beta.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
|
||||||
"prosemirror-inputrules": "^1.1.3"
|
|
||||||
},
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/ueberdosis/tiptap",
|
"url": "https://github.com/ueberdosis/tiptap",
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Node, mergeAttributes } from '@tiptap/core'
|
import { Node, mergeAttributes, wrappingInputRule } from '@tiptap/core'
|
||||||
import { wrappingInputRule } from 'prosemirror-inputrules'
|
|
||||||
|
|
||||||
export interface BlockquoteOptions {
|
export interface BlockquoteOptions {
|
||||||
HTMLAttributes: Record<string, any>,
|
HTMLAttributes: Record<string, any>,
|
||||||
@ -72,7 +71,10 @@ export const Blockquote = Node.create<BlockquoteOptions>({
|
|||||||
|
|
||||||
addInputRules() {
|
addInputRules() {
|
||||||
return [
|
return [
|
||||||
wrappingInputRule(inputRegex, this.type),
|
wrappingInputRule({
|
||||||
|
find: inputRegex,
|
||||||
|
type: this.type,
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -82,15 +82,27 @@ export const Bold = Mark.create<BoldOptions>({
|
|||||||
|
|
||||||
addInputRules() {
|
addInputRules() {
|
||||||
return [
|
return [
|
||||||
markInputRule(starInputRegex, this.type),
|
markInputRule({
|
||||||
markInputRule(underscoreInputRegex, this.type),
|
find: starInputRegex,
|
||||||
|
type: this.type,
|
||||||
|
}),
|
||||||
|
markInputRule({
|
||||||
|
find: underscoreInputRegex,
|
||||||
|
type: this.type,
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
addPasteRules() {
|
addPasteRules() {
|
||||||
return [
|
return [
|
||||||
markPasteRule(starPasteRegex, this.type),
|
markPasteRule({
|
||||||
markPasteRule(underscorePasteRegex, this.type),
|
find: starPasteRegex,
|
||||||
|
type: this.type,
|
||||||
|
}),
|
||||||
|
markPasteRule({
|
||||||
|
find: underscorePasteRegex,
|
||||||
|
type: this.type,
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -23,9 +23,6 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tiptap/core": "^2.0.0-beta.1"
|
"@tiptap/core": "^2.0.0-beta.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
|
||||||
"prosemirror-inputrules": "^1.1.3"
|
|
||||||
},
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/ueberdosis/tiptap",
|
"url": "https://github.com/ueberdosis/tiptap",
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Node, mergeAttributes } from '@tiptap/core'
|
import { Node, mergeAttributes, wrappingInputRule } from '@tiptap/core'
|
||||||
import { wrappingInputRule } from 'prosemirror-inputrules'
|
|
||||||
|
|
||||||
export interface BulletListOptions {
|
export interface BulletListOptions {
|
||||||
HTMLAttributes: Record<string, any>,
|
HTMLAttributes: Record<string, any>,
|
||||||
@ -55,7 +54,10 @@ export const BulletList = Node.create<BulletListOptions>({
|
|||||||
|
|
||||||
addInputRules() {
|
addInputRules() {
|
||||||
return [
|
return [
|
||||||
wrappingInputRule(inputRegex, this.type),
|
wrappingInputRule({
|
||||||
|
find: inputRegex,
|
||||||
|
type: this.type,
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -23,9 +23,6 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tiptap/core": "^2.0.0-beta.1"
|
"@tiptap/core": "^2.0.0-beta.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
|
||||||
"prosemirror-inputrules": "^1.1.3"
|
|
||||||
},
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/ueberdosis/tiptap",
|
"url": "https://github.com/ueberdosis/tiptap",
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Node } from '@tiptap/core'
|
import { Node, textblockTypeInputRule } from '@tiptap/core'
|
||||||
import { textblockTypeInputRule } from 'prosemirror-inputrules'
|
|
||||||
|
|
||||||
export interface CodeBlockOptions {
|
export interface CodeBlockOptions {
|
||||||
languageClassPrefix: string,
|
languageClassPrefix: string,
|
||||||
@ -21,8 +20,8 @@ declare module '@tiptap/core' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const backtickInputRegex = /^```(?<language>[a-z]*)? $/
|
export const backtickInputRegex = /^```(?<language>[a-z]*)?[\s\n]$/
|
||||||
export const tildeInputRegex = /^~~~(?<language>[a-z]*)? $/
|
export const tildeInputRegex = /^~~~(?<language>[a-z]*)?[\s\n]$/
|
||||||
|
|
||||||
export const CodeBlock = Node.create<CodeBlockOptions>({
|
export const CodeBlock = Node.create<CodeBlockOptions>({
|
||||||
name: 'codeBlock',
|
name: 'codeBlock',
|
||||||
@ -121,8 +120,16 @@ export const CodeBlock = Node.create<CodeBlockOptions>({
|
|||||||
|
|
||||||
addInputRules() {
|
addInputRules() {
|
||||||
return [
|
return [
|
||||||
textblockTypeInputRule(backtickInputRegex, this.type, ({ groups }: any) => groups),
|
textblockTypeInputRule({
|
||||||
textblockTypeInputRule(tildeInputRegex, this.type, ({ groups }: any) => groups),
|
find: backtickInputRegex,
|
||||||
|
type: this.type,
|
||||||
|
getAttributes: ({ groups }) => groups,
|
||||||
|
}),
|
||||||
|
textblockTypeInputRule({
|
||||||
|
find: tildeInputRegex,
|
||||||
|
type: this.type,
|
||||||
|
getAttributes: ({ groups }) => groups,
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -40,6 +40,8 @@ export const Code = Mark.create<CodeOptions>({
|
|||||||
|
|
||||||
excludes: '_',
|
excludes: '_',
|
||||||
|
|
||||||
|
code: true,
|
||||||
|
|
||||||
parseHTML() {
|
parseHTML() {
|
||||||
return [
|
return [
|
||||||
{ tag: 'code' },
|
{ tag: 'code' },
|
||||||
@ -72,13 +74,19 @@ export const Code = Mark.create<CodeOptions>({
|
|||||||
|
|
||||||
addInputRules() {
|
addInputRules() {
|
||||||
return [
|
return [
|
||||||
markInputRule(inputRegex, this.type),
|
markInputRule({
|
||||||
|
find: inputRegex,
|
||||||
|
type: this.type,
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
addPasteRules() {
|
addPasteRules() {
|
||||||
return [
|
return [
|
||||||
markPasteRule(pasteRegex, this.type),
|
markPasteRule({
|
||||||
|
find: pasteRegex,
|
||||||
|
type: this.type,
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -23,9 +23,6 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tiptap/core": "^2.0.0-beta.1"
|
"@tiptap/core": "^2.0.0-beta.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
|
||||||
"prosemirror-inputrules": "^1.1.3"
|
|
||||||
},
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/ueberdosis/tiptap",
|
"url": "https://github.com/ueberdosis/tiptap",
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Node, mergeAttributes } from '@tiptap/core'
|
import { Node, mergeAttributes, textblockTypeInputRule } from '@tiptap/core'
|
||||||
import { textblockTypeInputRule } from 'prosemirror-inputrules'
|
|
||||||
|
|
||||||
type Level = 1 | 2 | 3 | 4 | 5 | 6
|
type Level = 1 | 2 | 3 | 4 | 5 | 6
|
||||||
|
|
||||||
@ -93,7 +92,13 @@ export const Heading = Node.create<HeadingOptions>({
|
|||||||
|
|
||||||
addInputRules() {
|
addInputRules() {
|
||||||
return this.options.levels.map(level => {
|
return this.options.levels.map(level => {
|
||||||
return textblockTypeInputRule(new RegExp(`^(#{1,${level}})\\s$`), this.type, { level })
|
return textblockTypeInputRule({
|
||||||
|
find: new RegExp(`^(#{1,${level}})\\s$`),
|
||||||
|
type: this.type,
|
||||||
|
getAttributes: {
|
||||||
|
level,
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -97,13 +97,19 @@ export const Highlight = Mark.create<HighlightOptions>({
|
|||||||
|
|
||||||
addInputRules() {
|
addInputRules() {
|
||||||
return [
|
return [
|
||||||
markInputRule(inputRegex, this.type),
|
markInputRule({
|
||||||
|
find: inputRegex,
|
||||||
|
type: this.type,
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
addPasteRules() {
|
addPasteRules() {
|
||||||
return [
|
return [
|
||||||
markPasteRule(pasteRegex, this.type),
|
markPasteRule({
|
||||||
|
find: pasteRegex,
|
||||||
|
type: this.type,
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -92,7 +92,10 @@ export const HorizontalRule = Node.create<HorizontalRuleOptions>({
|
|||||||
|
|
||||||
addInputRules() {
|
addInputRules() {
|
||||||
return [
|
return [
|
||||||
nodeInputRule(/^(?:---|—-|___\s|\*\*\*\s)$/, this.type),
|
nodeInputRule({
|
||||||
|
find: /^(?:---|—-|___\s|\*\*\*\s)$/,
|
||||||
|
type: this.type,
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -79,10 +79,14 @@ export const Image = Node.create<ImageOptions>({
|
|||||||
|
|
||||||
addInputRules() {
|
addInputRules() {
|
||||||
return [
|
return [
|
||||||
nodeInputRule(inputRegex, this.type, match => {
|
nodeInputRule({
|
||||||
|
find: inputRegex,
|
||||||
|
type: this.type,
|
||||||
|
getAttributes: match => {
|
||||||
const [, alt, src, title] = match
|
const [, alt, src, title] = match
|
||||||
|
|
||||||
return { src, alt, title }
|
return { src, alt, title }
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -81,15 +81,27 @@ export const Italic = Mark.create<ItalicOptions>({
|
|||||||
|
|
||||||
addInputRules() {
|
addInputRules() {
|
||||||
return [
|
return [
|
||||||
markInputRule(starInputRegex, this.type),
|
markInputRule({
|
||||||
markInputRule(underscoreInputRegex, this.type),
|
find: starInputRegex,
|
||||||
|
type: this.type,
|
||||||
|
}),
|
||||||
|
markInputRule({
|
||||||
|
find: underscoreInputRegex,
|
||||||
|
type: this.type,
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
addPasteRules() {
|
addPasteRules() {
|
||||||
return [
|
return [
|
||||||
markPasteRule(starPasteRegex, this.type),
|
markPasteRule({
|
||||||
markPasteRule(underscorePasteRegex, this.type),
|
find: starPasteRegex,
|
||||||
|
type: this.type,
|
||||||
|
}),
|
||||||
|
markPasteRule({
|
||||||
|
find: underscorePasteRegex,
|
||||||
|
type: this.type,
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
"@tiptap/core": "^2.0.0-beta.1"
|
"@tiptap/core": "^2.0.0-beta.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"linkifyjs": "^3.0.1",
|
||||||
"prosemirror-state": "^1.3.4"
|
"prosemirror-state": "^1.3.4"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
mergeAttributes,
|
mergeAttributes,
|
||||||
} from '@tiptap/core'
|
} from '@tiptap/core'
|
||||||
import { Plugin, PluginKey } from 'prosemirror-state'
|
import { Plugin, PluginKey } from 'prosemirror-state'
|
||||||
|
import { find } from 'linkifyjs'
|
||||||
|
|
||||||
export interface LinkOptions {
|
export interface LinkOptions {
|
||||||
/**
|
/**
|
||||||
@ -39,16 +40,6 @@ declare module '@tiptap/core' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A regex that matches any string that contains a link
|
|
||||||
*/
|
|
||||||
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
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A regex that matches an url
|
|
||||||
*/
|
|
||||||
export const pasteRegexExact = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z]{2,}\b(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)(?:[-a-zA-Z0-9@:%._+~#=?!&/]*)$/gi
|
|
||||||
|
|
||||||
export const Link = Mark.create<LinkOptions>({
|
export const Link = Mark.create<LinkOptions>({
|
||||||
name: 'link',
|
name: 'link',
|
||||||
|
|
||||||
@ -102,7 +93,19 @@ export const Link = Mark.create<LinkOptions>({
|
|||||||
|
|
||||||
addPasteRules() {
|
addPasteRules() {
|
||||||
return [
|
return [
|
||||||
markPasteRule(pasteRegex, this.type, match => ({ href: match[0] })),
|
markPasteRule({
|
||||||
|
find: text => find(text)
|
||||||
|
.filter(link => link.isLink)
|
||||||
|
.map(link => ({
|
||||||
|
text: link.value,
|
||||||
|
index: link.start,
|
||||||
|
data: link,
|
||||||
|
})),
|
||||||
|
type: this.type,
|
||||||
|
getAttributes: match => ({
|
||||||
|
href: match.data?.href,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -151,12 +154,15 @@ export const Link = Mark.create<LinkOptions>({
|
|||||||
textContent += node.textContent
|
textContent += node.textContent
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!textContent || !textContent.match(pasteRegexExact)) {
|
const link = find(textContent)
|
||||||
|
.find(item => item.isLink && item.value === textContent)
|
||||||
|
|
||||||
|
if (!textContent || !link) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
this.editor.commands.setMark(this.type, {
|
this.editor.commands.setMark(this.type, {
|
||||||
href: textContent,
|
href: link.href,
|
||||||
})
|
})
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
@ -23,9 +23,6 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tiptap/core": "^2.0.0-beta.1"
|
"@tiptap/core": "^2.0.0-beta.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
|
||||||
"prosemirror-inputrules": "^1.1.3"
|
|
||||||
},
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/ueberdosis/tiptap",
|
"url": "https://github.com/ueberdosis/tiptap",
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Node, mergeAttributes } from '@tiptap/core'
|
import { Node, mergeAttributes, wrappingInputRule } from '@tiptap/core'
|
||||||
import { wrappingInputRule } from 'prosemirror-inputrules'
|
|
||||||
|
|
||||||
export interface OrderedListOptions {
|
export interface OrderedListOptions {
|
||||||
HTMLAttributes: Record<string, any>,
|
HTMLAttributes: Record<string, any>,
|
||||||
@ -74,12 +73,12 @@ export const OrderedList = Node.create<OrderedListOptions>({
|
|||||||
|
|
||||||
addInputRules() {
|
addInputRules() {
|
||||||
return [
|
return [
|
||||||
wrappingInputRule(
|
wrappingInputRule({
|
||||||
inputRegex,
|
find: inputRegex,
|
||||||
this.type,
|
type: this.type,
|
||||||
match => ({ start: +match[1] }),
|
getAttributes: match => ({ start: +match[1] }),
|
||||||
(match, node) => node.childCount + node.attrs.start === +match[1],
|
joinPredicate: (match, node) => node.childCount + node.attrs.start === +match[1],
|
||||||
),
|
}),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -83,13 +83,19 @@ export const Strike = Mark.create<StrikeOptions>({
|
|||||||
|
|
||||||
addInputRules() {
|
addInputRules() {
|
||||||
return [
|
return [
|
||||||
markInputRule(inputRegex, this.type),
|
markInputRule({
|
||||||
|
find: inputRegex,
|
||||||
|
type: this.type,
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
addPasteRules() {
|
addPasteRules() {
|
||||||
return [
|
return [
|
||||||
markPasteRule(pasteRegex, this.type),
|
markPasteRule({
|
||||||
|
find: pasteRegex,
|
||||||
|
type: this.type,
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -23,9 +23,6 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tiptap/core": "^2.0.0-beta.1"
|
"@tiptap/core": "^2.0.0-beta.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
|
||||||
"prosemirror-inputrules": "^1.1.3"
|
|
||||||
},
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/ueberdosis/tiptap",
|
"url": "https://github.com/ueberdosis/tiptap",
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Node, mergeAttributes } from '@tiptap/core'
|
import { Node, mergeAttributes, wrappingInputRule } from '@tiptap/core'
|
||||||
import { wrappingInputRule } from 'prosemirror-inputrules'
|
|
||||||
|
|
||||||
export interface TaskItemOptions {
|
export interface TaskItemOptions {
|
||||||
nested: boolean,
|
nested: boolean,
|
||||||
@ -146,13 +145,13 @@ export const TaskItem = Node.create<TaskItemOptions>({
|
|||||||
|
|
||||||
addInputRules() {
|
addInputRules() {
|
||||||
return [
|
return [
|
||||||
wrappingInputRule(
|
wrappingInputRule({
|
||||||
inputRegex,
|
find: inputRegex,
|
||||||
this.type,
|
type: this.type,
|
||||||
match => ({
|
getAttributes: match => ({
|
||||||
checked: match[match.length - 1] === 'x',
|
checked: match[match.length - 1] === 'x',
|
||||||
}),
|
}),
|
||||||
),
|
}),
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -23,9 +23,6 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tiptap/core": "^2.0.0-beta.1"
|
"@tiptap/core": "^2.0.0-beta.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
|
||||||
"prosemirror-inputrules": "^1.1.3"
|
|
||||||
},
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/ueberdosis/tiptap",
|
"url": "https://github.com/ueberdosis/tiptap",
|
||||||
|
@ -1,29 +1,109 @@
|
|||||||
import { Extension } from '@tiptap/core'
|
import { Extension, textInputRule } from '@tiptap/core'
|
||||||
import {
|
|
||||||
emDash,
|
|
||||||
ellipsis,
|
|
||||||
openDoubleQuote,
|
|
||||||
closeDoubleQuote,
|
|
||||||
openSingleQuote,
|
|
||||||
closeSingleQuote,
|
|
||||||
InputRule,
|
|
||||||
} from 'prosemirror-inputrules'
|
|
||||||
|
|
||||||
export const leftArrow = new InputRule(/<-$/, '←')
|
export const emDash = textInputRule({
|
||||||
export const rightArrow = new InputRule(/->$/, '→')
|
find: /--$/,
|
||||||
export const copyright = new InputRule(/\(c\)$/, '©')
|
replace: '—',
|
||||||
export const trademark = new InputRule(/\(tm\)$/, '™')
|
})
|
||||||
export const registeredTrademark = new InputRule(/\(r\)$/, '®')
|
|
||||||
export const oneHalf = new InputRule(/1\/2$/, '½')
|
export const ellipsis = textInputRule({
|
||||||
export const plusMinus = new InputRule(/\+\/-$/, '±')
|
find: /\.\.\.$/,
|
||||||
export const notEqual = new InputRule(/!=$/, '≠')
|
replace: '…',
|
||||||
export const laquo = new InputRule(/<<$/, '«')
|
})
|
||||||
export const raquo = new InputRule(/>>$/, '»')
|
|
||||||
export const multiplication = new InputRule(/\d+\s?([*x])\s?\d+$/, '×')
|
export const openDoubleQuote = textInputRule({
|
||||||
export const superscriptTwo = new InputRule(/\^2$/, '²')
|
find: /(?:^|[\s{[(<'"\u2018\u201C])(")$/,
|
||||||
export const superscriptThree = new InputRule(/\^3$/, '³')
|
replace: '“',
|
||||||
export const oneQuarter = new InputRule(/1\/4$/, '¼')
|
})
|
||||||
export const threeQuarters = new InputRule(/3\/4$/, '¾')
|
|
||||||
|
export const closeDoubleQuote = textInputRule({
|
||||||
|
find: /"$/,
|
||||||
|
replace: '”',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const openSingleQuote = textInputRule({
|
||||||
|
find: /(?:^|[\s{[(<'"\u2018\u201C])(')$/,
|
||||||
|
replace: '‘',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const closeSingleQuote = textInputRule({
|
||||||
|
find: /'$/,
|
||||||
|
replace: '’',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const leftArrow = textInputRule({
|
||||||
|
find: /<-$/,
|
||||||
|
replace: '←',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const rightArrow = textInputRule({
|
||||||
|
find: /->$/,
|
||||||
|
replace: '→',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const copyright = textInputRule({
|
||||||
|
find: /\(c\)$/,
|
||||||
|
replace: '©',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const trademark = textInputRule({
|
||||||
|
find: /\(tm\)$/,
|
||||||
|
replace: '™',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const registeredTrademark = textInputRule({
|
||||||
|
find: /\(r\)$/,
|
||||||
|
replace: '®',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const oneHalf = textInputRule({
|
||||||
|
find: /1\/2$/,
|
||||||
|
replace: '½',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const plusMinus = textInputRule({
|
||||||
|
find: /\+\/-$/,
|
||||||
|
replace: '±',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const notEqual = textInputRule({
|
||||||
|
find: /!=$/,
|
||||||
|
replace: '≠',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const laquo = textInputRule({
|
||||||
|
find: /<<$/,
|
||||||
|
replace: '«',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const raquo = textInputRule({
|
||||||
|
find: />>$/,
|
||||||
|
replace: '»',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const multiplication = textInputRule({
|
||||||
|
find: /\d+\s?([*x])\s?\d+$/,
|
||||||
|
replace: '×',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const superscriptTwo = textInputRule({
|
||||||
|
find: /\^2$/,
|
||||||
|
replace: '²',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const superscriptThree = textInputRule({
|
||||||
|
find: /\^3$/,
|
||||||
|
replace: '³',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const oneQuarter = textInputRule({
|
||||||
|
find: /1\/4$/,
|
||||||
|
replace: '¼',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const threeQuarters = textInputRule({
|
||||||
|
find: /3\/4$/,
|
||||||
|
replace: '¾',
|
||||||
|
})
|
||||||
|
|
||||||
export const Typography = Extension.create({
|
export const Typography = Extension.create({
|
||||||
name: 'typography',
|
name: 'typography',
|
||||||
|
@ -1,51 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
import { pasteRegex } from '@tiptap/extension-link'
|
|
||||||
|
|
||||||
describe('link paste rules', () => {
|
|
||||||
const validUrls = [
|
|
||||||
'https://example.com',
|
|
||||||
'https://example.com/with-path',
|
|
||||||
'http://example.com/with-http',
|
|
||||||
'https://www.example.com/with-www',
|
|
||||||
'https://www.example.com/with-numbers-123',
|
|
||||||
'https://www.example.com/with-parameters?var=true',
|
|
||||||
'https://www.example.com/with-multiple-parameters?var=true&foo=bar',
|
|
||||||
'https://www.example.com/with-spaces?var=true&foo=bar+3',
|
|
||||||
'https://www.example.com/with,comma',
|
|
||||||
'https://www.example.com/with(brackets)',
|
|
||||||
'https://www.example.com/with!exclamation!marks',
|
|
||||||
'http://thelongestdomainnameintheworldandthensomeandthensomemoreandmore.com/',
|
|
||||||
'https://example.longtopleveldomain',
|
|
||||||
'https://example-with-dashes.com',
|
|
||||||
]
|
|
||||||
|
|
||||||
validUrls.forEach(url => {
|
|
||||||
it(`paste regex matches url: ${url}`, {
|
|
||||||
// every second test fails, but the second try succeeds
|
|
||||||
retries: {
|
|
||||||
runMode: 2,
|
|
||||||
openMode: 2,
|
|
||||||
},
|
|
||||||
}, () => {
|
|
||||||
// TODO: Check the regex capture group to see *what* is matched
|
|
||||||
expect(url).to.match(pasteRegex)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const invalidUrls = [
|
|
||||||
'ftp://www.example.com',
|
|
||||||
]
|
|
||||||
|
|
||||||
invalidUrls.forEach(url => {
|
|
||||||
it(`paste regex doesn’t match url: ${url}`, {
|
|
||||||
// every second test fails, but the second try succeeds
|
|
||||||
retries: {
|
|
||||||
runMode: 2,
|
|
||||||
openMode: 2,
|
|
||||||
},
|
|
||||||
}, () => {
|
|
||||||
expect(url).to.not.match(pasteRegex)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
21
yarn.lock
21
yarn.lock
@ -2100,14 +2100,6 @@
|
|||||||
"@types/prosemirror-model" "*"
|
"@types/prosemirror-model" "*"
|
||||||
"@types/prosemirror-state" "*"
|
"@types/prosemirror-state" "*"
|
||||||
|
|
||||||
"@types/prosemirror-inputrules@^1.0.4":
|
|
||||||
version "1.0.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/prosemirror-inputrules/-/prosemirror-inputrules-1.0.4.tgz#4cb75054d954aa0f6f42099be05eb6c0e6958bae"
|
|
||||||
integrity sha512-lJIMpOjO47SYozQybUkpV6QmfuQt7GZKHtVrvS+mR5UekA8NMC5HRIVMyaIauJLWhKU6oaNjpVaXdw41kh165g==
|
|
||||||
dependencies:
|
|
||||||
"@types/prosemirror-model" "*"
|
|
||||||
"@types/prosemirror-state" "*"
|
|
||||||
|
|
||||||
"@types/prosemirror-keymap@^1.0.4":
|
"@types/prosemirror-keymap@^1.0.4":
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/prosemirror-keymap/-/prosemirror-keymap-1.0.4.tgz#f73c79810e8d0e0a20d153d84f998f02e5afbc0c"
|
resolved "https://registry.yarnpkg.com/@types/prosemirror-keymap/-/prosemirror-keymap-1.0.4.tgz#f73c79810e8d0e0a20d153d84f998f02e5afbc0c"
|
||||||
@ -5922,6 +5914,11 @@ lines-and-columns@^1.1.6:
|
|||||||
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
|
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
|
||||||
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
|
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
|
||||||
|
|
||||||
|
linkifyjs@^3.0.1:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-3.0.1.tgz#fda7b8d399eceef6fd7427f8a6e5d4f962ae74ed"
|
||||||
|
integrity sha512-HwXVwdNH1wESBfo2sH7Bkl+ywzbGA3+uJEfhquCyi/bMCa49bFUvd/re1NT1Lox/5jdnpQXzI9O/jykit71idg==
|
||||||
|
|
||||||
listr2@^3.8.3:
|
listr2@^3.8.3:
|
||||||
version "3.12.2"
|
version "3.12.2"
|
||||||
resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.12.2.tgz#2d55cc627111603ad4768a9e87c9c7bb9b49997e"
|
resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.12.2.tgz#2d55cc627111603ad4768a9e87c9c7bb9b49997e"
|
||||||
@ -7163,14 +7160,6 @@ prosemirror-history@^1.2.0:
|
|||||||
prosemirror-transform "^1.0.0"
|
prosemirror-transform "^1.0.0"
|
||||||
rope-sequence "^1.3.0"
|
rope-sequence "^1.3.0"
|
||||||
|
|
||||||
prosemirror-inputrules@^1.1.3:
|
|
||||||
version "1.1.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.1.3.tgz#93f9199ca02473259c30d7e352e4c14022d54638"
|
|
||||||
integrity sha512-ZaHCLyBtvbyIHv0f5p6boQTIJjlD6o2NPZiEaZWT2DA+j591zS29QQEMT4lBqwcLW3qRSf7ZvoKNbf05YrsStw==
|
|
||||||
dependencies:
|
|
||||||
prosemirror-state "^1.0.0"
|
|
||||||
prosemirror-transform "^1.0.0"
|
|
||||||
|
|
||||||
prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.1.2, prosemirror-keymap@^1.1.3:
|
prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.1.2, prosemirror-keymap@^1.1.3:
|
||||||
version "1.1.4"
|
version "1.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.1.4.tgz#8b481bf8389a5ac40d38dbd67ec3da2c7eac6a6d"
|
resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.1.4.tgz#8b481bf8389a5ac40d38dbd67ec3da2c7eac6a6d"
|
||||||
|
Loading…
Reference in New Issue
Block a user