From 27076900bb7a26f91c25dfd20472ebfcc5e3212f Mon Sep 17 00:00:00 2001 From: Esenjin Date: Sun, 16 Feb 2025 16:20:48 +0100 Subject: [PATCH] correction de l'affichage de la mise en forme du texte (partie 1) --- assets/css/content.css | 210 +++++++++++++++++++++++++++++++++ chapitre.php | 68 +---------- includes/DeltaConverter.php | 225 ++++++++++++++++++++++++++++++++++++ roman.php | 77 +----------- 4 files changed, 441 insertions(+), 139 deletions(-) create mode 100644 assets/css/content.css create mode 100644 includes/DeltaConverter.php diff --git a/assets/css/content.css b/assets/css/content.css new file mode 100644 index 0000000..55b4f35 --- /dev/null +++ b/assets/css/content.css @@ -0,0 +1,210 @@ +/* content.css - Styles pour le contenu de l'éditeur */ + +/* Conteneur principal du contenu */ +.chapter-content, +.novel-description { + font-size: 1.1rem; + line-height: 1.8; + color: var(--text-primary); + font-weight: normal; + width: 100%; + max-width: 100%; + overflow-wrap: break-word; +} + +/* Titres */ +.chapter-content h1, +.chapter-content h2, +.chapter-content h3, +.chapter-content h4, +.chapter-content h5, +.chapter-content h6, +.novel-description h1, +.novel-description h2, +.novel-description h3, +.novel-description h4, +.novel-description h5, +.novel-description h6 { + margin: 1.5em 0 0.8em; + line-height: 1.3; + color: var(--text-primary); +} + +.chapter-content h1, +.novel-description h1 { font-size: 2em; } + +.chapter-content h2, +.novel-description h2 { font-size: 1.75em; } + +.chapter-content h3, +.novel-description h3 { font-size: 1.5em; } + +.chapter-content h4, +.novel-description h4 { font-size: 1.25em; } + +.chapter-content h5, +.novel-description h5 { font-size: 1.1em; } + +.chapter-content h6, +.novel-description h6 { font-size: 1em; } + +/* Paragraphes et espacement */ +.chapter-content p, +.novel-description p { + margin: 0 0 1.5em 0; + min-height: 1.5em; +} + +/* Listes */ +.chapter-content ul, +.chapter-content ol, +.novel-description ul, +.novel-description ol { + margin: 1em 0; + padding-left: 2em; + list-style-position: outside; +} + +.chapter-content li, +.novel-description li { + margin: 0.5em 0; + padding-left: 0.5em; +} + +/* Citations */ +.chapter-content blockquote, +.novel-description blockquote { + margin: 1.5em 0; + padding: 1em 1.5em; + border-left: 4px solid var(--accent-primary); + background-color: var(--bg-secondary); + font-style: italic; + color: var(--text-secondary); + display: block; +} + +/* Blocs de code */ +.chapter-content pre, +.novel-description pre { + margin: 1.5em 0; + padding: 1em; + background-color: var(--bg-secondary); + border-radius: var(--radius-sm); + white-space: pre-wrap; + word-wrap: break-word; + max-width: 100%; + display: block; +} + +.chapter-content code, +.novel-description code { + font-family: "Consolas", "Monaco", monospace; + font-size: 0.9em; + padding: 0.2em 0.4em; + background-color: var(--bg-secondary); + border-radius: var(--radius-sm); + max-width: 100%; +} + +/* Images */ +.chapter-content img, +.novel-description img { + max-width: 100%; + height: auto; + margin: 1.5em 0; + border-radius: var(--radius-sm); + display: block; +} + +/* Alignements */ +.chapter-content [style*="text-align"], +.novel-description [style*="text-align"], +.novel-description p[style*="text-align"] { + display: block !important; + margin: 1em 0 !important; +} + +.novel-description .font-serif, +.novel-description .font-sans, +.novel-description .font-mono { + display: inline-block !important; +} + +/* Polices */ +.font-serif { + font-family: Georgia, "Times New Roman", serif !important; +} + +.font-sans { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important; +} + +.font-mono { + font-family: "Consolas", "Monaco", monospace !important; +} + +/* Barre de séparation */ +.chapter-divider { + margin: 2em auto; + border: none; + border-top: 2px solid var(--accent-primary); + opacity: 0.5; + width: 100%; + display: block; +} + +/* Styles pour les liens */ +.chapter-content a, +.novel-description a { + color: var(--accent-primary); + text-decoration: none; + transition: color var(--transition-fast); +} + +.chapter-content a:hover, +.novel-description a:hover { + color: var(--accent-secondary); + text-decoration: underline; +} + +/* Styles pour les indices et exposants */ +.chapter-content sub, +.novel-description sub { + vertical-align: sub; + font-size: smaller; +} + +.chapter-content sup, +.novel-description sup { + vertical-align: super; + font-size: smaller; +} + +/* Media queries pour le responsive */ +@media (max-width: 768px) { + .chapter-content, + .novel-description { + font-size: 1rem; + } + + .chapter-content blockquote, + .novel-description blockquote { + margin: 1em 0; + padding: 0.8em 1em; + } + + .chapter-content pre, + .novel-description pre { + padding: 0.8em; + font-size: 0.85em; + } + + .chapter-content h1, + .novel-description h1 { font-size: 1.75em; } + + .chapter-content h2, + .novel-description h2 { font-size: 1.5em; } + + .chapter-content h3, + .novel-description h3 { font-size: 1.25em; } +} \ No newline at end of file diff --git a/chapitre.php b/chapitre.php index 8d5a5b1..d66655c 100644 --- a/chapitre.php +++ b/chapitre.php @@ -1,6 +1,7 @@ {$text}"; - } - if (!empty($op['attributes']['italic'])) { - $text = "{$text}"; - } - if (!empty($op['attributes']['underline'])) { - $text = "{$text}"; - } - // Ajouter d'autres styles si nécessaire - } - - // Convertir les retours à la ligne en paragraphes - if ($text === "\n") { - $html .= "
"; - } else { - $html .= $text; - } - } - // Gérer les images - elseif (is_array($op['insert']) && isset($op['insert']['image'])) { - $imageUrl = $op['insert']['image']; - error_log('URL originale: ' . $imageUrl); - // Retirer tous les "../" au début de l'URL - $imageUrl = preg_replace('/^(?:\.\.\/)+/', '', $imageUrl); - error_log('URL nettoyée: ' . $imageUrl); - $html .= "\"Image"; - } - } - - // Envelopper le contenu dans des balises p si nécessaire - if (!empty($html)) { - $paragraphs = explode("\n\n", $html); - $html = ''; - foreach ($paragraphs as $p) { - if (trim($p) !== '') { - $html .= "

