' . $code . '' . "\n"; $i++; } // Heading elseif (preg_match('/^(#{1,6})\s+(.+)$/', $line, $m)) { $lvl = strlen($m[1]); $text = md_inline(h($m[2])); $html .= "{$text}\n"; $i++; } // Horizontal rule elseif (preg_match('/^[-*_]{3,}\s*$/', $line)) { $html .= "
\n"; $i++; } // Blockquote elseif (preg_match('/^>\s?(.*)$/', $line, $m)) { $bq = ''; while ($i < $total && preg_match('/^>\s?(.*)$/', $lines[$i], $bm)) { $bq .= $bm[1] . "\n"; $i++; } $html .= '
' . markdown_to_html($bq) . '
' . "\n"; } // Unordered list elseif (preg_match('/^[-*+]\s+(.+)$/', $line, $m)) { $html .= "\n"; } // Ordered list elseif (preg_match('/^\d+\.\s+(.+)$/', $line, $m)) { $html .= "
    \n"; while ($i < $total && preg_match('/^\d+\.\s+(.+)$/', $lines[$i], $lm)) { $html .= '
  1. ' . md_inline(h($lm[1])) . "
  2. \n"; $i++; } $html .= "
\n"; } // Blank line elseif (trim($line) === '') { $i++; } // Paragraph: collect consecutive non-blank, non-block lines else { $para = ''; while ($i < $total) { $l = $lines[$i]; if (trim($l) === '') break; // Stop at any line that looks like a block-level element start if (preg_match('/^(#{1,6}\s|[-*+]\s|\d+\.\s|>\s?|`{3}|[-*_]{3,}\s*$)/', $l)) break; $para .= $l . ' '; $i++; } $para = trim($para); if ($para !== '') { $html .= '

' . md_inline(h($para)) . "

\n"; } } // Absolute safety net: if $i did not advance in any branch above, // force it forward. This prevents infinite loops on any input. if ($i === $prev_i) { $i++; } } return $html; } // Inline markdown: bold, italic, code, links function md_inline(string $s): string { // Inline code (already HTML-escaped input, so backtick safe) $s = preg_replace('/`([^`]+)`/', '$1', $s); // Bold+italic $s = preg_replace('/\*\*\*(.+?)\*\*\*/', '$1', $s); // Bold $s = preg_replace('/\*\*(.+?)\*\*/', '$1', $s); // Italic $s = preg_replace('/\*(.+?)\*/', '$1', $s); // Links: must whitelist to http/https/ftp/mailto $s = preg_replace_callback( '/\[([^\]]+)\]\(((?:https?|ftp|mailto):[^\)]+)\)/', fn($m) => '' . $m[1] . '', $s ); return $s; }