]*>([\s\S]*?)<\/div>/s);
if (htmlCardMatch) {
cardType = (htmlCardMatch[1] || 'default');
content = htmlCardMatch[2].trim();
}
}
if (content) {
// Convert HTML content to markdown for clean editing
// But only if it actually contains HTML tags
const hasHtmlTags = /<[^>]+>/.test(content);
const markdownContent = hasHtmlTags ? convertHtmlToMarkdown(content) : content;
blocks.push({
id,
type: 'card',
cardType: cardType as any,
content: markdownContent
});
continue;
}
// Check if it's a button - try new syntax first
let buttonMatch = part.match(/\[button:(\w+)\]\(([^)]+)\)([^\[]+)\[\/button\]/);
if (buttonMatch) {
const style = buttonMatch[1] as ButtonStyle;
const url = buttonMatch[2];
const text = buttonMatch[3].trim();
blocks.push({
id,
type: 'button',
link: url,
text: text,
style: style,
align: 'center',
widthMode: 'fit'
});
continue;
}
// Try old [button url="..."] syntax
buttonMatch = part.match(/\[button\s+url=["']([^"']+)["'](?:\s+style=["'](\w+)["'])?\]([^\[]+)\[\/button\]/);
if (buttonMatch) {
const url = buttonMatch[1];
const style = (buttonMatch[2] || 'solid') as ButtonStyle;
const text = buttonMatch[3].trim();
blocks.push({
id,
type: 'button',
link: url,
text: text,
style: style,
align: 'center',
widthMode: 'fit'
});
continue;
}
// Check HTML button syntax
if (part.includes('class="button"') || part.includes('class="button-outline"')) {
const buttonMatch = part.match(/
]*href="([^"]*)"[^>]*class="(button[^"]*)"[^>]*>([^<]*)<\/a>/);
if (buttonMatch) {
const hasStyle = buttonMatch.length === 5;
const styleAttr = hasStyle ? buttonMatch[3] : '';
const textIndex = hasStyle ? 4 : 3;
const styleClassIndex = 2;
let widthMode: any = 'fit';
let customMaxWidth: number | undefined = undefined;
if (styleAttr.includes('width:100%') && !styleAttr.includes('max-width')) {
widthMode = 'full';
} else if (styleAttr.includes('max-width')) {
widthMode = 'custom';
const maxMatch = styleAttr.match(/max-width:(\d+)px/);
if (maxMatch) {
customMaxWidth = parseInt(maxMatch[1], 10);
}
}
// Extract alignment from parent tag if present
const alignMatch = part.match(/text-align:\s*(left|center|right)/);
const align = alignMatch ? alignMatch[1] as any : 'center';
blocks.push({
id,
type: 'button',
text: buttonMatch[textIndex],
link: buttonMatch[1],
style: buttonMatch[styleClassIndex].includes('outline') ? 'outline' : 'solid',
widthMode,
customMaxWidth,
align,
});
continue;
}
}
// Check if it's a divider
if (part.includes('
0) {
remaining = remaining.trim();
if (!remaining) break;
const id = `block-${Date.now()}-${blockId++}`;
// Check for [card] blocks - NEW syntax [card:type]...[/card]
const newCardMatch = remaining.match(/^\[card:(\w+)\]([\s\S]*?)\[\/card\]/);
if (newCardMatch) {
const cardType = newCardMatch[1] as CardType;
const content = newCardMatch[2].trim();
blocks.push({
id,
type: 'card',
cardType,
content,
});
remaining = remaining.substring(newCardMatch[0].length);
continue;
}
// Check for [card] blocks - OLD syntax [card type="..."]...[/card]
const cardMatch = remaining.match(/^\[card([^\]]*)\]([\s\S]*?)\[\/card\]/);
if (cardMatch) {
const attributes = cardMatch[1].trim();
const content = cardMatch[2].trim();
// Extract card type
const typeMatch = attributes.match(/type\s*=\s*["']([^"']+)["']/);
const cardType = (typeMatch?.[1] || 'default') as CardType;
// Extract background
const bgMatch = attributes.match(/bg=["']([^"']+)["']/);
const bg = bgMatch?.[1];
blocks.push({
id,
type: 'card',
cardType,
content,
bg,
});
// Advance past this card
remaining = remaining.substring(cardMatch[0].length);
continue;
}
// Check for [button] blocks - NEW syntax [button:style](url)Text[/button]
const newButtonMatch = remaining.match(/^\[button:(\w+)\]\(([^)]+)\)([^\[]+)\[\/button\]/);
if (newButtonMatch) {
blocks.push({
id,
type: 'button',
text: newButtonMatch[3].trim(),
link: newButtonMatch[2],
style: newButtonMatch[1] as ButtonStyle,
align: 'center',
widthMode: 'fit',
});
remaining = remaining.substring(newButtonMatch[0].length);
continue;
}
// Check for [button] blocks - OLD syntax [button url="..." style="..."]Text[/button]
const buttonMatch = remaining.match(/^\[button\s+url=["']([^"']+)["'](?:\s+style=["'](solid|outline)["'])?\]([^\[]+)\[\/button\]/);
if (buttonMatch) {
blocks.push({
id,
type: 'button',
text: buttonMatch[3].trim(),
link: buttonMatch[1],
style: (buttonMatch[2] || 'solid') as ButtonStyle,
align: 'center',
widthMode: 'fit',
});
remaining = remaining.substring(buttonMatch[0].length);
continue;
}
// Check for [image] blocks
const imageMatch = remaining.match(/^\[image\s+src=["']([^"']+)["'](?:\s+alt=["']([^"']*)["'])?(?:\s+width=["']([^"']+)["'])?(?:\s+align=["']([^"']+)["'])?\]/);
if (imageMatch) {
blocks.push({
id,
type: 'image',
src: imageMatch[1],
alt: imageMatch[2] || '',
widthMode: (imageMatch[3] || 'fit') as ContentWidth,
align: (imageMatch[4] || 'center') as ContentAlign,
});
remaining = remaining.substring(imageMatch[0].length);
continue;
}
// Check for [spacer] blocks
const spacerMatch = remaining.match(/^\[spacer\s+height=["'](\d+)["']\]/);
if (spacerMatch) {
blocks.push({
id,
type: 'spacer',
height: parseInt(spacerMatch[1]),
});
remaining = remaining.substring(spacerMatch[0].length);
continue;
}
// Check for horizontal rule
if (remaining.startsWith('---')) {
blocks.push({
id,
type: 'divider',
});
remaining = remaining.substring(3);
continue;
}
// If nothing matches, skip this character to avoid infinite loop
remaining = remaining.substring(1);
}
return blocks;
}