Files
WooNooW/admin-spa/src/components/EmailBuilder/markdown-converter.ts
dwindown 4471cd600f 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: ![alt](url)

 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
2025-11-15 20:05:50 +07:00

74 lines
2.1 KiB
TypeScript

import { EmailBlock, CardType, ButtonStyle } from './types';
/**
* Convert markdown to blocks - respects [card]...[/card] boundaries
*/
export function markdownToBlocks(markdown: string): EmailBlock[] {
const blocks: EmailBlock[] = [];
let blockId = 0;
let pos = 0;
while (pos < markdown.length) {
// Skip whitespace
while (pos < markdown.length && /\s/.test(markdown[pos])) pos++;
if (pos >= markdown.length) break;
const id = `block-${Date.now()}-${blockId++}`;
// Check for [card]
if (markdown.substr(pos, 5) === '[card') {
const cardStart = pos;
const cardOpenEnd = markdown.indexOf(']', pos);
const cardClose = markdown.indexOf('[/card]', pos);
if (cardOpenEnd !== -1 && cardClose !== -1) {
const attributes = markdown.substring(pos + 5, cardOpenEnd);
const content = markdown.substring(cardOpenEnd + 1, cardClose).trim();
// Parse type
const typeMatch = attributes.match(/type\s*=\s*["']([^"']+)["']/);
const cardType = (typeMatch?.[1] || 'default') as CardType;
blocks.push({
id,
type: 'card',
cardType,
content,
});
pos = cardClose + 7; // Skip [/card]
continue;
}
}
// Check for [button]
if (markdown.substr(pos, 7) === '[button') {
const buttonEnd = markdown.indexOf('[/button]', pos);
if (buttonEnd !== -1) {
const fullButton = markdown.substring(pos, buttonEnd + 9);
const match = fullButton.match(/\[button\s+url=["']([^"']+)["'](?:\s+style=["'](solid|outline)["'])?\]([^\[]+)\[\/button\]/);
if (match) {
blocks.push({
id,
type: 'button',
text: match[3].trim(),
link: match[1],
style: (match[2] || 'solid') as ButtonStyle,
align: 'center',
widthMode: 'fit',
});
pos = buttonEnd + 9;
continue;
}
}
}
// Skip unknown content
pos++;
}
return blocks;
}