/** * ───────────────────────────────────────────────────────────────────────────── * ChangelogHelper (portage de `settings/ChangelogHelper.kt`, v1.3.0) : * alimente le dialog « Nouveautés » affiché après chaque mise à jour. * * Le contenu vient de `docs/CHANGELOG.md` (relatif à la racine web, fetché au * démarrage — le fichier sert aussi de source pour les releases git ; c'est * l'équivalent de l'asset changelog.md embarqué côté Android, synchronisé par * la tâche Gradle copyChangelog). * * PUR → testable en Node (web/tests/changelog.test.js). * ───────────────────────────────────────────────────────────────────────────── */ /** * Compare deux versions SemVer « X.Y.Z » NUMÉRIQUEMENT (composant par * composant) — une comparaison lexicographique considérerait à tort * « 1.2.9 > 1.2.10 » (piège #38 côté Android : `v1.2.10 < v1.2.5` en * comparaison de chaînes). * * @param {string} current * @param {string|null|undefined} reference null = première installation → true * @returns {boolean} */ export function isVersionNewer(current, reference) { if (reference === null || reference === undefined || reference.trim() === '') return true; const c = parse(current); const r = parse(reference); if (!c) return true; // version courante non parsable → considérer plus neuve (défensif) if (!r) return true; const len = Math.max(c.length, r.length); for (let i = 0; i < len; i++) { const a = c[i] ?? 0; const b = r[i] ?? 0; if (a !== b) return a > b; } return false; // identiques } /** @private Parse "v1.2.3" → [1,2,3] ; null si format non numérique. */ function parse(version) { const cleaned = String(version).trim().replace(/^v/, ''); if (!/^\d+(\.\d+)*$/.test(cleaned)) return null; return cleaned.split('.').map(Number); } /** * Extrait de la CHANGELOG (markdown) les sections des versions STRICTEMENT * plus récentes que `lastSeenVersion` (null = première installation → la * version courante seule), jusqu'à `currentVersion` incluse. * * Format attendu : des sections « ## [X.Y.Z] — … » avec leur corps, la plus * récente EN PREMIER (convention Keep a Changelog du dépôt). * * @param {string} markdown * @param {string|null} lastSeenVersion * @param {string} currentVersion * @returns {string} markdown concaténé des nouveautés ("" si rien de nouveau) */ export function sectionsSince(markdown, lastSeenVersion, currentVersion) { const pattern = /## \[(\d+\.\d+\.\d+)\][^\n]*\n([\s\S]*?)(?=\n## \[|\s*$)/g; let out = ''; for (const match of markdown.matchAll(pattern)) { const version = match[1]; // Sections triées de la plus récente à la plus ancienne : // - inclure ce qui est STRICTEMENT plus récent que la dernière version // vue (lastSeen null = première installation → section courante seule) ; // - s'arrêter dès qu'on descend sous lastSeen (déjà vu). if (lastSeenVersion !== null && !isVersionNewer(version, lastSeenVersion)) break; if (lastSeenVersion === null && version !== currentVersion) continue; if (out !== '') out += '\n\n---\n\n'; out += `## Version ${version}\n\n${match[2].trim()}`; } return out.trim(); }