feat: massive overhaul
Signed-off-by: Louis Vallat <contact@louis-vallat.fr>
5
.gitignore
vendored
@ -1,4 +1,3 @@
|
||||
dst/
|
||||
.idea/
|
||||
.vscode/
|
||||
node_modules/
|
||||
package-lock.json
|
||||
public
|
||||
|
@ -1,48 +0,0 @@
|
||||
image: docker:latest
|
||||
services:
|
||||
- docker:dind
|
||||
|
||||
stages:
|
||||
- release
|
||||
- deploy
|
||||
|
||||
# Disable the Gradle daemon for Continuous Integration servers as correctness
|
||||
# is usually a priority over speed in CI environments. Using a fresh
|
||||
# runtime for each build is more reliable since the runtime is completely
|
||||
# isolated from any previous builds.
|
||||
variables:
|
||||
# Use TLS https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#tls-enabled
|
||||
DOCKER_HOST: tcp://docker:2376
|
||||
DOCKER_TLS_CERTDIR: "/certs"
|
||||
CONTAINER_BRANCH_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
|
||||
CONTAINER_RELEASE_IMAGE: $CI_REGISTRY_IMAGE:latest
|
||||
|
||||
release:
|
||||
stage: release
|
||||
script:
|
||||
- sed -i "s/__VERSION__/${CI_COMMIT_SHORT_SHA}/" src/_footer.html
|
||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||
- docker build . -t $CONTAINER_BRANCH_IMAGE
|
||||
- docker push $CONTAINER_BRANCH_IMAGE
|
||||
except:
|
||||
- master
|
||||
|
||||
release-master:
|
||||
stage: release
|
||||
script:
|
||||
- sed -i "s/__VERSION__/${CI_COMMIT_SHORT_SHA}/" src/_footer.html
|
||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||
- docker build . -t $CONTAINER_RELEASE_IMAGE
|
||||
- docker push $CONTAINER_RELEASE_IMAGE
|
||||
only:
|
||||
- master
|
||||
|
||||
deploy:
|
||||
stage: deploy
|
||||
before_script:
|
||||
- eval $(ssh-agent -s)
|
||||
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
|
||||
- mkdir -p ~/.ssh
|
||||
- chmod 700 ~/.ssh
|
||||
script:
|
||||
- ssh -o StrictHostKeyChecking=no -p $SSH_PORT $SSH_DESTINATION "cd $PATH_TO_APPLICATION; $UPGRADE_COMMAND;"
|
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "themes/terminimal"]
|
||||
path = themes/terminimal
|
||||
url = https://github.com/pawroman/zola-theme-terminimal.git
|
16
Dockerfile
@ -1,16 +0,0 @@
|
||||
FROM alpine AS builder
|
||||
|
||||
WORKDIR /root
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN apk --no-cache add gcc git make musl-dev npm && \
|
||||
git clone -b VERSION_0_10_0 https://github.com/kristapsdz/lowdown.git && \
|
||||
cd lowdown && ./configure && make && make regress && make install && cd .. && \
|
||||
npm install && \
|
||||
chmod u+x ./ssg5 && mkdir -p dst && \
|
||||
./ssg5 ./src/ ./dst/ "Le blog de Louis Vallat" "https://blog.louis-vallat.xyz" && rm dst/.files
|
||||
|
||||
FROM nginx
|
||||
|
||||
COPY --from=builder /root/dst /usr/share/nginx/html
|
18
README.md
@ -1,17 +1,9 @@
|
||||
# Blog
|
||||
|
||||
> This is the source code from [this live website](https://blog.louis-vallat.xyz).
|
||||
> This is the source code from [this live website](https://blog.louis-vallat.fr).
|
||||
|
||||
## Todo
|
||||
Build it using [Zola](https://www.getzola.org):
|
||||
|
||||
- [ ] Add dark and light theme switch and colors
|
||||
|
||||
## Sources
|
||||
|
||||
- [SSG](https://www.romanzolotarev.com/ssg.html)
|
||||
- [Wolfgang's video](https://youtu.be/N_ttw2Dihn8): `Fast & Minimal Blog Using POSIX Shell and Markdown`
|
||||
|
||||
## Licenses
|
||||
|
||||
The fonts provided in this repository are licensed under the [Open Font License](https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL).
|
||||
The code, if not stated otherwise, is licensed under the [GPLv3 License](LICENSE).
|
||||
```bash
|
||||
zola build
|
||||
```
|
||||
|
37
config.toml
Normal file
@ -0,0 +1,37 @@
|
||||
base_url = "https://blog.louis-vallat.fr"
|
||||
title = "Le blog de Louis"
|
||||
description = "Le blog très simple d'un développeur très compliqué."
|
||||
|
||||
theme = "terminimal"
|
||||
compile_sass = true
|
||||
default_language = "fr"
|
||||
|
||||
generate_feed = true
|
||||
feed_filename = "atom.xml"
|
||||
|
||||
taxonomies = [
|
||||
{name = "tags"},
|
||||
{name = "categories"}
|
||||
]
|
||||
|
||||
[markdown]
|
||||
highlight_code = true
|
||||
|
||||
[extra]
|
||||
logo_text = "Le blog de Louis"
|
||||
author = "Louis Vallat"
|
||||
page_titles = "combined"
|
||||
|
||||
enable_post_view_navigation = true
|
||||
post_view_navigation_prompt = "En savoir plus"
|
||||
|
||||
menu_items = [
|
||||
{name = "blog", url = "$BASE_URL"},
|
||||
{name = "tags", url = "$BASE_URL/tags"},
|
||||
{name = "archive", url = "$BASE_URL/archive"},
|
||||
{name = "portfolio", url = "https://louis-vallat.fr"},
|
||||
{name = "gitlab", url = "https://gitlab.com/lovallat", newtab = true},
|
||||
]
|
||||
|
||||
favicon = "/favicon.png"
|
||||
favicon_mimetype = "image/png"
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@ -1,12 +1,21 @@
|
||||
# Git Workflow
|
||||
+++
|
||||
title = "Git Workflow"
|
||||
description = "Git est un outil de versionnage très utile, voici comment je m'en sers."
|
||||
date = 2021-05-10
|
||||
|
||||
[taxonomies]
|
||||
tags = ["git", "workflow"]
|
||||
+++
|
||||
|
||||
Git est un outil de versionnage très utile, voici comment je m'en sers.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
> À noter que ce guide est destiné aux personnes qui ont déjà utilisé git auparavant,
|
||||
> il ne s'agit pas ici d'un tutoriel pour débutants (bien que je sois ici assez
|
||||
> exhaustif dans mes explications).
|
||||
|
||||
[TL;DR](#TL;DR)
|
||||
[TL;DR](#tldr)
|
||||
|
||||
## Les fondations
|
||||
|
||||
@ -21,7 +30,7 @@ par exemple, aka. vous avez committé et quelqu'un d'autre aussi en même temps)
|
||||
Nous allons donc demander à git de ne pull que lorsque l'historique est linéaire
|
||||
(fast-forward), et sinon de faire automatiquement un rebase :
|
||||
|
||||
```git
|
||||
```bash
|
||||
git config pull.rebase true
|
||||
```
|
||||
|
||||
@ -33,7 +42,7 @@ git. Voici un alias, à placer dans votre `~/.gitconfig`, qui vous permet d'affi
|
||||
un graphique de vos branches et vos commits de manière plus visuelle que le `git log`
|
||||
de base de git (honteusement volés [ici](https://stackoverflow.com/a/9074343)) :
|
||||
|
||||
```git
|
||||
```conf
|
||||
[alias]
|
||||
lg1 = log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold green)(%ar)%C(reset) %C(white)%s%C(reset) %C(dim white)- %an%C(reset)%C(bold yellow)%d%C(reset)' --all
|
||||
lg2 = log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold cyan)%aD%C(reset) %C(bold green)(%ar)%C(reset)%C(bold yellow)%d%C(reset)%n'' %C(white)%s%C(reset) %C(dim white)- %an%C(reset)' --all
|
||||
@ -43,13 +52,13 @@ de base de git (honteusement volés [ici](https://stackoverflow.com/a/9074343))
|
||||
Si vous n'avez pas besoin d'autant de décorations, ou que vous ne pouvez pas définir
|
||||
d'alias, cette commande fait quelque chose d'assez similaire :
|
||||
|
||||
```git
|
||||
```bash
|
||||
git log --all --decorate --oneline --graph
|
||||
```
|
||||
|
||||
Pour avoir des messages de commits unifiés et cohérents avec de jolis alias :
|
||||
|
||||
```git
|
||||
```conf
|
||||
fix = "!f() { git commit -sm \"fix: $1\"; }; f"
|
||||
feat = "!f() { git commit -sm \"feat: $1\"; }; f"
|
||||
misc = "!f() { git commit -sm \"misc: $1\"; }; f"
|
||||
@ -85,19 +94,19 @@ stabilité par exemple, ou qui n'est pas prête pour la mise en production.
|
||||
|
||||
Pour créer une branche fille à partir de la branche sur laquelle vous êtes :
|
||||
|
||||
```git
|
||||
```bash
|
||||
git checkout -b nom_de_la_nouvelle_branche
|
||||
```
|
||||
|
||||
Et pour basculer de branche en branche :
|
||||
|
||||
```git
|
||||
```bash
|
||||
git checkout nom_de_la_branche
|
||||
```
|
||||
|
||||
Si vous ne savez pas sur quelle branche vous vous trouvez, vous pouvez faire :
|
||||
|
||||
```git
|
||||
```bash
|
||||
git branch
|
||||
```
|
||||
|
||||
@ -120,7 +129,7 @@ notre branche de travail, par exemple la résolution d'un bogue handicapant.
|
||||
Un `rebase` va faire changer votre commit de départ et l'échanger avec un autre.
|
||||
Prenons le schéma suivant :
|
||||
|
||||
![Schéma d'un rebase](assets/images/git_flow/git_rebase.svg)
|
||||
![Schéma d'un rebase](images/git_rebase.svg)
|
||||
|
||||
Nous avons ici la branche `new_feature` qui part d'un commit de master. Disons
|
||||
que vous êtes en train de développer une fonctionnalité importante, et qu'un bug
|
||||
@ -129,7 +138,7 @@ de votre branche, et qu'il est disponible sur `master`. Pour le récupérer sur
|
||||
branche, vous n'avez qu'à `rebase` votre branche sur la dernière version de `master`.
|
||||
La commande pour faire ceci serait alors :
|
||||
|
||||
```git
|
||||
```bash
|
||||
git rebase nom_de_branche_parente
|
||||
```
|
||||
|
||||
@ -173,7 +182,7 @@ forcément toutes en tête.
|
||||
|
||||
Pour fusionner une branche dans celle sur laquelle vous vous trouvez :
|
||||
|
||||
```git
|
||||
```bash
|
||||
git merge nom_de_la_branche_à_fusionner_dans_celle_sur_laquelle_vous_êtes
|
||||
```
|
||||
|
||||
@ -188,7 +197,7 @@ temps de lui dire au revoir !
|
||||
|
||||
Commençons d'abord par la supprimer sur le repo distant :
|
||||
|
||||
```git
|
||||
```bash
|
||||
git push -d origin nom_de_branche
|
||||
```
|
||||
|
||||
@ -200,7 +209,7 @@ références locales à des branches fantômes distantes avec la commande `git f
|
||||
|
||||
Enfin, il est temps de supprimer votre copie locale de cette branche :
|
||||
|
||||
```git
|
||||
```bash
|
||||
git branch -d nom_de_branche
|
||||
```
|
||||
|
||||
@ -210,12 +219,12 @@ possible de forcer la suppression d'une branche locale non fusionnée avec l'opt
|
||||
|
||||
"Don't cry because it's over, smile because it happened." - *Dr. Seuss*
|
||||
|
||||
## TL;DR
|
||||
## TL;DR {#tldr}
|
||||
|
||||
On commence par demander à git de ne pull que s'il n'y a pas de divergence avec
|
||||
la version distante de notre branche, et sinon, de rebase notre branche automatiquement :
|
||||
|
||||
```git
|
||||
```bash
|
||||
git config pull.rebase true
|
||||
```
|
||||
|
Before Width: | Height: | Size: 553 KiB After Width: | Height: | Size: 553 KiB |
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 2.1 MiB |
@ -1,4 +1,11 @@
|
||||
# Robot timide – le robot qui fuit la lumière
|
||||
+++
|
||||
title = "Robot timide – le robot qui fuit la lumière"
|
||||
description = "Création d'un robot timide pour un projet scolaire."
|
||||
date = 2022-05-19
|
||||
|
||||
[taxonomies]
|
||||
tags = ["maker", "fablab", "robotique"]
|
||||
+++
|
||||
|
||||
Création d'un robot timide pour un projet scolaire.
|
||||
|
||||
@ -9,7 +16,9 @@ en filière Informatique, un robot timide a vu le jour au sein d’Eirlab.
|
||||
Cet article est un guide pour expliquer comment reproduire notre travail, dans
|
||||
le plus pur esprit Maker.
|
||||
|
||||
![Une vue de 3/4 face du robot finalisé](./assets/images/robot-timide/robot_timide_header.jpg)
|
||||
<!-- more -->
|
||||
|
||||
![Une vue de 3/4 face du robot finalisé](images/robot_timide_header.jpg)
|
||||
|
||||
## Concept de base
|
||||
|
||||
@ -20,7 +29,7 @@ mode est aussi disponible, permettant non pas d’aller vers l’endroit le plus
|
||||
sombre, mais à l’inverse de fuir l’endroit le plus lumineux, pour avoir deux
|
||||
manières de voir le problème.
|
||||
|
||||
![Une vue de face du robot timide dans sa forme finale.](./assets/images/robot-timide/robot_timide_enseirb.jpg)
|
||||
![Une vue de face du robot timide dans sa forme finale.](images/robot_timide_enseirb.jpg)
|
||||
|
||||
## Du matériel
|
||||
|
||||
@ -55,7 +64,7 @@ percée pour accueillir les différents modules du robot. Les éléments les plu
|
||||
légers ou risquant moins de se détacher sont fixés par un simple système de
|
||||
mâchoire, prenant en étau la plaque de bois dans une forme de U.
|
||||
|
||||
![La forme de U pour prendre la plaque de base en mâchoire et tenir un capteur par pression.](./assets/images/robot-timide/u_shape.jpg)
|
||||
![La forme de U pour prendre la plaque de base en mâchoire et tenir un capteur par pression.](images/u_shape.jpg)
|
||||
|
||||
### Alimentation
|
||||
|
||||
@ -69,7 +78,7 @@ et prend donc des tensions entre 5 et 9 Volts.
|
||||
Dans ce projet, nous utilisons une pile 9 Volts rechargeable dans un boîtier
|
||||
vissé au châssis.
|
||||
|
||||
![Une vue de la pile 9 Volts que nous utilisons, rechargeable en micro USB.](./assets/images/robot-timide/pile9v.jpg)
|
||||
![Une vue de la pile 9 Volts que nous utilisons, rechargeable en micro USB.](images/pile9v.jpg)
|
||||
|
||||
### Moteurs
|
||||
|
||||
@ -92,11 +101,11 @@ aussi appelé breadboard. Dans un second temps et pour éviter toute déconnexio
|
||||
de câbles liée à une potentielle accélération violente, nous avons fait un PCB,
|
||||
soudé manuellement sur une carte de prototypage prévu à cet effet.
|
||||
|
||||
![Une vue du dessus de notre PCB fait main.](./assets/images/robot-timide/pcb_irl.jpg)
|
||||
![Une vue du dessus de notre PCB fait main.](images/pcb_irl.jpg)
|
||||
|
||||
Au niveau électronique, le PCB est routé de la manière suivante :
|
||||
|
||||
![Le routage du PCB sur une carte de protypage de type Breadboard.](./assets/images/robot-timide/pcb_fritzing.png)
|
||||
![Le routage du PCB sur une carte de protypage de type Breadboard.](images/pcb_fritzing.png)
|
||||
|
||||
Les différents composants viennent se connecter sur le PCB au moyen de
|
||||
différentes broches femelles, pour que ce dernier puisse être détaché et modifié
|
4
content/_index.md
Normal file
@ -0,0 +1,4 @@
|
||||
+++
|
||||
paginate_by=5
|
||||
sort_by = "date"
|
||||
+++
|
6
content/pages/archive.md
Normal file
@ -0,0 +1,6 @@
|
||||
+++
|
||||
title = "Articles archivés"
|
||||
path = "archive"
|
||||
+++
|
||||
|
||||
N/A
|
@ -1,20 +0,0 @@
|
||||
const Prism = require('prismjs');
|
||||
const fs = require('fs');
|
||||
const cheerio = require('cheerio');
|
||||
const loadLanguages = require('prismjs/components/');
|
||||
const fsc = require('fs-cheerio');
|
||||
|
||||
loadLanguages([
|
||||
'rust', 'py', 'cpp', 'c', 'git'
|
||||
]);
|
||||
|
||||
const file = process.argv[2];
|
||||
fsc.readFile(file).then(function ($, err) {
|
||||
$('*[class^="language-"]').replaceWith(function () {
|
||||
return $(this).html(Prism.highlight(
|
||||
$(this).text(),
|
||||
Prism.languages[$(this).attr('class').split('-')[1]])
|
||||
);
|
||||
});
|
||||
fsc.writeFile(file, $);
|
||||
});
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"cheerio": "^1.0.0-rc.10",
|
||||
"fs-cheerio": "^3.0.0",
|
||||
"prismjs": "^1.26.0"
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
|
||||
<footer>
|
||||
<p>
|
||||
License: <a href="https://choosealicense.com/licenses/gpl-3.0/" target="_blank">G.P.L. v3</a><br>
|
||||
Made with ❤ and <a href="https://gitlab.com/lovallat/blog" target="_blank">Open Source Software</a> —
|
||||
Version: <code>__VERSION__</code>
|
||||
</p>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,49 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
PAGE CODED BY :
|
||||
__ _____ __ __ ______ ____ __ __ ______ __ __ ______ ______
|
||||
/\ \ /\ __`\/\ \/\ \/\__ _\/\ _`\ /\ \/\ \/\ _ \/\ \ /\ \ /\ _ \/\__ _\
|
||||
\ \ \ \ \ \/\ \ \ \ \ \/_/\ \/\ \,\L\_\ \ \ \ \ \ \ \L\ \ \ \ \ \ \ \ \ \L\ \/_/\ \/
|
||||
\ \ \ __\ \ \ \ \ \ \ \ \ \ \ \ \/_\__ \ \ \ \ \ \ \ __ \ \ \ __\ \ \ __\ \ __ \ \ \ \
|
||||
\ \ \L\ \\ \ \_\ \ \ \_\ \ \_\ \__/\ \L\ \ \ \ \_/ \ \ \/\ \ \ \L\ \\ \ \L\ \\ \ \/\ \ \ \ \
|
||||
\ \____/ \ \_____\ \_____\/\_____\ `\____\ \ `\___/\ \_\ \_\ \____/ \ \____/ \ \_\ \_\ \ \_\
|
||||
\/___/ \/_____/\/_____/\/_____/\/_____/ `\/__/ \/_/\/_/\/___/ \/___/ \/_/\/_/ \/_/
|
||||
-->
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/assets/images/icon.png">
|
||||
<!-- SEO -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="">
|
||||
<meta name="image" content="https://blog.louis-vallat.fr/assets/images/icon.png">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="">
|
||||
<meta name="twitter:description" content="">
|
||||
<meta name="twitter:site" content="@VallatLouis">
|
||||
<meta name="twitter:creator" content="@VallatLouis">
|
||||
<meta name="twitter:image:src" content="https://blog.louis-vallat.fr/assets/images/icon.png">
|
||||
<meta name="og:title" content="">
|
||||
<meta name="og:description" content="">
|
||||
<meta name="og:image" content="https://blog.louis-vallat.fr/assets/images/icon.png">
|
||||
<meta name="og:url" content="https://blog.louis-vallat.fr/">
|
||||
<meta name="og:site_name" content="Le blog de Louis Vallat">
|
||||
<meta name="og:locale" content="fr_FR">
|
||||
<meta name="og:type" content="website">
|
||||
<!-- Reset CSS -->
|
||||
<link rel="stylesheet" href="/assets/css/reset.css">
|
||||
<!-- Main CSS -->
|
||||
<link rel="stylesheet" href="/assets/css/style.css">
|
||||
<title></title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav>
|
||||
<a href="/" id="home_link">accueil</a>
|
||||
<div class="filler"></div>
|
||||
<a href="mailto:contact@louis-vallat.fr" target="_blank">contact</a>
|
||||
<a href="https://gitlab.com/lovallat" target="_blank">gitlab</a>
|
||||
<a href="https://louis-vallat.fr" target="_blank">site</a>
|
||||
<a href="https://twitter.com/VallatLouis" target="_blank">twitter</a>
|
||||
</nav>
|
@ -1,73 +0,0 @@
|
||||
# AoC 2021 Jour 0: Commencement
|
||||
|
||||
Le début d'une grande aventure !
|
||||
|
||||
Cette année, j'ai découvert l'[Advent Of Code](https://adventofcode.com/).
|
||||
Ce site géré de manière bénévole par [Eric Wastl](http://was.tl/) sur son temps
|
||||
libre met à disposition des puzzles sur un thème différent d'une année sur
|
||||
l'autre. Ces petits puzzles sont à résoudre à l'aide d'un programme informatique
|
||||
ou d'un algorithme que l'on est sensé imaginer et mettre en oeuvre, pour, à partir
|
||||
d'un fichier d'entrée qui nous est propre, donner un résultat à troquer sur le site
|
||||
du challenge en échange d'une étoile. Le langage et la manière d'obtenir le
|
||||
résultat sont libres, mais faire les défis "à la main" est rarement possible
|
||||
(en tous cas en temps humain). Pendant tout le mois de décembre, un défi est publié
|
||||
chaque jour du 1er au 25. Ils sont systématiquement découpés en 2 parties, la
|
||||
seconde n'étant accessible qu'une fois la première résolue.
|
||||
En échange d'une réponse valide au problème, une étoile nous est donnée, pour avoir
|
||||
un total de 50 étoiles par an.
|
||||
|
||||
Il n'y a pas de pénalité si le puzzle n'est pas résolu dans la journée, mais un
|
||||
*leaderboard* est disponible, et les 100 premiers à résoudre les puzzles obtiennent
|
||||
des points sur leur profil. C'est comme cela que j'ai découvert qu'il existait
|
||||
une discipline de [Programmation Compétitive](https://en.wikipedia.org/wiki/Competitive_programming).
|
||||
|
||||
Cette année, le thème c'était la quête de la clé du traîneau du Père Noël au fond
|
||||
d'une crevasse sous-marine, armé d'un petit sous-marin, et rencontrant au passage
|
||||
une faune et flore locale diverse.
|
||||
|
||||
![Vue de l'intérieur d'un sous-marin d'exploration. Photo par Michael Mrozek pour Unsplash.](./assets/images/aoc-2021-day-0/michal-mrozek-BEUd36I1-f8-unsplash.jpg)
|
||||
|
||||
J'ai déjà résolu les 25 jours et obtenu mes 50 étoiles, je souhaite maintenant
|
||||
raconter ici mon aventure, jour après jour, et les algorithmes que j'ai mis en
|
||||
place.
|
||||
|
||||
## Pourquoi
|
||||
|
||||
J'ai choisi de participer à cet évènement pour différentes raisons :
|
||||
|
||||
- j'aime les défis, je trouve amusant et plaisant d'avoir un but à atteindre en code
|
||||
- je voulais m'exercer, j'aime travailler sur des projets sur mon temps libre
|
||||
|
||||
## Comment
|
||||
|
||||
Mes algorithmes sont implémentés en [Rust](https://www.rust-lang.org/), un langage
|
||||
que j'aime beaucoup, pour différentes raisons qui sont hors sujet par rapport à
|
||||
cette série d'articles. Un article spécifique sur ce qui me plaît dans ce
|
||||
langage de programmation arrivera peut-être plus tard.
|
||||
|
||||
Mon but était de résoudre les puzzles en temps limité, si possible en moins d'une
|
||||
journée, et que le programme final donne sa réponse le plus rapidement possible.
|
||||
Si après 3 jours à travailler sur un puzzle, il donne une réponse en 300ms, cela
|
||||
me convient. **Le but n'étant pas ici de faire des algorithmes optimaux.**
|
||||
|
||||
Chaque jour a été mis à disposition sur [gitlab](https://gitlab.com/lovallat/advent-of-code-2021).
|
||||
|
||||
## Beurk
|
||||
|
||||
Vous trouvez le code illisible ? C'est **normal**.
|
||||
|
||||
J'ai tendance dans mes programmes à avoir de mauvaises habitudes, *hostiles* à
|
||||
la relecture de code. J'ai tendance à utiliser par exemple des variables dont le
|
||||
nom se résume à une unique lettre. J'ai aussi tendance à vouloir produire un
|
||||
fichier source le plus court possible (tout en respectant les standards du langage
|
||||
utilisé). Le code fourni est alors un bloc compact **difficile à lire**.
|
||||
|
||||
J'ai donc décidé de me faire subir le même sort que celui réservé à mes collègues
|
||||
lorsqu'il doivent relire mon code, afin de me faire **perdre** ces mauvaises
|
||||
habitudes. (*spoiler: c'était efficace*)
|
||||
|
||||
## À suivre
|
||||
|
||||
Seront publiés sur ce blog différents articles retraçant et expliquant jour après
|
||||
jour ce que mes algorithmes et programmes font pour résoudre les différents défis.
|
||||
|
@ -1,112 +0,0 @@
|
||||
# AoC 2021 Jour 1: Sonar Sweep
|
||||
|
||||
À la recherche de la clef du traîneau à l'aide d'un sonar.
|
||||
|
||||
Le défi peut être trouvé [ici](https://adventofcode.com/2021/day/1), et le code
|
||||
lié est sur [gitlab](https://gitlab.com/lovallat/advent-of-code-2021/-/tree/master/day1).
|
||||
|
||||
## Consigne du défi
|
||||
|
||||
Le premier puzzle est assez simple, et peut se résumer en "compter les variations
|
||||
dans un fichier d'entrée".
|
||||
|
||||
## Lecture du fichier d'entrée
|
||||
|
||||
Pour lire le fichier, rien de plus simple. L'entrée étant une simple liste d'entiers
|
||||
séparés par des retours à la ligne, il est assez facile de le transformer en
|
||||
tableau d'entiers.
|
||||
|
||||
En code, cela se traduit par :
|
||||
|
||||
```rust
|
||||
fn parse_input(s: &str) -> Vec<i32> {
|
||||
return s.split("\n").into_iter().filter_map(|f| f.parse::<i32>().ok()).collect::<Vec<i32>>();
|
||||
}
|
||||
```
|
||||
|
||||
Maintenant que j'ai acquis un peu plus d'expérience en Rust, je le ferais d'une
|
||||
manière assez différente :
|
||||
|
||||
- tout d'abord, `s.split("\n").into_iter()` peut être remplacé par `s.lines()`,
|
||||
plus robuste et [System Agnostic](https://en.wikipedia.org/wiki/Agnostic_(data)),
|
||||
les systèmes Windows utilisant une fin de ligne [différente](https://en.wikipedia.org/wiki/Newline#In_programming_languages).
|
||||
- ensuite, utiliser des `u16`, soit des entiers non signés et encodés sur 16 bits.
|
||||
Ici utiliser 32 bits signés pour stocker des valeurs relativement faibles (dans ce
|
||||
jeu de données) et strictement positives n'est pas optimal.
|
||||
|
||||
## Première partie
|
||||
|
||||
La première partie nécessite de compter les augmentations de nombres un-à-un.
|
||||
|
||||
Nous avons donc un tableau (vecteur) d'entiers, alors il suffira simplement de le
|
||||
parcourir, et si l'entier actuel est plus grand que le précédent, alors on retient
|
||||
qu'on a vu une augmentation supplémentaire.
|
||||
|
||||
Le code de cette partie est le suivant :
|
||||
|
||||
```rust
|
||||
fn get_increase_nb(e: &Vec<i32>) -> i32 {
|
||||
let mut n = 0;
|
||||
for i in 1..e.len() {
|
||||
n += if e[i] > e[i - 1] { 1 } else { 0 };
|
||||
}
|
||||
return n;
|
||||
}
|
||||
```
|
||||
|
||||
On a donc un entier `n` qui va retenir le nombre d'augmentations observées,
|
||||
et pour tous les entiers du tableau, de la case 1 (les tableaux en informatique
|
||||
débutent à la case 0), à la case `taille` (non incluse, toujours parce que les
|
||||
tableaux commencent à 0). Si le nombre actuel est plus grand que le précédent,
|
||||
alors `n` augmente de 1.
|
||||
|
||||
## Deuxième partie
|
||||
|
||||
Pour cette deuxième partie, le concept est le même mais cette fois on prend les
|
||||
entiers d'entrée par groupe de 3 et on compte les augmentations entre chaque
|
||||
sommes d'entiers.
|
||||
|
||||
On transforme donc le tableau d'entiers en un second tableau correspondant aux
|
||||
sommes de 3 entiers selon une fenêtre flottante.
|
||||
|
||||
```rust
|
||||
fn get_sums(v: &Vec<i32>) -> BTreeMap<usize, i32> {
|
||||
let mut sums = BTreeMap::new();
|
||||
for i in 0..(v.len() - 2) {
|
||||
sums.insert(i, v[i] + v[i + 1] + v[i + 2]);
|
||||
}
|
||||
return sums;
|
||||
}
|
||||
```
|
||||
|
||||
Ici, une [BTreeMap](https://doc.rust-lang.org/std/collections/struct.BTreeMap.html)
|
||||
est **totalement** inutile. Je ne sais pas trop pourquoi j'ai choisi ce type de
|
||||
structure de données. Surtout qu'après, cette Map est convertie en simple tableau
|
||||
en ignorant totalement la clef :
|
||||
|
||||
```rust
|
||||
let map = get_sums(&vec_in).iter().map(|e| e.1.to_owned()).collect::<Vec<i32>>();
|
||||
```
|
||||
|
||||
Ici il serait *beaucoup plus intéressant* d'utiliser un simple tableau, et à chaque
|
||||
nouvelle somme on peut l'envoyer sur le dessus du tableau (de sommes) d'entiers.
|
||||
|
||||
Ce tableau enfin obtenu sera alors envoyé à la même méthode que pour la partie une
|
||||
pour en obtenir le nombre d'augmentations.
|
||||
|
||||
Évidemment il serait aussi possible de simplement compter les augmentations en même
|
||||
temps que l'on parcourt les tableaux, pour éviter de multiples passages.
|
||||
|
||||
## Conclusion
|
||||
|
||||
Ce puzzle était loin d'être compliqué. J'ai passé un peu de temps sur la
|
||||
compréhension du système de sommes de la partie 2, ne sachant que faire dans le
|
||||
cas où il n'y avait plus assez d'entiers pour faire une somme.
|
||||
|
||||
Après un peu de temps et d'essais, j'ai pu m'en sortir sans difficulté.
|
||||
|
||||
Concernant la qualité du code, je me demande bien ce que j'ai pu penser sur le
|
||||
moment pour choisir de stocker les sommes dans une map à arbre binaire...
|
||||
|
||||
> À suivre
|
||||
|
@ -1,127 +0,0 @@
|
||||
# AoC 2021 Jour 2: Dive
|
||||
|
||||
Comment piloter le sous-marin des elfes ?
|
||||
|
||||
Le défi peut être trouvé [ici](https://adventofcode.com/2021/day/2), et le code
|
||||
lié est sur [gitlab](https://gitlab.com/lovallat/advent-of-code-2021/-/tree/master/day2).
|
||||
|
||||
## Consigne du défi
|
||||
|
||||
Le sous-marin peut prendre 3 types de commandes :
|
||||
|
||||
- `forward X`
|
||||
- `down X`
|
||||
- `up X`
|
||||
|
||||
Le fichier d'entrée est une suite de ces ordres, que l'on doit interpréter pour
|
||||
trouver la position du sous-marin dans l'espace (horizontalement et verticalement).
|
||||
|
||||
## Lecture du fichier d'entrée
|
||||
|
||||
La lecture du fichier d'entrée se fait à nouveau dans une seule fonction :
|
||||
|
||||
```rust
|
||||
fn parse_input(s: &str) -> Vec<(String, i32)> {
|
||||
let mut r:Vec<(String, i32)> = vec![];
|
||||
let v = s.split("\n").into_iter();
|
||||
for e in v {
|
||||
let p = e.split_whitespace().collect::<Vec<&str>>();
|
||||
if p.len() == 2 && p[1].parse::<i32>().is_ok() {
|
||||
r.push((p[0].to_owned(), p[1].parse::<i32>().unwrap()));
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
```
|
||||
|
||||
Ici, on va tout simplement transformer une ligne en tuple `(String, i32)`, et
|
||||
de transformer le fichier d'entrée en un tableau `Vec` de ces ordres. On découpe
|
||||
donc une ligne `ordre X` en fonction d'un espace, et si on a bien 2 éléments et
|
||||
que le deuxième peut être converti en entier signé stocké sur 32 bits, alors on
|
||||
l'ajoute à notre tableau d'instructions.
|
||||
|
||||
On pourrait ici utiliser une énumeration telle que `Direction::Forward`,
|
||||
`Direction::Up` et `Direction::Down` pour éviter de stocker les chaînes de
|
||||
caractère en entier dans le tableau. On pourrait aussi utiliser des entiers stockés
|
||||
sur moins de bits, étant relativement petits.
|
||||
|
||||
## Première partie
|
||||
|
||||
Pour la première partie, l'interprétation des instructions est la suivante :
|
||||
|
||||
- `forward X` qui permet de faire avancer le sous-marin de `X` unités horizontalement
|
||||
- `down X` fait plonger le sous-marin et donc augmente sa profondeur de `X` unités
|
||||
- `up X` fait remonter à l'inverse le sous-marin vers la surface
|
||||
|
||||
La réponse à la première partie se trouve en multipliant la position horizontale
|
||||
et verticale. Pour cela, on va faire un `fold` sur le tableau pour calculer toutes
|
||||
les instructions à la suite et donnant le résultat en une seule ligne :
|
||||
|
||||
```rust
|
||||
fn get_horizontal_depth(v: Vec<(String, i32)>) -> (i32, i32) {
|
||||
return v.into_iter().fold((0, 0), |acc, e| compute_horizontal_depth(e, acc));
|
||||
}
|
||||
```
|
||||
|
||||
Chaque ligne est traitée individuellement pour interprétation, en donnat en valeur
|
||||
de retour les nouvelles positions du sous-marin après application de l'instruction
|
||||
sur la position donnée en entrée :
|
||||
|
||||
```rust
|
||||
fn compute_horizontal_depth(t: (String, i32), a: (i32, i32)) -> (i32, i32) {
|
||||
return match t.0.as_str() {
|
||||
"up" => (a.0, a.1 - t.1),
|
||||
"down" => (a.0, a.1 + t.1),
|
||||
"forward" => (a.0 + t.1, a.1),
|
||||
_ => a
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Ce traitement est donc très simple. Pour chaque instruction et en partant d'une
|
||||
position `(0, 0)`, on va appliquer les règles les unes après les autres en mettant
|
||||
à jour la position à chaque fois. Cela se fait en temps linéaire sans calcul autre
|
||||
que des additions.
|
||||
|
||||
## Deuxième partie
|
||||
|
||||
Pour la deuxième partie, le traitement est assez similaire, mais on va calculer
|
||||
une composante supplémentaire : la *visée*.
|
||||
|
||||
Les instructions sont désormais à interpréter de cette manière :
|
||||
|
||||
- `forward X` permet deux choses :
|
||||
- augmente la position horizontale de `X` unités
|
||||
- augmente la profondeur par la *visée* multipliée par `X`
|
||||
- `down X` augmente la *visée* de `X` unités
|
||||
- `up X` à l'inverse diminue la *visée* de `X` unités
|
||||
|
||||
Le concept pour calculer est le même, on applique les instructions l'une après
|
||||
l'autre en partant de `(0, 0, 0)`.
|
||||
|
||||
```rust
|
||||
fn compute_horizontal_depth_aim(t: (String, i32), a: (i32, i32, i32)) -> (i32, i32, i32) {
|
||||
return match t.0.as_str() {
|
||||
"up" => (a.0, a.1, a.2 - t.1),
|
||||
"down" => (a.0, a.1, a.2 + t.1),
|
||||
"forward" => (a.0 + t.1, a.1 + a.2 * t.1, a.2),
|
||||
_ => a
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Enfin, la réponse à donner est la multiplication de la position horizontale et
|
||||
verticale, comme pour la précédente partie.
|
||||
|
||||
## Conclusion
|
||||
|
||||
Ce défi n'était pas compliqué et ma réponse me semble adaptée. Bien sûr il y a
|
||||
toujours des pistes d'améliorations, telle que le type de données stockées :
|
||||
les chaînes de caractères étant moins performantes à traiter, mais aussi la taille
|
||||
des entiers stockés, ou encore le fait que je ne passe pas de référence aux
|
||||
fonctions, nécéssitant une copie systématique, consommant des cycles supplémentaires.
|
||||
|
||||
Mais globalement je suis satisfait de ma réponse à ce défi.
|
||||
|
||||
> À suivre
|
||||
|
@ -1,167 +0,0 @@
|
||||
# AoC 2021 Jour 3: Binary Diagnostic
|
||||
|
||||
Grincements étranges et inquiétants dans le sous-marin.
|
||||
|
||||
Le défi peut être trouvé [ici](https://adventofcode.com/2021/day/3), et le code
|
||||
lié est sur [gitlab](https://gitlab.com/lovallat/advent-of-code-2021/-/tree/master/day3).
|
||||
|
||||
## Consigne du défi
|
||||
|
||||
Dans ce défi, on nous apprend que le sous-marin fait des bruits *pour le moins
|
||||
étranges*. Le fichier d'entrée se compose d'une longue liste de nombres binaires,
|
||||
que nous devons analyser.
|
||||
|
||||
## Lecture du fichier d'entrée
|
||||
|
||||
La lecture du fichier d'entrée se fait de la manière suivante, assez simplement :
|
||||
|
||||
```rust
|
||||
fn parse_input(s: &str) -> Vec<Vec<u32>> {
|
||||
let mut r: Vec<Vec<u32>> = vec![];
|
||||
for e in s.split("\n") {
|
||||
if !e.is_empty() {
|
||||
r.push(e.chars().filter_map(|e| e.to_digit(2)).collect());
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
```
|
||||
|
||||
On va donc transformer chaque ligne (par exemple `101000001100`) en un tableau
|
||||
de chiffres, ayant pour valeur `0` ou `1` (dans l'exemple
|
||||
`[ 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0 ]`). On fait cela pour chaque ligne, pour
|
||||
assembler une matrice de chiffres binaires, faciles à parcourir dans toutes les
|
||||
directions.
|
||||
|
||||
## Première partie
|
||||
|
||||
Pour la première partie, il va falloir créer un nombre binaire à partir des chiffres
|
||||
les **plus** fréquents dans les colonnes, et un nombre binaire à partir des chiffres
|
||||
les **moins** fréquents, toujours dans les colonnes. On obtiendra alors deux
|
||||
nombres binaires, qui une fois convertis en base 10 et ensuite multipliés entre eux,
|
||||
donne la réponse à cette première partie.
|
||||
|
||||
Une fonction utilitaire permet de convertir les tableaux de nombres binaires en
|
||||
nombre entier non signé :
|
||||
|
||||
```rust
|
||||
fn bit_vec_to_u32(v: &Vec<u32>, invert: bool) -> u32 {
|
||||
let mut acc = 0;
|
||||
for i in 0..v.len() {
|
||||
acc += if invert { (v[i] + 1) % 2 } else { v[i] } << (v.len() - 1 - i);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
```
|
||||
|
||||
On a donc ici un accumulateur qui fait la somme des éléments tout en faisant un
|
||||
décalage binaire, produisant un nombre entier en temps linéaire.
|
||||
|
||||
On va donc utiliser une fonction permettant d'obtenir, pour tout le fichier d'entrée,
|
||||
le bit le plus présent dans la colonne `c` :
|
||||
|
||||
```rust
|
||||
fn get_most_in_col(v: &Vec<Vec<u32>>, c: usize) -> u32 {
|
||||
let mut a = (0, 0);
|
||||
for r in 0..v.len() {
|
||||
if v[r][c] == 0 {
|
||||
a.0 += 1;
|
||||
} else {
|
||||
a.1 += 1;
|
||||
}
|
||||
}
|
||||
if a.1 == a.0 { return 1; }
|
||||
else { return if a.1 > a.0 { 1 } else { 0 }; }
|
||||
}
|
||||
```
|
||||
|
||||
À la fin de la fonction, on peut voir que lors d'une égalité, c'est le `1` qui est
|
||||
considéré comme valeur la plus présente dans la colonne étudiée.
|
||||
|
||||
**Amélioration possible :** il serait potentiellement plus intéressant de faire la somme des entiers stockés
|
||||
dans la colonne, et regarder si cette somme est supérieure au nombre de lignes
|
||||
divisée par deux, car cela nous évite de faire des comparaisons et rend inutiles
|
||||
les conditions `if` qui sont au milieu de la boucle, ralentissent fortement
|
||||
le calcul et sont donc à **proscrire**.
|
||||
|
||||
On va donc ici calculer en un seul coup la suite de chiffres binaires les plus
|
||||
représentés par colonne dans le fichier d'entrée. La valeur `gamma` sera donc la
|
||||
conversion directe de ce tableau en nombre entier non signé, et la valeur `epsilon`
|
||||
sera aussi une conversion, mais inversée.
|
||||
|
||||
```rust
|
||||
fn get_gamma_epsilon(v: &Vec<Vec<u32>>) -> (u32, u32) {
|
||||
let mut acc: Vec<u32> = vec![];
|
||||
|
||||
for c in 0..v[0].len() {
|
||||
acc.push(get_most_in_col(&v, c));
|
||||
}
|
||||
return (bit_vec_to_u32(&acc, false), bit_vec_to_u32(&acc, true));
|
||||
}
|
||||
```
|
||||
|
||||
La multiplication de ces deux valeurs nous donne la réponse à cette première partie.
|
||||
|
||||
## Deuxième partie
|
||||
|
||||
Toujours à partir de ce fichier d'entrée, on doit alors calculer deux nouvelles
|
||||
valeurs : le niveau de génération d'oxygène et le niveau de filtrage de CO2.
|
||||
|
||||
Pour obtenir ces deux valeurs, on joue encore sur les bits les plus et les moins
|
||||
communs dans une colonne donnée mais en rajoutant cette fois un concept
|
||||
d'élimination. On va donc faire ceci :
|
||||
|
||||
- On récupère le bit le plus (ou le moins) présent dans la colonne observée
|
||||
- On ne conserve que les nombres binaires qui ont cette valeur de bit pour cette colonne
|
||||
- On répète cette opération jusqu'à ce qu'il ne reste qu'une seule valeur
|
||||
|
||||
Ici, j'ai utilisé une approche récursive :
|
||||
|
||||
```rust
|
||||
fn get_o2(v: &Vec<Vec<u32>>, c: usize) -> u32 {
|
||||
if v.len() == 1 { return bit_vec_to_u32(&v[0], false); }
|
||||
|
||||
let a = get_most_in_col(&v, c);
|
||||
let f = v.into_iter().filter(|e| e[c] == (a + 1) % 2)
|
||||
.fold(vec![], |mut acc, e| { acc.push(e.to_owned()); return acc;});
|
||||
return get_o2(&f, c + 1);
|
||||
}
|
||||
|
||||
fn get_co2(v: &Vec<Vec<u32>>, c: usize) -> u32 {
|
||||
if v.len() == 1 { return bit_vec_to_u32(&v[0], false); }
|
||||
|
||||
let a = get_most_in_col(&v, c);
|
||||
let f = v.into_iter().filter(|e| e[c] == a)
|
||||
.fold(vec![], |mut acc, e| { acc.push(e.to_owned()); return acc;});
|
||||
return get_co2(&f, c + 1);
|
||||
}
|
||||
```
|
||||
|
||||
On va donc récupérer le bit le plus présent dans la colonne `c`, et conserver les
|
||||
valeurs qui nous intéressent, puis rappeler la méthode sur les valeurs restantes
|
||||
en avançant d'une colonne. Une fois qu'il ne reste qu'une valeur, on sait qu'on
|
||||
a notre résultat, et c'est ce qui est renvoyé.
|
||||
|
||||
En relisant ce code, j'ai plusieurs critiques à en faire :
|
||||
|
||||
- J'ai visiblement inversé le calcul de CO2 et de O2. Dans le calcul pour l'oxygène,
|
||||
je ne conserve que les bits les moins présents, alors que c'est les plus présents
|
||||
que je devrais conserver. Cela ne pose pas de problème car je dois multiplier les
|
||||
deux valeurs entre elles, et l'ordre des termes d'un produit importe peu.
|
||||
- J'utilise `(a + 1) % 2` pour obtenir l'inverse d'un bit, alors qu'un simple "différent"
|
||||
suffirait. Cela demande de faire des opérations coûteuses (la division est coûteuse
|
||||
en cycles CPU) pour rien.
|
||||
- Je fais une copie du tableau avec les éléments restants après le tri au lieu de
|
||||
faire un simple `filter`. Une copie est de toutes façons nécessaire, mais filtrer
|
||||
le tableau serait plus élégant.
|
||||
|
||||
## Conclusion
|
||||
|
||||
Ce défi fait monter très légèrement la difficulté et le temps de réflexion
|
||||
nécessaire, mais cela reste très accessible. Encore une fois, relire son code
|
||||
et se poser les bonnes questions est très important, surtout se demander si telle
|
||||
ou telle copie ou opération est bien nécessaire. C'est une réflexion que je
|
||||
m'appliquerai à faire plus souvent.
|
||||
|
||||
> À suivre
|
||||
|
@ -1,48 +0,0 @@
|
||||
/* http://meyerweb.com/eric/tools/css/reset/
|
||||
v2.0 | 20110126
|
||||
License: none (public domain)
|
||||
*/
|
||||
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
small, strike, strong, sub, sup, tt, var,
|
||||
b, u, i, center,
|
||||
dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, embed,
|
||||
figure, figcaption, footer, header, hgroup,
|
||||
menu, nav, output, ruby, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
}
|
||||
body {
|
||||
line-height: 1;
|
||||
}
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
blockquote, q {
|
||||
quotes: none;
|
||||
}
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after {
|
||||
content: '';
|
||||
content: none;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
@ -1,218 +0,0 @@
|
||||
@charset "utf-8";
|
||||
/* Coded by Louis Vallat */
|
||||
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
src: url("/assets/fonts/JetBrainsMono.ttf");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Mono';
|
||||
src: url("/assets/fonts/IBMPlexMono.ttf");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: url("/assets/fonts/OpenSans-Regular.ttf");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg: #2A2D34;
|
||||
--light_bg: #414652;
|
||||
--fg: #fefefe;
|
||||
--link: #009DDC;
|
||||
--link_h: #009B72;
|
||||
--link_v: #ACACDE;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 5px solid var(--light_bg);
|
||||
border-radius: 5px;
|
||||
margin-left: 10px;
|
||||
padding-left: 7px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
background-color: var(--bg);
|
||||
color: var(--fg);
|
||||
margin-right: 27vw;
|
||||
margin-left: 27vw;
|
||||
text-align: justify;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
p, ul {
|
||||
margin-top: 10px;
|
||||
line-height: 1.55em;
|
||||
}
|
||||
|
||||
li>ul {
|
||||
margin-top: 0;
|
||||
margin-left: 15px;
|
||||
list-style-type: circle;
|
||||
}
|
||||
|
||||
img {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--link);
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: var(--link_v);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--link_h);
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filler {
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
nav>a {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
nav>a:visited, nav>a:hover, nav>a {
|
||||
color: var(--fg);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
nav>a:not(#home_link) {
|
||||
margin-left: 25px;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: 'JetBrains Mono', sans-serif;
|
||||
font-weight: bold;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: disc;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.85em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.65em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.45em;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.35em;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
b, strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
em, i {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
footer {
|
||||
font-size: 14px;
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin-bottom: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
pre>code {
|
||||
padding: 10px;
|
||||
display: block;
|
||||
overflow: auto;
|
||||
line-height: 1.35em;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 2px 5px 2px 5px;
|
||||
border-radius: 5px;
|
||||
background-color: var(--light_bg);
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
}
|
||||
|
||||
/* Code blocks colors */
|
||||
.token.keyword, .token.macro {
|
||||
color: #D55FDE;
|
||||
}
|
||||
|
||||
.token.function {
|
||||
color: #52ADF2;
|
||||
}
|
||||
|
||||
.token.class-name {
|
||||
color: #E5C07B;
|
||||
}
|
||||
|
||||
.token.punctuation, .token.operator {
|
||||
color: #AAB1C0;
|
||||
}
|
||||
|
||||
.token.string {
|
||||
color: #89CA78;
|
||||
}
|
||||
|
||||
.token.number {
|
||||
color: #D8985F;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
body {
|
||||
margin-right: 10vw;
|
||||
margin-left: 10vw;
|
||||
}
|
||||
nav>a:not(#home_link) {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
body {
|
||||
margin-right: 5vw;
|
||||
margin-left: 5vw;
|
||||
}
|
||||
nav>a:not(#home_link) {
|
||||
margin-left: 0px;
|
||||
}
|
||||
blockquote {
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 511 KiB |
Before Width: | Height: | Size: 3.7 KiB |
12
src/index.md
@ -1,12 +0,0 @@
|
||||
#### Articles
|
||||
|
||||
Un blog on ne peut plus simple.
|
||||
|
||||
- [Archive/Projet Makers ENSEIRB - Robot timide](./robot-timide-robot-qui-fuit-la-lumiere.html)
|
||||
- Advent of Code 2021 :
|
||||
- [Advent of Code 2021 Jour 3 : Binary Diagnostic](./aoc-2021-jour-3-binary-diagnostic.html) - *07/02/2021*
|
||||
- [Advent of Code 2021 Jour 2 : Dive](./aoc-2021-jour-2-dive.html) - *24/01/2021*
|
||||
- [Advent of Code 2021 Jour 1 : Sonar Sweep](./aoc-2021-jour-1-sonar-sweep.html) - *19/01/2021*
|
||||
- [Advent of Code 2021 Jour 0 : le commencement](./aoc-2021-jour-0-commencement.html) - *17/01/2022*
|
||||
- [Git workflow](./git_flow.html) - *10/05/2021*
|
||||
|
286
ssg5
@ -1,286 +0,0 @@
|
||||
#!/bin/sh -e
|
||||
#
|
||||
# https://rgz.ee/bin/ssg5
|
||||
# Copyright 2018-2019 Roman Zolotarev <hi@romanzolotarev.com>
|
||||
#
|
||||
# Permission to use, copy, modify, and/or distribute this software for any
|
||||
# purpose with or without fee is hereby granted, provided that the above
|
||||
# copyright notice and this permission notice appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
#
|
||||
|
||||
main() {
|
||||
test -n "$1" || usage
|
||||
test -n "$2" || usage
|
||||
test -n "$3" || usage
|
||||
test -n "$4" || usage
|
||||
test -d "$1" || no_dir "$1"
|
||||
test -d "$2" || no_dir "$2"
|
||||
|
||||
|
||||
src=$(readlink_f "$1")
|
||||
dst=$(readlink_f "$2")
|
||||
|
||||
IGNORE=$(
|
||||
if ! test -f "$src/.ssgignore"
|
||||
then
|
||||
printf ' ! -path "*/.*"'
|
||||
return
|
||||
fi
|
||||
while read -r x
|
||||
do
|
||||
test -n "$x" || continue
|
||||
printf ' ! -path "*/%s*"' "$x"
|
||||
done < "$src/.ssgignore"
|
||||
)
|
||||
|
||||
# files
|
||||
|
||||
title="$3"
|
||||
|
||||
h_file="$src/_header.html"
|
||||
f_file="$src/_footer.html"
|
||||
test -f "$f_file" && FOOTER=$(cat "$f_file") && export FOOTER
|
||||
test -f "$h_file" && HEADER=$(cat "$h_file") && export HEADER
|
||||
|
||||
list_dirs "$src" |
|
||||
(cd "$src" && cpio -pdu "$dst")
|
||||
|
||||
fs=$(
|
||||
if test -f "$dst/.files"
|
||||
then list_affected_files "$src" "$dst/.files"
|
||||
else list_files "$1"
|
||||
fi
|
||||
)
|
||||
|
||||
if test -n "$fs"
|
||||
then
|
||||
echo "$fs" | tee "$dst/.files"
|
||||
|
||||
if echo "$fs" | grep -q '\.md$'
|
||||
then
|
||||
if test -x "$(which lowdown 2> /dev/null)"
|
||||
then
|
||||
echo "$fs" | grep '\.md$' |
|
||||
render_md_files_lowdown "$src" "$dst" "$title"
|
||||
else
|
||||
if test -x "$(which Markdown.pl 2> /dev/null)"
|
||||
then
|
||||
echo "$fs" | grep '\.md$' |
|
||||
render_md_files_Markdown_pl "$src" "$dst" "$title"
|
||||
else
|
||||
echo "couldn't find lowdown nor Markdown.pl"
|
||||
exit 3
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$fs" | grep '\.html$' |
|
||||
render_html_files "$src" "$dst" "$title"
|
||||
|
||||
|
||||
echo "$fs" | grep -Ev '\.md$|\.html$' |
|
||||
(cd "$src" && cpio -pu "$dst")
|
||||
fi
|
||||
|
||||
printf '[ssg] ' >&2
|
||||
print_status 'file, ' 'files, ' "$fs" >&2
|
||||
|
||||
|
||||
# sitemap
|
||||
|
||||
base_url="$4"
|
||||
date=$(date +%Y-%m-%d)
|
||||
urls=$(list_pages "$src")
|
||||
|
||||
test -n "$urls" &&
|
||||
render_sitemap "$urls" "$base_url" "$date" > "$dst/sitemap.xml"
|
||||
|
||||
print_status 'url' 'urls' "$urls" >&2
|
||||
echo >&2
|
||||
}
|
||||
|
||||
|
||||
readlink_f() {
|
||||
file="$1"
|
||||
cd "$(dirname "$file")"
|
||||
file=$(basename "$file")
|
||||
while test -L "$file"
|
||||
do
|
||||
file=$(readlink "$file")
|
||||
cd "$(dirname "$file")"
|
||||
file=$(basename "$file")
|
||||
done
|
||||
dir=$(pwd -P)
|
||||
echo "$dir/$file"
|
||||
}
|
||||
|
||||
|
||||
print_status() {
|
||||
test -z "$3" && printf 'no %s' "$2" && return
|
||||
|
||||
echo "$3" | awk -v singular="$1" -v plural="$2" '
|
||||
END {
|
||||
if (NR==1) printf NR " " singular
|
||||
if (NR>1) printf NR " " plural
|
||||
}'
|
||||
}
|
||||
|
||||
|
||||
usage() {
|
||||
echo "usage: ${0##*/} src dst title base_url" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
|
||||
no_dir() {
|
||||
echo "${0##*/}: $1: No such directory" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
list_dirs() {
|
||||
cd "$1" && eval "find . -type d ! -name '.' ! -path '*/_*' $IGNORE"
|
||||
}
|
||||
|
||||
|
||||
list_files() {
|
||||
cd "$1" && eval "find . -type f ! -name '.' ! -path '*/_*' $IGNORE"
|
||||
}
|
||||
|
||||
|
||||
list_dependant_files () {
|
||||
e="\\( -name '*.html' -o -name '*.md' -o -name '*.css' -o -name '*.js' \\)"
|
||||
cd "$1" && eval "find . -type f ! -name '.' ! -path '*/_*' $IGNORE $e"
|
||||
}
|
||||
|
||||
list_newer_files() {
|
||||
cd "$1" && eval "find . -type f ! -name '.' $IGNORE -newer $2"
|
||||
}
|
||||
|
||||
|
||||
has_partials() {
|
||||
grep -qE '^./_.*\.html$|^./_.*\.js$|^./_.*\.css$'
|
||||
}
|
||||
|
||||
|
||||
list_affected_files() {
|
||||
fs=$(list_newer_files "$1" "$2")
|
||||
|
||||
if echo "$fs" | has_partials
|
||||
then list_dependant_files "$1"
|
||||
else echo "$fs"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
render_html_files() {
|
||||
while read -r f
|
||||
do render_html_file "$3" < "$1/$f" > "$2/$f"
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
render_md_files_lowdown() {
|
||||
while read -r f
|
||||
do
|
||||
lowdown \
|
||||
--html-no-skiphtml \
|
||||
--html-no-escapehtml < "$1/$f" |
|
||||
render_html_file "$3" "$(head -n 3 ${1}/${f} | tail -n 1)" > "$2/${f%\.md}.html"
|
||||
node ./enrich_codeblocks.js "$2/${f%\.md}.html"
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
render_md_files_Markdown_pl() {
|
||||
while read -r f
|
||||
do
|
||||
Markdown.pl < "$1/$f" |
|
||||
render_html_file "$3" \
|
||||
> "$2/${f%\.md}.html"
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
render_html_file() {
|
||||
# h/t Devin Teske
|
||||
# TODO IMPROVE THIS IF POSSIBLE
|
||||
awk -v title="${1}" -v brief="${2}" '
|
||||
{ body = body "\n" $0 }
|
||||
END {
|
||||
body = substr(body, 2)
|
||||
if (body ~ /<[Hh][Tt][Mm][Ll]/) {
|
||||
print body
|
||||
exit
|
||||
}
|
||||
if (match(body, /<[[:space:]]*[Hh]1(>|[[:space:]][^>]*>)/)) {
|
||||
t = substr(body, RSTART + RLENGTH)
|
||||
sub("<[[:space:]]*/[[:space:]]*[Hh]1.*", "", t)
|
||||
gsub(/^[[:space:]]*|[[:space:]]$/, "", t)
|
||||
if (t) title = t " — " title
|
||||
}
|
||||
n = split(ENVIRON["HEADER"], header, /\n/)
|
||||
for (i = 1; i <= n; i++) {
|
||||
if (match(tolower(header[i]), "<meta name=\"twitter:title\" content=\"\">")) {
|
||||
head = substr(header[i], 1, RSTART - 1)
|
||||
tail = substr(header[i], RSTART + RLENGTH)
|
||||
print head "<meta name=\"twitter:title\" content=\"" title "\">" tail
|
||||
} else if (match(tolower(header[i]), "<meta name=\"og:title\" content=\"\">")) {
|
||||
head = substr(header[i], 1, RSTART - 1)
|
||||
tail = substr(header[i], RSTART + RLENGTH)
|
||||
print head "<meta name=\"og:title\" content=\"" title "\">" tail
|
||||
} else if (match(tolower(header[i]), "<title></title>")) {
|
||||
head = substr(header[i], 1, RSTART - 1)
|
||||
tail = substr(header[i], RSTART + RLENGTH)
|
||||
print head "<title>" title "</title>" tail
|
||||
} else if (match(tolower(header[i]), "<meta name=\"twitter:description\" content=\"\">")) {
|
||||
head = substr(header[i], 1, RSTART - 1)
|
||||
tail = substr(header[i], RSTART + RLENGTH)
|
||||
print head "<meta name=\"twitter:description\" content=\"" brief "\">" tail
|
||||
} else if (match(tolower(header[i]), "<meta name=\"description\" content=\"\">")) {
|
||||
head = substr(header[i], 1, RSTART - 1)
|
||||
tail = substr(header[i], RSTART + RLENGTH)
|
||||
print head "<meta name=\"description\" content=\"" brief "\">" tail
|
||||
} else if (match(tolower(header[i]), "<meta name=\"og:description\" content=\"\">")) {
|
||||
head = substr(header[i], 1, RSTART - 1)
|
||||
tail = substr(header[i], RSTART + RLENGTH)
|
||||
print head "<meta name=\"og:description\" content=\"" brief "\">" tail
|
||||
} else print header[i]
|
||||
}
|
||||
print body
|
||||
print ENVIRON["FOOTER"]
|
||||
}'
|
||||
}
|
||||
|
||||
|
||||
list_pages() {
|
||||
e="\\( -name '*.html' -o -name '*.md' \\)"
|
||||
cd "$1" && eval "find . -type f ! -path '*/.*' ! -path '*/_*' $IGNORE $e" |
|
||||
sed 's#^./##;s#.md$#.html#;s#/index.html$#/#'
|
||||
}
|
||||
|
||||
render_sitemap() {
|
||||
urls="$1"
|
||||
base_url="$2"
|
||||
date="$3"
|
||||
|
||||
echo '<?xml version="1.0" encoding="UTF-8"?>'
|
||||
echo '<urlset'
|
||||
echo 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||
echo 'xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9'
|
||||
echo 'http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"'
|
||||
echo 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
|
||||
echo "$urls" |
|
||||
sed -E 's#^(.*)$#<url><loc>'"$base_url"'/\1</loc><lastmod>'\
|
||||
"$date"'</lastmod><priority>1.0</priority></url>#'
|
||||
echo '</urlset>'
|
||||
}
|
||||
|
||||
main "$@"
|
BIN
static/favicon.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
8
templates/404.html
Normal file
@ -0,0 +1,8 @@
|
||||
{% extends "terminimal/templates/404.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="post">
|
||||
<h1 class="post-title">{% block heading %}Perdu ?{% endblock heading %}</h1>
|
||||
<p>{% block message %}Cette page n'existe pas.{% endblock message %}</p>
|
||||
</div>
|
||||
{% endblock content %}
|
56
templates/index.html
Normal file
@ -0,0 +1,56 @@
|
||||
{% extends "terminimal/templates/index.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="posts">
|
||||
{%- if paginator %}
|
||||
{%- set show_pages = paginator.pages -%}
|
||||
{% else %}
|
||||
{%- set show_pages = section.pages -%}
|
||||
{% endif -%}
|
||||
|
||||
{%- for page in show_pages %}
|
||||
<div class="post on-list">
|
||||
{{ post_macros::header(page=page) }}
|
||||
{{ post_macros::content(page=page, summary=true, show_only_description=page.extra.show_only_description | default(value=false)) }}
|
||||
</div>
|
||||
{% endfor -%}
|
||||
<div class="pagination">
|
||||
<div class="pagination__buttons">
|
||||
{%- if paginator.previous %}
|
||||
<span class="button previous">
|
||||
<a href="{{ paginator.previous | safe }}">
|
||||
<span class="button__icon">←</span>
|
||||
<span class="button__text">Posts plus récents</span>
|
||||
</a>
|
||||
</span>
|
||||
{% endif -%}
|
||||
{%- if paginator.next %}
|
||||
<span class="button next">
|
||||
<a href="{{ paginator.next | safe }}">
|
||||
<span class="button__text">Posts plus anciens</span>
|
||||
<span class="button__icon">→</span>
|
||||
</a>
|
||||
</span>
|
||||
{% endif -%}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block footer %}
|
||||
<footer class="footer">
|
||||
<div class="footer__inner">
|
||||
{%- if config.extra.copyright_html %}
|
||||
<div class="copyright copyright--user">{{ config.extra.copyright_html | safe }}</div>
|
||||
{% else %}
|
||||
<div class="copyright">
|
||||
<span>{{ config.extra.author }}</span>
|
||||
<span>
|
||||
<span>:: </span>
|
||||
Made with ❤, <a href="https://gitlab.com/lovallat/blog" target="_blank">Open Source Software</a>, <a href="https://www.getzola.org" target="_blank">Zola</a> and <a href="https://github.com/pawroman/zola-theme-terminimal/" target="_blank">Terminimal</a>
|
||||
</span>
|
||||
</div>
|
||||
{% endif -%}
|
||||
</div>
|
||||
</footer>
|
||||
{% endblock footer %}
|
17
templates/tags/list.html
Normal file
@ -0,0 +1,17 @@
|
||||
{% extends "terminimal/templates/tags/list.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="post">
|
||||
<h1 class="post-title">Liste des tags</h1>
|
||||
|
||||
<ul>
|
||||
{% for term in terms %}
|
||||
<li class="tag-list">
|
||||
<a href="{{ term.permalink | safe }}">
|
||||
{{ term.name }} ({{ term.pages | length }} post{{ term.pages | length | pluralize }})
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock content %}
|
17
templates/tags/single.html
Normal file
@ -0,0 +1,17 @@
|
||||
{% extends "terminimal/templates/tags/single.html" %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="post">
|
||||
<h1 class="post-title">
|
||||
tag: #{{ term.name }}
|
||||
({{ term.pages | length }} post{{ term.pages | length | pluralize }})
|
||||
</h1>
|
||||
|
||||
<a href="{{ config.base_url | safe }}/tags">
|
||||
Voir tous les tags
|
||||
</a>
|
||||
|
||||
{{ post_macros::list_posts(pages=term.pages) }}
|
||||
</div>
|
||||
{% endblock content %}
|
1
themes/terminimal
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 0ced77898f37eb388181c4bdfa564febe437841e
|