"""Modellausgaben sicher darstellen (→ Konzept 4.5).
Markdown wird serverseitig gerendert, ohne Roh-HTML. Bilder werden nie geladen
(kein Datenabfluss über Bild-URLs), Links zeigen ihr Ziel sichtbar an. Danach
bereinigt nh3 mit einer Allowlist, als zweite Sicherung gegen Fehler im Renderer.
"""
from html import escape
from urllib.parse import urlsplit
import nh3
from markdown_it import MarkdownIt
from markupsafe import Markup
SCHEMES = {"http", "https", "mailto"}
TAGS = {"p", "br", "strong", "em", "del", "code", "pre", "blockquote", "ul", "ol", "li", "hr",
"h1", "h2", "h3", "h4", "h5", "h6", "table", "thead", "tbody", "tr", "th", "td", "a", "span"}
# Keine style-Attribute: die CSP lässt keine Inline-Styles zu
ATTRIBUTES = {"a": {"href", "title"}, "ol": {"start"}}
TARGET_CHARS = 80
CLASSES = {"span": {"aiw-image", "aiw-link-target"}}
def _image(renderer, tokens, idx, options, env) -> str:
"""Bild als Text mit Ziel, nicht laden."""
token = tokens[idx]
alt = "".join(child.content for child in token.children or []) or "Bild"
target = token.attrGet("src") or ""
return f'[Bild: {escape(alt)} – {escape(str(target))}]'
def _link_close(renderer, tokens, idx, options, env) -> str:
"""Hinter dem Linktext das Ziel anzeigen, wenn der Text es nicht schon ist."""
start = max(i for i in range(idx) if tokens[i].type == "link_open")
href = str(tokens[start].attrGet("href") or "")
text = "".join(t.content for t in tokens[start + 1:idx])
if not href or text == href or href.startswith("mailto:") and text == href[7:]:
return ""
shown = href if len(href) <= TARGET_CHARS else href[:TARGET_CHARS] + "…"
return f' ({escape(shown)})'
_md = MarkdownIt("commonmark", {"html": False, "linkify": False}).enable(["table", "strikethrough"])
_md.add_render_rule("image", _image)
_md.add_render_rule("link_close", _link_close)
_md.validateLink = lambda url: urlsplit(url.strip()).scheme.lower() in SCHEMES # type: ignore[method-assign]
def render(text: str) -> Markup:
html = _md.render(text or "")
clean = nh3.clean(html, tags=TAGS, attributes=ATTRIBUTES, url_schemes=SCHEMES, allowed_classes=CLASSES,
link_rel="noopener noreferrer nofollow")
return Markup(clean)