HtmlToMarkdown.php
Pfad: src/Infrastructure/Content/HtmlToMarkdown.php
Ext: php
Größe: 3769 Bytes
Geändert: 2026-07-16 01:38:51+02
<?php
declare(strict_types=1);
namespace Demo\Infrastructure\Content;
use Demo\Domain\Dokumentation\HtmlKonverter;
use DOMDocument;
use DOMNode;
/**
* Wandelt Editor-HTML in die erlaubte Markdown-Teilmenge zurueck.
* Nur Whitelist-Elemente (h1-h6, p/div, ul/ol/li, strong/b) werden erzeugt;
* alles andere liefert nur seinen Text, Script/Style werden verworfen.
* Damit ist die Konvertierung zugleich die Sanitisierungs-Grenze.
*/
final class HtmlToMarkdown implements HtmlKonverter
{
/**
* Konvertiert HTML nach Markdown.
*
* @param string $html Rohes Editor-HTML.
* @return string Bereinigtes Markdown.
*/
public function convert(string $html): string
{
if (trim($html) === '') {
return '';
}
$doc = new DOMDocument();
libxml_use_internal_errors(true);
$doc->loadHTML(
'<?xml encoding="utf-8"?><html><body>' . $html . '</body></html>',
LIBXML_NOERROR | LIBXML_NOWARNING
);
libxml_clear_errors();
$body = $doc->getElementsByTagName('body')->item(0);
if ($body === null) {
return '';
}
$bloecke = [];
foreach (iterator_to_array($body->childNodes) as $node) {
$md = $this->block($node);
if ($md !== '') {
$bloecke[] = $md;
}
}
return implode("\n\n", $bloecke);
}
/**
* Wandelt einen Knoten auf Blockebene in eine Markdown-Zeile(n) um.
*
* @param DOMNode $node Blockknoten.
* @return string Markdown-Block oder leerer String.
*/
private function block(DOMNode $node): string
{
$tag = strtolower($node->nodeName);
if ($tag === '#text') {
return $this->inline($node);
}
if (preg_match('/^h([1-6])$/', $tag, $m) === 1) {
return str_repeat('#', (int) $m[1]) . ' ' . $this->inline($node);
}
if ($tag === 'ul' || $tag === 'ol') {
return $this->liste($node, $tag === 'ol');
}
if ($tag === 'script' || $tag === 'style') {
return '';
}
return $this->inline($node);
}
/**
* Rendert eine Liste; jedes li wird zu einer Markdown-Zeile.
*
* @param DOMNode $node ul- oder ol-Knoten.
* @param bool $nummeriert True fuer ol.
* @return string Mehrzeiliger Listen-Block.
*/
private function liste(DOMNode $node, bool $nummeriert): string
{
$zeilen = [];
$i = 1;
foreach ($node->childNodes as $kind) {
if (strtolower($kind->nodeName) !== 'li') {
continue;
}
$text = $this->inline($kind);
if ($text === '') {
continue;
}
$zeilen[] = ($nummeriert ? $i . '. ' : '- ') . $text;
$i++;
}
return implode("\n", $zeilen);
}
/**
* Sammelt den Inline-Text eines Knotens; strong/b werden zu **fett**.
*
* @param DOMNode $node Knoten.
* @return string Zusammengefasster, whitespace-normalisierter Text.
*/
private function inline(DOMNode $node): string
{
$tag = strtolower($node->nodeName);
if ($tag === '#text') {
return (string) $node->nodeValue;
}
if ($tag === 'script' || $tag === 'style') {
return '';
}
$inhalt = '';
foreach ($node->childNodes as $kind) {
$inhalt .= $this->inline($kind);
}
$inhalt = trim((string) preg_replace('/\s+/', ' ', $inhalt));
if (($tag === 'strong' || $tag === 'b') && $inhalt !== '') {
return '**' . $inhalt . '**';
}
return $inhalt;
}
}