fix: guide - bouton telechargement icone seule, diagramme Architecture rendu en image dans le PDF, emoji couleur (Noto) (#105)
CI / lint (push) Successful in 1m39s
CI / security (push) Successful in 1m5s
CI / test (push) Successful in 3m25s
CI / build (push) Successful in 1m18s
CI / e2e (push) Successful in 11m43s

This commit is contained in:
2026-09-18 15:01:14 -04:00
parent 99a5b735c8
commit b8054665bc
23 changed files with 212 additions and 36 deletions
+66
View File
@@ -0,0 +1,66 @@
"""Pré-rend les diagrammes Mermaid du guide intégré en PNG (#105).
Scanne les blocs ``<pre class="mermaid-code">`` de frontend/index.html,
extrait leur code, et appelle scripts/render_guide_diagram.mjs (Chromium +
mermaid v11) pour produire ``backend/assets/guide_diagrams/<sha1>.png``.
Ces PNG sont commités : le PDF du guide les embarque comme vraies images
(WeasyPrint ne sait pas exécuter Mermaid).
Usage : python scripts/build_guide_diagrams.py
"""
import hashlib
import html
import json
import re
import subprocess
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
ASSETS = ROOT / "backend" / "assets" / "guide_diagrams"
def extract_mermaid() -> dict[str, str]:
page_html = (ROOT / "frontend" / "index.html").read_text(encoding="utf-8")
out: dict[str, str] = {}
for m in re.finditer(
r'<pre class="mermaid-code"><code class="language-mermaid">(.*?)</code></pre>',
page_html,
re.DOTALL,
):
code = m.group(1)
code = html.unescape(code).strip()
sha = hashlib.sha1(code.encode("utf-8")).hexdigest()[:16]
out[sha] = code
return out
def main() -> int:
jobs = extract_mermaid()
if not jobs:
print("aucun bloc mermaid dans index.html")
return 1
todo = {k: v for k, v in jobs.items() if not (ASSETS / f"{k}.png").exists()}
print("diagrammes:", len(jobs), "| à rendre:", len(todo))
if not todo:
return 0
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as f:
json.dump(todo, f)
path = f.name
r = subprocess.run(
["node", str(ROOT / "scripts" / "render_guide_diagram.mjs"), path],
cwd=ROOT,
capture_output=True,
check=False,
text=True,
timeout=300,
)
print(r.stdout[-2000:])
if r.returncode != 0:
print(r.stderr[-2000:])
return r.returncode
if __name__ == "__main__":
sys.exit(main())
-3
View File
@@ -427,9 +427,6 @@ CONTENT: dict[str, tuple[str, str]] = {
# -- Boutons de téléchargement ---------------------------------------------------------------
"dl_md_title": ("Télécharger ce guide en Markdown", "Download this guide as Markdown"),
"dl_pdf_title": ("Télécharger ce guide en PDF", "Download this guide as PDF"),
"dl_md": ("Markdown", "Markdown"),
"dl_pdf": ("PDF", "PDF"),
# -- Export MD/PDF (côté serveur) -------------------------------------------------------------
"export_title": ("Guide d'utilisation ObsiGate", "ObsiGate User Guide"),
"export_footer": (
+49
View File
@@ -0,0 +1,49 @@
// Pré-rend les diagrammes Mermaid du guide (#105 PDF) en PNG.
// Usage: node scripts/render_guide_diagram.mjs
// Lit le(s) code(s) Mermaid via --input JSON {sha: code} et écrit
// backend/assets/guide_diagrams/<sha>.png (rendu chromium + mermaid v11 CDN,
// scale 2, fond blanc).
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const { chromium } = require('playwright');
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
const OUT_DIR = path.join(ROOT, 'backend', 'assets', 'guide_diagrams');
(async () => {
const jobs = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')); // {sha: code}
fs.mkdirSync(OUT_DIR, { recursive: true });
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1800, height: 1400 }, deviceScaleFactor: 2 });
await page.goto('about:blank');
await page.addScriptTag({ url: 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js' });
await page.waitForFunction(() => typeof window.mermaid !== 'undefined');
for (const [sha, code] of Object.entries(jobs)) {
const out = await page.evaluate(async (src) => {
window.mermaid.initialize({ startOnLoad: false, theme: 'default', securityLevel: 'loose', fontFamily: 'DejaVu Sans, Arial, sans-serif' });
try {
const { svg } = await window.mermaid.render('d_' + Math.random().toString(36).slice(2), src);
const box = document.createElement('div');
box.innerHTML = svg;
box.style.background = 'white';
document.body.replaceChildren(box);
const svgEl = box.querySelector('svg');
if (!svgEl) return { err: 'no svg' };
svgEl.style.maxWidth = 'none';
const r = svgEl.getBoundingClientRect();
return { w: Math.min(Math.ceil(r.width), 2000), h: Math.min(Math.ceil(r.height), 2600), ok: true };
} catch (e) {
return { err: String(e).slice(0, 300) };
}
}, code);
if (out.err) { console.error(sha, 'RENDER ERROR', out.err); process.exitCode = 1; continue; }
const file = path.join(OUT_DIR, sha + '.png');
await page.screenshot({ path: file, clip: { x: 0, y: 0, width: out.w, height: out.h } });
console.log(sha, '->', file, out.w + 'x' + out.h);
}
await browser.close();
})();