Commit d0bb1436 by PLN (Algolia)

fmt: yarn fmt — black-like French typography autofix (fine insécable before !?;,…

fmt: yarn fmt — black-like French typography autofix (fine insécable before !?;, insécable before : and in « », frontmatter/code/links protected, idempotent) + applied to hello-world-again and labo
parent 7834c8ca
......@@ -2,11 +2,11 @@
title: Hello World (here we go again)
---
Salut, Monde! Tu as bien changé. Moi aussi <3
Salut, Monde ! Tu as bien changé. Moi aussi <3
----
Pourquoi écrire ?
Pourquoi écrire ?
Parce que lire ne suffit pas.
Parce que ce qui se conçoit bien est plutôt cool.
......@@ -15,7 +15,7 @@ toutes ces idées, atteriront bien quelque-part.
----
Pourquoi publier ?
Pourquoi publier ?
Parce que notre époque est celle d'une nouvelle alchimie.
Qu'il est bon de partager nos grimoires, nos Codex, d'inventer une nouvelle science, _ensemble_.
......@@ -23,13 +23,13 @@ Qu'il est bon de partager nos grimoires, nos Codex, d'inventer une nouvelle scie
----
Et puis pour l'art.
Pourquoi créer ?
Quel intérêt d'écrire un des milliards de textes du jour ?
De prendre une des millions de photos du jour ?
De créer une des centaines de milliers de musiques ?
De peindre un des milliers de tableaux ?
Pourquoi créer ?
Quel intérêt d'écrire un des milliards de textes du jour ?
De prendre une des millions de photos du jour ?
De créer une des centaines de milliers de musiques ?
De peindre un des milliers de tableaux ?
Est-ce la rareté d'une fleur, ou la durée de sa croissance, ou la force de son parfum, qui fait sa valeur ?
Est-ce la rareté d'une fleur, ou la durée de sa croissance, ou la force de son parfum, qui fait sa valeur ?
À quoi bon est le poison de l'âme.
Alors écrire.
......@@ -4,10 +4,10 @@ title: Banc d'essai — médias riches
> **Brouillon agentique.** Ce billet est un banc d'essai écrit par l'agent pour
> vérifier le pipeline — chaque bloc existe pour casser quelque chose. La prose
> sera réécrite à la main ; les blocs, eux, resteront.
> sera réécrite à la main ; les blocs, eux, resteront.
Un blog que l'on reconstruit soi-même finit toujours par écrire sur sa propre
reconstruction. Voici donc le bilan de chantier : ce que le moteur rend, et
reconstruction. Voici donc le bilan de chantier : ce que le moteur rend, et
comment. Tout est généré à la construction — la page livrée est du HTML plat,
sans JavaScript indispensable.[^gfm]
......@@ -15,7 +15,7 @@ sans JavaScript indispensable.[^gfm]
Le surlignage passe par Shiki (thème *vesper*) pendant le build. Ce que vous
lisez est donc du HTML avec des couleurs inline — zéro coloriseur côté
navigateur :
navigateur :
```ts
// lib/markdown.ts — la seule fabrique d'HTML de /blog
......@@ -45,42 +45,42 @@ partout ailleurs — sans une ligne de JavaScript. Le clone de marge est fabriqu
à la construction depuis la définition GFM, la note de fin reste la source de
vérité pour les lecteurs d'écran.[^aria]
[^gfm]: GitHub-Flavored Markdown, activé dans la chaîne via `remark-gfm`.
[^aria]: Les clones en marge sont `aria-hidden` ; la note de fin reste la
[^gfm] : GitHub-Flavored Markdown, activé dans la chaîne via `remark-gfm`.
[^aria] : Les clones en marge sont `aria-hidden` ; la note de fin reste la
version lisible par les lecteurs d'écran.
## Images et légendes
Un paragraphe réduit à une image devient une `<figure>` ; le texte alternatif
sert de légende. Cliché de l'algorave de Lyon, 2025 :
Un paragraphe réduit à une image devient une `<figure>` ; le texte alternatif
sert de légende. Cliché de l'algorave de Lyon, 2025 :
![Algorave de Lyon, 2025 — visuels Hydra, son TidalCycles](/images/blog/labo/poster.jpg)
 ![Algorave de Lyon, 2025 — visuels Hydra, son TidalCycles](/images/blog/labo/poster.jpg)
## Le son
Un petit accord de test, servi par le lecteur natif du navigateur (directive
`::audio`, zéro client JS) :
`::audio`, zéro client JS) :
::audio[/audio/labo/test-chord.mp3]{title="accord de test"}
 ::audio[/audio/labo/test-chord.mp3]{title="accord de test"}
## Les embeds qui ne chargent rien
Le principe : rien de distant ne charge avant un clic. La façade est
construite à la construction ; le lecteur — YouTube *nocookie*, SoundCloud —
Le principe : rien de distant ne charge avant un clic. La façade est
construite à la construction ; le lecteur — YouTube *nocookie*, SoundCloud —
n'arrive que si vous l'appelez.[^facade]
::youtube[https://www.youtube.com/watch?v=dQw4w9WgXcQ]{title="Vidéo de test (remplacer)"}
 ::youtube[https://www.youtube.com/watch?v=dQw4w9WgXcQ]{title="Vidéo de test (remplacer)"}
Et un set ParVagues, cette fois réel :
Et un set ParVagues, cette fois réel :
::soundcloud[https://soundcloud.com/parvagues/live-39c3]{title="ParVagues — live 39C3"}
 ::soundcloud[https://soundcloud.com/parvagues/live-39c3]{title="ParVagues — live 39C3"}
[^facade]: Seule exception : la vignette YouTube (une requête vers
[^facade] : Seule exception : la vignette YouTube (une requête vers
`i.ytimg.com`, sans cookie). SoundCloud ne touche à rien du tout.
## Tableaux et notes
L'état du labo, en tableau GFM :
L'état du labo, en tableau GFM :
| Média | Directive | JS client | Requête distante |
| ---------- | -------------- | --------- | ------------------- |
......@@ -90,14 +90,14 @@ L'état du labo, en tableau GFM :
| YouTube | `::youtube` | au clic | vignette + iframe |
| SoundCloud | `::soundcloud` | au clic | iframe au clic |
:::note[À retenir]
Tout ce qui est décoratif doit pouvoir disparaître : la page se lit
 : ::note[À retenir]
Tout ce qui est décoratif doit pouvoir disparaître : la page se lit
entièrement sans JavaScript, chaque amélioration n'est qu'une décoration du
HTML déjà livré.
:::
 : : :
C'est le contrat de cette refonte : le lecteur sans JS reçoit l'article
C'est le contrat de cette refonte : le lecteur sans JS reçoit l'article
complet, et chaque gadget n'arrive que pour les autres.[^contrat]
[^contrat]: Voir `ArticleEnhance.jsx` : une île invisible qui décore le DOM
[^contrat] : Voir `ArticleEnhance.jsx` : une île invisible qui décore le DOM
déjà servi — copie de code, lightbox, mémoire de langue, façades.
......@@ -17,7 +17,8 @@
"post:image": "node scripts/add-image.mjs",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"prose": "node scripts/prose.mjs"
"prose": "node scripts/prose.mjs",
"fmt": "node scripts/fmt.mjs"
},
"engines": {
"node": ">=18.17.0"
......
#!/usr/bin/env node
/**
* yarn fmt [slug|file…] — opinionated, black-like typography autofix for
* content/blog. No config on purpose: like black, the formatting is the
* tool's, the words stay the author's.
*
* French rules (imprimerie nationale):
* - narrow no-break space (U+202F) before ! ? ;
* - no-break space (U+00A0) before :
* - no-break space just inside « » quotes
* English files are left untouched — en punctuation pacing is plain ASCII.
*
* Never touched: YAML frontmatter (structured data), code fences, inline
* code, markdown link/image destinations, bare URLs. Idempotent by design —
* the insécables are not in the [ \t] class the rules consume, so running
* twice changes nothing.
*/
import fs from 'node:fs';
import path from 'node:path';
const BLOG_ROOT = path.resolve(process.cwd(), 'content/blog');
/** Narrow no-break space before ! ? ; — the espace fine insécable. */
const FINE = '\u202f';
/** No-break space before : and inside « » — the espace insécable. */
const NBSP = '\u00a0';
const LANG_OF = (file) => {
const base = path.basename(file);
if (base === 'fr.md') return 'fr';
if (base === 'en.md') return 'en';
return null;
};
/** Protected prose forms: fences, inline code, link/image targets, URLs. */
const PROTECTED_RE =
/(^```[\s\S]*?^```[^\S\n]*$|^~~~[\s\S]*?^~~~[^\S\n]*$|`[^`\n]+`|\]\([^)\s]*\)|https?:\/\/\S+)/gm;
/**
* Split text into protected/unprotected segments. The French rules only run
* on unprotected segments; protected ones rejoin byte-for-byte.
*/
function segment(text) {
const segs = [];
let last = 0;
for (const m of text.matchAll(PROTECTED_RE)) {
if (m.index > last) segs.push({ prot: false, text: text.slice(last, m.index) });
segs.push({ prot: true, text: m[0] });
last = m.index + m[0].length;
}
if (last < text.length) segs.push({ prot: false, text: text.slice(last) });
return segs;
}
const FRENCH_RULES = [
// « salut » — insécables just inside the guillemets, added when absent
[new RegExp(`«(?!\\u00a0)[ \\t]*`, 'g'), `«${NBSP}`],
// the lookbehinds keep an already-insécable punctuation untouched —
// without them each pass would prepend another space (growth, not idempotence)
[new RegExp(`(?<!\\u00a0)[ \\t]*»`, 'g'), `${NBSP}»`],
// espace fine insécable before ! ? ; (normalize a plain space or add one)
[new RegExp(`(?<![\\u00a0\\u202f])[ \\t]*([!?;])(?!\\w)`, 'g'), `${FINE}$1`],
// espace insécable before : — but not in 12:30, 3:2, or a scheme://
[new RegExp(`(?<![\\u00a0\\u202f])[ \\t]*:(?![/\\w\\d])`, 'g'), `${NBSP}:`],
];
function fmtFrench(text) {
// The YAML frontmatter block is structured data, not prose.
const fm = text.match(/^---\n[\s\S]*?\n---\n/);
const head = fm ? fm[0] : '';
const body = text.slice(head.length);
return (
head +
segment(body)
.map((seg) =>
seg.prot ? seg.text : FRENCH_RULES.reduce((t, [re, rep]) => t.replace(re, rep), seg.text),
)
.join('')
);
}
function fmtFile(file) {
const lang = LANG_OF(file);
if (!lang) return null;
const original = fs.readFileSync(file, 'utf8');
const fixed = lang === 'fr' ? fmtFrench(original) : original;
if (fixed !== original) {
fs.writeFileSync(file, fixed);
return path.relative(process.cwd(), file);
}
return null;
}
// --- CLI -------------------------------------------------------------------
const args = process.argv.slice(2);
let files = [];
if (args.length === 0) {
files = fs
.readdirSync(BLOG_ROOT, { recursive: true })
.map((f) => path.join(BLOG_ROOT, f))
.filter((f) => fs.statSync(f).isFile() && f.endsWith('.md'));
} else {
for (const arg of args) {
const p = path.resolve(process.cwd(), arg);
if (fs.statSync(p).isDirectory()) {
files.push(
...fs
.readdirSync(p)
.map((f) => path.join(p, f))
.filter((f) => f.endsWith('.md')),
);
} else {
files.push(p);
}
}
}
const changed = files.map(fmtFile).filter(Boolean);
console.log(changed.length === 0 ? 'fmt: clean' : `fmt: ${changed.length} file(s) fixed`);
for (const f of changed) console.log(` ${f}`);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment