Marquage des liens considérés comme définitivement morts
This commit is contained in:
@@ -17,7 +17,7 @@ Je suis un utilisateur de Raspberry Pi depuis longtemps. Je possède au moins un
|
||||
|
||||
Pour commencer, il n'existe pas de version 64bits stable du système d'exploitation "officiel", Raspberry Pi OS. Une version "bêta" est en développement depuis au moins le 28 mai 2020. Je trouve ça plutôt embêtant parce que :
|
||||
|
||||
- Le Raspberry Pi 3 dispose d'un processeur 64bits [depuis 2016](https://www.raspberrypi.org/interets/informatique/raspberry-pi-3-on-sale/)
|
||||
- Le Raspberry Pi 3 dispose d'un processeur 64bits [depuis 2016](https://www.raspberrypi.com/news/raspberry-pi-3-on-sale/)
|
||||
|
||||
L'architecture ARM 64bits existe [depuis 2011](https://en.wikipedia.org/wiki/ARM_architecture#64/32-bit_architecture). Toutefois, l'absence de système 64bits officiellement supporté par la fondation Raspberry Pi à l'heure actuelle n'est pas entièrement de sa faute : l'architecture ARMv8 n'est disponible "que" [depuis 2019](https://www.debian.org/releases/buster/) sous Debian (distribution sur laquelle repose Raspberry Pi OS).
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ Ma Freebox est une [Pop](https://www.free.fr/freebox/freebox-pop/) configurée e
|
||||
|
||||

|
||||
|
||||
Mon routeur est un [AWOW AK34](https://awowtech.com/products/awow-mini-pc-ak34). Il dispose d’un Celeron N3450, 6Go de DDR4, un SSD de 128Go et surtout, deux ports gigabit. Il est installé sous [OpnSense](https://opnsense.org/).
|
||||
Mon routeur est un [AWOW AK34](https://awowtech.com/products/awow-mini-pc-ak34) [^deadlink-1]. Il dispose d’un Celeron N3450, 6Go de DDR4, un SSD de 128Go et surtout, deux ports gigabit. Il est installé sous [OpnSense](https://opnsense.org/).
|
||||
|
||||
L’un des ports est relié à la Freebox ; du point de vue de OpnSense, c’est l’interface _WAN_. L’autre port est relié au switch ; c’est l’interface _LAN_.
|
||||
|
||||
@@ -142,3 +142,5 @@ J’ai abandonné la souris pour un TrackPad.
|
||||
## Consommation électrique
|
||||
|
||||
Je dispose d’un onduleur Eaton Ellipse 1200 Pro qui présente la caractéristique intéressante de me fournir la consommation réelle de ce qui est branché dessus. Ainsi, je peux dire que l’ensemble de mon réseau consomment au total moins de 40W en moyenne. Je trouve que c’est très satisfaisant. L’onduleur pourrait ainsi fonctionner pendant deux heures sur batterie si une coupure de courant devait survenir. Largement de quoi voir venir…
|
||||
|
||||
[^deadlink-1]: Lien inaccessible depuis le 1 novembre 2025
|
||||
|
||||
@@ -70,7 +70,7 @@ Il n'est pas encore disponible à la vente à l'heure où j'écris ces lignes.
|
||||
|
||||
## Kemove Snowfox/Shadow
|
||||
|
||||
- [Site officiel](https://www.kmovetech.com/kemove-61-key-white-p0023-p0075.html)
|
||||
- [Site officiel](https://www.kmovetech.com/kemove-61-key-white-p0023-p0075.html) [^deadlink-1]
|
||||
- [x] 60%
|
||||
- [x] switches remplaçables
|
||||
- [x] câble USB-c remplaçable
|
||||
@@ -109,3 +109,5 @@ Bon par contre le full custom commence à chiffrer : on serait pas loin de 300
|
||||
Mais il s'agirait d'un clavier que je conserverai longtemps, très longtemps, et
|
||||
avec un confort et une qualité de frappe personnalisés que je ne retrouverai
|
||||
probablement pas sur des claviers pré-conçus...
|
||||
|
||||
[^deadlink-1]: Lien inaccessible depuis le 1 novembre 2025
|
||||
|
||||
@@ -91,7 +91,7 @@ On utilisera un pinceau adapté (donc de petite taille, 0 et/ou 1), tel que celu
|
||||
proposé par Glorious.
|
||||
|
||||
Si vous envisagez de devoir lubrifier _tous_ les switches de votre futur
|
||||
clavier, une [station de lubrification](https://keygem.store/collections/tools/products/kbdfans-switch-lube-station)
|
||||
clavier, une [station de lubrification](https://keygem.store/collections/tools/products/kbdfans-switch-lube-station) [^deadlink-1]
|
||||
peut être pratique.
|
||||
|
||||
Selon les switches et le degré de personnalisation visés, on pourra également
|
||||
@@ -156,13 +156,13 @@ En plastique ou nylon, destinées à isoler les têtes de vis du PCB. On peut au
|
||||
trouver des [manchons en silicone](https://kbdfans.com/products/kbdfans-standoff-silicone-cover)
|
||||
à placer sur les pas de vis.
|
||||
|
||||
- [Patins en silicone](https://keygem.store/products/replacement-feet-bumpers-4pcs?_pos=1&_sid=2b0f82876&_ss=r) à placer sous le clavier dans le cas où le boitier n'en serait pas déjà équippé - on en trouve aussi des [longs](https://mykeyboard.eu/catalogue/crin-extra-custom-feet_5417/) qui sont plus esthétiques et qualitatifs et qu'on pourra également utiliser sur un repose-poignets.
|
||||
- [Patins en silicone](https://keygem.store/products/replacement-feet-bumpers-4pcs?_pos=1&_sid=2b0f82876&_ss=r) à placer sous le clavier dans le cas où le boitier n'en serait pas déjà équippé - on en trouve aussi des [longs](https://mykeyboard.eu/catalogue/crin-extra-custom-feet_5417/) [^deadlink-2] qui sont plus esthétiques et qualitatifs et qu'on pourra également utiliser sur un repose-poignets.
|
||||
|
||||
- Mousse "modulaire"
|
||||
|
||||
À placer entre le PCB et la plaque supérieure. On peut aussi utiliser une mousse
|
||||
pré-conçue pour le PCB et le layout choisi (par exemple
|
||||
[celle-ci](https://kbdfans.com/products/dz60rgb-ansi-pcb-foam)
|
||||
[celle-ci](https://kbdfans.com/products/dz60rgb-ansi-pcb-foam) [^deadlink-3]
|
||||
pour le DZ60 RGB ANSI)).
|
||||
|
||||
## Le thème du clavier
|
||||
@@ -275,7 +275,7 @@ diffusant mieux la lumière, ou que sais-je encore (mais je note que certains on
|
||||
une préférence pour les switches qui cliquent "à l'aller et au retour", tandis
|
||||
que je préfère les switches à clic simple - dits "jacket").
|
||||
|
||||
- [Stabilisateurs](https://kbdfans.com/collections/keyboard-stabilizer/products/gmk-screw-in-stabilizers?variant=22154915348528)
|
||||
- [Stabilisateurs](https://kbdfans.com/collections/keyboard-stabilizer/products/gmk-screw-in-stabilizers?variant=22154915348528) [^deadlink-4]
|
||||
|
||||
Plus qu'un simple mod, c'est un composant essentiel qui assure la verticalité
|
||||
d'une pression sur une touche longue, peu importe d'où la touche est appuyée. En
|
||||
@@ -304,7 +304,7 @@ Bon sang, que j'ai hâte 😍
|
||||
|
||||
- Mousse pour le boitier - elle sert à réduire l'écho à l'intérieur du clavier
|
||||
|
||||
- [Repose-poignets en résine](https://kbdfans.com/collections/wrist-rest/products/handmade-resin-wrist-rest-1?variant=39444177223819) - modèle "Tropical Sea", magnifique et approprié
|
||||
- [Repose-poignets en résine](https://kbdfans.com/collections/wrist-rest/products/handmade-resin-wrist-rest-1?variant=39444177223819) [^deadlink-5] - modèle "Tropical Sea", magnifique et approprié
|
||||
|
||||

|
||||
|
||||
@@ -363,3 +363,13 @@ volatiles.
|
||||
## Budget
|
||||
|
||||
> "J'ai dépensé sans compter !" --- John Hammond, _Jurassic Park_, 1993
|
||||
|
||||
[^deadlink-1]: Lien inaccessible depuis le 1 novembre 2025
|
||||
|
||||
[^deadlink-2]: Lien inaccessible depuis le 1 novembre 2025
|
||||
|
||||
[^deadlink-3]: Lien inaccessible depuis le 1 novembre 2025
|
||||
|
||||
[^deadlink-4]: Lien inaccessible depuis le 1 novembre 2025
|
||||
|
||||
[^deadlink-5]: Lien inaccessible depuis le 1 novembre 2025
|
||||
|
||||
@@ -46,7 +46,7 @@ Il n’est pas fait pour être redistribué : n’espérez pas télécharger les
|
||||
Déjà parce que les sources de mes propres articles sont incluses[^sources_incluses], et puis parce que je l’ai développé pour répondre à mes besoins et envies spécifiques.
|
||||
Par contre, j’encourage la lecture du code : vous y trouverez peut-être une bonne idée ou deux.
|
||||
|
||||
[^sources_incluses]: Ce n’est plus le cas depuis le 6 septembre : [~~sources des articles~~](https://git.dern.ovh/Blog/contenu)
|
||||
[^sources_incluses]: Ce n’est plus le cas depuis le 6 septembre : [~~sources des articles~~](https://git.dern.ovh/Blog/contenu) [^deadlink-1]
|
||||
|
||||
Le moteur repose sur [Laravel](https://laravel.com/) dans sa dernière version.
|
||||
La partie UI/UX, c’est moi, vraiment à 100%.
|
||||
@@ -177,3 +177,5 @@ J’ai évidemment un peu de polissage à faire, et une tonne de choses dans ma
|
||||
Il y a de la marge pour l’améliorer, comme le veut l’expression consacrée, mais je peux déjà en faire plus qu’après deux ans d’expérience avec Hugo.
|
||||
|
||||
Maintenant, je me sens un peu plus chez moi.
|
||||
|
||||
[^deadlink-1]: Lien inaccessible depuis le 1 novembre 2025
|
||||
|
||||
@@ -13,7 +13,7 @@ Ainsi, des initiatives de science participative ont vu le jour, et les amateurs
|
||||
|
||||
Citons notamment :
|
||||
|
||||
- [Science Ensemble](https://www.science-ensemble.org/projets)
|
||||
- [Science Ensemble](https://www.science-ensemble.org/projets) [^deadlink-1]
|
||||
- [Le portail du Muséum d'Histoire Naturelle de Paris](https://www.mnhn.fr/fr/participer-a-la-science)
|
||||
- [Le portail de l'INRAE](https://science-ouverte.inrae.fr/fr/sciences-et-recherches-participatives-srp)
|
||||
|
||||
@@ -86,3 +86,5 @@ Mais, avec un peu de rigueur (scientifique, évidement), du matériel (comme les
|
||||
Car le doute est non seulement au coeur de l’expérience scientifique, il est aussi le lien social entre l’amateur et le professionnel, et c’est de ce lien dont a besoin la microscopie pour intégrer les rangs des sciences participatives.
|
||||
|
||||
Alors, à vos microscopes !
|
||||
|
||||
[^deadlink-1]: Lien inaccessible depuis le 1 novembre 2025
|
||||
|
||||
@@ -52,7 +52,7 @@ Parmi elles, on peut citer les collaborations entre amateurs et institutions aca
|
||||
- **[Vigie-Nature](https://fr.wikipedia.org/wiki/Vigie-Nature)** : Un programme de sciences participatives où les bénévoles collectent des données sur la faune et la flore françaises pour aider à la conservation de la biodiversité.
|
||||
- **[Atlasea](https://www.atlasea.fr/)** : Un projet collaboratif piloté par le CNRS et le CEA visant à constituer un atlas des génomes marins avec la participation de biologistes et de plongeurs amateurs.
|
||||
- **[Cultures Visuelles](https://savoirs.unistra.fr/innovation/la-plateforme-cultures-visuelles-une-initiative-engagee-pour-lexploration-et-la-collaboration-dans-le-domaine-des-images)** : Une plateforme de l’Université de Strasbourg favorisant l’échange de connaissances entre chercheurs et amateurs sur l’analyse des images et des arts visuels.
|
||||
- **[ANR Collabora](https://www.participarc.net/projets/anr-collabora-plateformes-contributives-culturelles)** : Un projet financé par l’Agence Nationale de la Recherche visant à créer un réseau de réflexion et de collaboration entre institutions culturelles, chercheurs et amateurs.
|
||||
- **[ANR Collabora](https://www.participarc.net/projets/anr-collabora-plateformes-contributives-culturelles)** [^deadlink-1] : Un projet financé par l’Agence Nationale de la Recherche visant à créer un réseau de réflexion et de collaboration entre institutions culturelles, chercheurs et amateurs.
|
||||
- [Wikipédia](https://fr.wikipedia.org/wiki/Wikipédia:Accueil_principal), où, régulièrement, des appels à projet sont émis, avec l'avantage de la visibilité : sur sa page d'accueil, où tout le monde peut les voir
|
||||
|
||||
Ces exemples montrent que l’intégration des amateurs dans la recherche scientifique est non seulement possible, mais aussi bénéfique pour l’avancement des connaissances.
|
||||
@@ -142,3 +142,5 @@ Il ne s'agit pas de permettre à des amateurs d'utiliser le [LHC](https://fr.wik
|
||||
D’ici là, la seule certitude est que l’exploration continue.
|
||||
|
||||
[^1]: Je ne me considère ni comme un amateur éclairé (mais j'y aspire), ni comme détenteur d'une idée révolutionnaire.
|
||||
|
||||
[^deadlink-1]: Lien inaccessible depuis le 1 novembre 2025
|
||||
|
||||
@@ -110,7 +110,7 @@ J.-C., et rappelées notamment par le rapport _Meadows_, _Les Limites à la croi
|
||||
malthusienne survenue chez l'Homme.
|
||||
|
||||
[^aristote_politique_1824]: Aristote, La Politique, trad. Jean-François Thurot (Paris : Didot, 1824). <https://fr.wikisource.org/wiki/La_Politique/Traduction_Jean-François_Thurot/Texte_entier>
|
||||
[^meadows_limits_nodate]: Donella H. Meadows et al. « The Limits to Growth: A Report for the Club of Rome’s Project on the Predicament of Mankind », _Issuu_ consulté le 25 janvier 2021. <https://issuu.com/dartmouth_college_library/docs/the_limits_to_growth/1>
|
||||
[^meadows_limits_nodate]: Donella H. Meadows et al. « The Limits to Growth: A Report for the Club of Rome’s Project on the Predicament of Mankind », _Issuu_ consulté le 25 janvier 2021. <https://issuu.com/dartmouth_college_library/docs/the_limits_to_growth/1>[^deadlink-1]
|
||||
|
||||
Notre expansion sans limites a fini par nous isoler dans nos cultures
|
||||
respectives: peuplant un monde trop vaste pour maintenir des échanges sociaux,
|
||||
@@ -138,3 +138,5 @@ Terre : nous sommes ses invités, comme le sont les Animaux.
|
||||
Nous avons encore un long chemin à parcourir pour prétendre être réellement
|
||||
évolués, à commencer par notre unification qui ne pourra passer que par une
|
||||
évolution de nos communications et des aspects sociaux qu'elles impliquent.
|
||||
|
||||
[^deadlink-1]: Lien inaccessible depuis le 1 novembre 2025
|
||||
|
||||
450
tools/mark_dead_links.js
Normal file
450
tools/mark_dead_links.js
Normal file
@@ -0,0 +1,450 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const yaml = require("js-yaml");
|
||||
|
||||
const SITE_ROOT = path.resolve(__dirname, "..");
|
||||
const CONFIG_PATH = path.join(__dirname, "config.json");
|
||||
|
||||
function loadConfig() {
|
||||
if (!fs.existsSync(CONFIG_PATH)) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`Impossible de parser ${path.relative(SITE_ROOT, CONFIG_PATH)} (${error.message}).`
|
||||
);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
const config = loadConfig();
|
||||
const externalConfig = {
|
||||
cacheDir: path.join(__dirname, "cache"),
|
||||
cacheFile: "external_links.yaml",
|
||||
...(config.externalLinks || {}),
|
||||
};
|
||||
|
||||
const CACHE_DIR = path.isAbsolute(externalConfig.cacheDir)
|
||||
? externalConfig.cacheDir
|
||||
: path.resolve(SITE_ROOT, externalConfig.cacheDir);
|
||||
const CACHE_PATH = path.isAbsolute(externalConfig.cacheFile)
|
||||
? externalConfig.cacheFile
|
||||
: path.join(CACHE_DIR, externalConfig.cacheFile);
|
||||
|
||||
function loadCache(cachePath) {
|
||||
if (!fs.existsSync(cachePath)) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
return yaml.load(fs.readFileSync(cachePath, "utf8")) || {};
|
||||
} catch (error) {
|
||||
console.error(`Erreur lors de la lecture du cache YAML (${error.message}).`);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function getCheckedDate(info) {
|
||||
if (info && typeof info.checked === "string") {
|
||||
const parsed = new Date(info.checked);
|
||||
if (!Number.isNaN(parsed.valueOf())) {
|
||||
return parsed.toISOString();
|
||||
}
|
||||
}
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function getStatusCode(info) {
|
||||
if (info && typeof info.status === "number") {
|
||||
return info.status;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const frenchDateFormatter = new Intl.DateTimeFormat("fr-FR", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
function formatDisplayDate(isoString) {
|
||||
if (typeof isoString === "string") {
|
||||
const parsed = new Date(isoString);
|
||||
if (!Number.isNaN(parsed.valueOf())) {
|
||||
return frenchDateFormatter.format(parsed);
|
||||
}
|
||||
}
|
||||
return frenchDateFormatter.format(new Date());
|
||||
}
|
||||
|
||||
function getFilesForUrl(info) {
|
||||
if (!info) return [];
|
||||
if (Array.isArray(info.files) && info.files.length > 0) {
|
||||
return info.files;
|
||||
}
|
||||
if (Array.isArray(info.locations) && info.locations.length > 0) {
|
||||
return Array.from(new Set(info.locations.map((entry) => String(entry).split(":")[0])));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function splitFrontmatter(content) {
|
||||
if (!content.startsWith("---")) {
|
||||
return null;
|
||||
}
|
||||
const match = content.match(/^---\n([\s\S]*?)\n---\n?/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const frontmatterText = match[1];
|
||||
let frontmatter = {};
|
||||
try {
|
||||
frontmatter = yaml.load(frontmatterText) || {};
|
||||
} catch (error) {
|
||||
console.error(`Frontmatter YAML invalide (${error.message}).`);
|
||||
return null;
|
||||
}
|
||||
const block = match[0];
|
||||
const body = content.slice(block.length);
|
||||
return { frontmatter, block, body };
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function ensureTrailingNewline(value) {
|
||||
if (!value.endsWith("\n")) {
|
||||
return `${value}\n`;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function ensureBlankLineBeforeAppend(body) {
|
||||
if (body.endsWith("\n\n")) {
|
||||
return body;
|
||||
}
|
||||
if (body.endsWith("\n")) {
|
||||
return `${body}\n`;
|
||||
}
|
||||
return `${body}\n\n`;
|
||||
}
|
||||
|
||||
function markInterestingLink(filePath, url, info) {
|
||||
const original = fs.readFileSync(filePath, "utf8");
|
||||
const parsed = splitFrontmatter(original);
|
||||
if (!parsed) {
|
||||
console.warn(`Frontmatter introuvable pour ${path.relative(SITE_ROOT, filePath)}, ignoré.`);
|
||||
return { changed: false };
|
||||
}
|
||||
|
||||
const { frontmatter } = parsed;
|
||||
let body = parsed.body;
|
||||
const checkedDate = getCheckedDate(info);
|
||||
const displayDate = formatDisplayDate(checkedDate);
|
||||
const httpCode = getStatusCode(info);
|
||||
let changed = false;
|
||||
|
||||
if (typeof frontmatter.title === "string" && !frontmatter.title.startsWith("[Lien mort]")) {
|
||||
frontmatter.title = `[Lien mort] ${frontmatter.title}`;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
let statusEntries = [];
|
||||
if (Array.isArray(frontmatter.status)) {
|
||||
statusEntries = [...frontmatter.status];
|
||||
}
|
||||
|
||||
let statusEntry = statusEntries.find(
|
||||
(entry) => entry && typeof entry === "object" && entry.date === checkedDate
|
||||
);
|
||||
if (!statusEntry) {
|
||||
statusEntry = { date: checkedDate, http_code: httpCode };
|
||||
statusEntries.push(statusEntry);
|
||||
changed = true;
|
||||
} else if (statusEntry.http_code !== httpCode) {
|
||||
statusEntry.http_code = httpCode;
|
||||
changed = true;
|
||||
}
|
||||
frontmatter.status = statusEntries;
|
||||
|
||||
const noteLine = `> Lien inaccessible depuis le ${displayDate}`;
|
||||
const noteRegex = /(>\s*Lien inaccessible depuis le\s+)([^\n]+)/;
|
||||
const existing = body.match(noteRegex);
|
||||
if (existing) {
|
||||
const current = existing[2].trim();
|
||||
if (current !== displayDate) {
|
||||
body = body.replace(noteRegex, `> Lien inaccessible depuis le ${displayDate}`);
|
||||
changed = true;
|
||||
}
|
||||
} else {
|
||||
body = ensureBlankLineBeforeAppend(body);
|
||||
body += `${noteLine}\n`;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return { changed: false };
|
||||
}
|
||||
|
||||
const newFrontmatter = yaml.dump(frontmatter);
|
||||
const updatedContent = `---\n${newFrontmatter}---\n${body}`;
|
||||
if (updatedContent === original) {
|
||||
return { changed: false };
|
||||
}
|
||||
fs.writeFileSync(filePath, updatedContent, "utf8");
|
||||
return { changed: true };
|
||||
}
|
||||
|
||||
function collectDeadlinkMaxId(body) {
|
||||
let maxId = 0;
|
||||
const regex = /\[\^deadlink-(\d+)\]/g;
|
||||
let match;
|
||||
while ((match = regex.exec(body)) !== null) {
|
||||
const value = parseInt(match[1], 10);
|
||||
if (Number.isInteger(value) && value > maxId) {
|
||||
maxId = value;
|
||||
}
|
||||
}
|
||||
return maxId;
|
||||
}
|
||||
|
||||
function findExistingDeadlinkReference(line, url) {
|
||||
if (!line.includes(url)) return null;
|
||||
const escapedUrl = escapeRegExp(url);
|
||||
const markdownRegex = new RegExp(`\\[[^\\]]*\\]\\(${escapedUrl}\\)`);
|
||||
const angleRegex = new RegExp(`<${escapedUrl}>`);
|
||||
|
||||
let referenceId = null;
|
||||
|
||||
const searchers = [
|
||||
{ regex: markdownRegex },
|
||||
{ regex: angleRegex },
|
||||
];
|
||||
|
||||
for (const { regex } of searchers) {
|
||||
const match = regex.exec(line);
|
||||
if (!match) continue;
|
||||
const start = match.index;
|
||||
const end = start + match[0].length;
|
||||
const tail = line.slice(end);
|
||||
const footnoteMatch = tail.match(/^([\s)*_~`]*?)\[\^deadlink-(\d+)\]/);
|
||||
if (footnoteMatch) {
|
||||
referenceId = `deadlink-${footnoteMatch[2]}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return referenceId;
|
||||
}
|
||||
|
||||
function insertDeadlinkReference(line, url, nextId) {
|
||||
const escapedUrl = escapeRegExp(url);
|
||||
const markdownRegex = new RegExp(`\\[[^\\]]*\\]\\(${escapedUrl}\\)`);
|
||||
const angleRegex = new RegExp(`<${escapedUrl}>`);
|
||||
|
||||
const footnoteRef = `[^deadlink-${nextId}]`;
|
||||
|
||||
const markdownMatch = markdownRegex.exec(line);
|
||||
if (markdownMatch) {
|
||||
const end = markdownMatch.index + markdownMatch[0].length;
|
||||
let insertPos = end;
|
||||
while (insertPos < line.length && /[*_]/.test(line[insertPos])) {
|
||||
insertPos += 1;
|
||||
}
|
||||
return line.slice(0, insertPos) + ' ' + footnoteRef + line.slice(insertPos);
|
||||
}
|
||||
|
||||
const angleMatch = angleRegex.exec(line);
|
||||
if (angleMatch) {
|
||||
const end = angleMatch.index + angleMatch[0].length;
|
||||
return line.slice(0, end) + footnoteRef + line.slice(end);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function upsertFootnoteDefinition(body, footnoteId, isoDate) {
|
||||
const displayDate = formatDisplayDate(isoDate);
|
||||
const desired = `Lien inaccessible depuis le ${displayDate}`;
|
||||
const definitionRegex = new RegExp(`^\\[\\^${footnoteId}\\]:\\s*(.+)$`, "m");
|
||||
const match = definitionRegex.exec(body);
|
||||
if (match) {
|
||||
if (match[1].trim() !== desired) {
|
||||
return {
|
||||
body: body.replace(definitionRegex, `[^${footnoteId}]: ${desired}`),
|
||||
changed: true,
|
||||
};
|
||||
}
|
||||
return { body, changed: false };
|
||||
}
|
||||
let updated = ensureTrailingNewline(body);
|
||||
updated = ensureBlankLineBeforeAppend(updated);
|
||||
updated += `[^${footnoteId}]: ${desired}\n`;
|
||||
return { body: updated, changed: true };
|
||||
}
|
||||
|
||||
function markMarkdownLink(filePath, url, info) {
|
||||
const original = fs.readFileSync(filePath, "utf8");
|
||||
const parsed = splitFrontmatter(original);
|
||||
const hasFrontmatter = Boolean(parsed);
|
||||
const block = parsed?.block ?? "";
|
||||
const bodyOriginal = parsed ? parsed.body : original;
|
||||
|
||||
const lines = bodyOriginal.split("\n");
|
||||
let inFence = false;
|
||||
let fenceChar = null;
|
||||
let referenceId = null;
|
||||
let changed = false;
|
||||
let maxId = collectDeadlinkMaxId(bodyOriginal);
|
||||
|
||||
for (let i = 0; i < lines.length; i += 1) {
|
||||
const line = lines[i];
|
||||
|
||||
const trimmed = line.trimStart();
|
||||
const fenceMatch = trimmed.match(/^([`~]{3,})/);
|
||||
if (fenceMatch) {
|
||||
const currentFenceChar = fenceMatch[1][0];
|
||||
if (!inFence) {
|
||||
inFence = true;
|
||||
fenceChar = currentFenceChar;
|
||||
continue;
|
||||
}
|
||||
if (fenceChar === currentFenceChar) {
|
||||
inFence = false;
|
||||
fenceChar = null;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (inFence) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!line.includes(url)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existingRef = findExistingDeadlinkReference(line, url);
|
||||
if (existingRef) {
|
||||
referenceId = existingRef;
|
||||
break;
|
||||
}
|
||||
|
||||
const nextId = maxId + 1;
|
||||
const updatedLine = insertDeadlinkReference(line, url, nextId);
|
||||
if (updatedLine) {
|
||||
lines[i] = updatedLine;
|
||||
referenceId = `deadlink-${nextId}`;
|
||||
maxId = nextId;
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!referenceId) {
|
||||
return { changed: false };
|
||||
}
|
||||
|
||||
let body = lines.join("\n");
|
||||
const { body: updatedBody, changed: definitionChanged } = upsertFootnoteDefinition(
|
||||
body,
|
||||
referenceId,
|
||||
getCheckedDate(info)
|
||||
);
|
||||
|
||||
body = updatedBody;
|
||||
if (definitionChanged) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return { changed: false };
|
||||
}
|
||||
|
||||
const updatedContent = hasFrontmatter ? `${block}${body}` : body;
|
||||
if (updatedContent === original) {
|
||||
return { changed: false };
|
||||
}
|
||||
fs.writeFileSync(filePath, updatedContent, "utf8");
|
||||
return { changed: true };
|
||||
}
|
||||
|
||||
function processFile(absolutePath, url, info) {
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
console.warn(`Fichier introuvable: ${absolutePath}`);
|
||||
return { changed: false };
|
||||
}
|
||||
const relative = path.relative(SITE_ROOT, absolutePath);
|
||||
if (relative.startsWith("content/interets/liens-interessants/")) {
|
||||
return markInterestingLink(absolutePath, url, info);
|
||||
}
|
||||
if (path.extname(relative).toLowerCase() === ".md") {
|
||||
return markMarkdownLink(absolutePath, url, info);
|
||||
}
|
||||
return { changed: false };
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (!fs.existsSync(CACHE_PATH)) {
|
||||
console.error("Cache introuvable. Exécutez d'abord tools/check_external_links.js.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const cache = loadCache(CACHE_PATH);
|
||||
const entries = Object.entries(cache).filter(
|
||||
([, info]) => info && info.manually_killed === true
|
||||
);
|
||||
|
||||
if (entries.length === 0) {
|
||||
console.log("Aucun lien marqué comme mort manuellement dans le cache.");
|
||||
return;
|
||||
}
|
||||
|
||||
let updates = 0;
|
||||
let warnings = 0;
|
||||
|
||||
for (const [url, info] of entries) {
|
||||
const files = getFilesForUrl(info);
|
||||
if (files.length === 0) {
|
||||
console.warn(`Aucun fichier associé à ${url}.`);
|
||||
warnings += 1;
|
||||
continue;
|
||||
}
|
||||
for (const relativePath of files) {
|
||||
const absolutePath = path.isAbsolute(relativePath)
|
||||
? relativePath
|
||||
: path.resolve(SITE_ROOT, relativePath);
|
||||
try {
|
||||
const { changed } = processFile(absolutePath, url, info);
|
||||
if (changed) {
|
||||
updates += 1;
|
||||
console.log(
|
||||
`✅ ${path.relative(SITE_ROOT, absolutePath)} mis à jour pour ${url}`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
warnings += 1;
|
||||
console.error(
|
||||
`Erreur lors du traitement de ${path.relative(SITE_ROOT, absolutePath)} (${error.message}).`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (updates === 0) {
|
||||
console.log("Aucune modification nécessaire.");
|
||||
} else {
|
||||
console.log(`${updates} fichier(s) mis à jour.`);
|
||||
}
|
||||
|
||||
if (warnings > 0) {
|
||||
console.warn(`${warnings} fichier(s) n'ont pas pu être traités complètement.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
Reference in New Issue
Block a user