1dbe6d8dd3
- CHANGELOG.md : structure semver (1.0.0 / 1.1.0 / 1.2.0) remplace le journal non versionné - public/version.txt : généré à chaque push depuis la première entrée CHANGELOG - scripts/push.sh : extrait la version CHANGELOG avant git add - src/UpdateChecker.php : compare version déployée vs version Gitea (raw file), cache 1 h - templates/layout.php : bandeau alerte admin (nouvelle version / migrations en attente) - templates/admin.php : dashboard moteur Folio (version déployée / disponible) - scripts/migrate_content.php + migration_001 : ajout # titre dans les articles existants - templates/maintenance.php : page HTTP 503 pendant une migration - src/helpers.php : extractMarkdownTitle(), normalisation \r\n dans lineDiff() - templates/wizard/step1.php : suppression champ titre, plan TOC dynamique - public/assets/js/wizard.js : scope titleEl, scrollToCursor, buildToc, handlers externalisés Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
152 lines
4.6 KiB
PHP
152 lines
4.6 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
function vd($var, ...$moreVars)
|
||
{
|
||
ob_start();
|
||
var_dump($var, ...$moreVars);
|
||
$output = ob_get_clean();
|
||
echo "<pre>$output</pre>";
|
||
}
|
||
|
||
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%)";
|
||
}
|