Aller au contenu

Générer des PDF en utilisant HTML et CSS

·17 mins
Sommaire

Il existe plein de manière de générer des documents PDF. Lorsqu’on n’a pas beaucoup de connaissance technique ou bien une certaine flemme, on utilise généralement un logiciel de traitement de texte tel que LibreOffice ou OnlyOffice. Les vrais nerds ainsi que les personnes ayant besoin de générer des documents de manière automatisée auront plutôt tendance à utiliser LaTeX. Bien qu’étant dans cette dernière catégorie de personnes, j’ai toujours détesté LaTeX qui, malgré ses nombreux avantages, est très difficile à prendre en main et surtout à styliser. Depuis pas mal d’années j’ai donc totalement abandonné LaTeX au profit de langage que je connais bien et avec lesquels j’ai d’excellents résultats : HTML et CSS.

Weasyprint
#

La meilleur manière que j’ai trouvé pour générer des documents PDF à partir de HTML et CSS est Weasyprint. Il s’agit d’un projet écrit en Python, packagé dans la plupart des distributions Linux, qui propose à la fois une interface en ligne de commande et une interface en Python. Dans ce billet je me concentrerai sur l’interface en ligne de commande qui est la plus générique et la plus pratique pour débuter.

Commençons donc par créer un fichier HTML assez simple :

<!doctype html>
<html lang="fr">
	<head>
		<meta charset="utf-8">
		<title>Mon premier document PDF</title>
		<meta name="author" content="Toto">
		<meta name="description" content="Un PDF de test">
		<meta name="generator" content="super_script.sh">
		<meta name="keywords" content="pdf,weasyprint,html,css">
		<meta name="dcterms.created" content="2026-01-28T17:21:42+01:00">
		<meta name="dcterms.modified" content="2026-01-28T18:21:42+01:00">
	</head>
	<body>
		<h1>Bonjour, monde&nbsp;!</h1>
	</body>
</html>

Et convertissons le en PDF :

$ weasyprint --pdf-variant "pdf/ua-1" "test.html" "test.pdf"
Capture d'écran montrant le fichier PDF ouvert dans un lecteur. Au premier plan, une fenêtre montre les différentes métadonnées du fichier.
On commence facilement, profitez-en car ça ne vas pas durer.

Vous remarquerez que Weasyprint a extrait toutes les métadonnées des balises meta et title afin de les enregistrer dans le PDF. Notez que, même si ce n’est pas affiché, c’est également le cas de la langue du document indiquée dans la balise html.

Dans cet exemple j’ai volontairement mis un grand nombre de métadonnées, mais si vous n’en avez pas l’usage vous pouvez ne pas les mettre. Je recommande cependant de toujours indiquer au moins la langue du document, le jeu de caractères et le titre.

Notez également que j’ai utilisé --pdf-variant dans la ligne de commande afin de forcer l’utilisation de PDF/UA qui est une norme d’accessibilité. Parce que non, tous les PDF ne se valent pas niveau accessibilité, alors soyons respectueux des autres et faisons attention à ça.

Un peu de style
#

HTML c’est bien, mais niveau design ce n’est pas folichon. Ajoutons donc un peu de CSS. Pour ceci, deux solutions possibles :

  1. tout mettre dans une balise HTML style comme un gros goret ;
  2. utiliser un ou plusieurs fichiers CSS séparés.

Étant des personnes respectables, nous opterons pour la seconde solution. Cependant, contrairement à ce dont nous avons l’habitude le web classique, nous n’utiliserons pas la balise HTML link. À la place, nous passons simplement un nouvel argument à la ligne de commande :

$ weasyprint --pdf-variant "pdf/ua-1" -s "test.css" "test.html" "test.pdf"

Je vous propose de commencer avec le fichier CSS suivant :

@page {
	size: A4 portrait;
	margin: 1.5cm 1cm 1.5cm 1cm;
}

* {
	margin: 0;
	padding: 0;
}

