From f52a321acf19b8925a5285abf09ae3ed51ea4ca8 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 21 Feb 2025 15:53:47 +0000 Subject: [PATCH] feat: convert pasted markdown to html so that it is correctly rendered (#3041) Resolves https://community.vikunja.io/t/markdown-as-first-class-citizen/2975/4 Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/3041 Co-authored-by: kolaente Co-committed-by: kolaente --- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 10 +++++++ .../src/components/input/editor/TipTap.vue | 30 +++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/frontend/package.json b/frontend/package.json index b9655d987..b2f2f598b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -93,6 +93,7 @@ "is-touch-device": "1.0.1", "klona": "2.0.6", "lowlight": "3.3.0", + "marked": "^15.0.6", "pinia": "2.3.1", "register-service-worker": "1.7.2", "sortablejs": "1.15.6", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 9c2938fd8..d68f54e8c 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -148,6 +148,9 @@ importers: lowlight: specifier: 3.3.0 version: 3.3.0 + marked: + specifier: ^15.0.6 + version: 15.0.6 pinia: specifier: 2.3.1 version: 2.3.1(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)) @@ -4754,6 +4757,11 @@ packages: resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} hasBin: true + marked@15.0.6: + resolution: {integrity: sha512-Y07CUOE+HQXbVDCGl3LXggqJDbXDP2pArc2C1N1RRMN0ONiShoSsIInMd5Gsxupe7fKLpgimTV+HOJ9r7bA+pg==} + engines: {node: '>= 18'} + hasBin: true + mdn-data@2.0.28: resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} @@ -11719,6 +11727,8 @@ snapshots: punycode.js: 2.3.1 uc.micro: 2.1.0 + marked@15.0.6: {} + mdn-data@2.0.28: {} mdn-data@2.0.30: {} diff --git a/frontend/src/components/input/editor/TipTap.vue b/frontend/src/components/input/editor/TipTap.vue index e5bae4449..12a6893e7 100644 --- a/frontend/src/components/input/editor/TipTap.vue +++ b/frontend/src/components/input/editor/TipTap.vue @@ -146,6 +146,8 @@ import EditorToolbar from './EditorToolbar.vue' import StarterKit from '@tiptap/starter-kit' import {Extension, mergeAttributes} from '@tiptap/core' import {BubbleMenu, EditorContent, type Extensions, useEditor} from '@tiptap/vue-3' +import {Plugin, PluginKey} from '@tiptap/pm/state' +import {marked} from 'marked' import Link from '@tiptap/extension-link' import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight' @@ -327,6 +329,32 @@ const additionalLinkProtocols = [ 'notion', ] +const MarkdownPasteHandler = Extension.create({ + name: 'markdownPasteHandler', + + addProseMirrorPlugins() { + return [ + new Plugin({ + key: new PluginKey('markdownPasteHandler'), + props: { + handlePaste: (view, event) => { + const text = event.clipboardData?.getData('text/plain') + if (!text) return false + + const html = marked.parse(text) + + // It is fine to paste the content without sanitizing because it will be sanitized later by TipTap + this.editor.commands.insertContent(html) + // https://github.com/ueberdosis/tiptap/discussions/4118#discussioncomment-8931999 + return true + }, + }, + }), + ] + }, +}) + + const extensions : Extensions = [ // Starterkit: StarterKit.configure({ @@ -418,6 +446,8 @@ const extensions : Extensions = [ Commands.configure({ suggestion: suggestionSetup(t), }), + + MarkdownPasteHandler, ] // Add a custom extension for the Escape key