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
This commit is contained in:
dwindown
2025-11-15 20:05:50 +07:00
parent 550b3b69ef
commit 4471cd600f
45 changed files with 9194 additions and 508 deletions

View File

@@ -1,14 +1,22 @@
<?php
/**
* Default Email Templates
* Default Email Templates (DEPRECATED)
*
* Provides default email content for all notification events
* @deprecated Use WooNooW\Email\DefaultTemplates instead
*
* This file is kept for backwards compatibility only.
* The new source of truth is /includes/Email/DefaultTemplates.php
* which contains clean markdown templates without HTML tags.
*
* TemplateProvider now uses the new Email\DefaultTemplates directly.
*
* @package WooNooW
*/
namespace WooNooW\Core\Notifications;
use WooNooW\Email\DefaultTemplates as NewDefaultTemplates;
class DefaultEmailTemplates {
/**
@@ -19,25 +27,29 @@ class DefaultEmailTemplates {
* @return array ['subject' => string, 'body' => string]
*/
public static function get_template($event_id, $recipient_type) {
$templates = self::get_all_templates();
// Get templates directly from this class
$allTemplates = self::get_all_templates();
if (isset($templates[$event_id][$recipient_type])) {
return $templates[$event_id][$recipient_type];
// Check if event exists for this recipient type
if (isset($allTemplates[$event_id][$recipient_type])) {
return $allTemplates[$event_id][$recipient_type];
}
// Fallback
return [
'subject' => __('Notification from {store_name}', 'woonoow'),
'body' => '[card type="default"]<p>' . __('You have a new notification.', 'woonoow') . '</p>[/card]',
'body' => '[card]' . __('You have a new notification.', 'woonoow') . '[/card]',
];
}
/**
* Get all default templates
* Get all default templates (legacy method - kept for backwards compatibility)
*
* @return array
*/
private static function get_all_templates() {
// This method is now deprecated but kept for backwards compatibility
// Use WooNooW\Email\DefaultTemplates instead
return [
// ORDER EVENTS
'order_placed' => [
@@ -68,7 +80,7 @@ class DefaultEmailTemplates {
{order_items_list}
[/card]
[button link="{order_url}" style="solid"]' . __('View Order Details', 'woonoow') . '[/button]',
[button url="{order_url}" style="solid"]' . __('View Order Details', 'woonoow') . '[/button]',
],
],
@@ -97,7 +109,7 @@ class DefaultEmailTemplates {
{order_items_list}
[/card]
[button link="{order_url}" style="solid"]' . __('Track Your Order', 'woonoow') . '[/button]',
[button url="{order_url}" style="solid"]' . __('Track Your Order', 'woonoow') . '[/button]',
],
],
@@ -122,8 +134,8 @@ class DefaultEmailTemplates {
<p>' . __('If you have any questions or concerns about your order, please don\'t hesitate to contact us.', 'woonoow') . '</p>
[/card]
[button link="{order_url}" style="solid"]' . __('View Order', 'woonoow') . '[/button]
[button link="{store_url}" style="outline"]' . __('Continue Shopping', 'woonoow') . '[/button]',
[button url="{order_url}" style="solid"]' . __('View Order', 'woonoow') . '[/button]
[button url="{store_url}" style="outline"]' . __('Continue Shopping', 'woonoow') . '[/button]',
],
],
@@ -143,7 +155,7 @@ class DefaultEmailTemplates {
<p><strong>' . __('Cancelled Date:', 'woonoow') . '</strong> {order_date}</p>
[/card]
[button link="{order_url}" style="solid"]' . __('View Order Details', 'woonoow') . '[/button]',
[button url="{order_url}" style="solid"]' . __('View Order Details', 'woonoow') . '[/button]',
],
],
@@ -168,7 +180,7 @@ class DefaultEmailTemplates {
<p>' . __('If you have any questions, please contact us.', 'woonoow') . '</p>
[/card]
[button link="{order_url}" style="solid"]' . __('View Order', 'woonoow') . '[/button]',
[button url="{order_url}" style="solid"]' . __('View Order', 'woonoow') . '[/button]',
],
],
@@ -194,7 +206,7 @@ class DefaultEmailTemplates {
<p>' . __('Please restock this product to avoid running out of inventory.', 'woonoow') . '</p>
[/card]
[button link="{product_url}" style="solid"]' . __('View Product', 'woonoow') . '[/button]',
[button url="{product_url}" style="solid"]' . __('View Product', 'woonoow') . '[/button]',
],
],
@@ -218,7 +230,7 @@ class DefaultEmailTemplates {
<p>' . __('This product is no longer available for purchase. Please restock as soon as possible.', 'woonoow') . '</p>
[/card]
[button link="{product_url}" style="solid"]' . __('Manage Product', 'woonoow') . '[/button]',
[button url="{product_url}" style="solid"]' . __('Manage Product', 'woonoow') . '[/button]',
],
],
@@ -248,8 +260,8 @@ class DefaultEmailTemplates {
</ul>
[/card]
[button link="{account_url}" style="solid"]' . __('Go to My Account', 'woonoow') . '[/button]
[button link="{store_url}" style="outline"]' . __('Start Shopping', 'woonoow') . '[/button]',
[button url="{account_url}" style="solid"]' . __('Go to My Account', 'woonoow') . '[/button]
[button url="{store_url}" style="outline"]' . __('Start Shopping', 'woonoow') . '[/button]',
],
],
@@ -272,9 +284,29 @@ class DefaultEmailTemplates {
<p>{customer_note}</p>
[/card]
[button link="{order_url}" style="solid"]' . __('View Order', 'woonoow') . '[/button]',
[button url="{order_url}" style="solid"]' . __('View Order', 'woonoow') . '[/button]',
],
],
];
}
/**
* Get all new templates (direct access to new class)
*
* @return array
*/
public static function get_new_templates() {
return NewDefaultTemplates::get_all_templates();
}
/**
* Get default subject from new templates
*
* @param string $recipient_type 'staff' or 'customer'
* @param string $event_id Event ID
* @return string
*/
public static function get_default_subject($recipient_type, $event_id) {
return NewDefaultTemplates::get_default_subject($recipient_type, $event_id);
}
}

