fix: TipTap button conversion in card save flow

ROOT CAUSE:
When saving card edit in EmailBuilder, htmlToMarkdown() was called.
The old code at line 26 converted ALL <a> tags to markdown links:
  <a href="url">text</a> → [text](url)

This lost TipTap button data-button attributes, converting buttons
to plain text instead of [button:style](url)Text[/button] shortcode.

FIX:
Added TipTap button detection BEFORE generic link conversion in
html-to-markdown.ts:
- Detects <a data-button...> elements
- Extracts style from data-style or class attribute
- Extracts URL from data-href or href attribute
- Converts to [button:style](url)Text[/button] format

FLOW NOW WORKS:
1. User adds button via TipTap toolbar
2. TipTap renders <a data-button data-style="solid"...>
3. User clicks Save Changes
4. htmlToMarkdown detects data-button → [button:solid](url)Text[/button]
5. Card content saved with proper button shortcode
6. On re-edit, button shortcode converted back to TipTap button
This commit is contained in:
Dwindi Ramadhana
2026-01-01 23:31:54 +07:00
parent 47a1e78eb7
commit 47f6370ce0

View File

@@ -5,26 +5,52 @@
export function htmlToMarkdown(html: string): string {
if (!html) return '';
let markdown = html;
// Headings
markdown = markdown.replace(/<h1>(.*?)<\/h1>/gi, '# $1\n\n');
markdown = markdown.replace(/<h2>(.*?)<\/h2>/gi, '## $1\n\n');
markdown = markdown.replace(/<h3>(.*?)<\/h3>/gi, '### $1\n\n');
markdown = markdown.replace(/<h4>(.*?)<\/h4>/gi, '#### $1\n\n');
// Bold
markdown = markdown.replace(/<strong>(.*?)<\/strong>/gi, '**$1**');
markdown = markdown.replace(/<b>(.*?)<\/b>/gi, '**$1**');
// Italic
markdown = markdown.replace(/<em>(.*?)<\/em>/gi, '*$1*');
markdown = markdown.replace(/<i>(.*?)<\/i>/gi, '*$1*');
// Links
// TipTap buttons - detect by data-button attribute, BEFORE generic links
// Format: <a data-button data-style="solid" data-href="..." data-text="...">text</a>
// or: <a href="..." class="button..." data-button ...>text</a>
markdown = markdown.replace(/<a[^>]*data-button[^>]*>(.*?)<\/a>/gi, (match, text) => {
// Extract style from data-style or class
let style = 'solid';
const styleMatch = match.match(/data-style=["'](\w+)["']/);
if (styleMatch) {
style = styleMatch[1];
} else if (match.includes('button-outline') || match.includes('outline')) {
style = 'outline';
}
// Extract href from data-href or href attribute
let url = '#';
const dataHrefMatch = match.match(/data-href=["']([^"']+)["']/);
const hrefMatch = match.match(/href=["']([^"']+)["']/);
if (dataHrefMatch) {
url = dataHrefMatch[1];
} else if (hrefMatch) {
url = hrefMatch[1];
}
return `[button:${style}](${url})${text.trim()}[/button]`;
});
// Regular links (not buttons)
markdown = markdown.replace(/<a\s+href="([^"]+)"[^>]*>(.*?)<\/a>/gi, '[$2]($1)');
// Lists
markdown = markdown.replace(/<ul[^>]*>(.*?)<\/ul>/gis, (match, content) => {
const items = content.match(/<li[^>]*>(.*?)<\/li>/gis) || [];
@@ -33,7 +59,7 @@ export function htmlToMarkdown(html: string): string {
return `- ${text}`;
}).join('\n') + '\n\n';
});
markdown = markdown.replace(/<ol[^>]*>(.*?)<\/ol>/gis, (match, content) => {
const items = content.match(/<li[^>]*>(.*?)<\/li>/gis) || [];
return items.map((item: string, index: number) => {
@@ -41,24 +67,24 @@ export function htmlToMarkdown(html: string): string {
return `${index + 1}. ${text}`;
}).join('\n') + '\n\n';
});
// Paragraphs - convert to double newlines
markdown = markdown.replace(/<p[^>]*>(.*?)<\/p>/gis, '$1\n\n');
// Line breaks
markdown = markdown.replace(/<br\s*\/?>/gi, '\n');
// Horizontal rules
markdown = markdown.replace(/<hr\s*\/?>/gi, '\n---\n\n');
// Remove remaining HTML tags
markdown = markdown.replace(/<[^>]+>/g, '');
// Clean up excessive newlines
markdown = markdown.replace(/\n{3,}/g, '\n\n');
// Trim
markdown = markdown.trim();
return markdown;
}