fix: direct markdown editing even visually
This commit is contained in:
70
package-lock.json
generated
70
package-lock.json
generated
@@ -19,7 +19,6 @@
|
|||||||
"@tiptap/pm": "^3.19.0",
|
"@tiptap/pm": "^3.19.0",
|
||||||
"@tiptap/react": "^3.19.0",
|
"@tiptap/react": "^3.19.0",
|
||||||
"@tiptap/starter-kit": "^3.19.0",
|
"@tiptap/starter-kit": "^3.19.0",
|
||||||
"@types/turndown": "^5.0.6",
|
|
||||||
"chokidar": "^5.0.0",
|
"chokidar": "^5.0.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"drizzle-orm": "^0.29.0",
|
"drizzle-orm": "^0.29.0",
|
||||||
@@ -32,7 +31,7 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hot-toast": "^2.6.0",
|
"react-hot-toast": "^2.6.0",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"turndown": "^7.2.2",
|
"tiptap-markdown": "^0.9.0",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"zustand": "^4.4.7"
|
"zustand": "^4.4.7"
|
||||||
},
|
},
|
||||||
@@ -3245,12 +3244,6 @@
|
|||||||
"node": ">= 10.0.0"
|
"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": {
|
"node_modules/@monaco-editor/loader": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz",
|
||||||
@@ -4571,12 +4564,6 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"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": {
|
"node_modules/@types/use-sync-external-store": {
|
||||||
"version": "0.0.6",
|
"version": "0.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
"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"
|
"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": {
|
"node_modules/marked": {
|
||||||
"version": "14.0.0",
|
"version": "14.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
|
||||||
@@ -11459,6 +11452,46 @@
|
|||||||
"node": ">=14.0.0"
|
"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": {
|
"node_modules/tldts": {
|
||||||
"version": "7.0.23",
|
"version": "7.0.23",
|
||||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz",
|
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz",
|
||||||
@@ -12039,15 +12072,6 @@
|
|||||||
"@esbuild/win32-x64": "0.27.3"
|
"@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": {
|
"node_modules/type": {
|
||||||
"version": "2.7.3",
|
"version": "2.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
|
||||||
|
|||||||
@@ -62,7 +62,6 @@
|
|||||||
"@tiptap/pm": "^3.19.0",
|
"@tiptap/pm": "^3.19.0",
|
||||||
"@tiptap/react": "^3.19.0",
|
"@tiptap/react": "^3.19.0",
|
||||||
"@tiptap/starter-kit": "^3.19.0",
|
"@tiptap/starter-kit": "^3.19.0",
|
||||||
"@types/turndown": "^5.0.6",
|
|
||||||
"chokidar": "^5.0.0",
|
"chokidar": "^5.0.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"drizzle-orm": "^0.29.0",
|
"drizzle-orm": "^0.29.0",
|
||||||
@@ -75,7 +74,7 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hot-toast": "^2.6.0",
|
"react-hot-toast": "^2.6.0",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"turndown": "^7.2.2",
|
"tiptap-markdown": "^0.9.0",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"zustand": "^4.4.7"
|
"zustand": "^4.4.7"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,82 +6,15 @@ import Image from '@tiptap/extension-image';
|
|||||||
import Placeholder from '@tiptap/extension-placeholder';
|
import Placeholder from '@tiptap/extension-placeholder';
|
||||||
import Link from '@tiptap/extension-link';
|
import Link from '@tiptap/extension-link';
|
||||||
import Underline from '@tiptap/extension-underline';
|
import Underline from '@tiptap/extension-underline';
|
||||||
import TurndownService from 'turndown';
|
import { Markdown } from 'tiptap-markdown';
|
||||||
import './WysiwygEditor.css';
|
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 {
|
interface WysiwygEditorProps {
|
||||||
content: string;
|
content: string;
|
||||||
onChange: (markdown: string) => void;
|
onChange: (markdown: string) => void;
|
||||||
placeholder?: string;
|
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> = ({
|
export const WysiwygEditor: React.FC<WysiwygEditorProps> = ({
|
||||||
content,
|
content,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -113,12 +46,17 @@ export const WysiwygEditor: React.FC<WysiwygEditorProps> = ({
|
|||||||
Placeholder.configure({
|
Placeholder.configure({
|
||||||
placeholder,
|
placeholder,
|
||||||
}),
|
}),
|
||||||
|
Markdown.configure({
|
||||||
|
html: false,
|
||||||
|
bulletListMarker: '-',
|
||||||
|
transformPastedText: true,
|
||||||
|
transformCopiedText: true,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
content: markdownToHtml(content),
|
content,
|
||||||
onUpdate: ({ editor }) => {
|
onUpdate: ({ editor }) => {
|
||||||
isInternalChange.current = true;
|
isInternalChange.current = true;
|
||||||
const html = editor.getHTML();
|
const markdown = editor.storage.markdown.getMarkdown();
|
||||||
const markdown = turndownService.turndown(html);
|
|
||||||
onChange(markdown);
|
onChange(markdown);
|
||||||
},
|
},
|
||||||
editable: true,
|
editable: true,
|
||||||
@@ -129,8 +67,7 @@ export const WysiwygEditor: React.FC<WysiwygEditorProps> = ({
|
|||||||
if (editor && content !== lastExternalContent.current) {
|
if (editor && content !== lastExternalContent.current) {
|
||||||
// This is an external change (e.g., switching posts)
|
// This is an external change (e.g., switching posts)
|
||||||
if (!isInternalChange.current) {
|
if (!isInternalChange.current) {
|
||||||
const newHtml = markdownToHtml(content);
|
editor.commands.setContent(content);
|
||||||
editor.commands.setContent(newHtml);
|
|
||||||
}
|
}
|
||||||
lastExternalContent.current = content;
|
lastExternalContent.current = content;
|
||||||
isInternalChange.current = false;
|
isInternalChange.current = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user