View File

@@ -0,0 +1,243 @@
<?php
/**
* Event Registry - Single Source of Truth for Notification Events
*
* Defines all notification events in the system with their metadata.
* Other components query this registry instead of hardcoding event lists.
*
* @package WooNooW\Core\Notifications
*/
namespace WooNooW\Core\Notifications;
class EventRegistry {
/**
* Get all registered notification events
*
* This is the SINGLE SOURCE OF TRUTH for all events in the system.
* All other components (API, TemplateProvider, etc.) must use this.
*
* @return array Event definitions with structure:
* [
* 'event_id' => [
* 'id' => 'event_id',
* 'label' => 'Human readable label',
* 'description' => 'What triggers this event',
* 'category' => 'orders|products|customers',
* 'recipient_type' => 'staff|customer',
* 'wc_email' => 'woocommerce_email_id',
* 'enabled' => true|false,
* ]
* ]
*/
public static function get_all_events() {
$events = [
// STAFF EVENTS
'order_placed' => [
'id' => 'order_placed',
'label' => __('Order Placed', 'woonoow'),
'description' => __('When a new order is placed', 'woonoow'),
'category' => 'orders',
'recipient_type' => 'staff',
'wc_email' => 'new_order',
'enabled' => true,
],
'order_processing' => [
'id' => 'order_processing',
'label' => __('Order Processing', 'woonoow'),
'description' => __('When order is confirmed and being processed', 'woonoow'),
'category' => 'orders',
'recipient_type' => 'staff',
'wc_email' => 'customer_processing_order',
'enabled' => true,
],
'order_shipped' => [
'id' => 'order_shipped',
'label' => __('Order Shipped', 'woonoow'),
'description' => __('When order is shipped', 'woonoow'),
'category' => 'orders',
'recipient_type' => 'staff',
'wc_email' => '',
'enabled' => true,
],
'order_completed' => [
'id' => 'order_completed',
'label' => __('Order Completed', 'woonoow'),
'description' => __('When order is marked as completed', 'woonoow'),
'category' => 'orders',
'recipient_type' => 'staff',
'wc_email' => 'customer_completed_order',
'enabled' => true,
],
'order_cancelled' => [
'id' => 'order_cancelled',
'label' => __('Order Cancelled', 'woonoow'),
'description' => __('When order is cancelled', 'woonoow'),
'category' => 'orders',
'recipient_type' => 'staff',
'wc_email' => 'cancelled_order',
'enabled' => true,
],
'payment_received' => [
'id' => 'payment_received',
'label' => __('Payment Received', 'woonoow'),
'description' => __('When payment is successfully received', 'woonoow'),
'category' => 'orders',
'recipient_type' => 'staff',
'wc_email' => '',
'enabled' => true,
],
'payment_failed' => [
'id' => 'payment_failed',
'label' => __('Payment Failed', 'woonoow'),
'description' => __('When payment processing fails', 'woonoow'),
'category' => 'orders',
'recipient_type' => 'staff',
'wc_email' => '',
'enabled' => true,
],
// CUSTOMER EVENTS
'order_placed_customer' => [
'id' => 'order_placed',
'label' => __('Order Placed', 'woonoow'),
'description' => __('When customer places an order', 'woonoow'),
'category' => 'orders',
'recipient_type' => 'customer',
'wc_email' => 'customer_on_hold_order',
'enabled' => true,
],
'order_processing_customer' => [
'id' => 'order_processing',
'label' => __('Order Processing', 'woonoow'),
'description' => __('When order status changes to processing', 'woonoow'),
'category' => 'orders',
'recipient_type' => 'customer',
'wc_email' => 'customer_processing_order',
'enabled' => true,
],
'order_shipped_customer' => [
'id' => 'order_shipped',
'label' => __('Order Shipped', 'woonoow'),
'description' => __('When order is shipped with tracking', 'woonoow'),
'category' => 'orders',
'recipient_type' => 'customer',
'wc_email' => '',
'enabled' => true,
],
'order_completed_customer' => [
'id' => 'order_completed',
'label' => __('Order Completed', 'woonoow'),
'description' => __('When order is delivered/completed', 'woonoow'),
'category' => 'orders',
'recipient_type' => 'customer',
'wc_email' => 'customer_completed_order',
'enabled' => true,
],
'order_cancelled_customer' => [
'id' => 'order_cancelled',
'label' => __('Order Cancelled', 'woonoow'),
'description' => __('When order is cancelled', 'woonoow'),
'category' => 'orders',
'recipient_type' => 'customer',
'wc_email' => 'customer_refunded_order',
'enabled' => true,
],
'payment_received_customer' => [
'id' => 'payment_received',
'label' => __('Payment Received', 'woonoow'),
'description' => __('When payment is confirmed', 'woonoow'),
'category' => 'orders',
'recipient_type' => 'customer',
'wc_email' => '',
'enabled' => true,
],
'payment_failed_customer' => [
'id' => 'payment_failed',
'label' => __('Payment Failed', 'woonoow'),
'description' => __('When payment fails - prompt retry', 'woonoow'),
'category' => 'orders',
'recipient_type' => 'customer',
'wc_email' => 'customer_failed_order',
'enabled' => true,
],
'new_customer' => [
'id' => 'new_customer',
'label' => __('New Customer', 'woonoow'),
'description' => __('When a new customer registers', 'woonoow'),
'category' => 'customers',
'recipient_type' => 'customer',
'wc_email' => 'customer_new_account',
'enabled' => true,
],
];
/**
* Filter: woonoow_notification_events_registry
*
* Allows plugins/themes to add custom notification events
*
* @param array $events Event definitions
*/
return apply_filters('woonoow_notification_events_registry', $events);
}
/**
* Get events by recipient type
*
* @param string $recipient_type 'staff' or 'customer'
* @return array Filtered events
*/
public static function get_events_by_recipient($recipient_type) {
$all_events = self::get_all_events();
return array_filter($all_events, function($event) use ($recipient_type) {
return $event['recipient_type'] === $recipient_type;
});
}
/**
* Get events by category
*
* @param string $category 'orders', 'products', 'customers'
* @return array Filtered events
*/
public static function get_events_by_category($category) {
$all_events = self::get_all_events();
return array_filter($all_events, function($event) use ($category) {
return $event['category'] === $category;
});
}
/**
* Get single event definition
*
* @param string $event_id Event ID
* @param string $recipient_type Recipient type
* @return array|null Event definition or null if not found
*/
public static function get_event($event_id, $recipient_type) {
$all_events = self::get_all_events();
foreach ($all_events as $event) {
if ($event['id'] === $event_id && $event['recipient_type'] === $recipient_type) {
return $event;
}
}
return null;
}
/**
* Check if event exists
*
* @param string $event_id Event ID
* @param string $recipient_type Recipient type
* @return bool
*/
public static function event_exists($event_id, $recipient_type) {
return self::get_event($event_id, $recipient_type) !== null;
}
}

