// Parseur iCalendar : flux brut → Event[] au contrat figé (T1). // Fonction pure. `aujourdhui` est injecté (obligatoire) pour des tests // déterministes. Voir docs/DECISIONS.md pour le contrat et les cas différés à T3. // // Event = { // uid: string, titre: string, description: string, lieu: string, // debut: Date, fin: Date, categories: string[], journeeEntiere: boolean // } import ICAL from "./vendor/ical.js"; export function parserEvenements(flux, aujourdhui) { const composant = new ICAL.Component(ICAL.parse(flux)); // Enregistrer les fuseaux embarqués : sans cela toJSDate() calcule un // mauvais offset pour les TZID non standards (ex. Africa/Lagos). composant.getAllSubcomponents("vtimezone").forEach((vt) => { const tzid = vt.getFirstPropertyValue("tzid"); if (tzid && !ICAL.TimezoneService.has(tzid)) { ICAL.TimezoneService.register(vt); } }); // Dédoublonnage par UID. Les exceptions d'occurrence (RECURRENCE-ID) ne sont // pas développées en T1 : on les ignore et on garde le maître. const parUid = new Map(); for (const vevent of composant.getAllSubcomponents("vevent")) { const ev = new ICAL.Event(vevent); if (ev.isRecurrenceException()) continue; if (parUid.has(ev.uid)) continue; parUid.set(ev.uid, toEvent(ev, vevent)); } const seuil = new Date(aujourdhui); seuil.setHours(0, 0, 0, 0); // Ordre du flux conservé (tri différé à la liste). return [...parUid.values()].filter( (event) => event.debut.getTime() >= seuil.getTime(), ); } function toEvent(ev, vevent) { return { uid: ev.uid, titre: ev.summary ?? "", description: ev.description ?? "", lieu: ev.location ?? "", debut: ev.startDate.toJSDate(), fin: ev.endDate.toJSDate(), categories: lireCategories(vevent), journeeEntiere: ev.startDate.isDate === true, }; } // CATEGORIES peut prendre deux formes (valeurs séparées par virgule et/ou // propriétés répétées) : getAllProperties + getValues couvre les deux. function lireCategories(vevent) { return vevent .getAllProperties("categories") .flatMap((prop) => prop.getValues()); }