$output"; } function slugify(string $s): string { $map = ['à' => 'a','â' => 'a','ä' => 'a','é' => 'e','è' => 'e','ê' => 'e','ë' => 'e','î' => 'i','ï' => 'i','ô' => 'o','ö' => 'o','ù' => 'u','û' => 'u','ü' => 'u','ç' => 'c','æ' => 'ae','œ' => 'oe']; $s = mb_strtolower($s); $s = strtr($s, $map); $s = (string)preg_replace('/[^a-z0-9]+/', '-', $s); return trim($s, '-'); } /** Extrait le titre depuis le premier titre Markdown `# ...` du contenu. */ function extractMarkdownTitle(string $content): string { foreach (explode("\n", str_replace("\r\n", "\n", $content)) as $line) { if (preg_match('/^#\s+(.+)/', rtrim($line), $m)) { return trim($m[1]); } } return ''; } /** * Diff ligne-à-ligne via LCS. Retourne un tableau de [op, line] où * op est '=' (inchangé), '-' (supprimé), '+' (ajouté). */ function lineDiff(string $old, string $new): array { $old = str_replace("\r\n", "\n", $old); $new = str_replace("\r\n", "\n", $new); $a = explode("\n", $old); $b = explode("\n", $new); $n = count($a); $m = count($b); if ($n * $m > 2_000_000) { $diff = [['!', "Diff trop grand ({$n}×{$m} lignes) — affichage simplifié."]]; foreach ($a as $line) { $diff[] = ['-', $line]; } foreach ($b as $line) { $diff[] = ['+', $line]; } return $diff; } $dp = array_fill(0, $n + 1, array_fill(0, $m + 1, 0)); for ($i = $n - 1; $i >= 0; $i--) { for ($j = $m - 1; $j >= 0; $j--) { $dp[$i][$j] = $a[$i] === $b[$j] ? 1 + $dp[$i + 1][$j + 1] : max($dp[$i + 1][$j], $dp[$i][$j + 1]); } } $diff = []; $i = 0; $j = 0; while ($i < $n || $j < $m) { if ($i < $n && $j < $m && $a[$i] === $b[$j]) { $diff[] = ['=', $a[$i]]; $i++; $j++; } elseif ($j < $m && ($i >= $n || $dp[$i][$j + 1] >= $dp[$i + 1][$j])) { $diff[] = ['+', $b[$j++]]; } else { $diff[] = ['-', $a[$i++]]; } } return $diff; } // 16 couleurs RGB de base — distribuées sur le spectre, visuellement distinctes const COLOR_PALETTE_16 = [ [220, 38, 38], // rouge [234, 88, 12], // orange [217, 119, 6], // ambre [161, 142, 14], // jaune-olive [77, 124, 15], // citron [22, 163, 74], // vert [4, 120, 87], // émeraude [15, 118, 110], // sarcelle [8, 145, 178], // cyan [3, 105, 161], // ciel [37, 99, 235], // bleu [79, 70, 229], // indigo [109, 40, 217], // violet [147, 51, 234], // pourpre [192, 38, 211], // fuchsia [219, 39, 119], // rose ]; /** * Génère un dégradé CSS pour une catégorie. * Avec $allCats, l'assignation est séquentielle (par ordre alpha) ; * au-delà de 16, un décalage de teinte et d'angle différencie les palettes. * Sans $allCats, fallback par hachage sur la palette. */ function coverGradient(string $seed, array $allCats = []): string { $key = strtolower(trim($seed)); if (!empty($allCats)) { $keys = array_map(fn ($k) => strtolower(trim((string)$k)), array_keys($allCats)); $pos = array_search($key, $keys, true); if ($pos !== false) { $idx = (int) $pos; $tier = (int) floor($idx / 16); $ci = $idx % 16; return _paletteGradient(COLOR_PALETTE_16[$ci], $tier); } } // Hachage déterministe en l'absence de liste $ci = abs(crc32($key)) % 16; return _paletteGradient(COLOR_PALETTE_16[$ci], 0); } function _paletteGradient(array $rgb, int $tier): string { [$r, $g, $b] = $rgb; // Tier 0 : dégradé standard clair → foncé, 135° // Tier 1 : plus saturé, angle inversé, 315° // Tier 2+ : plus sombre encore, 225° $tintMix = match ($tier) { 0 => 0.65, 1 => 0.48, default => 0.35 }; $shadeK = match ($tier) { 0 => 0.35, 1 => 0.25, default => 0.18 }; $angle = match ($tier) { 0 => 135, 1 => 315, default => 225 }; $tr = (int) round($r * (1 - $tintMix) + 255 * $tintMix); $tg = (int) round($g * (1 - $tintMix) + 255 * $tintMix); $tb = (int) round($b * (1 - $tintMix) + 255 * $tintMix); $sr = (int) round($r * $shadeK); $sg = (int) round($g * $shadeK); $sb = (int) round($b * $shadeK); return "linear-gradient({$angle}deg,rgb($tr,$tg,$tb) 0%,rgb($sr,$sg,$sb) 100%)"; }