| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103 |
- <?php
- /**
- * Ultra-small Markdown renderer for the legal pages (Impressum / Datenschutz).
- *
- * Deliberately supports only a safe, basic subset. The input is always
- * HTML-escaped first, so no raw HTML from the editor is ever emitted; only the
- * handful of constructs below are turned into tags:
- *
- * # / ## / ### headings
- * - item unordered lists (also "* item")
- * **bold** *italic*
- * [text](url) links (http/https/mailto schemes only)
- * blank line new paragraph; a single newline becomes <br>
- */
- 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 .= '<p>' . implode('<br>', $para) . "</p>\n";
- $para = [];
- }
- };
- $closeList = static function () use (&$inList, &$html): void {
- if ($inList) {
- $html .= "</ul>\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 .= "<h$level>" . markdown_inline($m[2]) . "</h$level>\n";
- continue;
- }
- if (preg_match('/^[-*]\s+(.*)$/', $trimmed, $m)) {
- $flushPara();
- if (!$inList) {
- $html .= "<ul>\n";
- $inList = true;
- }
- $html .= '<li>' . markdown_inline($m[1]) . "</li>\n";
- continue;
- }
- // Ordinary text: collect into the current paragraph; consecutive
- // non-blank lines are joined with <br>.
- $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 <a>.
- $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 '<a href="' . $url . '" target="_blank" rel="noopener noreferrer">' . $label . '</a>';
- },
- $text
- );
- // Bold first (**...**), then remaining single-asterisk italics (*...*).
- $text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text);
- $text = preg_replace('/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/', '<em>$1</em>', $text);
- return $text;
- }
|