🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
"""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
`<pre class="mermaid">`, 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 <pre><code class="language-mermaid">.
# Zamieniamy opakowanie na <pre class="mermaid"> (zostawiając escape'owaną treść).
_MERMAID_BLOCK_RE = re.compile(
r'<pre><code class="language-mermaid">(.*?)</code></pre>',
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: '<pre class="mermaid">' + m.group(1) + '</pre>',
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'<h([1-6])>(.*?)</h\1>', 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 <h1>..<h6> na podstawie tekstu (dla spisu treści)."""
if not html_text or '<h' not in html_text:
return html_text
seen = {}
def _repl(m):
level, inner = m.group(1), m.group(2)
slug = _slugify(_TAG_RE.sub('', inner))
if not slug:
return m.group(0)
if slug in seen:
seen[slug] += 1
slug = f"{slug}-{seen[slug]}"
else:
seen[slug] = 0
return f'<h{level} id="{slug}">{inner}</h{level}>'
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)))