', $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 = ''; $variables[$placeholder] = $matches[0]; $var_index++; return $placeholder; }, $html); // Headings (must be done in order from h4 to h1 to avoid conflicts) $html = preg_replace('/^#### (.*)$/m', '

$1

', $html); $html = preg_replace('/^### (.*)$/m', '

$1

', $html); $html = preg_replace('/^## (.*)$/m', '

$1

', $html); $html = preg_replace('/^# (.*)$/m', '

$1

', $html); // Bold (don't match across newlines) $html = preg_replace('/\*\*([^\n*]+?)\*\*/', '$1', $html); $html = preg_replace('/__([^\n_]+?)__/', '$1', $html); // Italic (don't match across newlines) $html = preg_replace('/\*([^\n*]+?)\*/', '$1', $html); $html = preg_replace('/_([^\n_]+?)_/', '$1', $html); // Horizontal rules $html = preg_replace('/^---$/m', '
', $html); // Links (but not button syntax) $html = preg_replace('/\[(?!button)([^\]]+)\]\(([^)]+)\)/', '$1', $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[] = '

' . $paragraph_content . '

'; $paragraph_content = ''; } }; foreach ($lines as $line) { $trimmed = trim($line); // Empty line - close paragraph or list if (empty($trimmed)) { if ($in_list) { $processed_lines[] = ''; $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[] = ''; $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 .= '
' . $trimmed; } else { // Start new paragraph $paragraph_content = $trimmed; } } // Close any open tags if ($in_list) { $processed_lines[] = ''; } $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
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(?!<)/', '
', $html); return $html; } }