Si vous n’avez pas l’habitude de la règle CSS @page, c’est elle qui nous permet de spécifier des directives d’affichages pour nos pages. Elle va rapidement devenir votre meilleur allié. C’est par exemple grace à elle que nous pouvons utiliser le descripteur CSS size qui nous permet d’indiquer que nous créons un document A4 au format portrait.

Au niveau des marges, on commence par spécifier celles de la page elle-même. Ensuite, tel Léodagan, on crame les marges de tous les éléments afin de partir sur une base saine. Notez que, dans la mesure où nous éditons un document de taille fixe, il est possible et même recommandé d’exprimer les dimensions dans une valeur absolue tel que le centimètre (cm), le millimètre (mm) ou encore le Q (Q). Ce dernier remplace avantageusement le pixel (px) et le point (pt) car 1 Q est à peu près égal à 0,95 px soit environ 0,7 pt.

Toujours pour bien faire les choses, je recommande d’exprimer les tailles de polices en rem lorsque vous souhaitez le faire d’une manière relative. Bien entendu les valeurs absolues sont également possibles.

Quelques astuces
#

Normalement, si vous connaissez CSS vous devriez être en mesure de vous en sortir convenablement. Il ne me reste plus qu’à vous passer quelques astuces qui vous aideront à bien faire les choses.

Les variables
#

S’il y a une chose qui change la vie en CSS c’est la possibilité de définir des variables. Un cas très utile est d’utiliser :root afin de définir des valeurs que nous utiliseront régulièrement. Mon astuce ici est de définir les dimensions du document lui-même, les marges du document ainsi que les couleurs.

:root {
	--doc-width: 21cm;
	--doc-height: 29.7cm;

	--doc-margin-top: 1.5cm;
	--doc-margin-bottom: var(--doc-margin-top);
	--doc-margin-right: 1cm;
	--doc-margin-left: var(--doc-margin-right);

	--dark-grey: #444;

	--main-text-color: var(--dark-grey);
}

@page {
	size: A4 portrait;
	margin: var(--doc-margin-top) var(--doc-margin-right) var(--doc-margin-bottom) var(--doc-margin-left);
}

h1 {
	color: var(--main-text-color);
}

Importer des fichiers
#

La règle CSS @import fonctionne, ce qui nous permet de diviser notre code CSS en plusieurs fichiers. Par exemple, mettons nos définitions de variables concernant les dimensions dans un fichier et celles concernant les couleurs dans un autre. Et tant qu’à faire, mettons notre reset des marges dans encore un autre fichier.

@import url("dimensions.css");
@import url("colors.css");
@import url("reset.css");

Les calculs de dimensions
#

Vous avez pu voir que dans les exemples précédent j’ai défini les variables --doc-width et --doc-height dans les utiliser car j’ai utilisé la valeur spéciale A4 pour indiquer la taille du document. L’intérêt de faire ça est de pouvoir les utiliser pour des calculs de dimensions à l’aide de la fonction CSS calc().

Par exemple, afin d’obtenir une largeur égale à un tiers de la largeur de la page sans les marges, on peut écrire :

:root {
	--doc-width: 21cm;
	--doc-height: 29.7cm;

	--doc-margin-top: 1.5cm;
	--doc-margin-bottom: var(--doc-margin-top);
	--doc-margin-right: 1cm;
	--doc-margin-left: var(--doc-margin-right);

	--doc-inner-width: calc(var(--doc-width) - var(--doc-margin-right) - var(--doc-margin-left));
	--doc-inner-height: calc(var(--doc-height) - var(--doc-margin-top) - var(--doc-margin-bottom));
}

.trucmuche {
	width: calc(var(--doc-inner-width) / 3);
}

Si dans le web classique utiliser des dimensions absolues comme ça est une mauvaise pratique, ce n’est pas le cas dans le domaine du « print » où c’est une pratique tout à fait normale.

Différents types de pages
#

Jusque là tout va bien, alors attelons nous à la création de documents plus complexes comportant plusieurs types de pages différentes. Prenons l’exemple classique d’un rapport contenant une page de garde, un sommaire, le contenu en lui-même qui est chapitré et enfin d’éventuelles annexes.

