diff --git a/includes/lib/markdown.php b/includes/lib/markdown.php
index c2968d7..e54260d 100644
--- a/includes/lib/markdown.php
+++ b/includes/lib/markdown.php
@@ -13,26 +13,30 @@
function markdown_to_html($markdown) {
// Échapper tout le contenu pour éviter les injections XSS
$markdown = htmlspecialchars($markdown, ENT_QUOTES, 'UTF-8');
-
+
// Tableau pour stocker les liens convertis
$links = [];
$link_count = 0;
-
+
+ // Note : le texte étant déjà échappé ci-dessus, les URLs extraites le sont
+ // aussi (« & » est devenu « & »). Il ne faut PAS les ré-échapper dans
+ // les callbacks ci-dessous, sinon on obtient un double encodage (« & »).
+
// Conversion des liens Markdown [texte](url)
$markdown = preg_replace_callback('/\[([^\]]+)\]\(([^)]+)\)/s', function($matches) use (&$links, &$link_count) {
$text = $matches[1];
$url = $matches[2];
-
+
// Assurer que l'URL est correctement formée
if (!preg_match('/^https?:\/\//i', $url)) {
// Ajouter http:// si l'URL ne commence pas par http:// ou https://
$url = 'http://' . $url;
}
-
+
$placeholder = "___LINK_{$link_count}___";
- $links[$placeholder] = '' . $text . '';
+ $links[$placeholder] = '' . $text . '';
$link_count++;
-
+
return $placeholder;
}, $markdown);
@@ -41,29 +45,29 @@ function markdown_to_html($markdown) {
$protocolUrlPattern = '/(https?:\/\/[^\s<]+[^\s<\.)])/i';
$markdown = preg_replace_callback($protocolUrlPattern, function($matches) use (&$links, &$link_count) {
$url = $matches[1];
-
+
$placeholder = "___LINK_{$link_count}___";
- $links[$placeholder] = '' . $url . '';
+ $links[$placeholder] = '' . $url . '';
$link_count++;
-
+
return $placeholder;
}, $markdown);
-
+
// 2. Domaines sans protocole (comme "o-k-i.net", "gong.gp", "NUVEL.NU")
// Détecte les domaines avec TLD communs qui ne font pas partie d'autre chose
$domainPattern = '/\b([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+([a-zA-Z]{2,63})\b/';
$markdown = preg_replace_callback($domainPattern, function($matches) use (&$links, &$link_count) {
$domain = $matches[0];
-
+
// Éviter de convertir des éléments qui ressemblent à des versions/numéros ou qui sont déjà dans des liens
if (preg_match('/^v?\d+\.\d+/', $domain) || strpos($domain, '___LINK_') !== false) {
return $domain;
}
-
+
$placeholder = "___LINK_{$link_count}___";
- $links[$placeholder] = '' . $domain . '';
+ $links[$placeholder] = '' . $domain . '';
$link_count++;
-
+
return $placeholder;
}, $markdown);
@@ -71,19 +75,45 @@ function markdown_to_html($markdown) {
$markdown = preg_replace('/\*\*(.*?)\*\*/s', '$1', $markdown);
$markdown = preg_replace('/\*(.*?)\*/s', '$1', $markdown);
- // Conversion des listes à puces
- $markdown = preg_replace('/^- (.*?)$/m', '
$1', $markdown);
- $markdown = preg_replace('/(.*?<\/li>\n?)+/s', '', $markdown);
-
- // Conversion des listes numérotées
- $markdown = preg_replace('/^\d+\. (.*?)$/m', '$1', $markdown);
- $markdown = preg_replace('/(.*?<\/li>\n?)+/s', '$0
', $markdown);
-
+ // Conversion des listes (à puces et numérotées) en une seule passe ligne à
+ // ligne : les items consécutifs de même type forment une seule liste ; une
+ // ligne hors liste ou un changement de type ferme la liste courante.
+ $lines = explode("\n", $markdown);
+ $markdown = '';
+ $listType = null; // 'ul', 'ol' ou null (hors liste)
+ foreach ($lines as $line) {
+ $itemType = null;
+ $itemText = null;
+ if (preg_match('/^- (.*)$/', $line, $matches)) {
+ $itemType = 'ul';
+ $itemText = $matches[1];
+ } elseif (preg_match('/^\d+\. (.*)$/', $line, $matches)) {
+ $itemType = 'ol';
+ $itemText = $matches[1];
+ }
+
+ if ($itemType !== $listType) {
+ if ($listType !== null) {
+ $markdown .= '' . $listType . ">\n";
+ }
+ if ($itemType !== null) {
+ $markdown .= '<' . $itemType . ">\n";
+ }
+ $listType = $itemType;
+ }
+
+ $markdown .= ($itemType !== null ? '' . $itemText . '' : $line) . "\n";
+ }
+ if ($listType !== null) {
+ $markdown .= '' . $listType . ">\n";
+ }
+ $markdown = rtrim($markdown, "\n");
+
// Gestion des retours à la ligne
$markdown = nl2br($markdown);
-
- // Nettoyage des balises br dans les listes
- $markdown = preg_replace('/<\/li>
/', '', $markdown);
+
+ // Nettoyage des balises br autour des listes
+ $markdown = preg_replace('/(<\/li>||)
/', '$1', $markdown);
// Restaurer les liens
foreach ($links as $placeholder => $link) {
diff --git a/tests/php/format-test.php b/tests/php/format-test.php
index 191ad81..6187da7 100644
--- a/tests/php/format-test.php
+++ b/tests/php/format-test.php
@@ -56,6 +56,21 @@ assertEquals(
'formatDate affiche les années au pluriel'
);
+// --- formatDate : dates malformées (repli sur la chaîne brute) ---------------
+
+assertEquals(
+ 'pas une date',
+ formatDate('pas une date'),
+ 'formatDate retourne la chaîne brute si la date est invalide'
+);
+assertEquals(
+ '2024-13-45T99:99:99Z',
+ formatDate('2024-13-45T99:99:99Z'),
+ 'formatDate retourne une date ISO malformée telle quelle'
+);
+assertEquals('', formatDate(''), 'formatDate retourne une chaîne vide telle quelle');
+assertEquals(' ', formatDate(' '), 'formatDate retourne une chaîne blanche telle quelle');
+
// --- formatVideosData -------------------------------------------------------
$rawVideos = [
@@ -125,6 +140,58 @@ assertEquals('', $videos[1]['description'], 'formatVideosData met une descriptio
assertEquals([], $videos[1]['tags'], 'formatVideosData met des tags vides par défaut');
assertFalse($videos[1]['isLive'], 'formatVideosData met isLive à false par défaut');
+// --- formatVideosData : données API incomplètes -------------------------------
+
+$incompleteVideos = formatVideosData([
+ [
+ // Sans uuid : doit être ignorée
+ 'name' => 'Sans uuid',
+ 'duration' => 10,
+ ],
+ [
+ // uuid vide : doit être ignorée aussi
+ 'uuid' => '',
+ 'name' => 'Uuid vide',
+ ],
+ [
+ // uuid seul : valeurs par défaut partout ailleurs
+ 'uuid' => 'ghi-789',
+ ],
+ [
+ // channel présent mais sans displayName ni avatars
+ 'uuid' => 'jkl-012',
+ 'channel' => ['name' => 'compte'],
+ ],
+]);
+
+assertEquals(2, count($incompleteVideos), 'formatVideosData ignore les entrées sans uuid');
+
+assertEquals('ghi-789', $incompleteVideos[0]['id'], 'formatVideosData conserve l\'uuid seul');
+assertEquals('', $incompleteVideos[0]['title'], 'formatVideosData met un titre vide par défaut');
+assertEquals(0, $incompleteVideos[0]['duration'], 'formatVideosData met une durée à 0 par défaut');
+assertEquals('', $incompleteVideos[0]['channel'], 'formatVideosData met une chaîne vide par défaut');
+assertEquals(0, $incompleteVideos[0]['views'], 'formatVideosData met les vues à 0 par défaut');
+assertEquals('', $incompleteVideos[0]['date'], 'formatVideosData met une date vide par défaut');
+assertNull($incompleteVideos[0]['aspectRatio'], 'formatVideosData met aspectRatio à null par défaut');
+assertFalse($incompleteVideos[0]['isLive'], 'formatVideosData met isLive à false par défaut');
+assertEquals(
+ 'img/default-thumbnail.jpg',
+ $incompleteVideos[0]['thumbnail'],
+ 'formatVideosData met la vignette par défaut pour une entrée minimale'
+);
+assertEquals(
+ 'img/default-avatar.png',
+ $incompleteVideos[0]['channelAvatar'],
+ 'formatVideosData met l\'avatar par défaut pour une entrée minimale'
+);
+
+assertEquals('', $incompleteVideos[1]['channel'], 'formatVideosData met une chaîne vide si displayName absent');
+assertEquals(
+ 'img/default-avatar.png',
+ $incompleteVideos[1]['channelAvatar'],
+ 'formatVideosData met l\'avatar par défaut si le tableau avatars est absent'
+);
+
// --- truncateText (includes/structured-data.php) ----------------------------
assertEquals('court', truncateText('court', 200), 'truncateText laisse un texte court intact');
diff --git a/tests/php/markdown-test.php b/tests/php/markdown-test.php
index 7f790a2..c1050ab 100644
--- a/tests/php/markdown-test.php
+++ b/tests/php/markdown-test.php
@@ -1,9 +1,6 @@
déjà
-// produits par la passe des puces, d'où un double enveloppement .
-// C'est le comportement actuel, verrouillé ici contre toute régression.
+// Le texte est échappé une seule fois : « & » devient « & », jamais « & »
assertEquals(
- "",
- markdown_to_html("- a\n- b"),
- 'markdown_to_html convertit les listes à puces (double enveloppement actuel)'
+ 'Voir https://exemple.com/page?a=1&b=2 suite',
+ markdown_to_html('Voir https://exemple.com/page?a=1&b=2 suite'),
+ 'markdown_to_html n\'encode pas deux fois le & des URLs brutes'
);
assertEquals(
- "- a
\n- b
",
+ 'lien',
+ markdown_to_html('[lien](https://exemple.com/?x=1&y=2)'),
+ 'markdown_to_html n\'encode pas deux fois le & des liens Markdown'
+);
+assertNotContains(
+ '&',
+ markdown_to_html('https://exemple.com/page?a=1&b=2'),
+ 'markdown_to_html ne produit jamais de &'
+);
+
+// --- Listes ------------------------------------------------------------------
+
+assertEquals(
+ "",
+ markdown_to_html("- a\n- b"),
+ 'markdown_to_html convertit les listes à puces en uniquement'
+);
+assertEquals(
+ "\n- a
\n- b
\n
",
markdown_to_html("1. a\n2. b"),
'markdown_to_html convertit les listes numérotées en '
);
+assertEquals(
+ "
\n\n- b
\n
\n",
+ markdown_to_html("- a\n1. b\n- c"),
+ 'markdown_to_html sépare les listes de types différents'
+);
+assertEquals(
+ "
\ntexte",
+ markdown_to_html("- a\ntexte"),
+ 'markdown_to_html ferme la liste avant le texte qui suit'
+);
// --- Retours à la ligne ------------------------------------------------------