diff --git a/package-lock.json b/package-lock.json index 069ca5e..831fa1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,6 @@ "@tiptap/pm": "^3.19.0", "@tiptap/react": "^3.19.0", "@tiptap/starter-kit": "^3.19.0", - "@types/turndown": "^5.0.6", "chokidar": "^5.0.0", "date-fns": "^4.1.0", "drizzle-orm": "^0.29.0", @@ -32,7 +31,7 @@ "react-dom": "^18.2.0", "react-hot-toast": "^2.6.0", "sharp": "^0.34.5", - "turndown": "^7.2.2", + "tiptap-markdown": "^0.9.0", "uuid": "^9.0.1", "zustand": "^4.4.7" }, @@ -3245,12 +3244,6 @@ "node": ">= 10.0.0" } }, - "node_modules/@mixmark-io/domino": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz", - "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==", - "license": "BSD-2-Clause" - }, "node_modules/@monaco-editor/loader": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", @@ -4571,12 +4564,6 @@ "license": "MIT", "optional": true }, - "node_modules/@types/turndown": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.6.tgz", - "integrity": "sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg==", - "license": "MIT" - }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", @@ -9246,6 +9233,12 @@ "markdown-it": "bin/markdown-it.mjs" } }, + "node_modules/markdown-it-task-lists": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz", + "integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==", + "license": "ISC" + }, "node_modules/marked": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", @@ -11459,6 +11452,46 @@ "node": ">=14.0.0" } }, + "node_modules/tiptap-markdown": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/tiptap-markdown/-/tiptap-markdown-0.9.0.tgz", + "integrity": "sha512-dKLQ9iiuGNgrlGVjrNauF/UBzWu4LYOx5pkD0jNkmQt/GOwfCJsBuzZTsf1jZ204ANHOm572mZ9PYvGh1S7tpQ==", + "license": "MIT", + "workspaces": [ + "example" + ], + "dependencies": { + "@types/markdown-it": "^13.0.7", + "markdown-it": "^14.1.0", + "markdown-it-task-lists": "^2.1.1", + "prosemirror-markdown": "^1.11.1" + }, + "peerDependencies": { + "@tiptap/core": "^3.0.1" + } + }, + "node_modules/tiptap-markdown/node_modules/@types/linkify-it": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz", + "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==", + "license": "MIT" + }, + "node_modules/tiptap-markdown/node_modules/@types/markdown-it": { + "version": "13.0.9", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-13.0.9.tgz", + "integrity": "sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==", + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^3", + "@types/mdurl": "^1" + } + }, + "node_modules/tiptap-markdown/node_modules/@types/mdurl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz", + "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==", + "license": "MIT" + }, "node_modules/tldts": { "version": "7.0.23", "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz", @@ -12039,15 +12072,6 @@ "@esbuild/win32-x64": "0.27.3" } }, - "node_modules/turndown": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.2.tgz", - "integrity": "sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ==", - "license": "MIT", - "dependencies": { - "@mixmark-io/domino": "^2.2.0" - } - }, "node_modules/type": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", diff --git a/package.json b/package.json index 00d5023..bf9ce53 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,6 @@ "@tiptap/pm": "^3.19.0", "@tiptap/react": "^3.19.0", "@tiptap/starter-kit": "^3.19.0", - "@types/turndown": "^5.0.6", "chokidar": "^5.0.0", "date-fns": "^4.1.0", "drizzle-orm": "^0.29.0", @@ -75,7 +74,7 @@ "react-dom": "^18.2.0", "react-hot-toast": "^2.6.0", "sharp": "^0.34.5", - "turndown": "^7.2.2", + "tiptap-markdown": "^0.9.0", "uuid": "^9.0.1", "zustand": "^4.4.7" }, diff --git a/src/renderer/components/WysiwygEditor/WysiwygEditor.tsx b/src/renderer/components/WysiwygEditor/WysiwygEditor.tsx index 4c440a8..58670bd 100644 --- a/src/renderer/components/WysiwygEditor/WysiwygEditor.tsx +++ b/src/renderer/components/WysiwygEditor/WysiwygEditor.tsx @@ -6,82 +6,15 @@ import Image from '@tiptap/extension-image'; import Placeholder from '@tiptap/extension-placeholder'; import Link from '@tiptap/extension-link'; import Underline from '@tiptap/extension-underline'; -import TurndownService from 'turndown'; +import { Markdown } from 'tiptap-markdown'; import './WysiwygEditor.css'; -// Convert HTML to Markdown -const turndownService = new TurndownService({ - headingStyle: 'atx', - codeBlockStyle: 'fenced', - bulletListMarker: '-', -}); - -// Add custom rules for turndown -turndownService.addRule('strikethrough', { - filter: ['del', 's', 'strike'], - replacement: (content) => `~~${content}~~`, -}); - interface WysiwygEditorProps { content: string; onChange: (markdown: string) => void; placeholder?: string; } -// Simple markdown to HTML converter for TipTap -function markdownToHtml(markdown: string): string { - // Simple markdown parser - for production use a proper library - let html = markdown - // Headers - .replace(/^### (.*$)/gim, '

