"""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)