bifroest/app/core/markdown.py
2026-10-27 08:34:17 +01:00

53 lines
2.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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