From 28d5ae094bfba7a6e660342bf200911cf5a96825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Thu, 2 Apr 2020 08:53:59 +0200 Subject: [PATCH] add basic inputrules and pasterules --- packages/core/index.ts | 5 +- packages/core/src/Editor.ts | 2 + packages/core/src/ExtensionManager.ts | 12 ++++ packages/core/src/inputRules/markInputRule.ts | 60 +++++++++++++++++++ packages/core/src/pasteRules/markPasteRule.ts | 60 +++++++++++++++++++ packages/extension-bold/index.ts | 14 ++++- packages/extension-codeblock/index.ts | 23 +++++++ packages/extension-codeblock/package.json | 17 ++++++ packages/extension-italic/index.ts | 16 ++++- src/demos/HandleExtensions/index.vue | 4 +- 10 files changed, 209 insertions(+), 4 deletions(-) create mode 100644 packages/core/src/inputRules/markInputRule.ts create mode 100644 packages/core/src/pasteRules/markPasteRule.ts create mode 100644 packages/extension-codeblock/index.ts create mode 100644 packages/extension-codeblock/package.json diff --git a/packages/core/index.ts b/packages/core/index.ts index dc714f7a..71555208 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -4,4 +4,7 @@ export default Editor export { Editor } export { default as Extension } from './src/Extension' export { default as Node } from './src/Node' -export { default as Mark } from './src/Mark' \ No newline at end of file +export { default as Mark } from './src/Mark' + +export { default as markInputRule } from './src/inputRules/markInputRule' +export { default as markPasteRule } from './src/pasteRules/markPasteRule' \ No newline at end of file diff --git a/packages/core/src/Editor.ts b/packages/core/src/Editor.ts index 5b591571..f4a2b956 100644 --- a/packages/core/src/Editor.ts +++ b/packages/core/src/Editor.ts @@ -122,6 +122,8 @@ export class Editor extends EventEmitter { return [ ...this.extensionManager.plugins, ...this.extensionManager.keymaps, + ...this.extensionManager.pasteRules, + inputRules({ rules: this.extensionManager.inputRules }), keymap({ Backspace: undoInputRule }), keymap(baseKeymap), dropCursor(), diff --git a/packages/core/src/ExtensionManager.ts b/packages/core/src/ExtensionManager.ts index 5bcf172c..7b3dec90 100644 --- a/packages/core/src/ExtensionManager.ts +++ b/packages/core/src/ExtensionManager.ts @@ -48,6 +48,18 @@ export default class ExtensionManager { .toArray() } + get inputRules(): any { + return collect(this.extensions) + .flatMap(extension => extension.inputRules()) + .toArray() + } + + get pasteRules(): any { + return collect(this.extensions) + .flatMap(extension => extension.pasteRules()) + .toArray() + } + get keymaps() { return collect(this.extensions) .map(extension => extension.keys()) diff --git a/packages/core/src/inputRules/markInputRule.ts b/packages/core/src/inputRules/markInputRule.ts new file mode 100644 index 00000000..fa99778b --- /dev/null +++ b/packages/core/src/inputRules/markInputRule.ts @@ -0,0 +1,60 @@ +import { InputRule } from 'prosemirror-inputrules' +import { EditorState } from 'prosemirror-state' +import { MarkType } from 'prosemirror-model' + +function getMarksBetween(start: number, end: number, state: EditorState) { + let marks: any[] = [] + + state.doc.nodesBetween(start, end, (node, pos) => { + marks = [...marks, ...node.marks.map(mark => ({ + start: pos, + end: pos + node.nodeSize, + mark, + }))] + }) + + return marks +} + +export default function (regexp: RegExp, markType: MarkType, getAttrs?: Function) { + return new InputRule(regexp, (state, match, start, end) => { + const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs + const { tr } = state + const m = match.length - 1 + let markEnd = end + let markStart = start + + if (match[m]) { + const matchStart = start + match[0].indexOf(match[m - 1]) + const matchEnd = matchStart + match[m - 1].length - 1 + const textStart = matchStart + match[m - 1].lastIndexOf(match[m]) + const textEnd = textStart + match[m].length + + const excludedMarks = getMarksBetween(start, end, state) + .filter(item => { + const { excluded } = item.mark.type + return excluded.find((type: MarkType) => type.name === markType.name) + }) + .filter(item => item.end > matchStart) + + if (excludedMarks.length) { + return null + } + + if (textEnd < matchEnd) { + tr.delete(textEnd, matchEnd) + } + + if (textStart > matchStart) { + tr.delete(matchStart, textStart) + } + + markStart = matchStart + markEnd = markStart + match[m].length + } + + tr.addMark(markStart, markEnd, markType.create(attrs)) + tr.removeStoredMark(markType) + return tr + }) +} diff --git a/packages/core/src/pasteRules/markPasteRule.ts b/packages/core/src/pasteRules/markPasteRule.ts new file mode 100644 index 00000000..e170b8b2 --- /dev/null +++ b/packages/core/src/pasteRules/markPasteRule.ts @@ -0,0 +1,60 @@ +import { Plugin } from 'prosemirror-state' +import { Slice, Fragment, MarkType } from 'prosemirror-model' + +export default function (regexp: RegExp, type: MarkType, getAttrs?: Function) { + + const handler = (fragment: Fragment, parent?: any) => { + const nodes: any[] = [] + + fragment.forEach(child => { + if (child.isText && child.text) { + const { text } = child + let pos = 0 + let match + + // eslint-disable-next-line + while ((match = regexp.exec(text)) !== null) { + if (parent.type.allowsMarkType(type) && match[1]) { + const start = match.index + const end = start + match[0].length + const textStart = start + match[0].indexOf(match[1]) + const textEnd = textStart + match[1].length + const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs + + // adding text before markdown to nodes + if (start > 0) { + nodes.push(child.cut(pos, start)) + } + + // adding the markdown part to nodes + nodes.push(child + .cut(textStart, textEnd) + // @ts-ignore + .mark(type.create(attrs).addToSet(child.marks))) + + pos = end + } + } + + // adding rest of text to nodes + if (pos < text.length) { + nodes.push(child.cut(pos)) + } + } else { + nodes.push(child.copy(handler(child.content, child))) + } + }) + + return Fragment.fromArray(nodes) + } + + return new Plugin({ + props: { + transformPasted: slice => { + console.log({slice}) + return new Slice(handler(slice.content), slice.openStart, slice.openEnd) + }, + }, + }) + +} diff --git a/packages/extension-bold/index.ts b/packages/extension-bold/index.ts index b9f03924..a49fec37 100644 --- a/packages/extension-bold/index.ts +++ b/packages/extension-bold/index.ts @@ -1,4 +1,4 @@ -import { Mark } from '@tiptap/core' +import { Mark, markInputRule, markPasteRule } from '@tiptap/core' import { toggleMark } from 'prosemirror-commands' import { MarkSpec } from 'prosemirror-model' @@ -44,4 +44,16 @@ export default class Bold extends Mark { } } + inputRules() { + return [ + markInputRule(/(?:\*\*|__)([^*_]+)(?:\*\*|__)$/, this.schemaType), + ] + } + + pasteRules() { + return [ + markPasteRule(/(?:\*\*|__)([^*_]+)(?:\*\*|__)/g, this.schemaType), + ] + } + } \ No newline at end of file diff --git a/packages/extension-codeblock/index.ts b/packages/extension-codeblock/index.ts new file mode 100644 index 00000000..bf6eb97c --- /dev/null +++ b/packages/extension-codeblock/index.ts @@ -0,0 +1,23 @@ +import { Node } from '@tiptap/core' +import { NodeSpec } from 'prosemirror-model' + +export default class CodeBlock extends Node { + + name = 'code_block' + + schema(): NodeSpec { + return { + content: 'text*', + marks: 'italic', + group: 'block', + code: true, + defining: true, + draggable: false, + parseDOM: [ + { tag: 'pre', preserveWhitespace: 'full' }, + ], + toDOM: () => ['pre', ['code', 0]], + } + } + +} \ No newline at end of file diff --git a/packages/extension-codeblock/package.json b/packages/extension-codeblock/package.json new file mode 100644 index 00000000..05f4ce9f --- /dev/null +++ b/packages/extension-codeblock/package.json @@ -0,0 +1,17 @@ +{ + "name": "@tiptap/extension-codeblock", + "version": "1.0.0", + "source": "index.ts", + "main": "dist/tiptap-extension-codeblock.js", + "umd:main": "dist/tiptap-extension-codeblock.umd.js", + "module": "dist/tiptap-extension-codeblock.mjs", + "unpkg": "dist/tiptap-extension-codeblock.js", + "jsdelivr": "dist/tiptap-extension-codeblock.js", + "files": [ + "src", + "dist" + ], + "peerDependencies": { + "@tiptap/core": "2.x" + } +} diff --git a/packages/extension-italic/index.ts b/packages/extension-italic/index.ts index f2909768..14a905d7 100644 --- a/packages/extension-italic/index.ts +++ b/packages/extension-italic/index.ts @@ -1,4 +1,4 @@ -import { Mark } from '@tiptap/core' +import { Mark, markInputRule, markPasteRule } from '@tiptap/core' import { toggleMark } from 'prosemirror-commands' import { MarkSpec } from 'prosemirror-model' @@ -36,4 +36,18 @@ export default class Italic extends Mark { } } + inputRules() { + return [ + markInputRule(/(?:^|[^_])(_([^_]+)_)$/, this.schemaType), + // markInputRule(/(?:^|[^*])(\*([^*]+)\*)$/, this.schemaType), + ] + } + + pasteRules() { + return [ + markPasteRule(/_([^_]+)_/g, this.schemaType), + // markPasteRule(/\*([^*]+)\*/g, this.schemaType), + ] + } + } \ No newline at end of file diff --git a/src/demos/HandleExtensions/index.vue b/src/demos/HandleExtensions/index.vue index 6b0aca84..84de33ff 100644 --- a/src/demos/HandleExtensions/index.vue +++ b/src/demos/HandleExtensions/index.vue @@ -27,6 +27,7 @@ import Text from '@tiptap/extension-text' import History from '@tiptap/extension-history' import Bold from '@tiptap/extension-bold' import Italic from '@tiptap/extension-italic' +import CodeBlock from '@tiptap/extension-codeblock' export default { components: { @@ -41,11 +42,12 @@ export default { mounted() { this.editor = new Editor({ - content: '

foo

', + content: '

foo

code **bold** _italic_
', extensions: [ new Document(), new Paragraph(), new Text(), + new CodeBlock(), new History(), new Bold(), new Italic(),