fix: button rendering from RichEditor to markdown to HTML

- Added multiple htmlToMarkdown patterns for TipTap button output:
  1. data-button with data-href/data-style attributes
  2. Alternate attribute order (data-style before data-href)
  3. Simple data-button fallback with href and class
  4. Buttons wrapped in p tags (from preview HTML)
  5. Direct button links without p wrapper

- Button shortcodes now correctly roundtrip:
  RichEditor -> HTML -> [button url=... style=...] -> Preview/Email

- All patterns now explicitly include style=solid for consistency
This commit is contained in:
Dwindi Ramadhana
2026-01-01 21:37:55 +07:00
parent ccdd88a629
commit e84fa969bb

View File

@@ -87,7 +87,7 @@ export function markdownToHtml(markdown: string): string {
const parsedContent = parseMarkdownBasics(content.trim()); const parsedContent = parseMarkdownBasics(content.trim());
return `<div class="${cardClass}">${parsedContent}</div>`; return `<div class="${cardClass}">${parsedContent}</div>`;
}); });
// Parse [card type="..."] blocks (old syntax - backward compatibility) // Parse [card type="..."] blocks (old syntax - backward compatibility)
html = html.replace(/\[card(?:\s+type="([^"]+)")?\]([\s\S]*?)\[\/card\]/g, (match, type, content) => { html = html.replace(/\[card(?:\s+type="([^"]+)")?\]([\s\S]*?)\[\/card\]/g, (match, type, content) => {
const cardClass = type ? `card card-${type}` : 'card'; const cardClass = type ? `card card-${type}` : 'card';
@@ -100,7 +100,7 @@ export function markdownToHtml(markdown: string): string {
const buttonClass = style === 'outline' ? 'button-outline' : 'button'; const buttonClass = style === 'outline' ? 'button-outline' : 'button';
return `<p style="text-align: center;"><a href="${url}" class="${buttonClass}">${text.trim()}</a></p>`; return `<p style="text-align: center;"><a href="${url}" class="${buttonClass}">${text.trim()}</a></p>`;
}); });
// Parse [button url="..."] shortcodes (old syntax - backward compatibility) // Parse [button url="..."] shortcodes (old syntax - backward compatibility)
html = html.replace(/\[button\s+url="([^"]+)"(?:\s+style="([^"]+)")?\]([^\[]+)\[\/button\]/g, (match, url, style, text) => { html = html.replace(/\[button\s+url="([^"]+)"(?:\s+style="([^"]+)")?\]([^\[]+)\[\/button\]/g, (match, url, style, text) => {
const buttonClass = style === 'outline' ? 'button-outline' : 'button'; const buttonClass = style === 'outline' ? 'button-outline' : 'button';
@@ -155,7 +155,7 @@ export function parseMarkdownBasics(text: string): string {
const buttonClass = style === 'outline' ? 'button-outline' : 'button'; const buttonClass = style === 'outline' ? 'button-outline' : 'button';
return `<p style="text-align: center;"><a href="${url}" class="${buttonClass}">${text.trim()}</a></p>`; return `<p style="text-align: center;"><a href="${url}" class="${buttonClass}">${text.trim()}</a></p>`;
}); });
// Parse [button url="..."] shortcodes (old syntax - backward compatibility) // Parse [button url="..."] shortcodes (old syntax - backward compatibility)
html = html.replace(/\[button\s+url="([^"]+)"(?:\s+style="([^"]+)")?\]([^\[]+)\[\/button\]/g, (match, url, style, text) => { html = html.replace(/\[button\s+url="([^"]+)"(?:\s+style="([^"]+)")?\]([^\[]+)\[\/button\]/g, (match, url, style, text) => {
const buttonClass = style === 'outline' ? 'button-outline' : 'button'; const buttonClass = style === 'outline' ? 'button-outline' : 'button';
@@ -267,8 +267,33 @@ export function htmlToMarkdown(html: string): string {
}); });
// Convert buttons back to [button] syntax // Convert buttons back to [button] syntax
// TipTap button format with data attributes: <a data-button data-href="..." data-style="..." data-text="...">text</a>
markdown = markdown.replace(/<a[^>]*data-button[^>]*data-href="([^"]+)"[^>]*data-style="([^"]*)"[^>]*>([^<]+)<\/a>/gi, (match, url, style, text) => {
const styleAttr = style === 'outline' ? ' style="outline"' : ' style="solid"';
return `[button url="${url}"${styleAttr}]${text.trim()}[/button]`;
});
// Alternate order: data-style before data-href
markdown = markdown.replace(/<a[^>]*data-button[^>]*data-style="([^"]*)"[^>]*data-href="([^"]+)"[^>]*>([^<]+)<\/a>/gi, (match, style, url, text) => {
const styleAttr = style === 'outline' ? ' style="outline"' : ' style="solid"';
return `[button url="${url}"${styleAttr}]${text.trim()}[/button]`;
});
// Simple data-button fallback (just has href and class)
markdown = markdown.replace(/<a[^>]*href="([^"]+)"[^>]*class="(button[^"]*)"[^>]*data-button[^>]*>([^<]+)<\/a>/gi, (match, url, className, text) => {
const style = className.includes('outline') ? ' style="outline"' : ' style="solid"';
return `[button url="${url}"${style}]${text.trim()}[/button]`;
});
// Buttons wrapped in p tags (from preview HTML): <p><a href="..." class="button...">text</a></p>
markdown = markdown.replace(/<p[^>]*><a href="([^"]+)" class="(button[^"]*)"[^>]*>([^<]+)<\/a><\/p>/g, (match, url, className, text) => { markdown = markdown.replace(/<p[^>]*><a href="([^"]+)" class="(button[^"]*)"[^>]*>([^<]+)<\/a><\/p>/g, (match, url, className, text) => {
const style = className.includes('outline') ? ' style="outline"' : ''; const style = className.includes('outline') ? ' style="outline"' : ' style="solid"';
return `[button url="${url}"${style}]${text.trim()}[/button]`;
});
// Direct button links without p wrapper
markdown = markdown.replace(/<a href="([^"]+)" class="(button[^"]*)"[^>]*>([^<]+)<\/a>/g, (match, url, className, text) => {
const style = className.includes('outline') ? ' style="outline"' : ' style="solid"';
return `[button url="${url}"${style}]${text.trim()}[/button]`; return `[button url="${url}"${style}]${text.trim()}[/button]`;
}); });