feat: derive event publication status from the mairie agenda

Pure matching (date locale Cugnaux + titre normalisé) and paginated
transport, both fully tested. HTML extraction via DOMParser stays impure
and untested (absent from Bun), like formulaire-mairie.js.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Pierre Martin
2026-07-17 18:18:15 +02:00
co-authored by Claude Opus 4.8
parent a7b780876b
commit 6c4f4219f1
4 changed files with 424 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
// Matching événement Nextcloud ↔ carte de l'agenda mairie. Fonctions PURES,
// sans DOM (testables) : c'est le cœur du risque produit (précision du match,
// fuseau, normalisation du titre). Biais PRÉCISION v1 : dans le doute, on
// n'affirme pas « publié » (faux négatif bénin, cf. plan / DECISIONS.md).
const FUSEAU = "Europe/Paris";
// en-CA rend directement "YYYY-MM-DD" : même forme que le <time datetime> de
// l'agenda, comparable sans reformatage.
const FORMAT_JOUR_PARIS = new Intl.DateTimeFormat("en-CA", {
timeZone: FUSEAU,
year: "numeric",
month: "2-digit",
day: "2-digit",
});
// Forme stable d'un titre : ce qui doit matcher malgré les écarts cosmétiques
// entre le calendrier et la mairie. On RETIRE la ponctuation (sans la remplacer
// par une espace) pour que « Atelier B.D. » == « Atelier BD ».
export function normaliserTitre(titre) {
return titre
.normalize("NFD")
.replace(/[̀-ͯ]/gu, "") // diacritiques isolés par NFD
.replace(/[^\p{Letter}\p{Number}\s]/gu, "") // ponctuation ET emojis
.toLowerCase()
.replace(/\s+/gu, " ")
.trim();
}
// Le <time datetime> de l'agenda est une date NUE (pas d'heure) : on la compare
// à la date locale de Cugnaux du début, jamais à une conversion de fuseau.
export function dateLocaleCugnaux(evenement) {
return evenement.journeeEntiere
? formaterDateFlottante(evenement.debut)
: FORMAT_JOUR_PARIS.format(evenement.debut);
}
// Journée entière = date flottante (RFC 5545) : composantes LOCALES, jamais un
// fuseau, sinon la date glisse d'un jour (cf. presentation.js / DECISIONS.md).
function formaterDateFlottante(date) {
const annee = date.getFullYear();
const mois = String(date.getMonth() + 1).padStart(2, "0");
const jour = String(date.getDate()).padStart(2, "0");
return `${annee}-${mois}-${jour}`;
}
// Index des cartes par `date|titre normalisé` → lien. Deux cartes de même clé
// (même jour + même titre normalisé, rare) : la DERNIÈRE indexée l'emporte.
export function indexerCartes(cartes) {
const index = new Map();
for (const carte of cartes) {
index.set(cle(carte.date, carte.titre), carte.lien);
}
return index;
}
// Match STRICT : même date locale ET titre normalisé identique. Rien d'autre ne
// vaut « publié » en v1.
export function trouverPublication(evenement, index) {
const lien = index.get(cle(dateLocaleCugnaux(evenement), evenement.titre));
return lien ? { publie: true, lien } : { publie: false };
}
function cle(date, titre) {
return `${date}|${normaliserTitre(titre)}`;
}