Commençons par le plus simple, le HTML. Pour ceci, divisons tout en sections et utilisons des id pour identifier la page de garde et le sommaire ainsi que des classes pour les différentes parties du contenu ainsi que chaque annexe.

<!doctype html>
<html lang="fr">
	<head>
		<meta charset="utf-8">
		<title>Mon premier document PDF</title>
		<meta name="author" content="Toto">
		<meta name="description" content="Un PDF de test">
		<meta name="generator" content="super_script.sh">
		<meta name="keywords" content="pdf,weasyprint,html,css">
		<meta name="dcterms.created" content="2026-01-28T17:21:42+01:00">
		<meta name="dcterms.modified" content="2026-01-28T18:21:42+01:00">
	</head>
	<body>
		<section id="first-page">
			<h1>Rapport trucmuche</h1>
			<p>28 janvier 2026</p>
		</section>

		<section id="toc">
			<h2>Sommaire</h2>
			<ol>
				<li>Lorem ipsum</li>
				<li>Etiam aliquet</li>
				<li>Ut convallis convallis lacus non pretium</li>
			</ol>
		</section>

		<section class="main-content" id="chapter-1">
			<h2>Lorem ipsum</h2>
			<p>
			Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut facilisis ultricies
			lorem nec imperdiet. Ut malesuada tellus dapibus risus elementum, a aliquet arcu
			ornare. Integer ultrices, quam nec tempus suscipit, neque erat dapibus elit, non
			vestibulum quam mi sit amet erat. Integer id lectus a nisi convallis vehicula.
			Integer lobortis velit ut nulla consectetur mollis. Mauris pulvinar cursus neque
			vel finibus. Aenean aliquam pharetra quam, sit amet semper tortor imperdiet et.
			Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos
			himenaeos. Suspendisse sagittis lacus sit amet nulla porttitor ultricies
			eu in erat. Fusce nec enim lacus. Curabitur pretium tempor rhoncus. Proin
			accumsan tortor sed euismod aliquet. Quisque sit amet aliquam leo. Duis finibus
			vestibulum orci, nec finibus sapien congue id. Integer cursus posuere felis a
			iaculis. Curabitur iaculis elit sed mauris interdum eleifend.
			</p>
			<p>
			Vestibulum metus odio, dignissim sed nunc pulvinar, sagittis mattis leo. Proin
			ornare urna in nisi ultricies maximus. Sed lacinia, neque nec rhoncus mollis,
			dolor magna interdum tortor, id ultricies nisl ex vel mauris. Etiam a elementum
			tortor. Phasellus vel aliquam orci. Nam tincidunt massa sem. Aenean porta lectus
			in facilisis bibendum. Quisque non risus et purus dignissim posuere.
			</p>
			<p>
			Aliquam ut nulla et justo aliquet viverra molestie quis tortor. Morbi massa
			eros, blandit ac ipsum eu, convallis lacinia massa. Sed finibus mattis metus,
			at efficitur odio accumsan a. Phasellus et maximus nisl. Sed nisi odio,
			commodo at vulputate ac, rutrum ac eros. Sed sit amet augue laoreet, vulputate
			arcu accumsan, laoreet tortor. Quisque aliquet massa massa, ac vulputate ex
			molestie sit amet. Nunc vestibulum sem vel consectetur lacinia. Class aptent
			taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.
			Pellentesque ac risus id arcu placerat consequat.
			</p>
		</section>

		<section class="main-content" id="chapter-2">
			<h2>Etiam aliquet</h2>
			<p>
			Etiam aliquet, nunc sed tristique feugiat, ex tortor dapibus nibh, eget
			hendrerit eros massa ut lectus. Morbi blandit, sem quis vehicula ultrices, ipsum
			eros egestas risus, ut aliquet augue augue et dolor. Curabitur vel elit congue,
			scelerisque risus vitae, sodales est. Etiam id mauris porttitor, finibus tortor
			id, feugiat leo. Pellentesque tincidunt mattis nisi, in cursus magna. Sed eget
			ipsum dui. Pellentesque habitant morbi tristique senectus et netus et malesuada
			fames ac turpis egestas. Nulla quis justo aliquet, dictum ante vel, mattis
			nulla. Mauris eu velit libero. Donec sit amet tristique dui, mattis fringilla
			ante. Fusce maximus ultrices quam blandit interdum. Cras aliquet, tortor blandit
			vehicula dapibus, quam lectus viverra ligula, non finibus tortor arcu non neque.
			Nulla scelerisque ligula et blandit dapibus. Pellentesque non interdum purus.
			Vivamus malesuada mi nisi, nec pretium justo gravida quis. Maecenas dapibus
			accumsan magna, a pretium justo feugiat eget.
			</p>
			<p>
			Aenean volutpat felis velit, eu porttitor eros commodo sed. Suspendisse turpis
			lectus, maximus sed fermentum vitae, ullamcorper nec enim. Mauris in lobortis
			sem. Praesent viverra, libero vitae ornare accumsan, enim orci blandit odio,
			ac sollicitudin ante lectus non augue. In rhoncus mi leo, vitae luctus odio
			lobortis in. Aliquam dapibus lacus lacus. Nam sed tempor urna. Etiam eleifend
			gravida euismod. Suspendisse potenti. Donec dictum turpis quis efficitur
			suscipit. Nullam congue mauris sit amet mauris auctor vehicula. Nunc quis mauris
			id risus suscipit sodales. Quisque vitae laoreet orci.
			</p>
			<p>
			Ut tincidunt lacus id augue vestibulum cursus. Nullam ac ante id dolor
			elementum blandit. Proin ullamcorper ipsum id sagittis efficitur. Phasellus
			faucibus, arcu vitae varius consequat, purus lectus tincidunt nibh, id
			pellentesque orci felis eu nunc. Quisque ut convallis lectus. Ut pretium sapien
			dolor, vitae egestas est sagittis vel. Quisque commodo libero id elit fermentum
			consequat. Morbi imperdiet placerat felis, sit amet luctus ante lobortis
			venenatis. Suspendisse eleifend, libero et vulputate blandit, ipsum est posuere
			dolor, sit amet rutrum elit metus id nibh. Aenean pellentesque quam nunc, eget
			tincidunt purus tempus vitae. Aliquam vehicula fermentum enim, vel rhoncus arcu
			tincidunt eu.
			</p>
		</section>

		<section class="main-content" id="chapter-3">
			<h2>Ut convallis convallis lacus non pretium</h2>
			<p>
			Ut convallis convallis lacus non pretium. Sed bibendum malesuada nulla vitae
			pulvinar. Morbi mollis libero faucibus, efficitur lectus in, consequat lacus.
			Maecenas eu ultrices dolor. Donec nisl dui, viverra dignissim dictum vel,
			suscipit dictum dui. Phasellus nunc diam, facilisis et ligula et, interdum
			auctor odio. Quisque urna justo, efficitur sed tristique id, scelerisque quis
			sapien. Duis posuere, nulla quis dapibus faucibus, est dolor accumsan felis,
			ac imperdiet ipsum mi id purus. Sed porta leo non risus ullamcorper, sit
			amet feugiat magna pellentesque. Proin pretium dignissim ornare. Curabitur
			auctor augue libero, nec convallis erat congue et. Nullam et pharetra felis.
			Pellentesque magna dui, hendrerit a accumsan id, ornare id lectus. Aliquam erat
			volutpat. Fusce maximus lacus mi, non vulputate arcu cursus a.
			</p>
			<p>
			Quisque maximus orci fringilla nibh lobortis consectetur. Ut sit amet metus
			mattis, pharetra nisl id, placerat velit. Praesent eu massa vel libero iaculis
			lobortis. Mauris ligula odio, auctor eu lacus ac, elementum tempus ipsum.
			Suspendisse elit arcu, pellentesque eget tempus sit amet, egestas non quam. In
			nec felis eget nulla porta vestibulum at vitae purus. Integer non massa urna.
			</p>
			<p>
			Aliquam mattis orci quis sem iaculis, vel mollis est sodales. Pellentesque
			lobortis lacinia velit at egestas. Ut sed leo ultrices, viverra augue sit amet,
			pulvinar turpis. Maecenas gravida tortor justo, id mattis magna lacinia eu.
			Nunc eu viverra lorem. Maecenas dapibus elit nibh, non gravida orci facilisis
			at. Mauris volutpat dolor eros, at consectetur mauris gravida a. Aliquam et
			venenatis enim. Ut vel rutrum justo. Integer congue et elit ut lacinia. Donec
			varius est in venenatis mattis.
			</p>
		</section>

		<section class="appendix" id="appendix-1">
			<h2>Annexe 1</h2>
			<p>
			Suspendisse in est in nisl fermentum pharetra. Curabitur at maximus enim.
			Phasellus pharetra gravida justo nec fermentum. Donec rhoncus sodales lectus,
			a varius turpis convallis ut. Donec lobortis metus ut justo vulputate consequat
			ut at est. Maecenas id hendrerit arcu. Maecenas eu tortor ut neque scelerisque
			gravida suscipit volutpat erat.
			</p>
		</section>

		<section class="appendix" id="appendix-2">
			<h2>Annexe 2</h2>
			<p>
			Ut et odio nec nunc sollicitudin ultrices pellentesque in erat. Sed suscipit,
			orci lobortis tempus egestas, nulla massa placerat neque, ac malesuada lorem
			sapien condimentum nisi. Mauris vel ex sapien. Aliquam sed lorem ut justo
			feugiat interdum. Fusce felis erat, interdum in arcu euismod, pellentesque
			feugiat massa. Phasellus et pharetra eros. Vestibulum nec suscipit orci, vitae
			tincidunt augue. Curabitur at nulla laoreet, fermentum justo non, feugiat erat.
			Nulla sapien elit, accumsan in est eget, porttitor interdum nisl.
			</p>
		</section>
	</body>
