feat: Complete markdown syntax refinement and variable protection
✅ New cleaner syntax implemented: - [card:type] instead of [card type='type'] - [button:style](url)Text[/button] instead of [button url='...' style='...'] - Standard markdown images:  ✅ Variable protection from markdown parsing: - Variables with underscores (e.g., {order_items_table}) now protected - HTML comment placeholders prevent italic/bold parsing - All variables render correctly in preview ✅ Button rendering fixes: - Buttons work in Visual mode inside cards - Buttons work in Preview mode - Button clicks prevented in visual editor - Proper styling for solid and outline buttons ✅ Backward compatibility: - Old syntax still supported - No breaking changes ✅ Bug fixes: - Fixed order_item_table → order_items_table naming - Fixed button regex to match across newlines - Added button/image parsing to parseMarkdownBasics - Prevented button clicks on .button and .button-outline classes 📚 Documentation: - NEW_MARKDOWN_SYNTAX.md - Complete user guide - MARKDOWN_SYNTAX_AND_VARIABLES.md - Technical analysis
This commit is contained in:
@@ -1,36 +1,53 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { EditorView, basicSetup } from 'codemirror';
|
||||
import { html } from '@codemirror/lang-html';
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { Button } from './button';
|
||||
import { parseMarkdownToEmail, parseEmailToMarkdown } from '@/lib/markdown-parser';
|
||||
import { MarkdownToolbar } from './markdown-toolbar';
|
||||
|
||||
interface CodeEditorProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
supportMarkdown?: boolean;
|
||||
supportMarkdown?: boolean; // Keep for backward compatibility but always use markdown
|
||||
}
|
||||
|
||||
export function CodeEditor({ value, onChange, placeholder, supportMarkdown = false }: CodeEditorProps) {
|
||||
const [mode, setMode] = useState<'html' | 'markdown'>('html');
|
||||
export function CodeEditor({ value, onChange, placeholder }: CodeEditorProps) {
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
const viewRef = useRef<EditorView | null>(null);
|
||||
|
||||
// Handle markdown insertions from toolbar
|
||||
const handleInsert = (before: string, after: string = '') => {
|
||||
if (!viewRef.current) return;
|
||||
|
||||
const view = viewRef.current;
|
||||
const selection = view.state.selection.main;
|
||||
const selectedText = view.state.doc.sliceString(selection.from, selection.to);
|
||||
|
||||
// Insert the markdown syntax
|
||||
const newText = before + selectedText + after;
|
||||
view.dispatch({
|
||||
changes: { from: selection.from, to: selection.to, insert: newText },
|
||||
selection: { anchor: selection.from + before.length + selectedText.length }
|
||||
});
|
||||
|
||||
// Focus back to editor
|
||||
view.focus();
|
||||
};
|
||||
|
||||
// Initialize editor once
|
||||
useEffect(() => {
|
||||
if (!editorRef.current) return;
|
||||
|
||||
const view = new EditorView({
|
||||
doc: mode === 'markdown' ? parseEmailToMarkdown(value) : value,
|
||||
doc: value,
|
||||
extensions: [
|
||||
basicSetup,
|
||||
mode === 'markdown' ? markdown() : html(),
|
||||
markdown(),
|
||||
oneDark,
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged) {
|
||||
const content = update.state.doc.toString();
|
||||
onChange(mode === 'markdown' ? parseMarkdownToEmail(content) : content);
|
||||
onChange(content);
|
||||
}
|
||||
}),
|
||||
],
|
||||
@@ -42,52 +59,33 @@ export function CodeEditor({ value, onChange, placeholder, supportMarkdown = fal
|
||||
return () => {
|
||||
view.destroy();
|
||||
};
|
||||
}, [mode]);
|
||||
}, []); // Only run once on mount
|
||||
|
||||
// Update editor when value prop changes
|
||||
// Update editor when value prop changes from external source
|
||||
useEffect(() => {
|
||||
if (viewRef.current) {
|
||||
const displayValue = mode === 'markdown' ? parseEmailToMarkdown(value) : value;
|
||||
if (displayValue !== viewRef.current.state.doc.toString()) {
|
||||
viewRef.current.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: viewRef.current.state.doc.length,
|
||||
insert: displayValue,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (viewRef.current && value !== viewRef.current.state.doc.toString()) {
|
||||
viewRef.current.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: viewRef.current.state.doc.length,
|
||||
insert: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [value, mode]);
|
||||
|
||||
const toggleMode = () => {
|
||||
setMode(mode === 'html' ? 'markdown' : 'html');
|
||||
};
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{supportMarkdown && (
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={toggleMode}
|
||||
className="text-xs"
|
||||
>
|
||||
{mode === 'html' ? '📝 Switch to Markdown' : '🔧 Switch to HTML'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
ref={editorRef}
|
||||
className="border rounded-md overflow-hidden min-h-[400px] font-mono text-sm"
|
||||
/>
|
||||
{supportMarkdown && mode === 'markdown' && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
💡 Markdown syntax: Use <code>:::</code> for cards, <code>[button](url){text}</code> for buttons
|
||||
</p>
|
||||
)}
|
||||
<div className="border rounded-md overflow-hidden">
|
||||
<MarkdownToolbar onInsert={handleInsert} />
|
||||
<div
|
||||
ref={editorRef}
|
||||
className="min-h-[400px] font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
💡 Use the toolbar above or type markdown directly: **bold**, ## headings, [card]...[/card], [button]...[/button]
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user