fix: direct markdown editing even visually

This commit is contained in:
2026-02-10 18:09:07 +01:00
parent 30461c0c68
commit 85711bf205
3 changed files with 58 additions and 98 deletions

View File

@@ -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, '<h3>$1</h3>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
// Bold
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/__(.+?)__/g, '<strong>$1</strong>')
// Italic
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/_(.+?)_/g, '<em>$1</em>')
// Strikethrough
.replace(/~~(.+?)~~/g, '<del>$1</del>')
// Code blocks
.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code class="$1">$2</code></pre>')
// Inline code
.replace(/`([^`]+)`/g, '<code>$1</code>')
// Links
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
// Images
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" />')
// Blockquotes
.replace(/^> (.*$)/gim, '<blockquote>$1</blockquote>')
// Unordered lists
.replace(/^\s*[-*+] (.+)$/gim, '<li>$1</li>')
// Ordered lists
.replace(/^\d+\. (.+)$/gim, '<li>$1</li>')
// Horizontal rule
.replace(/^(?:---+|___+|\*\*\*+)$/gim, '<hr>')
// Paragraphs (double newlines)
.replace(/\n\n/g, '</p><p>')
// Single line breaks
.replace(/\n/g, '<br>');
// Wrap in paragraphs if not starting with a block element
if (!html.startsWith('<')) {
html = '<p>' + html + '</p>';
}
// Fix consecutive blockquotes
html = html.replace(/<\/blockquote>\s*<blockquote>/g, '<br>');
// Wrap list items in ul/ol
html = html.replace(/(<li>.*<\/li>)+/g, (match) => {
// Check if it's ordered (starts with number) or unordered
return '<ul>' + match + '</ul>';
});
return html;
}
export const WysiwygEditor: React.FC<WysiwygEditorProps> = ({
content,
onChange,
@@ -113,12 +46,17 @@ export const WysiwygEditor: React.FC<WysiwygEditorProps> = ({
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<WysiwygEditorProps> = ({
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;