2025-02-16 16:20:48 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
class DeltaConverter {
|
|
|
|
private static $blockAttrs = ['align', 'header', 'blockquote', 'code-block', 'list'];
|
2025-02-16 16:33:45 +01:00
|
|
|
private static $inlineAttrs = ['bold', 'italic', 'underline', 'strike', 'color', 'background', 'link', 'font', 'script'];
|
2025-02-16 16:20:48 +01:00
|
|
|
|
|
|
|
public static function toHtml($content) {
|
|
|
|
if (empty($content)) return '';
|
|
|
|
|
|
|
|
// Si le contenu est déjà en HTML
|
|
|
|
if (is_string($content) && !self::isJson($content)) {
|
2025-02-16 16:33:45 +01:00
|
|
|
// 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
|
|
|
|
);
|
|
|
|
|
2025-02-16 16:20:48 +01:00
|
|
|
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
|
2025-02-16 16:33:45 +01:00
|
|
|
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;
|
2025-02-16 16:20:48 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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';
|
2025-02-16 16:33:45 +01:00
|
|
|
$classes[] = 'code-block';
|
2025-02-16 16:20:48 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Appliquer l'alignement au niveau du bloc
|
2025-02-16 16:33:45 +01:00
|
|
|
if (!empty($block['attrs']['text-align'])) {
|
|
|
|
$styles[] = "text-align: " . $block['attrs']['text-align'] . " !important";
|
2025-02-16 16:20:48 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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)) {
|
2025-02-16 16:33:45 +01:00
|
|
|
$attributes .= ' style="' . implode('; ', array_unique($styles)) . '"';
|
2025-02-16 16:20:48 +01:00
|
|
|
}
|
|
|
|
if (!empty($classes)) {
|
2025-02-16 16:33:45 +01:00
|
|
|
$attributes .= ' class="' . implode(' ', array_unique($classes)) . '"';
|
2025-02-16 16:20:48 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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>";
|
|
|
|
}
|
2025-02-16 16:33:45 +01:00
|
|
|
if (!empty($attrs['strike'])) {
|
|
|
|
$text = "<s>$text</s>";
|
|
|
|
}
|
2025-02-16 16:20:48 +01:00
|
|
|
if (!empty($attrs['color'])) {
|
2025-02-16 16:33:45 +01:00
|
|
|
$text = "<span style=\"color: {$attrs['color']} !important\">$text</span>";
|
2025-02-16 16:20:48 +01:00
|
|
|
}
|
|
|
|
if (!empty($attrs['background'])) {
|
2025-02-16 16:33:45 +01:00
|
|
|
$text = "<span style=\"background-color: {$attrs['background']} !important\">$text</span>";
|
2025-02-16 16:20:48 +01:00
|
|
|
}
|
|
|
|
if (!empty($attrs['link'])) {
|
|
|
|
$text = "<a href=\"{$attrs['link']}\" target=\"_blank\">$text</a>";
|
|
|
|
}
|
|
|
|
if (!empty($attrs['font'])) {
|
2025-02-16 16:33:45 +01:00
|
|
|
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;
|
|
|
|
}
|
2025-02-16 16:20:48 +01:00
|
|
|
}
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|