</html>

Si vous générez le PDF, vous vous apercevrez que tout est collé ensemble, c’est assez moche. Nous pourrions y aller tout schuss à styliser tout ça, mais pensons donc d’abord à la sémantique de chaque élément. En effet, la page de garde est une page à part, le sommaire également. D’ailleurs, même s’il peut s’étaler sur plusieurs pages, le contenu est également une page, de même que les annexes. Nous pouvons matérialiser ceci en CSS à l’aide de la propriété page.

#first-page {
	page: first-page;
}

#toc {
	page: toc;
}

.main-content {
	page: main-content;
	break-after: page;
}

.appendix {
	page: appendix;
	break-after: page;
}

Définir des pages différentes permet, au delà du sens sémantique, d’insérer un saut de page. Dans la mesure où tous les chapitres sont regroupés en une seule page sémantique, il n’y a pas de saute de page entre chaque chapitre. Pour les ajouter, nous utilisons break-after: page;. Faisons de même pour les annexes.

Avoir ainsi sémantiquement défini des pages nous permet d’utiliser @page pour leur appliquer un affichage spécifique. Par exemple, si les annexes ont besoin d’être en paysage alors que tout le reste doit rester en portrait :

@page appendix {
	size: A4 landscape;
}

Un vrai sommaire
#

