// Transport + pagination + extraction DOM de l'agenda public de la mairie. // Séparation nette : les parties PURES (calculerPeriode, construireUrlPage) et // la boucle (recupererAgendaPublie, testée via impls injectées) sont testables ; // extraireCartes utilise DOMParser (absent de Bun) et n'est PAS testé // unitairement — validé par le test humain, comme formulaire-mairie.js. const BASE = "https://www.ville-cugnaux.fr/mes-loisirs/agenda/"; const FUSEAU = "Europe/Paris"; // Sécurité anti-boucle : l'agenda ne renverra jamais 12 pages pleines pour nos // événements, mais une structure HTML changée pourrait empêcher la détection de // page vide → on plafonne pour ne jamais boucler indéfiniment. const PLAFOND_PAGES = 12; const FORMAT_MOIS = new Intl.DateTimeFormat("fr-FR", { timeZone: FUSEAU, year: "numeric", month: "2-digit", }); // Fenêtre à interroger : du mois courant au mois du dernier événement à venir. // `aujourdhui` injecté (obligatoire) pour des tests déterministes. export function calculerPeriode(evenements, aujourdhui) { const dernier = evenements.reduce( (max, ev) => (ev.debut.getTime() > max.getTime() ? ev.debut : max), aujourdhui, ); return { debut: moisAnnee(aujourdhui), fin: moisAnnee(dernier) }; } function moisAnnee(date) { const parts = FORMAT_MOIS.formatToParts(date); const mois = parts.find((p) => p.type === "month").value; const annee = parts.find((p) => p.type === "year").value; return `${mois}/${annee}`; } // Page 1 = URL de base ; page N>1 = segment /page/N/. Le « MM/YYYY » est encodé // en « MM%2FYYYY » par encodeURIComponent. export function construireUrlPage(n, periode) { const segment = n > 1 ? `page/${n}/` : ""; const requete = `?f=1&theme=&date=periode` + `&date_debut=${encodeURIComponent(periode.debut)}` + `&date_fin=${encodeURIComponent(periode.fin)}` + `&public=`; return `${BASE}${segment}${requete}`; } // Parcourt les pages jusqu'à une page vide (ou le plafond). Toute erreur (throw, // réponse non-ok) → { ok:false } : la liste s'affiche alors sans badge // (dégradation gracieuse). `fetchImpl` et `extraireCartesImpl` injectés pour // tester la boucle sans DOM ni réseau. export async function recupererAgendaPublie(fetchImpl, extraireCartesImpl, periode) { const cartes = []; try { for (let n = 1; n <= PLAFOND_PAGES; n++) { const res = await fetchImpl(construireUrlPage(n, periode)); if (!res.ok) { console.error(`Écho du Huit : agenda mairie a répondu ${res.status}.`); return { ok: false }; } const cartesPage = extraireCartesImpl(await res.text()); if (cartesPage.length === 0) break; cartes.push(...cartesPage); } return { ok: true, cartes }; } catch (erreur) { console.error("Écho du Huit : agenda mairie injoignable.", erreur); return { ok: false }; } } // Extraction DOM (IMPUR : DOMParser). Chaque carte incomplète (date, titre ou // lien manquant) est IGNORÉE — jamais d'exception. Le lien est LU dans la carte, // jamais reconstruit (le slug porte des suffixes WordPress, ex. « -2 »). export function extraireCartes(html) { const doc = new DOMParser().parseFromString(html, "text/html"); const cartes = []; // Sélecteur agnostique de balise : l'agenda sert des
, // pas des
. `div.card-event` ne matcherait rien (panne silencieuse). for (const carte of doc.querySelectorAll(".card-event")) { const date = carte.querySelector("time[datetime]")?.getAttribute("datetime"); const ancre = carte.querySelector(".card-title a"); const titre = ancre?.textContent?.trim(); const lien = normaliserLien(ancre?.getAttribute("href")); if (!date || !titre || !lien) continue; cartes.push({ date, titre, lien }); } return cartes; } // L'agenda sert le href brut, parfois RELATIF (« /agenda/slug/ ») : résolu contre // la page d'extension (moz-extension://), ce serait un lien mort. On le résout // contre l'agenda et on rejette tout schéma autre que http(s) — le lien vient // d'une source externe et finit en `href`. Href absurde → carte ignorée. function normaliserLien(href) { if (!href) return null; try { const url = new URL(href, BASE); return url.protocol === "https:" || url.protocol === "http:" ? url.href : null; } catch { return null; } }