Logik aus dem Jinja-Template (Heuchelei-Marker, Konsistenz-Block, decisive-Outcome-Selection) in app/marker.py extrahiert. Template ruft die drei Helper als Jinja-Globals auf. Damit ist die Logik testbar ohne Render-Kontext. Plus: app/pm_render.py als Python-Spiegelbild des JS-Mini-Markdown- Renderers in aktuelle-themen.html — fuer Tests und potenzielle Server-side-Render-Optionen (z.B. PM-Mail). Tests: - tests/test_marker.py (35 Cases): heuchelei_score, decisive_outcome, consistency_state inkl. Multi-Vote, ambivalente Empfehlung, Edge-Cases. - tests/test_pm_render.py (21 Cases): Bold, Italic, Listen, HTML-Escape, Paragraph-Splitting, snake_case-Schutz. Refs: ADR 0010, ADR 0011 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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)
|