8.0 KiB
Solution : [T1] Parser les événements iCalendar (version minimale)
Problème résolu
Transformer le flux iCalendar brut (déjà récupéré par nextcloud.js en T1
précédente) en une liste d'objets Event exploitables par tout l'aval de
l'extension (liste, pré-remplissage du formulaire mairie, statut CalDAV en T2).
Enjeu central : cette tâche FIGE la forme de l'objet Event. Le coût d'un
mauvais contrat n'est pas dans T1, il est dans la refonte de la liste + du form +
du statut le jour où T3 voudra ajouter la robustesse. La forme prime sur le
choix du parseur.
Frontière T1 tenue : on parse, on dédoublonne, on filtre le futur, on lit
CATEGORIES. La robustesse (expansion RRULE, heure exigée pour les journées
entières, durcissement des entrées dégénérées) est différée à T3, « traitée
au plus simple » et documentée.
Approche choisie
Approche 3 du brainstorm : ical.js (Mozilla) vendoré en .js + forme
Event figée qui anticipe T3 en portant dès maintenant le drapeau
journeeEntiere. T1 remplit le contrat ; le comportement des cas limites
reste différé.
Pourquoi pas les alternatives :
- Parseur maison (regex) : iCalendar est piégeux (line-folding à 75 octets,
échappements
\,\n\;,TZID,VALUE=DATE). Le réinventer = dette, et T3 (RRULE, fuseaux) deviendrait un enfer maison.RECHERCHE.mdle déconseille explicitement. ical.js+ forme minimale stricte (approche 2) : la forme n'exprimerait pas la journée entière → T3 devrait rouvrir le contrat figé, exactement le coût que T1 doit éviter. L'ajout d'un seul booléen rend le contrat durable pour un coût marginal.
Contrainte structurante respectée : pas de build, pas de runtime. ical.js
v2.2.1 est vendoré tel quel (build ESM, export default = namespace ICAL),
importé par une page propre de l'extension (type=module) — donc aucun
changement de manifest.json (pas de web_accessible_resources).
Décisions clés
- Contrat
Eventfigé (ne PAS rouvrir sans accord) :{ uid, titre, description, lieu, debut: Date, fin: Date, categories: string[], journeeEntiere: boolean }. Champs texte coercés en""si absents (l'aval suppose des chaînes, jamaisnull). Le défaut « café » du lieu est appliqué en aval, pas dans le parseur. uidseul, pas dehref/ETag. Le transport actuel (?export) renvoie un ICS concaténé sanshref/ETagpar événement. L'UIDest la clé stable ; T2 résoudra l'adressage CalDAV par UID au moment du PUT. Le contrat n'est pas alourdi d'une préoccupation de transport.- Fonction pure à dépendance injectée :
parserEvenements(flux, aujourdhui).aujourdhuiest obligatoire (pas de défaut) → tests déterministes du filtre futur. Convention du repo « params obligatoires par défaut ». - Enregistrement des VTIMEZONE embarqués avant toute conversion de date —
non négociable : sans lui,
toJSDate()calcule un mauvais offset pour lesTZIDnon standards (prouvé par le test Africa/Lagos vs Europe/Paris). - Passer par
ICAL.Event(pas lireDTENDen brut) : il dériveendDatedepuisDURATIONouDTSTARTquandDTENDest absent, et exposestartDate.isDatepour la journée entière. - Cas limites « au plus simple », différés à T3 : pas d'expansion
RRULE(récurrents pris auDTSTARTmaître + filtre futur → un récurrent au maître passé disparaît, trou assumé et documenté) ; exceptions d'occurrence (RECURRENCE-ID) ignorées viaisRecurrenceException(); journées entières au minuit local. - Tri & message « 0 événement futur » hors parseur : le parseur renvoie les événements dans l'ordre du flux, non triés (responsabilité de la liste).
- Périmètre : parseur NON câblé en prod en T1.
liste.jsreste surcompterEvenements(preuve de vie). Le câblage UI appartient à la tâche d'affichage — éviter un demi-câblage jetable et un changement d'UX silencieux. Conséquence : changement purement additif, rollback trivial.
Patterns à réutiliser
Enregistrement des fuseaux embarqués avant conversion de dates (le piège n°1 du parsing iCalendar multi-fuseaux) :
composant.getAllSubcomponents("vtimezone").forEach((vt) => {
const tzid = vt.getFirstPropertyValue("tzid");
if (tzid && !ICAL.TimezoneService.has(tzid)) {
ICAL.TimezoneService.register(vt); // idempotent (garde !has)
}
});
Dédup par UID en un seul passage O(n), maître conservé :
const parUid = new Map();
for (const vevent of composant.getAllSubcomponents("vevent")) {
const ev = new ICAL.Event(vevent);
if (ev.isRecurrenceException()) continue; // RECURRENCE-ID non développés en T1
if (parUid.has(ev.uid)) continue; // garde le premier rencontré
parUid.set(ev.uid, toEvent(ev, vevent));
}
Mapping robuste via ICAL.Event (coercition null → "", endDate dérivé) :
{
uid: ev.uid,
titre: ev.summary ?? "",
description: ev.description ?? "",
lieu: ev.location ?? "",
debut: ev.startDate.toJSDate(),
fin: ev.endDate.toJSDate(), // gère DTEND absent / DURATION
journeeEntiere: ev.startDate.isDate === true,
}
Lecture CATEGORIES tolérante aux deux formes (valeurs séparées par virgule
ET/OU propriétés répétées) :
vevent.getAllProperties("categories").flatMap((prop) => prop.getValues());
Filtre futur inclusif au minuit local (aujourd'hui inclus) :
const seuil = new Date(aujourdhui); seuil.setHours(0, 0, 0, 0);
return [...parUid.values()].filter((e) => e.debut.getTime() >= seuil.getTime());
Tests Bun avec fixtures ICS inline (jamais l'export réel = données perso des
bénévoles) : helpers vcalendar(...) / vevent(...) qui joignent les lignes en
\r\n (CRLF de la RFC 5545), aujourdhui figé pour le déterminisme. Un
VTIMEZONE Paris et un Lagos inline prouvent l'enregistrement des fuseaux.
Pièges à éviter
- VTIMEZONE non enregistré → offset faux. Le piège central :
toJSDate()produit silencieusement une mauvaise heure. Toujours enregistrer les VTIMEZONE embarqués avant conversion. Couvert par le test Africa/Lagos (UTC+1) ≠ Europe/Paris (UTC+2 l'été). - Lire
DTENDen brut : il est souvent absent. Passer parICAL.Event.endDatequi le dérive (durée / start), sinon plantage. summary/description/locationpeuvent renvoyernull: toujours coercer en""— l'aval (form, liste) suppose des chaînes.- Récurrents au DTSTART maître passé disparaissent (pas d'expansion RRULE en T1). Trou produit assumé et documenté, comblé en T3. Or les récurrents (atelier hebdo, permanence) sont souvent ceux qu'on veut publier → priorité T3.
- Pureté nuancée par un état global (signalé P3 en review) :
TimezoneService.registermute un registre global partagé. Idempotent et bénin, mais si deux flux portent le mêmeTZIDavec des définitions différentes, la première enregistrée gagne pour la session. Le commentaire le note ; ne pas qualifier la fonction de « pure » sans nuance. - Entrées dégénérées non durcies en T1 (constats Inzaghi, hors périmètre,
pas des anomalies) : flux chaîne vide
""ou non-ICS →ICAL.parselève ; VEVENT sansDTSTART→ lève ; VEVENT sansUID→ clénull(collision). Le transport renvoie toujours un VCALENDAR valide ; à durcir en T3 si besoin. - Ne pas committer l'export réel du calendrier : données personnelles des bénévoles. Fixtures inline uniquement.
- Ne pas câbler le parseur dans
liste.jsen T1 : tentation de « finir le travail », mais le rendu des cartes appartient à la tâche d'affichage. Un demi-câblage serait jetable et changerait l'UX en douce.
Tags
tags: [firefox-extension, manifest-v3, webextension, vanilla-js, ical, ical.js, icalendar, caldav, nextcloud, vendoring, esm, timezone, vtimezone, rrule, recurrence, pure-function, dependency-injection, bun-test, contract-design, low-tech]