53 lines
2.3 KiB
Python
53 lines
2.3 KiB
Python
"""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'<span class="aiw-image">[Bild: {escape(alt)} – {escape(str(target))}]</span>'
|
||
|
||
|
||
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 "</a>"
|
||
shown = href if len(href) <= TARGET_CHARS else href[:TARGET_CHARS] + "…"
|
||
return f'</a> <span class="aiw-link-target">({escape(shown)})</span>'
|
||
|
||
|
||
_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)
|