{$p}

"; - } - } - } - - return $html; + return DeltaConverter::toHtml($content); } function isJson($string) { @@ -126,6 +63,7 @@ $config = Config::load(); + diff --git a/includes/DeltaConverter.php b/includes/DeltaConverter.php new file mode 100644 index 0000000..2300d8d --- /dev/null +++ b/includes/DeltaConverter.php @@ -0,0 +1,225 @@ + '', + 'attrs' => [], + 'inlineAttrs' => [] + ]; + + foreach ($content['ops'] as $op) { + $text = ''; + $attrs = isset($op['attributes']) ? $op['attributes'] : []; + + // Gérer l'insertion de texte + if (is_string($op['insert'])) { + $text = $op['insert']; + + // Si c'est un saut de ligne, on finalise le bloc + if ($text === "\n") { + // Vérifier les attributs de bloc pour ce saut de ligne + foreach ($attrs as $key => $value) { + if (in_array($key, self::$blockAttrs)) { + $currentBlock['attrs'][$key] = $value; + } + } + + // Finaliser le bloc courant + if (!empty($currentBlock['content']) || !empty($currentBlock['attrs'])) { + $html .= self::renderBlock($currentBlock); + } + + // Réinitialiser pour le prochain bloc + $currentBlock = [ + 'content' => '', + 'attrs' => [], + 'inlineAttrs' => [] + ]; + } else { + // Ajouter le texte avec ses attributs inline + $currentBlock['content'] .= $text; + if (!empty($attrs)) { + $currentBlock['inlineAttrs'][] = [ + 'start' => mb_strlen($currentBlock['content']) - mb_strlen($text), + 'length' => mb_strlen($text), + 'attrs' => $attrs + ]; + } + } + } + // Gérer les images + elseif (is_array($op['insert']) && isset($op['insert']['image'])) { + if (!empty($currentBlock['content'])) { + $html .= self::renderBlock($currentBlock); + $currentBlock = [ + 'content' => '', + 'attrs' => [], + 'inlineAttrs' => [] + ]; + } + + $imageUrl = self::cleanImageUrl($op['insert']['image']); + $imgAttributes = ['class' => 'chapter-image']; + if (isset($attrs['alt'])) { + $imgAttributes['alt'] = $attrs['alt']; + } else { + $imgAttributes['alt'] = "Image"; + } + + $html .= self::createImageTag($imageUrl, $imgAttributes); + } + } + + // Finaliser le dernier bloc si nécessaire + if (!empty($currentBlock['content'])) { + $html .= self::renderBlock($currentBlock); + } + + return $html; + } + + private static function renderBlock($block) { + if (empty($block['content'])) return ''; + + $tag = 'p'; + $styles = []; + $classes = []; + + // Déterminer le tag et attributs de base + if (!empty($block['attrs']['header'])) { + $tag = 'h' . $block['attrs']['header']; + } elseif (!empty($block['attrs']['blockquote'])) { + $tag = 'blockquote'; + } elseif (!empty($block['attrs']['code-block'])) { + $tag = 'pre'; + } + + // Appliquer l'alignement au niveau du bloc + if (!empty($block['attrs']['align'])) { + $styles[] = "text-align: " . $block['attrs']['align'] . " !important"; + } + + // Appliquer la police au niveau du bloc si présente + if (!empty($block['attrs']['font'])) { + $classes[] = 'font-' . strtolower($block['attrs']['font']); + } + + // Construire le contenu avec les attributs inline + $content = $block['content']; + if (!empty($block['inlineAttrs'])) { + // Trier les attributs par position de fin pour les appliquer de la fin vers le début + usort($block['inlineAttrs'], function($a, $b) { + return ($a['start'] + $a['length']) - ($b['start'] + $b['length']); + }); + + // Appliquer les attributs inline + foreach (array_reverse($block['inlineAttrs']) as $attr) { + $start = $attr['start']; + $length = $attr['length']; + $segment = mb_substr($content, $start, $length); + + if (!empty($attr['attrs'])) { + $segment = self::applyInlineAttributes($segment, $attr['attrs']); + } + + $content = mb_substr($content, 0, $start) . $segment . mb_substr($content, $start + $length); + } + } + + // Si c'est un bloc de code, wrapper dans code + if ($tag === 'pre') { + $content = "$content"; + } + + // Construire les attributs HTML finaux + $attributes = ''; + if (!empty($styles)) { + $attributes .= ' style="' . implode('; ', $styles) . '"'; + } + if (!empty($classes)) { + $attributes .= ' class="' . implode(' ', $classes) . '"'; + } + + return "<$tag$attributes>$content"; + } + + private static function applyInlineAttributes($text, $attrs) { + $text = htmlspecialchars($text); + + if (!empty($attrs['bold'])) { + $text = "$text"; + } + if (!empty($attrs['italic'])) { + $text = "$text"; + } + if (!empty($attrs['underline'])) { + $text = "$text"; + } + if (!empty($attrs['color'])) { + $text = "$text"; + } + if (!empty($attrs['background'])) { + $text = "$text"; + } + if (!empty($attrs['link'])) { + $text = "$text"; + } + if (!empty($attrs['font'])) { + $text = "$text"; + } + if (!empty($attrs['script'])) { + if ($attrs['script'] === 'super') $text = "$text"; + if ($attrs['script'] === 'sub') $text = "$text"; + } + + return $text; + } + + private static function isJson($string) { + json_decode($string); + return json_last_error() === JSON_ERROR_NONE; + } + + private static function cleanImageUrl($url) { + return preg_replace('/^(?:\.\.\/)+/', '', $url); + } + + private static function cleanImageUrls($html) { + return preg_replace_callback( + '/]+src=([\'"])((?:\.\.\/)*(?:assets\/[^"\']+))\1[^>]*>/', + function($matches) { + $cleanUrl = self::cleanImageUrl($matches[2]); + return str_replace($matches[2], $cleanUrl, $matches[0]); + }, + $html + ); + } + + private static function createImageTag($src, $attributes) { + $html = ' $value) { + $html .= ' ' . $key . '="' . htmlspecialchars($value) . '"'; + } + $html .= '>'; + return $html; + } +} \ No newline at end of file diff --git a/roman.php b/roman.php index bd0e699..bf070a6 100644 --- a/roman.php +++ b/roman.php @@ -1,6 +1,7 @@ ]+src=([\'"])((?:\.\.\/)*(?:assets\/[^"\']+))\1[^>]*>/', - function($matches) { - // $matches[2] contient l'URL - $cleanUrl = preg_replace('/^(?:\.\.\/)+/', '', $matches[2]); - return str_replace($matches[2], $cleanUrl, $matches[0]); - }, - $content - ); - } - - // Convertir la chaîne JSON en tableau si nécessaire - if (is_string($content)) { - $delta = json_decode($content, true); - } else { - $delta = $content; - } - - if (!isset($delta['ops'])) return ''; - - $html = ''; - foreach ($delta['ops'] as $op) { - if (is_string($op['insert'])) { - $text = htmlspecialchars($op['insert']); - - // Gérer les styles de texte - if (isset($op['attributes'])) { - if (!empty($op['attributes']['bold'])) { - $text = "{$text}"; - } - if (!empty($op['attributes']['italic'])) { - $text = "{$text}"; - } - if (!empty($op['attributes']['underline'])) { - $text = "{$text}"; - } - // Ajouter d'autres styles si nécessaire - } - - // Convertir les retours à la ligne en paragraphes - if ($text === "\n") { - $html .= "
"; - } else { - $html .= $text; - } - } - // Gérer les images - elseif (is_array($op['insert']) && isset($op['insert']['image'])) { - $imageUrl = $op['insert']['image']; - error_log('URL originale: ' . $imageUrl); - // Retirer tous les "../" au début de l'URL - $imageUrl = preg_replace('/^(?:\.\.\/)+/', '', $imageUrl); - error_log('URL nettoyée: ' . $imageUrl); - $html .= "\"Image"; - } - } - - // Envelopper le contenu dans des balises p si nécessaire - if (!empty($html)) { - $paragraphs = explode("\n\n", $html); - $html = ''; - foreach ($paragraphs as $p) { - if (trim($p) !== '') { - $html .= "

{$p}

"; - } - } - } - - return $html; + return DeltaConverter::toHtml($content); } function isJson($string) { @@ -113,6 +41,7 @@ $config = Config::load(); +