eptm_dashboard/eptm_dashboard/pages/doc.py

99 lines
3.1 KiB
Python

"""Page /doc — documentation interne (markdown rendu en HTML côté Python)."""
from __future__ import annotations
import os
import re
from pathlib import Path
import markdown as md
import reflex as rx
from ..state import AuthState
from ..sidebar import layout
from ..components import scan_docs
_RE_TITLE = re.compile(r"^#\s+(.+?)\s*$", re.MULTILINE)
# Une seule instance de Markdown réutilisée (extensions activées).
_MD = md.Markdown(extensions=["tables", "fenced_code", "attr_list", "sane_lists"])
def _render_md(text: str) -> str:
_MD.reset()
return _MD.convert(text)
class DocState(AuthState):
sections: list[dict] = []
selected_slug: str = ""
selected_title: str = ""
selected_html: str = ""
def load_data(self):
if not self.authenticated:
return rx.redirect("/login")
self.sections = scan_docs()
if not self.sections:
self.selected_slug = ""
self.selected_title = ""
self.selected_html = "<p><em>Aucune documentation disponible. Ajoutez des fichiers <code>.md</code> dans <code>data/docs/</code>.</em></p>"
return
slugs = [s["slug"] for s in self.sections]
if self.selected_slug not in slugs:
self.selected_slug = slugs[0]
self._load_selected()
def select_section(self, slug: str):
self.selected_slug = slug
self._load_selected()
def _load_selected(self):
section = next((s for s in self.sections if s["slug"] == self.selected_slug), None)
if not section:
self.selected_title = ""
self.selected_html = ""
return
try:
text = Path(section["path"]).read_text(encoding="utf-8")
except Exception as e:
self.selected_title = section["title"]
self.selected_html = f"<p><em>Erreur de lecture : {e}</em></p>"
return
# Retirer le H1 (déjà affiché en titre)
m = _RE_TITLE.search(text)
if m:
text = text[m.end():].lstrip("\n")
self.selected_title = section["title"]
self.selected_html = _render_md(text)
# ── UI ────────────────────────────────────────────────────────────────────────
def _content() -> rx.Component:
return rx.box(
rx.heading(DocState.selected_title, size="6", margin_bottom="1rem"),
rx.html(DocState.selected_html, class_name="doc-content"),
padding="1.5rem 2rem",
background_color="var(--surface)",
border="1px solid var(--gray-5)",
border_radius="8px",
width="100%",
class_name="anim-fade",
)
def doc_page() -> rx.Component:
return layout(
rx.vstack(
rx.heading("Documentation", size="7"),
rx.text(
"Guide d'utilisation et fonctionnement interne de l'application.",
size="2", color="#666",
),
_content(),
spacing="3",
width="100%",
)
)