MarkdownToHtml.php
Pfad: src/Infrastructure/Content/MarkdownToHtml.php
Ext: php
Größe: 3815 Bytes
Geändert: 2026-07-15T21:45:39+02:00
Frühere Version vom 2026-07-15T21:45:39+02:00 · zur aktuellen Fassung
<?php
declare(strict_types=1);
namespace Demo\Infrastructure\Content;
/**
* Rendert die erlaubte Markdown-Teilmenge (h1-h6, Absatz, ul/ol, strong)
* zu sicherem HTML. Textinhalt wird vollstaendig escaped -> kein XSS.
*/
final class MarkdownToHtml
{
/**
* Wandelt Markdown in HTML um.
*
* @param string $markdown Quelltext.
* @return string Sicheres HTML.
*/
public function render(string $markdown): string
{
$lines = preg_split('/\r\n|\r|\n/', $markdown) ?: [];
$html = [];
$para = [];
$liste = null;
foreach ($lines as $line) {
$t = trim($line);
if ($t === '') {
$this->flushAbsatz($html, $para);
$liste = $this->schliesseListe($html, $liste);
continue;
}
if (preg_match('/^(#{1,6})\s+(.*)$/', $t, $m) === 1) {
$this->flushAbsatz($html, $para);
$liste = $this->schliesseListe($html, $liste);
$n = strlen($m[1]);
$html[] = "<h{$n}>" . $this->inline($m[2]) . "</h{$n}>";
continue;
}
if (preg_match('/^[-*]\s+(.*)$/', $t, $m) === 1) {
$this->flushAbsatz($html, $para);
$liste = $this->oeffneListe($html, $liste, 'ul');
$html[] = '<li>' . $this->inline($m[1]) . '</li>';
continue;
}
if (preg_match('/^\d+\.\s+(.*)$/', $t, $m) === 1) {
$this->flushAbsatz($html, $para);
$liste = $this->oeffneListe($html, $liste, 'ol');
$html[] = '<li>' . $this->inline($m[1]) . '</li>';
continue;
}
$liste = $this->schliesseListe($html, $liste);
$para[] = $t;
}
$this->flushAbsatz($html, $para);
$this->schliesseListe($html, $liste);
return implode("\n", $html);
}
/**
* Gibt einen gesammelten Absatz aus und leert den Puffer.
*
* @param list<string> $html Ausgabe-Puffer (per Referenz).
* @param list<string> $para Absatz-Zeilen (per Referenz).
*/
private function flushAbsatz(array &$html, array &$para): void
{
if ($para !== []) {
$html[] = '<p>' . $this->inline(implode(' ', $para)) . '</p>';
$para = [];
}
}
/**
* Oeffnet bei Bedarf eine Liste des gewuenschten Typs.
*
* @param list<string> $html Ausgabe-Puffer (per Referenz).
* @param string|null $aktuell Aktuell offener Listentyp.
* @param string $wunsch Gewuenschter Listentyp (ul|ol).
* @return string Der nun offene Listentyp.
*/
private function oeffneListe(array &$html, ?string $aktuell, string $wunsch): string
{
if ($aktuell !== $wunsch) {
$this->schliesseListe($html, $aktuell);
$html[] = "<{$wunsch}>";
}
return $wunsch;
}
/**
* Schliesst eine offene Liste, falls vorhanden.
*
* @param list<string> $html Ausgabe-Puffer (per Referenz).
* @param string|null $aktuell Aktuell offener Listentyp.
* @return null Immer null (keine Liste mehr offen).
*/
private function schliesseListe(array &$html, ?string $aktuell)
{
if ($aktuell !== null) {
$html[] = "</{$aktuell}>";
}
return null;
}
/**
* Escaped Text und wandelt **fett** in <strong> um.
*
* @param string $text Roher Zeilentext.
* @return string Sicheres Inline-HTML.
*/
private function inline(string $text): string
{
$escaped = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
return preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $escaped) ?? $escaped;
}
}