C’est bien d’avoir un sommaire statique, mais c’est encore mieux lorsque l’on peut cliquer dessus pour directement accéder à la page en question. C’est exactement pour ça que nous avons utilisé des id pour chaque chapitre et annexe. Tout comme dans le web classique, les liens sur les ancres fonctionnent.

<section id="toc">
	<h2>Sommaire</h2>
	<ol>
		<li><a href="#chapter-1">Lorem ipsum</a></li>
		<li><a href="#chapter-2">Etiam aliquet</a></li>
		<li><a href="#chapter-3">Ut convallis convallis lacus non pretium</a></li>
	</ol>
</section>

C’est pas mal, mais en général dans un sommaire on affiche également le numéro de la page en question.

<section id="toc">
	<h2>Sommaire</h2>
	<ol>
		<li><a href="#chapter-1">Lorem ipsum</a><a class="page-nb" href="#chapter-1"></a></li>
		<li><a href="#chapter-2">Etiam aliquet</a><a class="page-nb" href="#chapter-2"></a></li>
		<li><a href="#chapter-3">Ut convallis convallis lacus non pretium</a><a class="page-nb" href="#chapter-3"></a></li>
	</ol>
</section>
#toc a.page-nb::before {
	content: target-counter(attr(href), page);
}

Oui je sais, là on entre dans la magie noire du CSS, à tel point qu’au jour de l’écriture de cet article target-counter n’est pas documenté dans la documentation MDN. Ça nous permet de récupérer le compteur, ici page dont le but est de contenir un numéro de page, qui s’applique à une cible, ici attr(href) donc l’attribut href du lien.

