"""Wspólny renderer Markdown dla portalu PaganOS (docs / about / git README). Poza zwykłym Markdownem obsługuje diagramy Mermaid — bloki kodu oznaczone ```mermaid (np. `graph TD`, `sequenceDiagram`, …) są zamieniane na elementy `
`, które rysuje mermaid.js dołączony w `base.html`.

Renderowanie diagramów odbywa się PO STRONIE PRZEGLĄDARKI, więc tutaj nie
potrzebujemy żadnej zależności JS — tylko przekazujemy definicję w odpowiednim
znaczniku. Treść pozostaje HTML-escape'owana (mermaid czyta `textContent`,
więc przeglądarka najpierw odkoduje encje) — to bezpieczne dla treści
pochodzących od użytkowników.

Ten moduł celowo nie importuje Flask/Blueprintów, aby móc go używać zarówno
w `app.py`, jak i w `blueprints/*` bez cyklicznych importów.
"""
import re
import html as _html

try:
    import mistune
    _HAS_MISTUNE = True
except ImportError:  # pragma: no cover – środowisko bez mistune (fallback)
    mistune = None
    _HAS_MISTUNE = False

# mistune (v2/v3) renderuje ```mermaid jako 
.
# Zamieniamy opakowanie na 
 (zostawiając escape'owaną treść).
_MERMAID_BLOCK_RE = re.compile(
    r'
(.*?)
', re.DOTALL | re.IGNORECASE, ) # Cache instancji renderera per (escape, plugins) – tworzenie mistune jest tanie, # ale trzymanie jednej instancji jest czystsze i szybsze. _MD_INSTANCES = {} def to_mermaid(html_text: str) -> str: """Zamienia bloki ```mermaid w znaczniki renderowane przez mermaid.js.""" if not html_text or "language-mermaid" not in html_text.lower(): return html_text return _MERMAID_BLOCK_RE.sub( lambda m: '
' + m.group(1) + '
', html_text, ) # ── Spis treści ────────────────────────────────────────────────────────────── # mistune NIE nadaje nagłówkom atrybutu id, więc linki "spisu treści" typu # [Szybki start](#szybki-start) prowadziły donikąd. Dodajemy id z tekstu # nagłówka, wg algorytmu zbliżonego do GitHub (polskie znaki zachowane). _TAG_RE = re.compile(r'<[^>]+>') _HEADING_RE = re.compile(r'(.*?)', re.DOTALL) # Usuwamy interpunkcję, zostawiamy litery/cyfry/_/-, spacje i znaki Unicode. _SLUG_STRIP_RE = re.compile(r'[^\w -]', re.UNICODE) def _slugify(text: str) -> str: """Zamienia tekst nagłówka na anchor zgodny z linkami TOC (jak GitHub). - lowercase, bez interpunkcji (backticki, `/`, `.`, `–`, `:` itd.) - KAŻDA spacja → `-` (wielokrotne spacje dają wielokrotne myślniki, dokładnie jak w GitHub/Gitea – dlatego `#...--pacnew--pacsave` działa) """ text = _html.unescape(text).strip().lower() text = _SLUG_STRIP_RE.sub('', text) return text.replace(' ', '-') def add_heading_ids(html_text: str) -> str: """Dodaje `id` do

..

na podstawie tekstu (dla spisu treści).""" if not html_text or '{inner}' return _HEADING_RE.sub(_repl, html_text) # Pluginy GFM włączane ZAWSZE (obok przekazanych przez wywołującego). # 'table' jest krytyczny: mistune 3 domyślnie NIE renderuje tabel pipe — bez # niego `| a | b |` z README/docs wyświetla się jako zwykły akapit. _ALWAYS_PLUGINS = ("table",) def _get_markdown(escape: bool, plugins): effective = list(plugins) if plugins else [] for _p in _ALWAYS_PLUGINS: if _p not in effective: effective.append(_p) key = (bool(escape), tuple(effective)) md = _MD_INSTANCES.get(key) if md is None: md = mistune.create_markdown( escape=escape, plugins=effective, ) _MD_INSTANCES[key] = md return md def render_markdown(text: str, *, escape: bool = False, plugins=None) -> str: """Renderuje Markdown + tabele GFM + Mermaid + identyfikatory nagłówków. escape/plugins odpowiadają argumentom mistune.create_markdown, dzięki czemu można zachować dotychczasowe zachowanie poszczególnych widoków: - docs/about: render_markdown(text) (bez escape, bez pluginów) - README/pliki repo: render_markdown(text, escape=True, plugins=[...]) Plugin 'table' jest dodawany zawsze (patrz _ALWAYS_PLUGINS). """ if not text: return "" if not _HAS_MISTUNE: # Bez mistune nie ma HTML-a — zwracamy surowy tekst jak dotychczas. return text return add_heading_ids(to_mermaid(_get_markdown(escape, plugins)(text)))