View File

@@ -9,6 +9,8 @@
namespace WooNooW\Core\Notifications;
use WooNooW\Email\DefaultTemplates as EmailDefaultTemplates;
class TemplateProvider {
/**
@@ -35,12 +37,13 @@ class TemplateProvider {
*
* @param string $event_id Event ID
* @param string $channel_id Channel ID
* @param string $recipient_type Recipient type ('customer' or 'staff')
* @return array|null
*/
public static function get_template($event_id, $channel_id) {
public static function get_template($event_id, $channel_id, $recipient_type = 'customer') {
$templates = self::get_templates();
$key = "{$event_id}_{$channel_id}";
$key = "{$recipient_type}_{$event_id}_{$channel_id}";
if (isset($templates[$key])) {
return $templates[$key];
@@ -48,7 +51,12 @@ class TemplateProvider {
// Return default if exists
$defaults = self::get_default_templates();
return $defaults[$key] ?? null;
if (isset($defaults[$key])) {
return $defaults[$key];
}
return null;
}
/**
@@ -57,16 +65,18 @@ class TemplateProvider {
* @param string $event_id Event ID
* @param string $channel_id Channel ID
* @param array $template Template data
* @param string $recipient_type Recipient type ('customer' or 'staff')
* @return bool
*/
public static function save_template($event_id, $channel_id, $template) {
public static function save_template($event_id, $channel_id, $template, $recipient_type = 'customer') {
$templates = get_option(self::OPTION_KEY, []);
$key = "{$event_id}_{$channel_id}";
$key = "{$recipient_type}_{$event_id}_{$channel_id}";
$templates[$key] = [
'event_id' => $event_id,
'channel_id' => $channel_id,
'recipient_type' => $recipient_type,
'subject' => $template['subject'] ?? '',
'body' => $template['body'] ?? '',
'variables' => $template['variables'] ?? [],
@@ -81,12 +91,13 @@ class TemplateProvider {
*
* @param string $event_id Event ID
* @param string $channel_id Channel ID
* @param string $recipient_type Recipient type ('customer' or 'staff')
* @return bool
*/
public static function delete_template($event_id, $channel_id) {
public static function delete_template($event_id, $channel_id, $recipient_type = 'customer') {
$templates = get_option(self::OPTION_KEY, []);
$key = "{$event_id}_{$channel_id}";
$key = "{$recipient_type}_{$event_id}_{$channel_id}";
if (isset($templates[$key])) {
unset($templates[$key]);
@@ -130,92 +141,104 @@ class TemplateProvider {
public static function get_default_templates() {
$templates = [];
// Define all events with their recipient types
$events = [
'order_placed' => 'staff',
'order_processing' => 'customer',
'order_completed' => 'customer',
'order_cancelled' => 'staff',
'order_refunded' => 'customer',
'low_stock' => 'staff',
'out_of_stock' => 'staff',
'new_customer' => 'customer',
'customer_note' => 'customer',
];
// Get all events from EventRegistry (single source of truth)
$all_events = EventRegistry::get_all_events();
// Generate email templates from DefaultEmailTemplates
foreach ($events as $event_id => $recipient_type) {
$default = DefaultEmailTemplates::get_template($event_id, $recipient_type);
// Get email templates from DefaultTemplates
$allEmailTemplates = EmailDefaultTemplates::get_all_templates();
foreach ($all_events as $event) {
$event_id = $event['id'];
$recipient_type = $event['recipient_type'];
// Get template body from the new clean markdown source
$body = $allEmailTemplates[$recipient_type][$event_id] ?? '';
$subject = EmailDefaultTemplates::get_default_subject($recipient_type, $event_id);
$templates["{$event_id}_email"] = [
// If template doesn't exist, create a simple fallback
if (empty($body)) {
$body = "[card]\n\n## Notification\n\nYou have a new notification about {$event_id}.\n\n[/card]";
$subject = __('Notification from {store_name}', 'woonoow');
}
$templates["{$recipient_type}_{$event_id}_email"] = [
'event_id' => $event_id,
'channel_id' => 'email',
'subject' => $default['subject'],
'body' => $default['body'],
'recipient_type' => $recipient_type,
'subject' => $subject,
'body' => $body,
'variables' => self::get_variables_for_event($event_id),
];
}
// Add push notification templates
$templates['order_placed_push'] = [
$templates['staff_order_placed_push'] = [
'event_id' => 'order_placed',
'channel_id' => 'push',
'recipient_type' => 'staff',
'subject' => __('New Order #{order_number}', 'woonoow'),
'body' => __('New order from {customer_name} - {order_total}', 'woonoow'),
'variables' => self::get_order_variables(),
];
$templates['order_processing_push'] = [
$templates['customer_order_processing_push'] = [
'event_id' => 'order_processing',
'channel_id' => 'push',
'recipient_type' => 'customer',
'subject' => __('Order Processing', 'woonoow'),
'body' => __('Your order #{order_number} is being processed', 'woonoow'),
'variables' => self::get_order_variables(),
];
$templates['order_completed_push'] = [
$templates['customer_order_completed_push'] = [
'event_id' => 'order_completed',
'channel_id' => 'push',
'recipient_type' => 'customer',
'subject' => __('Order Completed', 'woonoow'),
'body' => __('Your order #{order_number} has been completed!', 'woonoow'),
'variables' => self::get_order_variables(),
];
$templates['order_cancelled_push'] = [
$templates['staff_order_cancelled_push'] = [
'event_id' => 'order_cancelled',
'channel_id' => 'push',
'recipient_type' => 'staff',
'subject' => __('Order Cancelled', 'woonoow'),
'body' => __('Order #{order_number} has been cancelled', 'woonoow'),
'variables' => self::get_order_variables(),
];
$templates['order_refunded_push'] = [
$templates['customer_order_refunded_push'] = [
'event_id' => 'order_refunded',
'channel_id' => 'push',
'recipient_type' => 'customer',
'subject' => __('Order Refunded', 'woonoow'),
'body' => __('Your order #{order_number} has been refunded', 'woonoow'),
'variables' => self::get_order_variables(),
];
$templates['low_stock_push'] = [
$templates['staff_low_stock_push'] = [
'event_id' => 'low_stock',
'channel_id' => 'push',
'recipient_type' => 'staff',
'subject' => __('Low Stock Alert', 'woonoow'),
'body' => __('{product_name} is running low on stock', 'woonoow'),
'variables' => self::get_product_variables(),
];
$templates['out_of_stock_push'] = [
$templates['staff_out_of_stock_push'] = [
'event_id' => 'out_of_stock',
'channel_id' => 'push',
'recipient_type' => 'staff',
'subject' => __('Out of Stock Alert', 'woonoow'),
'body' => __('{product_name} is now out of stock', 'woonoow'),
'variables' => self::get_product_variables(),
];
$templates['new_customer_push'] = [
$templates['customer_new_customer_push'] = [
'event_id' => 'new_customer',
'channel_id' => 'push',
'recipient_type' => 'customer',
'subject' => __('Welcome!', 'woonoow'),
'body' => __('Welcome to {store_name}, {customer_name}!', 'woonoow'),
'variables' => self::get_customer_variables(),
];
$templates['customer_note_push'] = [
$templates['customer_customer_note_push'] = [
'event_id' => 'customer_note',
'channel_id' => 'push',
'recipient_type' => 'customer',
'subject' => __('Order Note Added', 'woonoow'),
'body' => __('A note has been added to order #{order_number}', 'woonoow'),
'variables' => self::get_order_variables(),