Je rédige l’ensemble de mes cours avec Docusaurus, un outil que j’adore:
- tout est écrit en Markdown/MDX, donc facilement portable si besoin
- on peut facilement organiser les chapitres dans l’ordre que l’on souhaite, le sommaire est auto-généré
- grâce au système de plugins, j’ai intégré un moteur de recherche dans le cours (j’ai même testé avec Algolia, c’est pas mal)
- Docusaurus est basé sur React, donc j’ai même pu développer des composants personnalisés que j’ai ensuite intégrés dans mon cours : quiz de fin de chapitre, éditeur de code intégré…
- Depuis la version 3.6 et l’utilisation de Rspack, SWC et Lightning CSS, les temps de build ont été fortement réduits, un vrai plaisir
Un jour, en cours, un élève me demande s’il peut avoir mon cours en version PDF.
Recherche
La première chose que j’ai faite pour répondre à sa demande, c’est de chercher un outil disponible en ligne.
Je me suis dit qu’il y aurait forcément un outil disponible sur NPM pour passer un site Docusaurus vers un PDF ou un ensemble de fichiers PDF, et qu’il me suffirait de l’exécuter avec npx.
Après en avoir testé deux, et essuyé deux échecs, j’ai décidé d’en développer un.
Les besoins
Les besoins que j’avais étaient les suivants :
- Utiliser l’outil en ligne de commande
- Lui passer l’URL et, de manière optionnelle, un dossier de sortie où déposer les fichiers PDF
Au niveau du script lui-même :
- Effectuer une requête vers une page web
- Analyser le contenu d’une page web
- Générer un PDF à partir d’une page web
- Générer un PDF par élément de menu (numéroté)
L’outil
Rust
J’ai décidé d’utiliser Rust pour développer cet outil. C’est un langage que j’apprécie beaucoup, et que je trouve assez adapté pour ce cas-là.
Pour récupérer les pages web, j’ai utilisé chromiumoxide, qui me permet de lancer un navigateur headless, me rendre sur des pages web et inspecter les éléments qui s’y trouvent, puis exporter une page web en PDF.
Multi-threading
Je génère un fichier PDF par élément présent dans le menu.
À la base, je générais les fichiers PDF de manière séquentielle, chapitre par chapitre. Pour plus de rapidité, j’ai décidé d’utiliser un thread par page, que j’exécute en parallèle. Il est possible que ça tienne moins bien sur des sites avec des centaines et des centaines de pages (cela nécessiterait probablement une temporisation), mais dans mon cas je n’y suis pas encore : ça marche, et ça marche plutôt vite.
Résultat
Les différentes étapes du script sont les suivantes :
- Récupérer un navigateur headless de chromiumoxide, et créer une page qui me servira à naviguer (en commençant par la page principale de la documentation)
- Collecter les chapitres (libellé et URL)
- Générer chaque chapitre en PDF dans le dossier de sortie
#[async_std::main]
async fn main() -> Result<(), Box<dyn Error>> {
let start = Instant::now();
let args = Args::parse();
let (mut browser, handle) = browser::get_browser_and_handle().await?;
let base_url = util::get_base_url(&args.initial_docs_url);
let page = browser::get_new_page(&browser, true).await?;
page.goto(&args.initial_docs_url).await?;
println!("Collecting chapters...");
let main_side_menu = page.find_element(".theme-doc-sidebar-menu").await?;
let chapters = docusaurus::collect_chapters(&main_side_menu, None).await?;
println!("Chapters found: {:?}", chapters.len());
println!("Generating PDF files in {}...", args.output_dir);
fs::create_dir_all(&args.output_dir)?;
pdf::generate_pdfs(&chapters, &browser, &base_url, &args.output_dir).await?;
println!("Done in {:.2?}", start.elapsed());
browser.close().await?;
handle.await;
Ok(())
}
Intégration dans NixOS
J’utilise NixOS, qui me permet de sauvegarder toute ma configuration en ligne.
- Leur système de configuration déclarative me permet de tout rassembler au même endroit et de sauvegarder tout ça : si je perds ma machine, je peux rapidement reproduire mon système à l’identique (ou presque), sur une nouvelle machine
- Pour mettre mon système et mes programmes à jour, je lance une commande et cela me génère une nouvelle génération : s’il y a un problème, je peux revenir facilement en arrière en sélectionnant la génération précédente au boot de la machine
- À ce jour, plus de 120 000 packages sont disponibles sur NixOS : dès que je veux installer un nouveau programme, je l’ajoute à mon fichier de configuration et je rebuild
- Chaque version de Nix est stable : les mises à jour sont des correctifs de sécurité ou bien de bugs majeurs. Si je veux tout de même mettre à jour certains programmes sans que la nouvelle version ait été validée pour Nix, je souscris moi-même au canal
unstablepour ces programmes nix-shellpermet de lancer un nouveau shell dans un nouvel environnement temporaire. Utile pour tester des outils, ou bien pour avoir un environnement temporaire avec une version précise d’un langage
J’ai encore beaucoup de choses à découvrir dans l’écosystème Nix, mais dans ce cas-là, j’ai pu créer un package que je charge ensuite dans ma configuration : l’outil est installé comme un exécutable normal dans ma distribution.
Dans ma configuration (environment.systemPackages) :
(callPackage ./dcsrs-to-pdf/default.nix {})
Le package se trouve à côté du fichier de configuration : Nix récupère la toolchain Rust, compile le script, l’installe et le rend disponible n’importe où sur mon système : je peux utiliser mon script comme un programme normal.

