Two pure modules: statut-mairie.js (string[] -> string[] status logic) and ics-categories.js, which rewrites CATEGORIES in an iCalendar resource behind a structural guard: nothing is written if anything but CATEGORIES changed.
82 lines
3.0 KiB
JavaScript
82 lines
3.0 KiB
JavaScript
// Réécriture de CATEGORIES dans une ressource iCalendar, avec garde-fou de
|
|
// non-régression. PUR (ical.js tourne aussi dans Bun) : c'est ici que se joue
|
|
// l'intégrité de l'agenda d'une association, on le teste à fond.
|
|
//
|
|
// Règle du rayon d'explosion : si le round-trip ical.js a touché autre chose que
|
|
// CATEGORIES, on n'écrit PAS. Un tag non posé se repose ; un événement abîmé se
|
|
// répare à la main.
|
|
|
|
import ICAL from "./vendor/ical.js";
|
|
|
|
// `transformer` (obligatoire) : string[] -> string[]. Il est appliqué aux
|
|
// catégories FRAÎCHEMENT lues dans la ressource, jamais à celles de la liste
|
|
// (potentiellement périmées).
|
|
export function appliquerAuxCategories(ics, uid, transformer) {
|
|
const racine = parser(ics);
|
|
if (!racine) return { ok: false, erreur: "ics-illisible" };
|
|
|
|
// Le VEVENT maître = celui SANS RECURRENCE-ID, cohérent avec evenements.js
|
|
// qui ignore les exceptions d'occurrence.
|
|
const maitre = racine
|
|
.getAllSubcomponents("vevent")
|
|
.find((v) => v.getFirstPropertyValue("uid") === uid && !v.hasProperty("recurrence-id"));
|
|
if (!maitre) return { ok: false, erreur: "vevent-introuvable" };
|
|
|
|
const categories = transformer(lireCategories(maitre));
|
|
|
|
// Une seule propriété multi-valeurs : les formes répétées sont fusionnées.
|
|
maitre.removeAllProperties("categories");
|
|
if (categories.length > 0) {
|
|
const propriete = new ICAL.Property("categories");
|
|
propriete.setValues(categories);
|
|
maitre.addProperty(propriete);
|
|
}
|
|
|
|
const apres = racine.toString();
|
|
if (!seulesLesCategoriesOntChange(ics, apres)) {
|
|
return { ok: false, erreur: "reecriture-risquee" };
|
|
}
|
|
return { ok: true, ics: apres, categories };
|
|
}
|
|
|
|
// Garde-fou E3 : comparaison STRUCTURELLE (jCal), pas textuelle. Le repliement
|
|
// de ligne et l'espacement peuvent changer librement ; une valeur, non.
|
|
export function seulesLesCategoriesOntChange(icsAvant, icsApres) {
|
|
const avant = parser(icsAvant);
|
|
const apres = parser(icsApres);
|
|
if (!avant || !apres) return false;
|
|
|
|
return (
|
|
JSON.stringify(sansCategories(avant.jCal)) ===
|
|
JSON.stringify(sansCategories(apres.jCal))
|
|
);
|
|
}
|
|
|
|
function parser(ics) {
|
|
try {
|
|
const racine = new ICAL.Component(ICAL.parse(ics));
|
|
// `ICAL.parse("")` ne LÈVE PAS : il rend `[]`, et le composant vide qui en
|
|
// sort explose au premier accès. Le try/catch ne suffit donc pas — on exige
|
|
// un VCALENDAR reconnaissable avant de rendre la main.
|
|
return racine.name === "vcalendar" ? racine : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// jCal : [nom, propriétés, sous-composants]. On retire `categories` PARTOUT,
|
|
// y compris dans les exceptions d'occurrence.
|
|
function sansCategories([nom, proprietes, sousComposants]) {
|
|
return [
|
|
nom,
|
|
proprietes.filter(([nomPropriete]) => nomPropriete !== "categories"),
|
|
sousComposants.map(sansCategories),
|
|
];
|
|
}
|
|
|
|
// CATEGORIES peut prendre deux formes (virgules et/ou propriétés répétées),
|
|
// comme dans evenements.js.
|
|
function lireCategories(vevent) {
|
|
return vevent.getAllProperties("categories").flatMap((propriete) => propriete.getValues());
|
|
}
|