$1

') - .replace(/^## (.*$)/gim, '

$1

') - .replace(/^# (.*$)/gim, '

$1

') - // Bold - .replace(/\*\*(.+?)\*\*/g, '$1') - .replace(/__(.+?)__/g, '$1') - // Italic - .replace(/\*(.+?)\*/g, '$1') - .replace(/_(.+?)_/g, '$1') - // Strikethrough - .replace(/~~(.+?)~~/g, '$1') - // Code blocks - .replace(/```(\w*)\n([\s\S]*?)```/g, '
$2
') - // Inline code - .replace(/`([^`]+)`/g, '$1') - // Links - .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') - // Images - .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$1') - // Blockquotes - .replace(/^> (.*$)/gim, '
$1
') - // Unordered lists - .replace(/^\s*[-*+] (.+)$/gim, '
  • $1
  • ') - // Ordered lists - .replace(/^\d+\. (.+)$/gim, '
  • $1
  • ') - // Horizontal rule - .replace(/^(?:---+|___+|\*\*\*+)$/gim, '
    ') - // Paragraphs (double newlines) - .replace(/\n\n/g, '

    ') - // Single line breaks - .replace(/\n/g, '
    '); - - // Wrap in paragraphs if not starting with a block element - if (!html.startsWith('<')) { - html = '

    ' + html + '

    '; - } - - // Fix consecutive blockquotes - html = html.replace(/<\/blockquote>\s*
    /g, '
    '); - - // Wrap list items in ul/ol - html = html.replace(/(
  • .*<\/li>)+/g, (match) => { - // Check if it's ordered (starts with number) or unordered - return ''; - }); - - return html; -} - export const WysiwygEditor: React.FC = ({ content, onChange, @@ -113,12 +46,17 @@ export const WysiwygEditor: React.FC = ({ Placeholder.configure({ placeholder, }), + Markdown.configure({ + html: false, + bulletListMarker: '-', + transformPastedText: true, + transformCopiedText: true, + }), ], - content: markdownToHtml(content), + content, onUpdate: ({ editor }) => { isInternalChange.current = true; - const html = editor.getHTML(); - const markdown = turndownService.turndown(html); + const markdown = editor.storage.markdown.getMarkdown(); onChange(markdown); }, editable: true, @@ -129,8 +67,7 @@ export const WysiwygEditor: React.FC = ({ if (editor && content !== lastExternalContent.current) { // This is an external change (e.g., switching posts) if (!isInternalChange.current) { - const newHtml = markdownToHtml(content); - editor.commands.setContent(newHtml); + editor.commands.setContent(content); } lastExternalContent.current = content; isInternalChange.current = false;