En-tête et pied de page
#

Le plus souvent il est souhaitable de numéroter les pages d’un document. En revanche, nous ne souhaitons numéroter que les pages du contenu, donc certainement pas la page de garde par exemple. Comme vous pouvez vous en douter, nous allons donc retrouver à la fois @page et les compteurs.

@page main-content {
	@bottom-right {
		content: "Page " counter(page) " / " counter(pages);
	}
}

Oui, c’est intégralement du CSS, nous n’avons eu besoin d’ajouter aucun élément HTML. Je le répète : le CSS c’est magique. Notons que si page est le compteur indiquant le numéro de la page courante, pages est le compteur indiquant le nombre total de pages. Notons également l’utilisation de la règle de marge @bottom-right mais il en existe beaucoup d’autres.

Lorsque l’on imprime un livre, on souhaite généralement avoir le numéro de page d’un côté puis de l’autre afin qu’il soit toujours vers l’extérieur. C’est là qu’entrent en jeu les sélecteurs de page :left et :right.

@page main-content :left {
	@bottom-left {
		content: counter(page);
	}
}

@page main-content :right {
	@bottom-right {
		content: counter(page);
	}
}

Bon, des numéros de page c’est bien gentil, mais comment fait-on lorsqu’on a besoin d’insérer quelque chose de plus complexe ? Pour ça, commençons par créer un footer en HTML que nous allons placer directement tout en haut (j’insiste) du body.

<body>
		<footer id="main-content-footer">
			Société <b>Exemple</b> SARL<br>
			Email&nbsp;: contact@example.com
		</footer>

		<!--
			Le reste du HTML.
		-->
</body>

Et là on utilise le CSS pour le mettre en position: running() tout en lui passant un nom qui servira d’identifiant de référence. Cette opération va l’enlever du flux courant et nous pouvons donc l’utiliser dans @page.

@page main-content {
	@bottom-center {
		content: element(main-content-footer);
	}
}

#main-content-footer {
	position: running(main-content-footer);
}

Et voilà, nous avons un joli pied de page écrit en HTML qui s’affiche en bas et au centre de chaque page du contenu principal, tout ça juste en le mettant en position de running.

Détournement se voulant humoristique de deux captures d'écran du film « Forest Gump » l'une au dessus l'autre. La première montre Jenny en train de crier « Run footer, run! ». La seconde montre Forest en train de courir avec l'inscription « <footer></footer> » sur son visage.
Run footer, run!

Des icônes
#

Dans la mesure où nous avons un document HTML et CSS, nous pouvons utiliser toutes les ressources auxquelles l’on a généralement accès pour du développement web. Par exemple, des jeux d’icônes. Personnellement j’aime beaucoup Remix Icon mais il en existe beaucoup d’autres.

Intégrer des trucs
#

S’il est généralement possible d’inclure des éléments externes tels que des images ou des polices d’écriture à l’aide d’un lien relatif ou absolu pointant vers le fichier concerné, parfois ce n’est malheureusement pas possible. Je pense par exemple à une application qui devrait automatiquement générer un PDF contenant des images stockées dans une base de donnée.

