simplification et amélioration de la gestion du texte pour les chapitres

This commit is contained in:
Esenjin 2025-02-17 00:02:33 +01:00
parent fd134917f0
commit c99146c38a
5 changed files with 7 additions and 285 deletions

View File

@ -12,7 +12,7 @@ $input = json_decode(file_get_contents('php://input'), true);
$storyId = $input['storyId'] ?? null;
$chapterId = $input['chapterId'] ?? null;
$title = $input['title'] ?? '';
$content = $input['content'] ?? '';
$content = $input['content'];
try {
$story = Stories::get($storyId);

View File

@ -308,7 +308,7 @@ document.addEventListener('DOMContentLoaded', function() {
storyId: storyId,
chapterId: currentChapterId,
title: title,
content: JSON.stringify(quill.getContents())
content: quill.root.innerHTML
})
});

View File

@ -1,7 +1,6 @@
<?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'] ?? '';
@ -35,16 +34,6 @@ if (!$currentChapter) {
exit;
}
// Fonction pour convertir le contenu Delta en HTML
function deltaToHtml($content) {
return DeltaConverter::toHtml($content);
}
function isJson($string) {
json_decode($string);
return json_last_error() === JSON_ERROR_NONE;
}
// Récupération des chapitres précédent et suivant
$prevChapter = $currentIndex > 0 ? $story['chapters'][$currentIndex - 1] : null;
$nextChapter = $currentIndex < count($story['chapters']) - 1 ? $story['chapters'][$currentIndex + 1] : null;
@ -76,7 +65,7 @@ $config = Config::load();
<!-- Contenu principal -->
<div class="novel-content">
<div class="novel-description chapter-content">
<?= deltaToHtml($currentChapter['content']) ?>
<?= $currentChapter['content'] ?>
<!-- Navigation entre chapitres -->
<div class="chapter-navigation">
@ -112,7 +101,8 @@ $config = Config::load();
</div>
<div class="back-to-home">
<a href="index.php">&larr; Retour à l'accueil</a> | <a href="roman.php?id=<?= urlencode($storyId) ?>">&larr; Retour au roman</a>
<a href="index.php">&larr; Retour à l'accueil</a> |
<a href="roman.php?id=<?= urlencode($storyId) ?>">&larr; Retour au roman</a>
</div>
<button class="scroll-top" aria-label="Retour en haut de page"></button>
@ -121,7 +111,7 @@ $config = Config::load();
document.addEventListener('DOMContentLoaded', function() {
const scrollTopBtn = document.querySelector('.scroll-top');
// Afficher/masquer le bouton
// Afficher/masquer le bouton de retour en haut
window.addEventListener('scroll', function() {
if (window.pageYOffset > 300) {
scrollTopBtn.classList.add('visible');

View File

@ -1,268 +0,0 @@
<?php
class DeltaConverter {
private static $blockAttrs = ['align', 'header', 'blockquote', 'code-block', 'list'];
private static $inlineAttrs = ['bold', 'italic', 'underline', 'strike', '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)) {
// Convertir les classes ql-align-* en styles d'alignement
$content = preg_replace_callback(
'/class="ql-align-(justify|center|right)"/',
function($matches) {
return 'style="text-align: ' . $matches[1] . ' !important"';
},
$content
);
// Convertir les classes ql-font-* en classes de police
$content = preg_replace_callback(
'/class="ql-font-(serif|monospace|sans-serif)"/',
function($matches) {
switch($matches[1]) {
case 'serif':
return 'class="font-serif"';
case 'monospace':
return 'class="font-mono"';
case 'sans-serif':
return 'class="font-sans"';
default:
return '';
}
},
$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
if (!empty($attrs['align'])) {
$currentBlock['attrs']['text-align'] = $attrs['align'];
}
if (!empty($attrs['header'])) {
$currentBlock['attrs']['header'] = $attrs['header'];
}
if (!empty($attrs['blockquote'])) {
$currentBlock['attrs']['blockquote'] = true;
}
if (!empty($attrs['code-block'])) {
$currentBlock['attrs']['code-block'] = true;
}
// 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';
$classes[] = 'code-block';
}
// Appliquer l'alignement au niveau du bloc
if (!empty($block['attrs']['text-align'])) {
$styles[] = "text-align: " . $block['attrs']['text-align'] . " !important";
}
// 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('; ', array_unique($styles)) . '"';
}
if (!empty($classes)) {
$attributes .= ' class="' . implode(' ', array_unique($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['strike'])) {
$text = "<s>$text</s>";
}
if (!empty($attrs['color'])) {
$text = "<span style=\"color: {$attrs['color']} !important\">$text</span>";
}
if (!empty($attrs['background'])) {
$text = "<span style=\"background-color: {$attrs['background']} !important\">$text</span>";
}
if (!empty($attrs['link'])) {
$text = "<a href=\"{$attrs['link']}\" target=\"_blank\">$text</a>";
}
if (!empty($attrs['font'])) {
switch($attrs['font']) {
case 'serif':
$text = "<span class=\"font-serif\">$text</span>";
break;
case 'monospace':
$text = "<span class=\"font-mono\">$text</span>";
break;
case 'sans-serif':
$text = "<span class=\"font-sans\">$text</span>";
break;
}
}
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 +1 @@
1.1.2
1.1.3