correction de l'affichage de la mise en forme du texte (partie 1)

This commit is contained in:
Esenjin 2025-02-16 16:20:48 +01:00
parent 117999c60a
commit 27076900bb
4 changed files with 441 additions and 139 deletions

210
assets/css/content.css Normal file
View File

@ -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; }
}

View File

@ -1,6 +1,7 @@
<?php
require_once 'includes/config.php';
require_once 'includes/stories.php';
require_once 'includes/DeltaConverter.php';
// Récupération des paramètres
$storyId = $_GET['story'] ?? '';
@ -36,71 +37,7 @@ if (!$currentChapter) {
// Fonction pour convertir le contenu Delta en HTML
function deltaToHtml($content) {
if (empty($content)) return '';
// Si le contenu est déjà en HTML (ancien format)
if (is_string($content) && !isJson($content)) {
return $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 = "<strong>{$text}</strong>";
}
if (!empty($op['attributes']['italic'])) {
$text = "<em>{$text}</em>";
}
if (!empty($op['attributes']['underline'])) {
$text = "<u>{$text}</u>";
}
// Ajouter d'autres styles si nécessaire
}
// Convertir les retours à la ligne en paragraphes
if ($text === "\n") {
$html .= "<br>";
} 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 .= "<img src=\"{$imageUrl}\" alt=\"Image du chapitre\">";
}
}
// 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>{$p}</p>";
}
}
}
return $html;
return DeltaConverter::toHtml($content);
}
function isJson($string) {
@ -126,6 +63,7 @@ $config = Config::load();
<?php endif; ?>
<link rel="stylesheet" href="assets/css/public.css">
<link rel="stylesheet" href="assets/css/content.css">
<meta name="description" content="<?= htmlspecialchars($story['title']) ?> - <?= htmlspecialchars($currentChapter['title']) ?>">
</head>

225
includes/DeltaConverter.php Normal file
View File

@ -0,0 +1,225 @@
<?php
class DeltaConverter {
private static $blockAttrs = ['align', 'header', 'blockquote', 'code-block', 'list'];
private static $inlineAttrs = ['bold', 'italic', 'underline', 'color', 'background', 'link', 'font', 'script'];
public static function toHtml($content) {
if (empty($content)) return '';
// Si le contenu est déjà en HTML
if (is_string($content) && !self::isJson($content)) {
return self::cleanImageUrls($content);
}
// Convertir la chaîne JSON en tableau si nécessaire
if (is_string($content)) {
$content = json_decode($content, true);
}
if (!isset($content['ops'])) return '';
$html = '';
$currentBlock = [
'content' => '',
'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 = "<code>$content</code>";
}
// Construire les attributs HTML finaux
$attributes = '';
if (!empty($styles)) {
$attributes .= ' style="' . implode('; ', $styles) . '"';
}
if (!empty($classes)) {
$attributes .= ' class="' . implode(' ', $classes) . '"';
}
return "<$tag$attributes>$content</$tag>";
}
private static function applyInlineAttributes($text, $attrs) {
$text = htmlspecialchars($text);
if (!empty($attrs['bold'])) {
$text = "<strong>$text</strong>";
}
if (!empty($attrs['italic'])) {
$text = "<em>$text</em>";
}
if (!empty($attrs['underline'])) {
$text = "<u>$text</u>";
}
if (!empty($attrs['color'])) {
$text = "<span style=\"color: {$attrs['color']}\">$text</span>";
}
if (!empty($attrs['background'])) {
$text = "<span style=\"background-color: {$attrs['background']}\">$text</span>";
}
if (!empty($attrs['link'])) {
$text = "<a href=\"{$attrs['link']}\" target=\"_blank\">$text</a>";
}
if (!empty($attrs['font'])) {
$text = "<span class=\"font-" . strtolower($attrs['font']) . "\">$text</span>";
}
if (!empty($attrs['script'])) {
if ($attrs['script'] === 'super') $text = "<sup>$text</sup>";
if ($attrs['script'] === 'sub') $text = "<sub>$text</sub>";
}
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(
'/<img[^>]+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 = '<img src="' . htmlspecialchars($src) . '"';
foreach ($attributes as $key => $value) {
$html .= ' ' . $key . '="' . htmlspecialchars($value) . '"';
}
$html .= '>';
return $html;
}
}

View File

@ -1,6 +1,7 @@
<?php
require_once 'includes/config.php';
require_once 'includes/stories.php';
require_once 'includes/DeltaConverter.php';
// Récupération de l'ID du roman depuis l'URL
$storyId = $_GET['id'] ?? '';
@ -18,80 +19,7 @@ if (!$story) {
// Fonction pour convertir le contenu Delta en HTML
function deltaToHtml($content) {
if (empty($content)) return '';
// Si le contenu est déjà en HTML
if (is_string($content) && !isJson($content)) {
// Nettoyer les URLs des images dans le HTML
return preg_replace_callback(
'/<img[^>]+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 = "<strong>{$text}</strong>";
}
if (!empty($op['attributes']['italic'])) {
$text = "<em>{$text}</em>";
}
if (!empty($op['attributes']['underline'])) {
$text = "<u>{$text}</u>";
}
// Ajouter d'autres styles si nécessaire
}
// Convertir les retours à la ligne en paragraphes
if ($text === "\n") {
$html .= "<br>";
} 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 .= "<img src=\"{$imageUrl}\" alt=\"Image du chapitre\">";
}
}
// 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>{$p}</p>";
}
}
}
return $html;
return DeltaConverter::toHtml($content);
}
function isJson($string) {
@ -113,6 +41,7 @@ $config = Config::load();
<?php endif; ?>
<link rel="stylesheet" href="assets/css/public.css">
<link rel="stylesheet" href="assets/css/content.css">
<meta name="description" content="<?= htmlspecialchars(strip_tags($story['description'])) ?>">
</head>