|
|
@@ -0,0 +1,103 @@
|
|
|
+<?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;
|
|
|
+}
|