Ajout du guide PDF HestiaCP réutilisable (HestiaCP 1.10.4)
- 14 chapitres + annexe en Markdown (chapitres/), 99 étapes - Générateur WeasyPrint (build/build.py) : PDF générique ou personnalisé via --vars fichier.conf - PDF générique + PDF personnalisé srv1.skglobalinstitute.com - Commandes v-* vérifiées contre le code source HestiaCP 1.10.4 - 03-bis-hestiacp.md pointe vers le nouveau guide Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018J2PsVQ28oW4xEkWJpcU9G
This commit is contained in:
245
hestiacp/build/build.py
Normal file
245
hestiacp/build/build.py
Normal file
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Génère le guide PDF HestiaCP à partir des chapitres Markdown.
|
||||
|
||||
Usage :
|
||||
python3 build/build.py # PDF générique (variables <IP>, <DOMAINE>...)
|
||||
python3 build/build.py --vars mon-vps.conf # PDF personnalisé avec les valeurs du VPS
|
||||
python3 build/build.py --html # garde aussi le HTML intermédiaire (debug)
|
||||
|
||||
Dépendances : pip install weasyprint markdown pygments
|
||||
"""
|
||||
import argparse
|
||||
import datetime
|
||||
import html
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import markdown
|
||||
from weasyprint import HTML
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
BUILD = ROOT / "build"
|
||||
CHAPTERS = ROOT / "chapitres"
|
||||
|
||||
# Variables du guide : clé -> (description, exemple)
|
||||
VARIABLES = {
|
||||
"IP": ("Adresse IPv4 publique du VPS", "203.0.113.10"),
|
||||
"IPV6": ("Adresse IPv6 publique du VPS", "2001:db8::10"),
|
||||
"DOMAINE": ("Domaine principal du serveur", "exemple.com"),
|
||||
"FQDN": ("Nom complet du serveur (hostname)", "srv1.exemple.com"),
|
||||
"HOST": ("Nom court du serveur", "srv1"),
|
||||
"USER": ("Utilisateur Linux sudo (SSH)", "jean"),
|
||||
"ADMIN": ("Administrateur du panel HestiaCP", "hadmin"),
|
||||
"EMAIL": ("E-mail de l'administrateur", "admin@exemple.com"),
|
||||
"SSH_PORT": ("Port SSH personnalisé", "50022"),
|
||||
"PANEL_PORT": ("Port du panel HestiaCP", "8083"),
|
||||
"CLIENT": ("Utilisateur Hestia qui héberge les projets", "client1"),
|
||||
"SITE": ("Domaine d'un projet hébergé", "monprojet.com"),
|
||||
"BACKUP_HOST": ("Serveur de sauvegarde distant (SFTP)", "backup.exemple.net"),
|
||||
"BACKUP_USER": ("Utilisateur SFTP de la sauvegarde", "sauvegarde"),
|
||||
"BACKUP_PORT": ("Port SSH du serveur de sauvegarde", "22"),
|
||||
}
|
||||
|
||||
TOKEN = "ZZV{}ZZ"
|
||||
TOKEN_RE = re.compile(r"ZZV([A-Z0-9_]+?)ZZ")
|
||||
PLACEHOLDER_RE = re.compile(r"\{\{([A-Z0-9_]+)\}\}")
|
||||
|
||||
CALLOUTS = {
|
||||
"NOTE": ("note", "À savoir"),
|
||||
"TIP": ("tip", "Astuce"),
|
||||
"WARNING": ("warning", "Attention"),
|
||||
"DANGER": ("danger", "Danger"),
|
||||
"CHECK": ("check", "Vérification"),
|
||||
}
|
||||
|
||||
|
||||
def load_vars(path):
|
||||
values = {}
|
||||
for line in Path(path).read_text(encoding="utf-8").splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, val = line.split("=", 1)
|
||||
values[key.strip().upper()] = val.strip().strip('"').strip("'")
|
||||
return values
|
||||
|
||||
|
||||
def protect_placeholders(text):
|
||||
def repl(m):
|
||||
key = m.group(1)
|
||||
if key not in VARIABLES:
|
||||
sys.exit(f"Variable inconnue dans le guide : {{{{{key}}}}}")
|
||||
return TOKEN.format(key)
|
||||
|
||||
return PLACEHOLDER_RE.sub(repl, text)
|
||||
|
||||
|
||||
def render_placeholders(html_text, values):
|
||||
def repl(m):
|
||||
key = m.group(1)
|
||||
if key in values and values[key]:
|
||||
return f'<span class="var filled">{html.escape(values[key])}</span>'
|
||||
return f'<span class="var"><{key}></span>'
|
||||
|
||||
return TOKEN_RE.sub(repl, html_text)
|
||||
|
||||
|
||||
def convert_callouts(text):
|
||||
"""> [!WARNING] Titre optionnel -> bloc <div class="callout warning">."""
|
||||
out, lines, i = [], text.split("\n"), 0
|
||||
while i < len(lines):
|
||||
m = re.match(r"^>\s*\[!(\w+)\]\s*(.*)$", lines[i])
|
||||
if m and m.group(1).upper() in CALLOUTS:
|
||||
kind, label = CALLOUTS[m.group(1).upper()]
|
||||
title = m.group(2).strip() or label
|
||||
body = []
|
||||
i += 1
|
||||
while i < len(lines) and lines[i].startswith(">"):
|
||||
body.append(re.sub(r"^>\s?", "", lines[i]))
|
||||
i += 1
|
||||
out.append(f'<div class="callout {kind}" markdown="1">')
|
||||
out.append(f'<p class="callout-title">{html.escape(title)}</p>\n')
|
||||
out.extend(body)
|
||||
out.append("</div>\n")
|
||||
continue
|
||||
out.append(lines[i])
|
||||
i += 1
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def slug(text, prefix):
|
||||
s = re.sub(r"<[^>]+>", "", text)
|
||||
s = re.sub(r"[^\w]+", "-", s.lower(), flags=re.UNICODE).strip("-")
|
||||
return f"{prefix}-{s}"[:80]
|
||||
|
||||
|
||||
def build_chapter(md_text, chap_no, label):
|
||||
md_text = convert_callouts(protect_placeholders(md_text))
|
||||
body = markdown.markdown(
|
||||
md_text,
|
||||
extensions=["fenced_code", "tables", "attr_list", "md_in_html", "codehilite", "sane_lists"],
|
||||
extension_configs={"codehilite": {"guess_lang": False, "css_class": "code"}},
|
||||
)
|
||||
toc = []
|
||||
step_counter = {"n": 0}
|
||||
|
||||
def h1(m):
|
||||
title = m.group(1)
|
||||
cid = f"chap-{chap_no}"
|
||||
toc.append(("chap", cid, title, "A" if label == "Annexe" else chap_no))
|
||||
return (f'<h1 id="{cid}" class="chapter-title"><span class="chap-no">{label}</span>'
|
||||
f'<span class="chap-name">{title}</span></h1>')
|
||||
|
||||
def h2(m):
|
||||
title = m.group(1)
|
||||
step_counter["n"] += 1
|
||||
sid = slug(title, f"s{chap_no}-{step_counter['n']}")
|
||||
toc.append(("step", sid, title, chap_no))
|
||||
return f'<h2 id="{sid}" class="step"><span class="box"></span>{title}</h2>'
|
||||
|
||||
body = re.sub(r"<h1>(.*?)</h1>", h1, body, count=1)
|
||||
body = re.sub(r"<h2>(.*?)</h2>", h2, body)
|
||||
return f'<section class="chapter">{body}</section>', toc
|
||||
|
||||
|
||||
def cover(values, version, date):
|
||||
personal = bool(values)
|
||||
subtitle = values.get("FQDN") if personal else "Guide réutilisable pour tout VPS Ubuntu 24.04"
|
||||
return f"""
|
||||
<section class="cover">
|
||||
<div class="cover-top">
|
||||
<p class="kicker">Guide d'installation pas à pas</p>
|
||||
<h1 class="cover-title">Serveur VPS<br>avec <span>HestiaCP</span></h1>
|
||||
<p class="cover-sub">{html.escape(subtitle)}</p>
|
||||
</div>
|
||||
<div class="cover-meta">
|
||||
<div><span>Panel</span><strong>HestiaCP {version}</strong></div>
|
||||
<div><span>Système</span><strong>Ubuntu 24.04 LTS</strong></div>
|
||||
<div><span>Pile</span><strong>Nginx · Apache · PHP-FPM · MariaDB · Exim · Dovecot · Bind</strong></div>
|
||||
<div><span>Commandes vérifiées le</span><strong>{date}</strong></div>
|
||||
</div>
|
||||
<p class="cover-foot">Web · DNS · Mail · SSL · Sécurité · Docker · Supervision · Sauvegardes</p>
|
||||
</section>"""
|
||||
|
||||
|
||||
def vars_page(values):
|
||||
rows = []
|
||||
for key, (desc, example) in VARIABLES.items():
|
||||
val = values.get(key, "")
|
||||
cell = f'<td class="val filled">{html.escape(val)}</td>' if val else '<td class="val"></td>'
|
||||
rows.append(f"<tr><td><code><{key}></code></td><td>{desc}</td>"
|
||||
f"<td class=\"ex\">{example}</td>{cell}</tr>")
|
||||
intro = ("Ce PDF a été généré avec les valeurs de ce serveur."
|
||||
if values else
|
||||
"Remplissez la dernière colonne avant de commencer. Dans tout le guide, remplacez chaque "
|
||||
"<span class=\"var\"><VARIABLE></span> par votre valeur. Pour obtenir un PDF déjà "
|
||||
"personnalisé, voir l'annexe « Régénérer ce guide ».")
|
||||
return f"""
|
||||
<section class="vars-page">
|
||||
<h1 class="plain">Fiche du serveur</h1>
|
||||
<p>{intro}</p>
|
||||
<table class="vars"><thead><tr><th>Variable</th><th>Signification</th><th>Exemple</th><th>Votre valeur</th></tr></thead>
|
||||
<tbody>{''.join(rows)}</tbody></table>
|
||||
<div class="legend">
|
||||
<p><strong>Conventions</strong></p>
|
||||
<ul>
|
||||
<li>Les commandes se copient telles quelles (pas d'invite <code>$</code>). Les lignes grises qui commencent par <code>#</code> sont des commentaires.</li>
|
||||
<li>Chapitres 1 à 3 : connecté en <strong>root</strong>. À partir du chapitre 4 : avec votre utilisateur <span class="var"><USER></span> et <code>sudo</code>.</li>
|
||||
<li>Chaque <strong>étape</strong> a une case à cocher : avancez dans l'ordre, une étape à la fois.</li>
|
||||
<li>Les encadrés <em>Vérification</em> indiquent le résultat attendu avant de passer à la suite.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>"""
|
||||
|
||||
|
||||
def toc_page(entries):
|
||||
items = []
|
||||
for kind, anchor, title, chap in entries:
|
||||
title_txt = re.sub(r"<[^>]+>", "", title)
|
||||
if kind == "chap":
|
||||
items.append(f'<li class="toc-chap"><a href="#{anchor}"><span class="n">{chap}</span>{title_txt}</a></li>')
|
||||
else:
|
||||
items.append(f'<li class="toc-step"><a href="#{anchor}">{title_txt}</a></li>')
|
||||
return f'<section class="toc"><h1 class="plain">Sommaire</h1><ol>{"".join(items)}</ol></section>'
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--vars", help="fichier de variables (voir vps.conf.exemple)")
|
||||
ap.add_argument("--out", help="chemin du PDF de sortie")
|
||||
ap.add_argument("--html", action="store_true", help="écrire aussi le HTML intermédiaire")
|
||||
args = ap.parse_args()
|
||||
|
||||
values = load_vars(args.vars) if args.vars else {}
|
||||
meta = load_vars(BUILD / "meta.conf")
|
||||
version, date = meta.get("HESTIA_VERSION", "?"), meta.get("VERIFIED_ON", datetime.date.today().isoformat())
|
||||
|
||||
chapters, toc = [], []
|
||||
for no, path in enumerate(sorted(CHAPTERS.glob("*.md")), start=1):
|
||||
label = "Annexe" if path.name.startswith("99") else f"Chapitre {no}"
|
||||
html_chap, entries = build_chapter(path.read_text(encoding="utf-8"), no, label)
|
||||
chapters.append(html_chap)
|
||||
toc.extend(entries)
|
||||
|
||||
body = cover(values, version, date) + vars_page(values) + toc_page(toc) + "".join(chapters)
|
||||
body = render_placeholders(body, values)
|
||||
doc_title = "Guide VPS HestiaCP" + (f" — {values['FQDN']}" if values.get("FQDN") else "")
|
||||
page = f"""<!doctype html><html lang="fr"><head><meta charset="utf-8">
|
||||
<title>{html.escape(doc_title)}</title><meta name="author" content="scriptshadow">
|
||||
<link rel="stylesheet" href="style.css"></head><body>{body}</body></html>"""
|
||||
|
||||
if args.out:
|
||||
out = Path(args.out)
|
||||
elif values.get("FQDN"):
|
||||
out = ROOT / f"Guide-VPS-HestiaCP-{values['FQDN']}.pdf"
|
||||
else:
|
||||
out = ROOT / "Guide-VPS-HestiaCP.pdf"
|
||||
if args.html:
|
||||
(BUILD / "guide.html").write_text(page, encoding="utf-8")
|
||||
HTML(string=page, base_url=str(BUILD)).write_pdf(out)
|
||||
print(f"PDF généré : {out}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
BIN
hestiacp/build/fonts/Inter-400.ttf
Normal file
BIN
hestiacp/build/fonts/Inter-400.ttf
Normal file
Binary file not shown.
BIN
hestiacp/build/fonts/Inter-500.ttf
Normal file
BIN
hestiacp/build/fonts/Inter-500.ttf
Normal file
Binary file not shown.
BIN
hestiacp/build/fonts/Inter-600.ttf
Normal file
BIN
hestiacp/build/fonts/Inter-600.ttf
Normal file
Binary file not shown.
BIN
hestiacp/build/fonts/Inter-700.ttf
Normal file
BIN
hestiacp/build/fonts/Inter-700.ttf
Normal file
Binary file not shown.
BIN
hestiacp/build/fonts/Inter-800.ttf
Normal file
BIN
hestiacp/build/fonts/Inter-800.ttf
Normal file
Binary file not shown.
BIN
hestiacp/build/fonts/JetBrainsMono-400.ttf
Normal file
BIN
hestiacp/build/fonts/JetBrainsMono-400.ttf
Normal file
Binary file not shown.
BIN
hestiacp/build/fonts/JetBrainsMono-600.ttf
Normal file
BIN
hestiacp/build/fonts/JetBrainsMono-600.ttf
Normal file
Binary file not shown.
2
hestiacp/build/meta.conf
Normal file
2
hestiacp/build/meta.conf
Normal file
@@ -0,0 +1,2 @@
|
||||
HESTIA_VERSION=1.10.4
|
||||
VERIFIED_ON=10 septembre 2026
|
||||
254
hestiacp/build/style.css
Normal file
254
hestiacp/build/style.css
Normal file
@@ -0,0 +1,254 @@
|
||||
/* ---------- Polices (embarquées, licence OFL) ---------- */
|
||||
@font-face { font-family: "Inter"; font-weight: 400; src: url(fonts/Inter-400.ttf); }
|
||||
@font-face { font-family: "Inter"; font-weight: 500; src: url(fonts/Inter-500.ttf); }
|
||||
@font-face { font-family: "Inter"; font-weight: 600; src: url(fonts/Inter-600.ttf); }
|
||||
@font-face { font-family: "Inter"; font-weight: 700; src: url(fonts/Inter-700.ttf); }
|
||||
@font-face { font-family: "Inter"; font-weight: 800; src: url(fonts/Inter-800.ttf); }
|
||||
@font-face { font-family: "JetBrains Mono"; font-weight: 400; src: url(fonts/JetBrainsMono-400.ttf); }
|
||||
@font-face { font-family: "JetBrains Mono"; font-weight: 600; src: url(fonts/JetBrainsMono-600.ttf); }
|
||||
|
||||
:root {
|
||||
--ink: #0f172a;
|
||||
--text: #1e293b;
|
||||
--muted: #64748b;
|
||||
--line: #e2e8f0;
|
||||
--surface: #f8fafc;
|
||||
--accent: #0f766e;
|
||||
--accent-2: #14b8a6;
|
||||
--accent-soft: #ccfbf1;
|
||||
--code-bg: #0d1117;
|
||||
--var: #b45309;
|
||||
--var-bg: #fef3c7;
|
||||
}
|
||||
|
||||
/* ---------- Pages ---------- */
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 22mm 18mm 20mm 18mm;
|
||||
@top-left {
|
||||
content: string(chapter);
|
||||
font: 500 7.5pt "Inter", sans-serif;
|
||||
color: #94a3b8;
|
||||
vertical-align: bottom;
|
||||
padding-bottom: 4mm;
|
||||
}
|
||||
@top-right {
|
||||
content: "HestiaCP · Ubuntu 24.04";
|
||||
font: 500 7.5pt "Inter", sans-serif;
|
||||
color: #94a3b8;
|
||||
vertical-align: bottom;
|
||||
padding-bottom: 4mm;
|
||||
}
|
||||
@bottom-right {
|
||||
content: counter(page);
|
||||
font: 700 8pt "Inter", sans-serif;
|
||||
color: var(--accent);
|
||||
}
|
||||
@bottom-left {
|
||||
content: "Guide VPS HestiaCP";
|
||||
font: 400 7.5pt "Inter", sans-serif;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
@page cover { margin: 0; @top-left { content: none; } @top-right { content: none; } @bottom-right { content: none; } @bottom-left { content: none; } }
|
||||
@page front { @top-left { content: none; } @top-right { content: none; } }
|
||||
|
||||
html { font-family: "Inter", "DejaVu Sans", sans-serif; font-size: 9.4pt; line-height: 1.55; color: var(--text); }
|
||||
body { margin: 0; }
|
||||
|
||||
/* ---------- Couverture ---------- */
|
||||
.cover {
|
||||
page: cover;
|
||||
height: 297mm;
|
||||
box-sizing: border-box;
|
||||
padding: 34mm 22mm 20mm;
|
||||
background: var(--ink);
|
||||
background-image:
|
||||
radial-gradient(circle at 88% 12%, rgba(20,184,166,.35) 0, rgba(20,184,166,0) 38%),
|
||||
radial-gradient(circle at 5% 95%, rgba(20,184,166,.18) 0, rgba(20,184,166,0) 40%);
|
||||
color: #e2e8f0;
|
||||
position: relative;
|
||||
}
|
||||
.kicker { text-transform: uppercase; letter-spacing: .18em; font-size: 8.5pt; font-weight: 600; color: var(--accent-2); margin: 0 0 8mm; }
|
||||
.cover-title { font-size: 40pt; line-height: 1.05; font-weight: 800; color: #fff; margin: 0; letter-spacing: -.02em; }
|
||||
.cover-title span { color: var(--accent-2); }
|
||||
.cover-sub { font-size: 13pt; color: #94a3b8; margin: 7mm 0 0; font-weight: 500; }
|
||||
.cover-meta { position: absolute; left: 22mm; right: 22mm; bottom: 42mm; border-top: 1px solid #334155; padding-top: 7mm; }
|
||||
.cover-meta div { display: flex; justify-content: space-between; padding: 2.2mm 0; border-bottom: 1px solid #1e293b; font-size: 9.5pt; }
|
||||
.cover-meta span { color: #94a3b8; }
|
||||
.cover-meta strong { color: #f1f5f9; font-weight: 600; text-align: right; max-width: 60%; }
|
||||
.cover-foot { position: absolute; left: 22mm; bottom: 20mm; margin: 0; font-size: 8.5pt; letter-spacing: .06em; color: var(--accent-2); }
|
||||
|
||||
/* ---------- Fiche serveur & sommaire ---------- */
|
||||
.vars-page, .toc { page: front; break-before: page; }
|
||||
h1.plain { font-size: 22pt; font-weight: 800; color: var(--ink); margin: 0 0 5mm; letter-spacing: -.01em; }
|
||||
table.vars td.val { border-bottom: 1px dashed #94a3b8; min-width: 45mm; width: 32%; }
|
||||
table.vars td.val.filled { font-family: "JetBrains Mono", monospace; font-size: 8pt; color: var(--var); border-bottom: 1px solid var(--line); }
|
||||
table.vars td.ex { color: var(--muted); font-size: 8pt; }
|
||||
td:first-child { white-space: nowrap; }
|
||||
td:first-child code { white-space: nowrap; overflow-wrap: normal; }
|
||||
.legend { margin-top: 8mm; background: var(--surface); border: 1px solid var(--line); border-radius: 3mm; padding: 3mm 5mm; }
|
||||
.legend p { margin: 1mm 0; }
|
||||
.legend ul { margin: 1mm 0 1mm; padding-left: 5mm; }
|
||||
|
||||
.toc ol { list-style: none; padding: 0; margin: 0; }
|
||||
.toc a { color: var(--text); text-decoration: none; }
|
||||
.toc a::after { content: leader(dotted) target-counter(attr(href), page); color: var(--muted); }
|
||||
.toc-chap { margin-top: 3.2mm; font-weight: 700; font-size: 10pt; color: var(--ink); break-after: avoid; }
|
||||
.toc-chap a { color: var(--ink); }
|
||||
.toc-chap .n { display: inline-block; width: 8mm; color: var(--accent); }
|
||||
.toc-step { padding-left: 8mm; font-size: 8.6pt; line-height: 1.7; }
|
||||
|
||||
/* ---------- Chapitres ---------- */
|
||||
.chapter { break-before: page; }
|
||||
h1.chapter-title { margin: 0 0 7mm; padding-bottom: 5mm; border-bottom: 2px solid var(--ink); }
|
||||
.chap-no { display: block; text-transform: uppercase; letter-spacing: .16em; font-size: 8pt; font-weight: 700; color: var(--accent); margin-bottom: 1.5mm; }
|
||||
.chap-name { display: block; font-size: 21pt; font-weight: 800; color: var(--ink); line-height: 1.15; letter-spacing: -.01em; string-set: chapter content(); }
|
||||
|
||||
body { counter-reset: step; }
|
||||
h2.step {
|
||||
counter-increment: step;
|
||||
font-size: 12.5pt; font-weight: 700; color: var(--ink);
|
||||
margin: 8mm 0 2.5mm; padding-top: 1mm;
|
||||
break-after: avoid;
|
||||
}
|
||||
h2.step::before {
|
||||
content: "Étape " counter(step);
|
||||
display: inline-block;
|
||||
font-size: 7.5pt; font-weight: 700; letter-spacing: .04em;
|
||||
color: #fff; background: var(--accent);
|
||||
border-radius: 1.2mm; padding: .6mm 2mm; margin-right: 2.5mm;
|
||||
vertical-align: 1.2pt;
|
||||
}
|
||||
h2.step .box { float: right; width: 3.6mm; height: 3.6mm; border: 1.3px solid #94a3b8; border-radius: .8mm; margin-top: 1mm; }
|
||||
.toc-step a::before { content: "Étape " target-counter(attr(href), step) " · "; color: var(--muted); font-weight: 500; }
|
||||
|
||||
h3 { font-size: 10.2pt; font-weight: 700; color: var(--ink); margin: 5mm 0 1.5mm; break-after: avoid; }
|
||||
h4 { font-size: 9.4pt; font-weight: 600; color: var(--accent); margin: 4mm 0 1mm; break-after: avoid; }
|
||||
p { margin: 1.6mm 0 2.4mm; orphans: 3; widows: 3; }
|
||||
ul, ol { margin: 1.5mm 0 2.5mm; padding-left: 5.5mm; }
|
||||
li { margin: .6mm 0; }
|
||||
a { color: var(--accent); text-decoration: none; }
|
||||
strong { color: var(--ink); font-weight: 600; }
|
||||
hr { border: 0; border-top: 1px solid var(--line); margin: 6mm 0; }
|
||||
|
||||
/* ---------- Code ---------- */
|
||||
code, pre, .var { font-variant-ligatures: none; font-feature-settings: "liga" 0, "calt" 0; }
|
||||
code { font-family: "JetBrains Mono", "DejaVu Sans Mono", monospace; font-size: 8pt; }
|
||||
p code, li code, td code {
|
||||
background: #eef2f7; color: #0f172a; border-radius: 1mm; padding: .2mm 1.1mm;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.code, pre {
|
||||
background: var(--code-bg); border-radius: 2.2mm;
|
||||
margin: 2mm 0 3.5mm; padding: 3mm 4mm;
|
||||
break-inside: avoid;
|
||||
}
|
||||
.code pre { margin: 0; padding: 0; background: none; }
|
||||
pre, .code pre {
|
||||
color: #e6edf3; font-family: "JetBrains Mono", "DejaVu Sans Mono", monospace; font-size: 7.9pt; line-height: 1.55;
|
||||
white-space: pre-wrap; overflow-wrap: anywhere;
|
||||
}
|
||||
pre code { font-size: inherit; background: none; padding: 0; color: inherit; }
|
||||
|
||||
/* ---------- Variables ---------- */
|
||||
.var { font-family: "JetBrains Mono", monospace; font-size: .92em; color: var(--var); background: var(--var-bg); border-radius: .8mm; padding: 0 .6mm; }
|
||||
pre .var, .code .var { background: rgba(251,191,36,.14); color: #fbbf24; }
|
||||
.var.filled { background: none; padding: 0; }
|
||||
pre .var.filled, .code .var.filled { background: none; color: #fcd34d; }
|
||||
|
||||
/* ---------- Tableaux ---------- */
|
||||
table { width: 100%; border-collapse: collapse; margin: 2.5mm 0 4mm; font-size: 8.6pt; break-inside: auto; }
|
||||
th { text-align: left; font-weight: 700; color: var(--ink); background: var(--surface); border-bottom: 1.5px solid var(--ink); padding: 1.8mm 2.2mm; }
|
||||
td { border-bottom: 1px solid var(--line); padding: 1.6mm 2.2mm; vertical-align: top; }
|
||||
tr { break-inside: avoid; }
|
||||
|
||||
/* ---------- Encadrés ---------- */
|
||||
.callout { border-left: 3px solid; border-radius: 0 2mm 2mm 0; padding: 2.2mm 4mm 1.5mm; margin: 3mm 0 4mm; break-inside: avoid; }
|
||||
.callout p { margin: .8mm 0 1.6mm; }
|
||||
.callout .callout-title { font-weight: 700; font-size: 8pt; text-transform: uppercase; letter-spacing: .08em; margin: 0 0 1mm; }
|
||||
.callout .code { margin: 1.5mm 0 2mm; }
|
||||
.callout.note { background: #eff6ff; border-color: #3b82f6; } .callout.note .callout-title { color: #1d4ed8; }
|
||||
.callout.tip { background: #f0fdfa; border-color: var(--accent-2); } .callout.tip .callout-title { color: var(--accent); }
|
||||
.callout.warning { background: #fffbeb; border-color: #f59e0b; } .callout.warning .callout-title { color: #b45309; }
|
||||
.callout.danger { background: #fef2f2; border-color: #ef4444; } .callout.danger .callout-title { color: #b91c1c; }
|
||||
.callout.check { background: #f0fdf4; border-color: #22c55e; } .callout.check .callout-title { color: #15803d; }
|
||||
|
||||
/* ---------- Coloration syntaxique (Pygments github-dark) ---------- */
|
||||
.code .c { color: #8B949E; font-style: italic } /* Comment */
|
||||
.code .err { color: #F85149 } /* Error */
|
||||
.code .esc { color: #E6EDF3 } /* Escape */
|
||||
.code .g { color: #E6EDF3 } /* Generic */
|
||||
.code .k { color: #FF7B72 } /* Keyword */
|
||||
.code .l { color: #A5D6FF } /* Literal */
|
||||
.code .n { color: #E6EDF3 } /* Name */
|
||||
.code .o { color: #FF7B72; font-weight: bold } /* Operator */
|
||||
.code .x { color: #E6EDF3 } /* Other */
|
||||
.code .p { color: #E6EDF3 } /* Punctuation */
|
||||
.code .ch { color: #8B949E; font-style: italic } /* Comment.Hashbang */
|
||||
.code .cm { color: #8B949E; font-style: italic } /* Comment.Multiline */
|
||||
.code .cp { color: #8B949E; font-weight: bold; font-style: italic } /* Comment.Preproc */
|
||||
.code .cpf { color: #8B949E; font-style: italic } /* Comment.PreprocFile */
|
||||
.code .c1 { color: #8B949E; font-style: italic } /* Comment.Single */
|
||||
.code .cs { color: #8B949E; font-weight: bold; font-style: italic } /* Comment.Special */
|
||||
.code .ge { color: #E6EDF3; font-style: italic } /* Generic.Emph */
|
||||
.code .ges { color: #E6EDF3; font-weight: bold; font-style: italic } /* Generic.EmphStrong */
|
||||
.code .gr { color: #FFA198 } /* Generic.Error */
|
||||
.code .gh { color: #79C0FF; font-weight: bold } /* Generic.Heading */
|
||||
.code .go { color: #8B949E } /* Generic.Output */
|
||||
.code .gp { color: #8B949E } /* Generic.Prompt */
|
||||
.code .gs { color: #E6EDF3; font-weight: bold } /* Generic.Strong */
|
||||
.code .gu { color: #79C0FF } /* Generic.Subheading */
|
||||
.code .gt { color: #FF7B72 } /* Generic.Traceback */
|
||||
.code .g-Underline { color: #E6EDF3; text-decoration: underline } /* Generic.Underline */
|
||||
.code .kc { color: #79C0FF } /* Keyword.Constant */
|
||||
.code .kd { color: #FF7B72 } /* Keyword.Declaration */
|
||||
.code .kn { color: #FF7B72 } /* Keyword.Namespace */
|
||||
.code .kp { color: #79C0FF } /* Keyword.Pseudo */
|
||||
.code .kr { color: #FF7B72 } /* Keyword.Reserved */
|
||||
.code .kt { color: #FF7B72 } /* Keyword.Type */
|
||||
.code .ld { color: #79C0FF } /* Literal.Date */
|
||||
.code .m { color: #A5D6FF } /* Literal.Number */
|
||||
.code .s { color: #A5D6FF } /* Literal.String */
|
||||
.code .na { color: #E6EDF3 } /* Name.Attribute */
|
||||
.code .nb { color: #E6EDF3 } /* Name.Builtin */
|
||||
.code .nc { color: #F0883E; font-weight: bold } /* Name.Class */
|
||||
.code .no { color: #79C0FF; font-weight: bold } /* Name.Constant */
|
||||
.code .nd { color: #D2A8FF; font-weight: bold } /* Name.Decorator */
|
||||
.code .ni { color: #FFA657 } /* Name.Entity */
|
||||
.code .ne { color: #F0883E; font-weight: bold } /* Name.Exception */
|
||||
.code .nf { color: #D2A8FF; font-weight: bold } /* Name.Function */
|
||||
.code .nl { color: #79C0FF; font-weight: bold } /* Name.Label */
|
||||
.code .nn { color: #FF7B72 } /* Name.Namespace */
|
||||
.code .nx { color: #E6EDF3 } /* Name.Other */
|
||||
.code .py { color: #79C0FF } /* Name.Property */
|
||||
.code .nt { color: #7EE787 } /* Name.Tag */
|
||||
.code .nv { color: #79C0FF } /* Name.Variable */
|
||||
.code .ow { color: #FF7B72; font-weight: bold } /* Operator.Word */
|
||||
.code .pm { color: #E6EDF3 } /* Punctuation.Marker */
|
||||
.code .w { color: #6E7681 } /* Text.Whitespace */
|
||||
.code .mb { color: #A5D6FF } /* Literal.Number.Bin */
|
||||
.code .mf { color: #A5D6FF } /* Literal.Number.Float */
|
||||
.code .mh { color: #A5D6FF } /* Literal.Number.Hex */
|
||||
.code .mi { color: #A5D6FF } /* Literal.Number.Integer */
|
||||
.code .mo { color: #A5D6FF } /* Literal.Number.Oct */
|
||||
.code .sa { color: #79C0FF } /* Literal.String.Affix */
|
||||
.code .sb { color: #A5D6FF } /* Literal.String.Backtick */
|
||||
.code .sc { color: #A5D6FF } /* Literal.String.Char */
|
||||
.code .dl { color: #79C0FF } /* Literal.String.Delimiter */
|
||||
.code .sd { color: #A5D6FF } /* Literal.String.Doc */
|
||||
.code .s2 { color: #A5D6FF } /* Literal.String.Double */
|
||||
.code .se { color: #79C0FF } /* Literal.String.Escape */
|
||||
.code .sh { color: #79C0FF } /* Literal.String.Heredoc */
|
||||
.code .si { color: #A5D6FF } /* Literal.String.Interpol */
|
||||
.code .sx { color: #A5D6FF } /* Literal.String.Other */
|
||||
.code .sr { color: #79C0FF } /* Literal.String.Regex */
|
||||
.code .s1 { color: #A5D6FF } /* Literal.String.Single */
|
||||
.code .ss { color: #A5D6FF } /* Literal.String.Symbol */
|
||||
.code .bp { color: #E6EDF3 } /* Name.Builtin.Pseudo */
|
||||
.code .fm { color: #D2A8FF; font-weight: bold } /* Name.Function.Magic */
|
||||
.code .vc { color: #79C0FF } /* Name.Variable.Class */
|
||||
.code .vg { color: #79C0FF } /* Name.Variable.Global */
|
||||
.code .vi { color: #79C0FF } /* Name.Variable.Instance */
|
||||
.code .vm { color: #79C0FF } /* Name.Variable.Magic */
|
||||
.code .il { color: #A5D6FF } /* Literal.Number.Integer.Long */
|
||||
Reference in New Issue
Block a user