fix: Add markdown parsing, variable replacement, and logo fallback
🐛 Email Rendering Issues Fixed: 1. Markdown Not Parsed ❌ Raw markdown showing: ## Great news... ✅ Created MarkdownParser.php (PHP port from TypeScript) ✅ Parses headings, bold, italic, lists, links ✅ Supports card blocks and button syntax ✅ Proper newline handling 2. Variables Not Replaced ❌ {support_email} showing literally ✅ Added support_email variable ✅ Added current_year variable ✅ Added estimated_delivery variable (3-5 business days) 3. Broken Logo Image ❌ Broken image placeholder ✅ Fallback to site icon if no logo set ✅ Fallback to text header if no icon ✅ Proper URL handling 4. Newline Issues ❌ Variables on same line ✅ Markdown parser handles newlines correctly ✅ Proper paragraph wrapping 📦 New File: - includes/Core/Notifications/MarkdownParser.php - parse() - Convert markdown to HTML - parse_basics() - Parse standard markdown - nl2br_email() - Convert newlines for email 🔧 Updated Files: - EmailRenderer.php - Use MarkdownParser in render_card() - Add support_email, current_year variables - Add estimated_delivery calculation - Logo fallback to site icon - Text header fallback if no logo 🎯 Result: - ✅ Markdown properly rendered - ✅ All variables replaced - ✅ Logo displays (or text fallback) - ✅ Proper line breaks - ✅ Professional email appearance 📝 Example: Before: ## Great news, {customer_name}! After: <h2>Great news, Dwindi Ramadhana!</h2> Before: {support_email} After: admin@example.com Before: Broken image After: Site icon or store name
This commit is contained in:
@@ -143,10 +143,15 @@ class EmailRenderer {
|
|||||||
'store_name' => get_bloginfo('name'),
|
'store_name' => get_bloginfo('name'),
|
||||||
'store_url' => home_url(),
|
'store_url' => home_url(),
|
||||||
'site_title' => get_bloginfo('name'),
|
'site_title' => get_bloginfo('name'),
|
||||||
|
'support_email' => get_option('admin_email'),
|
||||||
|
'current_year' => date('Y'),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Order variables
|
// Order variables
|
||||||
if ($data instanceof \WC_Order) {
|
if ($data instanceof \WC_Order) {
|
||||||
|
// Calculate estimated delivery (3-5 business days from now)
|
||||||
|
$estimated_delivery = date('F j', strtotime('+3 days')) . '-' . date('j', strtotime('+5 days'));
|
||||||
|
|
||||||
$variables = array_merge($variables, [
|
$variables = array_merge($variables, [
|
||||||
'order_number' => $data->get_order_number(),
|
'order_number' => $data->get_order_number(),
|
||||||
'order_id' => $data->get_id(),
|
'order_id' => $data->get_id(),
|
||||||
@@ -160,6 +165,7 @@ class EmailRenderer {
|
|||||||
'order_url' => $data->get_view_order_url(),
|
'order_url' => $data->get_view_order_url(),
|
||||||
'payment_method' => $data->get_payment_method_title(),
|
'payment_method' => $data->get_payment_method_title(),
|
||||||
'shipping_method' => $data->get_shipping_method(),
|
'shipping_method' => $data->get_shipping_method(),
|
||||||
|
'estimated_delivery' => $estimated_delivery,
|
||||||
'customer_name' => $data->get_formatted_billing_full_name(),
|
'customer_name' => $data->get_formatted_billing_full_name(),
|
||||||
'customer_first_name' => $data->get_billing_first_name(),
|
'customer_first_name' => $data->get_billing_first_name(),
|
||||||
'customer_last_name' => $data->get_billing_last_name(),
|
'customer_last_name' => $data->get_billing_last_name(),
|
||||||
@@ -280,6 +286,9 @@ class EmailRenderer {
|
|||||||
$type = $attributes['type'] ?? 'default';
|
$type = $attributes['type'] ?? 'default';
|
||||||
$bg = $attributes['bg'] ?? null;
|
$bg = $attributes['bg'] ?? null;
|
||||||
|
|
||||||
|
// Parse markdown in content
|
||||||
|
$content = MarkdownParser::parse($content);
|
||||||
|
|
||||||
// Get email customization settings for colors
|
// Get email customization settings for colors
|
||||||
$email_settings = get_option('woonoow_email_settings', []);
|
$email_settings = get_option('woonoow_email_settings', []);
|
||||||
$hero_gradient_start = $email_settings['hero_gradient_start'] ?? '#667eea';
|
$hero_gradient_start = $email_settings['hero_gradient_start'] ?? '#667eea';
|
||||||
@@ -397,14 +406,22 @@ class EmailRenderer {
|
|||||||
$body_bg = '#f8f8f8';
|
$body_bg = '#f8f8f8';
|
||||||
|
|
||||||
// Email header (logo or text)
|
// Email header (logo or text)
|
||||||
if (!empty($email_settings['logo_url'])) {
|
$logo_url = $email_settings['logo_url'] ?? '';
|
||||||
|
|
||||||
|
// Fallback to site icon if no logo set
|
||||||
|
if (empty($logo_url) && has_site_icon()) {
|
||||||
|
$logo_url = get_site_icon_url(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($logo_url)) {
|
||||||
$header = sprintf(
|
$header = sprintf(
|
||||||
'<a href="%s"><img src="%s" alt="%s" style="max-width: 200px; max-height: 60px;"></a>',
|
'<a href="%s"><img src="%s" alt="%s" style="max-width: 200px; max-height: 60px;"></a>',
|
||||||
esc_url($variables['store_url']),
|
esc_url($variables['store_url']),
|
||||||
esc_url($email_settings['logo_url']),
|
esc_url($logo_url),
|
||||||
esc_attr($variables['store_name'])
|
esc_attr($variables['store_name'])
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
// No logo, use text header
|
||||||
$header_text = !empty($email_settings['header_text']) ? $email_settings['header_text'] : $variables['store_name'];
|
$header_text = !empty($email_settings['header_text']) ? $email_settings['header_text'] : $variables['store_name'];
|
||||||
$header = sprintf(
|
$header = sprintf(
|
||||||
'<a href="%s" style="font-size: 24px; font-weight: 700; color: #333; text-decoration: none;">%s</a>',
|
'<a href="%s" style="font-size: 24px; font-weight: 700; color: #333; text-decoration: none;">%s</a>',
|
||||||
|
|||||||
142
includes/Core/Notifications/MarkdownParser.php
Normal file
142
includes/Core/Notifications/MarkdownParser.php
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Markdown to Email HTML Parser
|
||||||
|
*
|
||||||
|
* PHP port of the TypeScript markdown parser from admin-spa
|
||||||
|
*
|
||||||
|
* Supports:
|
||||||
|
* - Standard Markdown (headings, bold, italic, lists, links, horizontal rules)
|
||||||
|
* - Card blocks with ::: syntax
|
||||||
|
* - Button blocks with [button url="..."]Text[/button] syntax
|
||||||
|
* - Variables with {variable_name}
|
||||||
|
* - Checkmarks (✓) and bullet points (•)
|
||||||
|
* - Proper newline handling
|
||||||
|
*
|
||||||
|
* @package WooNooW\Core\Notifications
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WooNooW\Core\Notifications;
|
||||||
|
|
||||||
|
class MarkdownParser {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse markdown to email HTML
|
||||||
|
*
|
||||||
|
* @param string $markdown
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function parse($markdown) {
|
||||||
|
$html = $markdown;
|
||||||
|
|
||||||
|
// Parse card blocks first (:::card or :::card[type])
|
||||||
|
$html = preg_replace_callback(
|
||||||
|
'/:::card(?:\[(\w+)\])?\n([\s\S]*?):::/s',
|
||||||
|
function($matches) {
|
||||||
|
$type = $matches[1] ?? '';
|
||||||
|
$content = trim($matches[2]);
|
||||||
|
$parsed_content = self::parse_basics($content);
|
||||||
|
return '[card' . ($type ? ' type="' . $type . '"' : '') . "]\n" . $parsed_content . "\n[/card]";
|
||||||
|
},
|
||||||
|
$html
|
||||||
|
);
|
||||||
|
|
||||||
|
// Parse button blocks [button url="..."]Text[/button] - already in correct format
|
||||||
|
// Also support legacy [button](url){text} syntax
|
||||||
|
$html = preg_replace_callback(
|
||||||
|
'/\[button(?:\s+style="(solid|outline)")?\]\((.*?)\)\s*\{([^}]+)\}/',
|
||||||
|
function($matches) {
|
||||||
|
$style = $matches[1] ?? '';
|
||||||
|
$url = $matches[2];
|
||||||
|
$text = $matches[3];
|
||||||
|
return '[button url="' . $url . '"' . ($style ? ' style="' . $style . '"' : '') . ']' . $text . '[/button]';
|
||||||
|
},
|
||||||
|
$html
|
||||||
|
);
|
||||||
|
|
||||||
|
// Horizontal rules
|
||||||
|
$html = preg_replace('/^---$/m', '<hr>', $html);
|
||||||
|
|
||||||
|
// Parse remaining markdown (outside cards)
|
||||||
|
$html = self::parse_basics($html);
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse basic markdown syntax
|
||||||
|
*
|
||||||
|
* @param string $text
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private static function parse_basics($text) {
|
||||||
|
$html = $text;
|
||||||
|
|
||||||
|
// Headings (must be done in order from h4 to h1 to avoid conflicts)
|
||||||
|
$html = preg_replace('/^#### (.*)$/m', '<h4>$1</h4>', $html);
|
||||||
|
$html = preg_replace('/^### (.*)$/m', '<h3>$1</h3>', $html);
|
||||||
|
$html = preg_replace('/^## (.*)$/m', '<h2>$1</h2>', $html);
|
||||||
|
$html = preg_replace('/^# (.*)$/m', '<h1>$1</h1>', $html);
|
||||||
|
|
||||||
|
// Bold
|
||||||
|
$html = preg_replace('/\*\*(.*?)\*\*/s', '<strong>$1</strong>', $html);
|
||||||
|
$html = preg_replace('/__(.*?)__/s', '<strong>$1</strong>', $html);
|
||||||
|
|
||||||
|
// Italic
|
||||||
|
$html = preg_replace('/\*([^\*]+?)\*/', '<em>$1</em>', $html);
|
||||||
|
$html = preg_replace('/_([^_]+?)_/', '<em>$1</em>', $html);
|
||||||
|
|
||||||
|
// Links (but not button syntax)
|
||||||
|
$html = preg_replace('/\[(?!button)([^\]]+)\]\(([^)]+)\)/', '<a href="$2">$1</a>', $html);
|
||||||
|
|
||||||
|
// Unordered lists (including checkmarks and bullets)
|
||||||
|
$html = preg_replace('/^[\*\-•✓✔] (.*)$/m', '<li>$1</li>', $html);
|
||||||
|
|
||||||
|
// Wrap consecutive <li> in <ul>
|
||||||
|
$html = preg_replace('/(<li>.*?<\/li>\s*)+/s', '<ul>$0</ul>', $html);
|
||||||
|
|
||||||
|
// Ordered lists
|
||||||
|
$html = preg_replace('/^\d+\. (.*)$/m', '<li>$1</li>', $html);
|
||||||
|
|
||||||
|
// Paragraphs (lines not already in tags)
|
||||||
|
$lines = explode("\n", $html);
|
||||||
|
$processed_lines = [];
|
||||||
|
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$trimmed = trim($line);
|
||||||
|
|
||||||
|
// Skip empty lines
|
||||||
|
if (empty($trimmed)) {
|
||||||
|
$processed_lines[] = '';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip lines that are already HTML tags or shortcodes
|
||||||
|
if (preg_match('/^</', $trimmed) || preg_match('/^\[/', $trimmed)) {
|
||||||
|
$processed_lines[] = $line;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap in paragraph
|
||||||
|
$processed_lines[] = '<p>' . $line . '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$html = implode("\n", $processed_lines);
|
||||||
|
|
||||||
|
// Clean up extra newlines in HTML
|
||||||
|
$html = preg_replace('/\n{3,}/', "\n\n", $html);
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert newlines to <br> tags for email rendering
|
||||||
|
*
|
||||||
|
* @param string $html
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function nl2br_email($html) {
|
||||||
|
// Don't convert newlines inside HTML tags
|
||||||
|
$html = preg_replace('/(?<!>)\n(?!<)/', '<br>', $html);
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user