diff --git a/package-lock.json b/package-lock.json index a6e8d55d..8a48b87f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13918,6 +13918,23 @@ "@tiptap/pm": "^3.15.3" } }, + "node_modules/@tiptap/markdown": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/@tiptap/markdown/-/markdown-3.15.3.tgz", + "integrity": "sha512-JjjZ/X7H2+/Jeapk8GurbncJVyG9ai5YD/eJLBKDyWqsoQwsrHbDlYBx26q9J5VwWXzxe9G6fmHNlAfw0Pokow==", + "license": "MIT", + "dependencies": { + "marked": "^15.0.12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.15.3", + "@tiptap/pm": "^3.15.3" + } + }, "node_modules/@tiptap/pm": { "version": "3.15.3", "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.15.3.tgz", @@ -23969,6 +23986,18 @@ "markdown-it": "bin/markdown-it.mjs" } }, + "node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/marky": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", @@ -32679,6 +32708,7 @@ "@tiptap/extension-text": "^3.15.3", "@tiptap/extension-underline": "^3.15.3", "@tiptap/extensions": "^3.15.3", + "@tiptap/markdown": "^3.15.3", "@tiptap/pm": "^3.15.3", "@tiptap/react": "^3.15.3", "@tiptap/suggestion": "^3.15.3", diff --git a/packages/ui/package.json b/packages/ui/package.json index 0e0e3639..4a3f2fa2 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -66,6 +66,7 @@ "@tiptap/extension-text": "^3.15.3", "@tiptap/extension-underline": "^3.15.3", "@tiptap/extensions": "^3.15.3", + "@tiptap/markdown": "^3.15.3", "@tiptap/pm": "^3.15.3", "@tiptap/react": "^3.15.3", "@tiptap/suggestion": "^3.15.3", diff --git a/packages/ui/src/components/documents/document-editor.tsx b/packages/ui/src/components/documents/document-editor.tsx index af63ec4e..94f68941 100644 --- a/packages/ui/src/components/documents/document-editor.tsx +++ b/packages/ui/src/components/documents/document-editor.tsx @@ -83,6 +83,8 @@ import { DatabaseNode, AutoJoiner, HardBreakNode, + ParserExtension, + Markdown, } from '@colanode/ui/editor/extensions'; import { ToolbarMenu, ActionMenu } from '@colanode/ui/editor/menus'; @@ -170,6 +172,8 @@ export const DocumentEditor = ({ { extensions: [ IdExtension, + ParserExtension, + Markdown, DocumentNode, PageNode, FolderNode, diff --git a/packages/ui/src/editor/extensions/index.tsx b/packages/ui/src/editor/extensions/index.tsx index 09496f0a..ea5a4a80 100644 --- a/packages/ui/src/editor/extensions/index.tsx +++ b/packages/ui/src/editor/extensions/index.tsx @@ -27,11 +27,13 @@ import { IdExtension } from '@colanode/ui/editor/extensions/id'; import { LinkMark } from '@colanode/ui/editor/extensions/link'; import { ListItemNode } from '@colanode/ui/editor/extensions/list-item'; import { ListKeymapExtension } from '@colanode/ui/editor/extensions/list-keymap'; +import { Markdown } from '@colanode/ui/editor/extensions/markdown'; import { MentionExtension } from '@colanode/ui/editor/extensions/mention'; import { MessageNode } from '@colanode/ui/editor/extensions/message'; import { OrderedListNode } from '@colanode/ui/editor/extensions/ordered-list'; import { PageNode } from '@colanode/ui/editor/extensions/page'; import { ParagraphNode } from '@colanode/ui/editor/extensions/paragraph'; +import { ParserExtension } from '@colanode/ui/editor/extensions/parser'; import { PlaceholderExtension } from '@colanode/ui/editor/extensions/placeholder'; import { TabKeymapExtension } from '@colanode/ui/editor/extensions/tab-keymap'; import { TableNode } from '@colanode/ui/editor/extensions/table'; @@ -87,4 +89,6 @@ export { AutoJoiner, MentionExtension, HardBreakNode, + ParserExtension, + Markdown, }; diff --git a/packages/ui/src/editor/extensions/markdown.tsx b/packages/ui/src/editor/extensions/markdown.tsx new file mode 100644 index 00000000..7e55dcc2 --- /dev/null +++ b/packages/ui/src/editor/extensions/markdown.tsx @@ -0,0 +1,3 @@ +import { Markdown } from '@tiptap/markdown'; + +export { Markdown }; diff --git a/packages/ui/src/editor/extensions/parser.tsx b/packages/ui/src/editor/extensions/parser.tsx new file mode 100644 index 00000000..44a272d3 --- /dev/null +++ b/packages/ui/src/editor/extensions/parser.tsx @@ -0,0 +1,89 @@ +import { Plugin, PluginKey } from '@tiptap/pm/state'; +import { Extension, JSONContent } from '@tiptap/react'; + +export const ParserExtension = Extension.create({ + name: 'parser', + addOptions() { + return { + nodes: [], + }; + }, + addProseMirrorPlugins() { + const editor = this.editor; + const markdown = editor.markdown; + + if (!markdown) { + console.error('Markdown extension not found'); + return []; + } + + return [ + new Plugin({ + key: new PluginKey('parser'), + props: { + handlePaste(view, event) { + const html = event.clipboardData?.getData('text/html'); + if (html) { + return false; + } + + const text = event.clipboardData?.getData('text/plain'); + if (!text) { + return false; + } + + const parsedContent = markdown.parse(text); + if (!parsedContent || !parsedContent.content) { + return false; + } + + const blocks = parsedContent.content; + if (!blocks || blocks.length === 0) { + return false; + } + + const normalizedBlocks = normalizeHeadings(blocks); + editor.commands.insertContent(normalizedBlocks); + return true; + }, + }, + }), + ]; + }, +}); + +const normalizeHeadings = (content: JSONContent[]): JSONContent[] => { + return content.map(normalizeNode); +}; + +const normalizeNode = (node: JSONContent): JSONContent => { + if (node.type === 'heading' && node.attrs?.level) { + const level = node.attrs.level; + let newType: string; + + if (level === 1) { + newType = 'heading1'; + } else if (level === 2) { + newType = 'heading2'; + } else { + newType = 'heading3'; + } + + const { level: _, ...restAttrs } = node.attrs; + return { + ...node, + type: newType, + attrs: Object.keys(restAttrs).length > 0 ? restAttrs : undefined, + content: node.content ? node.content.map(normalizeNode) : undefined, + }; + } + + if (node.content) { + return { + ...node, + content: node.content.map(normalizeNode), + }; + } + + return node; +};