# 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.md` le 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 `Event` figé** (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, jamais `null`). Le défaut « café » du lieu est appliqué **en aval**, pas dans le parseur. - **`uid` seul, pas de `href`/`ETag`.** Le transport actuel (`?export`) renvoie un ICS concaténé sans `href`/`ETag` par événement. L'`UID` est 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)`. `aujourdhui` est **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 les `TZID` non standards (prouvé par le test Africa/Lagos vs Europe/Paris). - **Passer par `ICAL.Event`** (pas lire `DTEND` en brut) : il dérive `endDate` depuis `DURATION` ou `DTSTART` quand `DTEND` est absent, et expose `startDate.isDate` pour la journée entière. - **Cas limites « au plus simple », différés à T3** : pas d'expansion `RRULE` (récurrents pris au `DTSTART` maître + filtre futur → un récurrent au maître **passé** disparaît, **trou assumé et documenté**) ; exceptions d'occurrence (`RECURRENCE-ID`) ignorées via `isRecurrenceException()` ; 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.js` reste sur `compterEvenements` (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) : ```js 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é** : ```js 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é) : ```js { 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) : ```js vevent.getAllProperties("categories").flatMap((prop) => prop.getValues()); ``` **Filtre futur inclusif au minuit local** (aujourd'hui inclus) : ```js 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 `DTEND` en brut** : il est souvent absent. Passer par `ICAL.Event.endDate` qui le dérive (durée / start), sinon plantage. - **`summary`/`description`/`location` peuvent renvoyer `null`** : 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.register` mute un registre **global** partagé. Idempotent et bénin, mais si deux flux portent le même `TZID` avec 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.parse` lève ; VEVENT sans `DTSTART` → lève ; VEVENT sans `UID` → 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.js` en 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]