82 lines
2.8 KiB
Python
82 lines
2.8 KiB
Python
|
|
"""Mini-Markdown-Renderer für PM-Bodies — Python-Spiegelbild der
|
||
|
|
JS-Funktion ``renderPmBody`` in ``v2/screens/aktuelle-themen.html``.
|
||
|
|
|
||
|
|
Der Renderer interpretiert eine kleine Untermenge von Markdown:
|
||
|
|
|
||
|
|
- ``**bold**`` und ``__bold__`` → ``<strong>...</strong>``
|
||
|
|
- ``*italic*`` und ``_italic_`` → ``<em>...</em>`` (vorsichtig — nur
|
||
|
|
wenn nicht zwischen Ziffern oder Word-Charakters)
|
||
|
|
- Zeilen, die mit ``- `` oder ``* `` beginnen → ``<ul><li>...</li></ul>``
|
||
|
|
- Doppel-Newlines trennen Absätze → ``<p>...</p>``
|
||
|
|
- Einzelne Newlines innerhalb eines Absatzes → ``<br>``
|
||
|
|
- HTML-Special-Chars (``&``, ``<``, ``>``) werden escaped.
|
||
|
|
|
||
|
|
Die Python-Variante existiert primär zum Testen. Im Produktiv-Frontend
|
||
|
|
wird die JS-Variante benutzt (kein Round-Trip zum Server).
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import re
|
||
|
|
|
||
|
|
# 1) HTML-Escape (analog zur JS-Variante, in dieser Reihenfolge)
|
||
|
|
def _html_escape(s: str) -> str:
|
||
|
|
return s.replace("&", "&").replace("<", "<").replace(">", ">")
|
||
|
|
|
||
|
|
|
||
|
|
# 2) Bold-Marker: **...** und __...__
|
||
|
|
_RE_BOLD_STAR = re.compile(r"\*\*([^*\n]+?)\*\*")
|
||
|
|
_RE_BOLD_UNDER = re.compile(r"__([^_\n]+?)__")
|
||
|
|
|
||
|
|
# 3) Italic-Marker: *…* und _…_, jeweils nicht zw. Word-Chars
|
||
|
|
_RE_ITALIC_STAR = re.compile(r"(?<![*\w])\*([^*\n]+?)\*(?![*\w])")
|
||
|
|
_RE_ITALIC_UNDER = re.compile(r"(?<![_\w])_([^_\n]+?)_(?![_\w])")
|
||
|
|
|
||
|
|
# 4) Listen-Bullet
|
||
|
|
_RE_BULLET = re.compile(r"^\s*[-*]\s+")
|
||
|
|
|
||
|
|
|
||
|
|
def render_pm_body(body: str) -> str:
|
||
|
|
"""Render Mini-Markdown zu HTML. Leere/None-Eingabe → leerer String."""
|
||
|
|
if not body:
|
||
|
|
return ""
|
||
|
|
s = _html_escape(body)
|
||
|
|
s = _RE_BOLD_STAR.sub(r"<strong>\1</strong>", s)
|
||
|
|
s = _RE_BOLD_UNDER.sub(r"<strong>\1</strong>", s)
|
||
|
|
s = _RE_ITALIC_STAR.sub(r"<em>\1</em>", s)
|
||
|
|
s = _RE_ITALIC_UNDER.sub(r"<em>\1</em>", s)
|
||
|
|
|
||
|
|
# Listen: konsekutive "- "/"* "-Zeilen zu einem <ul> zusammenfassen
|
||
|
|
out_lines = []
|
||
|
|
in_list = False
|
||
|
|
for line in s.split("\n"):
|
||
|
|
if _RE_BULLET.match(line):
|
||
|
|
if not in_list:
|
||
|
|
out_lines.append('<ul style="margin:0.5em 0;padding-left:1.4em;">')
|
||
|
|
in_list = True
|
||
|
|
out_lines.append("<li>" + _RE_BULLET.sub("", line) + "</li>")
|
||
|
|
else:
|
||
|
|
if in_list:
|
||
|
|
out_lines.append("</ul>")
|
||
|
|
in_list = False
|
||
|
|
out_lines.append(line)
|
||
|
|
if in_list:
|
||
|
|
out_lines.append("</ul>")
|
||
|
|
s = "\n".join(out_lines)
|
||
|
|
|
||
|
|
# Paragraphen-Split bei doppelten Newlines, einzelne → <br>
|
||
|
|
paras = re.split(r"\n\s*\n", s)
|
||
|
|
rendered = []
|
||
|
|
for p in paras:
|
||
|
|
trimmed = p.strip()
|
||
|
|
if not trimmed:
|
||
|
|
continue
|
||
|
|
if trimmed.startswith("<"):
|
||
|
|
rendered.append(trimmed)
|
||
|
|
else:
|
||
|
|
rendered.append(
|
||
|
|
'<p style="margin:0 0 0.9em;">'
|
||
|
|
+ trimmed.replace("\n", "<br>")
|
||
|
|
+ "</p>"
|
||
|
|
)
|
||
|
|
return "\n".join(rendered)
|