*/
declare(strict_types=1);
/** Render a basic-Markdown string to safe HTML. */
function markdown_basic(string $text): string
{
$lines = explode("\n", str_replace("\r\n", "\n", $text));
$html = '';
$inList = false;
$para = [];
$flushPara = static function () use (&$para, &$html): void {
if ($para) {
$html .= '
' . implode('
', $para) . "
\n";
$para = [];
}
};
$closeList = static function () use (&$inList, &$html): void {
if ($inList) {
$html .= "\n";
$inList = false;
}
};
foreach ($lines as $line) {
$trimmed = trim($line);
if ($trimmed === '') {
$flushPara();
$closeList();
continue;
}
if (preg_match('/^(#{1,3})\s+(.*)$/', $trimmed, $m)) {
$flushPara();
$closeList();
$level = strlen($m[1]);
$html .= "" . markdown_inline($m[2]) . "\n";
continue;
}
if (preg_match('/^[-*]\s+(.*)$/', $trimmed, $m)) {
$flushPara();
if (!$inList) {
$html .= "\n";
$inList = true;
}
$html .= '- ' . markdown_inline($m[1]) . "
\n";
continue;
}
// Ordinary text: collect into the current paragraph; consecutive
// non-blank lines are joined with
.
$closeList();
$para[] = markdown_inline($trimmed);
}
$flushPara();
$closeList();
return $html;
}
/** Inline formatting for one line. Escapes first, then applies the subset. */
function markdown_inline(string $text): string
{
// Escape everything up front so no raw HTML survives from the input.
$text = e($text);
// Links [text](url) — only http/https/mailto schemes are turned into .
$text = preg_replace_callback(
'/\[([^\]]+)\]\(([^)\s]+)\)/',
static function (array $m): string {
[$whole, $label, $url] = $m;
if (!preg_match('#^(https?:|mailto:)#i', $url)) {
return $whole; // leave untouched if the scheme is not allowed
}
return '' . $label . '';
},
$text
);
// Bold first (**...**), then remaining single-asterisk italics (*...*).
$text = preg_replace('/\*\*(.+?)\*\*/', '$1', $text);
$text = preg_replace('/(?$1', $text);
return $text;
}