Dans ce genre de cas, il est possible d’utiliser une URL spéciale :

  • le « protocole », en l’occurrence data ;
  • le séparateur : ;
  • le type MIME du fichier, par exemple image/png ;
  • le séparateur ; ;
  • base64 afin de dire qu’on va passer le fichier en base46 ;
  • le séparateur , ;
  • en enfin le contenu du fichier en base64.

Par exemple, voici comment insérer une image PNG en fond d’un élément sans sortir de CSS :

.img-bg {
	background-image: url("data:image/png;base64,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");
}

Ça fonctionne partout où la fonction CSS url est supportée, donc c’est bien plus large que les images. C’est également utilisable directement en HTML, par exemple dans l’attribut src d’une balise img ou autre.

Notons que les fichiers SVG n’ont pas besoin d’être mis en base64 pour être directement inclus, c’est tout à fait possible de les insérer directement dans du HTML.

Styliser les compteurs
#

Les compteurs CSS servent à bien plus de choses qu’à compter ou afficher des nombres. En fait ce sont plus des suites de symboles qu’autre chose. Il est donc possible d’utiliser @counter-style afin de personaliser tout ça, par exemple avoir un « compteur » qui utilise les émojis représentant les différents cycles de la lune afin de boucler dessus.

@counter-style moon-cycle {
	system: cyclic;
	symbols: "🌑" "🌒" "🌓" "🌔" "🌕️" "🌖" "🌗" "🌘";
}

Pour revenir dans le domaine des nombres, il est par exemple possible de les afficher sur 2 chiffres avec un 0 devant si besoin.

@counter-style decimal-pad-2 {
	system: numeric;
	symbols: "0" "1" "2" "4" "5" "6" "7" "8" "9";
	pad: 2 "0";
}

Pour utiliser votre style personnalisé, précisez-le simplement dans l’appel à counter. Les listes intégrant un compteur pour leurs éléments, vous pouvez également utiliser votre style personnalisé dans list-style-type.

@page main-content {
	@bottom-right {
		content: counter(page, decimal-pad-2);
	}
}

ol {
	list-style-type: moon-cycle;
}

Créer ses propres compteurs
#

Les compteurs pré-définis c’est bien, mais parfois il est utile de créer les siens. Comme ça nous pouvons par exemple enfin arrêter d’utiliser des listes ordonnées dans votre table des matières. À la place on va se faire un petit style sympa à base de grid.

<section id="toc">
	<h2>Sommaire</h2>

	<div class="toc-elem">
		<a class="toc-elem-title" href="#chapter-1">Lorem ipsum</a>
		<span></span>
		<a class="toc-elem-page-nb" href="#chapter-1"></a>
	</div>

	<div class="toc-elem">
		<a class="toc-elem-title" href="#chapter-2">Etiam aliquet</a>
		<span></span>
		<a class="toc-elem-page-nb" href="#chapter-2"></a>
	</div>

	<div class="toc-elem">
		<a class="toc-elem-title" href="#chapter-3">Ut convallis convallis lacus non pretium</a>
		<span></span>
		<a class="toc-elem-page-nb" href="#chapter-3"></a>
	</div>
</section>

De là c’est assez simple : on commence par initialiser un compteur à 0 et à lui donner un nom à l’aide de counter-reset. Ensuite, avant chaque nouvel élément du sommaire, on l’incrémente de 1 à l’aide de counter-increment puis on l’affiche.

#toc {
	counter-reset: toc-lvl-1;
}

.toc-elem::before {
	counter-increment: toc-lvl-1;
	content: counter(toc-lvl-1);
	margin-right: 2mm;
}

.toc-elem {
	display: grid;
	grid-template-columns: max-content max-content auto max-content;
}

.toc-elem > span {
	border-bottom: 1Q dotted black;
}

.toc-elem-page-nb::before {
	content: target-counter(attr(href), page);
}

Les grids c’est le petit bonus. Je ne voulais pas faire une section dédiée car malheureusement Weasyprint est encore en train de progresser sur le sujet et vous risquez de rapidement en rencontrer les limites.