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:
dwindown
2025-11-18 18:36:28 +07:00
parent af2a3d3dd5
commit c599bce71a
2 changed files with 161 additions and 2 deletions

View File

@@ -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>',

View 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;
}
}