fix: resolve container width issues, spa redirects, and appearance settings overwrite. feat: enhance order/sub details and newsletter layout

This commit is contained in:
Dwindi Ramadhana
2026-02-05 00:09:40 +07:00
parent a0b5f8496d
commit 5f08c18ec7
77 changed files with 7027 additions and 4546 deletions

View File

@@ -1,4 +1,5 @@
<?php
/**
* Markdown to Email HTML Parser
*
@@ -17,21 +18,23 @@
namespace WooNooW\Core\Notifications;
class MarkdownParser {
class MarkdownParser
{
/**
* Parse markdown to email HTML
*
* @param string $markdown
* @return string
*/
public static function parse($markdown) {
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) {
function ($matches) {
$type = $matches[1] ?? '';
$content = trim($matches[2]);
$parsed_content = self::parse_basics($content);
@@ -39,12 +42,12 @@ class MarkdownParser {
},
$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) {
function ($matches) {
$style = $matches[1] ?? '';
$url = $matches[2];
$text = $matches[3];
@@ -52,71 +55,88 @@ class MarkdownParser {
},
$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) {
private static function parse_basics($text)
{
$html = $text;
// Protect variables from markdown parsing by temporarily replacing them
$variables = [];
$var_index = 0;
$html = preg_replace_callback('/\{([^}]+)\}/', function($matches) use (&$variables, &$var_index) {
$html = preg_replace_callback('/\{([^}]+)\}/', function ($matches) use (&$variables, &$var_index) {
$placeholder = '<!--VAR' . $var_index . '-->';
$variables[$placeholder] = $matches[0];
$var_index++;
return $placeholder;
}, $html);
// Protect existing HTML tags (h1-h6, p) with style attributes from being overwritten
$html_tags = [];
$tag_index = 0;
$html = preg_replace_callback('/<(h[1-6]|p)([^>]*style=[^>]*)>/', function ($matches) use (&$html_tags, &$tag_index) {
$placeholder = '<!--HTMLTAG' . $tag_index . '-->';
$html_tags[$placeholder] = $matches[0];
$tag_index++;
return $placeholder;
}, $html);
// Headings (must be done in order from h4 to h1 to avoid conflicts)
// Only match markdown syntax (lines starting with #), not existing HTML
$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);
// Restore protected HTML tags
foreach ($html_tags as $placeholder => $original) {
$html = str_replace($placeholder, $original, $html);
}
// Bold (don't match across newlines)
$html = preg_replace('/\*\*([^\n*]+?)\*\*/', '<strong>$1</strong>', $html);
$html = preg_replace('/__([^\n_]+?)__/', '<strong>$1</strong>', $html);
// Italic (don't match across newlines)
$html = preg_replace('/\*([^\n*]+?)\*/', '<em>$1</em>', $html);
$html = preg_replace('/_([^\n_]+?)_/', '<em>$1</em>', $html);
// Horizontal rules
$html = preg_replace('/^---$/m', '<hr>', $html);
// Links (but not button syntax)
$html = preg_replace('/\[(?!button)([^\]]+)\]\(([^)]+)\)/', '<a href="$2">$1</a>', $html);
// Process lines for paragraphs and lists
$lines = explode("\n", $html);
$in_list = false;
$paragraph_content = '';
$processed_lines = [];
$close_paragraph = function() use (&$paragraph_content, &$processed_lines) {
$close_paragraph = function () use (&$paragraph_content, &$processed_lines) {
if ($paragraph_content) {
$processed_lines[] = '<p>' . $paragraph_content . '</p>';
$paragraph_content = '';
}
};
foreach ($lines as $line) {
$trimmed = trim($line);
// Empty line - close paragraph or list
if (empty($trimmed)) {
if ($in_list) {
@@ -127,7 +147,7 @@ class MarkdownParser {
$processed_lines[] = '';
continue;
}
// Check if line is a list item
if (preg_match('/^[\*\-•✓✔]\s/', $trimmed)) {
$close_paragraph();
@@ -139,20 +159,20 @@ class MarkdownParser {
$processed_lines[] = '<li>' . $content . '</li>';
continue;
}
// Close list if we're in one
if ($in_list) {
$processed_lines[] = '</ul>';
$in_list = false;
}
// Block-level HTML tags - don't wrap in paragraph
if (preg_match('/^<(div|h1|h2|h3|h4|h5|h6|p|ul|ol|li|hr|table|blockquote)/i', $trimmed)) {
$close_paragraph();
$processed_lines[] = $line;
continue;
}
// Regular text line - accumulate in paragraph
if ($paragraph_content) {
// Add line break before continuation (THIS IS THE KEY FIX!)
@@ -162,30 +182,31 @@ class MarkdownParser {
$paragraph_content = $trimmed;
}
}
// Close any open tags
if ($in_list) {
$processed_lines[] = '</ul>';
}
$close_paragraph();
$html = implode("\n", $processed_lines);
// Restore variables
foreach ($variables as $placeholder => $original) {
$html = str_replace($placeholder, $original, $html);
}
return $html;
}
/**
* Convert newlines to <br> tags for email rendering
*
* @param string $html
* @return string
*/
public static function nl2br_email($html) {
public static function nl2br_email($html)
{
// Don't convert newlines inside HTML tags
$html = preg_replace('/(?<!>)\n(?!<)/', '<br>', $html);
return $html;