From 85711bf20570b28a3a6d0bcd3ce5b908b1b5e026 Mon Sep 17 00:00:00 2001
From: hugo
Date: Tue, 10 Feb 2026 18:09:07 +0100
Subject: [PATCH] fix: direct markdown editing even visually
---
package-lock.json | 70 +++++++++++-----
package.json | 3 +-
.../WysiwygEditor/WysiwygEditor.tsx | 83 +++----------------
3 files changed, 58 insertions(+), 98 deletions(-)
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, '
')
- // 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;