215 lines
5.8 KiB
PHP
215 lines
5.8 KiB
PHP
<?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;
|
|
|
|
// Protect variables from markdown parsing by temporarily replacing them
|
|
$variables = [];
|
|
$var_index = 0;
|
|
$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) {
|
|
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) {
|
|
$processed_lines[] = '</ul>';
|
|
$in_list = false;
|
|
}
|
|
$close_paragraph();
|
|
$processed_lines[] = '';
|
|
continue;
|
|
}
|
|
|
|
// Check if line is a list item
|
|
if (preg_match('/^[\*\-•✓✔]\s/', $trimmed)) {
|
|
$close_paragraph();
|
|
$content = preg_replace('/^[\*\-•✓✔]\s/', '', $trimmed);
|
|
if (!$in_list) {
|
|
$processed_lines[] = '<ul>';
|
|
$in_list = true;
|
|
}
|
|
$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!)
|
|
$paragraph_content .= '<br>' . $trimmed;
|
|
} else {
|
|
// Start new paragraph
|
|
$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)
|
|
{
|
|
// Don't convert newlines inside HTML tags
|
|
$html = preg_replace('/(?<!>)\n(?!<)/', '<br>', $html);
|
|
return $html;
|
|
}
|
|
}
|