Skip to content

Commit 5144892

Browse files
committed
Site : documentation générée depuis le README et CONTRIBUTING
- docs/build.py rend README.md et CONTRIBUTING.md en pages Guide et Contribuer, avec un sommaire qui suit la lecture (même principe que le site d'Otter) ; publication par GitHub Actions - Page d'accueil : lien vers la documentation, installation en trois étapes avec scripts/deploy.sh - README : installation sur n'importe quel Linux, comptes gérés depuis l'appli, construction du site
1 parent 06f1b68 commit 5144892

8 files changed

Lines changed: 409 additions & 36 deletions

File tree

‎.github/workflows/pages.yml‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: Site
2+
3+
# Construit le site (docs/, plus la documentation rendue depuis README.md et CONTRIBUTING.md)
4+
# sur les pull requests, et le publie sur GitHub Pages depuis master.
5+
on:
6+
push:
7+
branches: [master]
8+
paths: ["docs/**", "README.md", "CONTRIBUTING.md", ".github/workflows/pages.yml"]
9+
pull_request:
10+
paths: ["docs/**", "README.md", "CONTRIBUTING.md", ".github/workflows/pages.yml"]
11+
workflow_dispatch:
12+
13+
permissions:
14+
contents: read
15+
16+
concurrency:
17+
group: pages-${{ github.ref }}
18+
cancel-in-progress: true
19+
20+
jobs:
21+
build:
22+
name: Construction du site
23+
runs-on: ubuntu-latest
24+
steps:
25+
- uses: actions/checkout@v7
26+
- uses: actions/setup-python@v7
27+
with:
28+
python-version: "3.13"
29+
- run: pip install markdown==3.11 pymdown-extensions==12.1
30+
- run: python docs/build.py
31+
- uses: actions/upload-pages-artifact@v5
32+
with:
33+
path: _site
34+
35+
deploy:
36+
name: Publication sur GitHub Pages
37+
if: github.event_name != 'pull_request'
38+
needs: build
39+
runs-on: ubuntu-latest
40+
permissions:
41+
pages: write
42+
id-token: write
43+
environment:
44+
name: github-pages
45+
url: ${{ steps.deployment.outputs.page_url }}
46+
steps:
47+
- id: deployment
48+
uses: actions/deploy-pages@v5

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
/target
22
/emulator/receipts
3+
/_site

‎CONTRIBUTING.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,10 @@ d'exemple (voir `barnum.rs`).
283283
sur des données fictives. N'y mets jamais de vraies données personnelles.
284284
- Les déclinaisons du logo se régénèrent avec
285285
`uv run --with pillow python docs/brand/make-assets.py`.
286+
- Le site se construit avec `docs/build.py` : la page d'accueil (`docs/index.html`) et la
287+
documentation, rendue depuis `README.md` et `CONTRIBUTING.md`. Une modification de ces
288+
fichiers met donc aussi le site à jour. Aperçu local :
289+
`uvx --with markdown --with pymdown-extensions python docs/build.py && python3 -m http.server -d _site`.
286290

287291
## Proposer une modification
288292

‎README.md‎

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
<a href="https://xninety9.github.io/Printr/">Site</a> ·
1919
<a href="#tickets-json">Les blocs</a> ·
2020
<a href="#interface-web">L'appli</a> ·
21-
<a href="#installation-sur-le-pi">Installer</a>
21+
<a href="#installation">Installer</a>
2222
</p>
2323

2424
Printr transforme une imprimante à tickets **Epson TM-T88V** en gadget familial. On compose un
@@ -57,10 +57,12 @@ sur un Raspberry Pi, qui parle ESC/POS directement à l'imprimante.
5757

5858
```sh
5959
cargo run --release -- --preview print examples/complet.json # aperçu dans le terminal, sans imprimante
60-
cargo run --release -- user add Camille # un compte pour l'interface web
6160
cargo run --release -- --device /dev/usb/lp0 serve # interface sur http://localhost:8080
61+
scripts/deploy.sh pi@printer.local # installation sur un Raspberry Pi (ou --local)
6262
```
6363

64+
Au premier lancement, l'interface propose de créer son compte, puis ceux de la famille.
65+
6466
Pas encore d'imprimante ? `scripts/demo.sh` imprime le ticket complet dans un
6567
[émulateur](#tester-sans-imprimante-émulateur) et en donne une image PNG.
6668

@@ -242,7 +244,9 @@ mêmes résultats, sans nouvel appel. `--refresh` force une nouvelle génératio
242244
avec `POST /api/shopping` et le jeton.
243245
- **Historique** : qui a imprimé quoi, et les éventuelles erreurs.
244246

245-
Chaque membre de la famille a son compte :
247+
Chaque membre de la famille a son compte. Le premier se crée sur l'écran d'accueil de
248+
l'appli ; les suivants depuis le menu **Mon compte**, où chacun change aussi son mot de passe.
249+
En ligne de commande, même pendant que le serveur tourne (il relit ses données) :
246250

247251
```sh
248252
printr user add Camille # demande le mot de passe (ou le lit sur l'entrée standard)
@@ -363,36 +367,74 @@ En cas de souci :
363367

364368
## Site et illustrations
365369

366-
Le site vitrine vit dans [`docs/`](docs/) (GitHub Pages, branche `master`, dossier `/docs`).
370+
Le site vit dans [`docs/`](docs/) : la page d'accueil, et des pages de documentation générées
371+
depuis ce README et le [guide de contribution](CONTRIBUTING.md), pour ne jamais s'en écarter.
372+
GitHub Actions le construit et le publie sur GitHub Pages à chaque modification sur `master`.
373+
Pour le voir en local :
374+
375+
```sh
376+
uvx --with markdown --with pymdown-extensions python docs/build.py && python3 -m http.server -d _site
377+
```
378+
367379
Ses illustrations sont générées avec des données fictives uniquement :
368380

369381
```sh
370382
docs/demo/photo-session.sh # captures de l'appli + tickets de l'émulateur
371383
uv run --with pillow python docs/brand/make-assets.py # déclinaisons du logo et icônes de l'appli
372384
```
373385

374-
## Compiler pour le Raspberry Pi
386+
## Installation
375387

376-
Avec [`cross`](https://github.com/cross-rs/cross) (nécessite Docker) :
388+
printr s'installe comme service systemd sur n'importe quel Linux : Raspberry Pi (Raspberry Pi
389+
OS Lite 64 bits conseillé), serveur ou PC, en ARM 64 bits, ARM 32 bits ou x86_64. L'imprimante
390+
peut être en USB ou sur le réseau.
377391

378392
```sh
379-
cross build --release --target aarch64-unknown-linux-gnu # Pi 3 / Zero 2 W, OS 64 bits
380-
cross build --release --target arm-unknown-linux-gnueabihf # Pi Zero W (ARMv6)
381-
scp target/aarch64-unknown-linux-gnu/release/printr pi@raspberrypi:
393+
scripts/deploy.sh pi@printer.local # machine distante, par SSH (clé) ; --port N si besoin
394+
scripts/deploy.sh --local # cette machine
382395
```
383396

384-
## Installation sur le Pi
397+
Le script détecte l'architecture de la machine, compile pour elle (avec
398+
[`cross`](https://github.com/cross-rs/cross), pour une glibc compatible), envoie le programme
399+
et lance [`deploy/install.sh`](deploy/install.sh), qui demande le mot de passe `sudo` **une
400+
seule fois** et la clé Claude (facultative). L'installation :
401+
402+
- crée l'utilisateur système `printr` et ajoute ton compte aux groupes `printr` et `lp` ;
403+
- installe le programme dans `/opt/printr` (et la commande `printr`) ;
404+
- installe la règle udev de l'imprimante USB (lien stable `/dev/tm88`) et charge `usblp` ;
405+
- clone [Barnum](https://github.com/XNinety9/Barnum) dans `/opt/barnum` (si `python3` et `git`
406+
sont là) ;
407+
- crée `/etc/printr.env` et le service, puis vérifie qu'il répond.
385408

386-
L'imprimante est exposée par le module noyau `usblp` en `/dev/usb/lp0`.
387-
Pour y accéder sans `sudo` :
409+
**La relancer ne perd rien.** Avant toute modification, elle vérifie que les données sont
410+
lisibles (sinon elle s'arrête sans rien toucher) et les sauvegarde dans
411+
`/var/backups/printr` (les cinq dernières). La configuration existante est gardée : seules les
412+
options nouvelles y sont ajoutées. L'ancien programme reste dans `/opt/printr/printr.old` et les
413+
anciens fichiers système modifiés rejoignent les sauvegardes. Chaque étape dit ce qui a changé.
414+
415+
Ensuite, **plus besoin de sudo** (après s'être déconnecté et reconnecté, pour le groupe) :
388416

389417
```sh
390-
sudo cp deploy/70-tm-t88v.rules /etc/udev/rules.d/
391-
sudo udevadm control --reload && sudo udevadm trigger
392-
sudo usermod -aG lp $USER # puis se reconnecter
418+
scripts/deploy.sh pi@printer.local # depuis le PC : mise à jour du programme et redémarrage
419+
nano /etc/printr.env # configuration (clé Claude, imprimante, port…)
420+
systemctl restart printr # redémarrage
421+
journalctl -u printr -f # suivre les impressions
393422
```
394423

395-
La règle crée aussi le lien stable `/dev/tm88` : `printr --device /dev/tm88 test`.
424+
`--install` relance l'installation complète (nouveau fichier de service, nouvelle règle udev…).
425+
Pour une imprimante réseau, définir `PRINTR_TCP=adresse:9100` dans `/etc/printr.env`. Sur la
426+
machine, la commande `printr` lit aussi ce fichier : un ticket lancé à la main utilise la même
427+
imprimante et la même clé que le serveur.
428+
429+
Pour tout faire à la main, compiler pour la machine visée, avec
430+
[`cross`](https://github.com/cross-rs/cross) (Docker) depuis un autre ordinateur, puis lancer
431+
l'installation sur la machine (`--help` pour les options) :
432+
433+
```sh
434+
cross build --release --target aarch64-unknown-linux-gnu # Pi 3, Pi 4, Pi 5, Zero 2 W (64 bits)
435+
cross build --release --target armv7-unknown-linux-gnueabihf # Pi 2, Pi 3 en 32 bits
436+
cross build --release --target arm-unknown-linux-gnueabihf # Pi Zero W, Pi 1 (ARMv6)
437+
cargo build --release # cette machine
438+
sudo deploy/install.sh --binary chemin/vers/printr
439+
```
396440

397-
Pour lancer le serveur au démarrage, voir [deploy/printr.service](deploy/printr.service)
398-
et [deploy/printr.env.example](deploy/printr.env.example) (jeton, clé API, périphérique).

‎docs/build.py‎

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
"""Construit le site dans _site/ : la page d'accueil (docs/) et les pages de documentation,
2+
rendues depuis le README et le guide de contribution du dépôt pour ne jamais s'en écarter.
3+
4+
uvx --with markdown --with pymdown-extensions python docs/build.py && python3 -m http.server -d _site
5+
"""
6+
7+
import html
8+
import os
9+
import re
10+
import shutil
11+
from pathlib import Path
12+
13+
import markdown
14+
from pymdownx.slugs import slugify
15+
16+
ROOT = Path(__file__).resolve().parent.parent
17+
SITE = ROOT / "docs"
18+
OUT = ROOT / "_site"
19+
REPO = "https://github.com/XNinety9/Printr"
20+
BRANCH = "master"
21+
22+
# (page produite, titre, source, libellé dans la navigation)
23+
PAGES = [
24+
("guide.html", "Guide", ROOT / "README.md", "Guide"),
25+
("contribuer.html", "Contribuer", ROOT / "CONTRIBUTING.md", "Contribuer"),
26+
]
27+
# Fichiers du dépôt qui ont leur page sur le site.
28+
PAGE_FOR = {"README.md": "guide.html", "CONTRIBUTING.md": "contribuer.html"}
29+
# Ce qui, dans docs/, ne sert qu'à fabriquer le site ou ses illustrations.
30+
SKIP = shutil.ignore_patterns("build.py", "demo", "*.sh", "*.py", "__pycache__")
31+
32+
TEMPLATE = """<!DOCTYPE html>
33+
<html lang="fr">
34+
<head>
35+
<meta charset="utf-8">
36+
<meta name="viewport" content="width=device-width, initial-scale=1">
37+
<title>{title} · Printr</title>
38+
<meta name="description" content="Documentation de Printr : {title_lower}.">
39+
<link rel="icon" href="mark.png" type="image/png">
40+
<link rel="preconnect" href="https://fonts.googleapis.com">
41+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
42+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
43+
<script>
44+
// Thème choisi sur la page d'accueil, avant le premier affichage.
45+
try {{ var t = localStorage.getItem("printr-theme"); if (t === "dark" || t === "light") document.documentElement.setAttribute("data-theme", t); }} catch (e) {{}}
46+
</script>
47+
<link rel="stylesheet" href="docs.css">
48+
</head>
49+
<body>
50+
<nav class="nav">
51+
<div class="wrap">
52+
<a class="brand" href="./"><img src="mark.png" alt="">Printr</a>
53+
<div class="links">{nav}<a href="{repo}">GitHub</a></div>
54+
</div>
55+
</nav>
56+
<main class="wrap docs">
57+
<aside class="toc"><div class="title">Sur cette page</div>{toc}</aside>
58+
<article class="doc">
59+
<h1>{title}</h1>
60+
{content}
61+
<p class="caption">Page générée depuis <a href="{repo}/blob/{branch}/{source}">{source}</a>.</p>
62+
</article>
63+
</main>
64+
<script src="toc.js" defer></script>
65+
</body>
66+
</html>
67+
"""
68+
69+
70+
def readme_body(text: str) -> str:
71+
"""Le README sans son logo, sa devise, ses badges et ses liens d'en-tête : le site les a."""
72+
return re.sub(r"\A(?:\s*<p align=\"center\">.*?</p>)+\s*", "", text, flags=re.S)
73+
74+
75+
def rewrite_links(body: str, source: Path) -> str:
76+
"""Liens vers les autres pages Markdown : leur page du site ; images de docs/ : le site ;
77+
autres fichiers du dépôt : GitHub."""
78+
base = source.parent.relative_to(ROOT)
79+
80+
def target(url: str) -> str:
81+
if re.match(r"^(https?:|mailto:|#|data:)", url):
82+
return url
83+
path, _, anchor = url.partition("#")
84+
repo_path = os.path.normpath((base / path).as_posix()).replace("\\", "/")
85+
suffix = f"#{anchor}" if anchor else ""
86+
if repo_path in PAGE_FOR:
87+
return PAGE_FOR[repo_path] + suffix
88+
if repo_path.startswith("docs/") and (ROOT / repo_path).is_file():
89+
return repo_path.removeprefix("docs/") + suffix
90+
kind = "tree" if (ROOT / repo_path).is_dir() else "blob"
91+
return f"{REPO}/{kind}/{BRANCH}/{repo_path}{suffix}"
92+
93+
def fix(match: re.Match) -> str:
94+
attr, url = match.group(1), html.unescape(match.group(2))
95+
if attr == "srcset":
96+
url = ", ".join(" ".join([target(p.split()[0]), *p.split()[1:]]) for p in url.split(","))
97+
else:
98+
url = target(url)
99+
return f'{attr}="{html.escape(url)}"'
100+
101+
return re.sub(r'(href|src|srcset)="([^"]+)"', fix, body)
102+
103+
104+
def render(page: str, title: str, source: Path) -> str:
105+
text = source.read_text()
106+
text = readme_body(text) if source.name == "README.md" else re.sub(r"^# .*\n", "", text, count=1)
107+
md = markdown.Markdown(
108+
# superfences : blocs de code dans les listes ; md_in_html : Markdown dans le HTML du README.
109+
extensions=["pymdownx.superfences", "tables", "toc", "sane_lists", "attr_list", "md_in_html"],
110+
extension_configs={
111+
# Ancres à la GitHub (accents gardés) : les liens internes du README restent valides.
112+
"toc": {"permalink": "#", "toc_depth": "2-3", "slugify": slugify(case="lower")},
113+
},
114+
)
115+
body = rewrite_links(md.convert(text), source)
116+
nav = "".join(
117+
f'<a href="{p}"{" aria-current=\"page\"" if p == page else ""}>{label}</a>' for p, _, _, label in PAGES
118+
)
119+
return TEMPLATE.format(
120+
title=title,
121+
title_lower=title.lower(),
122+
nav=nav,
123+
# Le sommaire de Python-Markdown a son propre <div class="toc"> : la page fournit le sien.
124+
toc=re.sub(r'\A\s*<div class="toc">\s*|\s*</div>\s*\Z', "", md.toc),
125+
content=body,
126+
repo=REPO,
127+
branch=BRANCH,
128+
source=source.relative_to(ROOT).as_posix(),
129+
)
130+
131+
132+
def main() -> None:
133+
shutil.rmtree(OUT, ignore_errors=True)
134+
shutil.copytree(SITE, OUT, ignore=SKIP)
135+
for page, title, source, _ in PAGES:
136+
(OUT / page).write_text(render(page, title, source))
137+
(OUT / ".nojekyll").touch() # fichiers servis tels quels
138+
pages = sorted(p.name for p in OUT.glob("*.html"))
139+
print(f"site construit dans {OUT.relative_to(ROOT)} : " + ", ".join(pages))
140+
141+
142+
if __name__ == "__main__":
143+
main()

0 commit comments

Comments
 (0)