diff --git a/assets/css/style.css b/assets/css/style.css index 8f53fb0..ff8b770 100755 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -249,6 +249,11 @@ article > header, article > footer { color: #111; background-color: #fff; resize: vertical; + /* Добавляем эти свойства для правильного отображения переносов */ + white-space: pre-wrap; /* Сохраняет пробелы и переносы строк, переносит текст */ + word-wrap: break-word; /* Переносит длинные слова */ + overflow-wrap: break-word; /* Альтернативное название word-wrap */ + tab-size: 4; /* Размер табуляции */ } #content:focus { diff --git a/assets/js/markdown-editor.js b/assets/js/markdown-editor.js index 4828775..11cdf44 100755 --- a/assets/js/markdown-editor.js +++ b/assets/js/markdown-editor.js @@ -8,9 +8,21 @@ document.addEventListener('DOMContentLoaded', function() { let originalStyles = {}; let isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + + function normalizeContent(text) { + // Заменяем множественные переносы на двойные + text = text.replace(/\n{3,}/g, '\n\n'); + // Убираем пустые строки в начале и конце + return text.trim(); + } + initEditor(); function initEditor() { + // Нормализуем контент при загрузке + if (contentTextarea.value) { + contentTextarea.value = normalizeContent(contentTextarea.value); + } autoResize(); contentTextarea.addEventListener('input', autoResize); contentTextarea.addEventListener('input', processDialogues); diff --git a/controllers/ChapterController.php b/controllers/ChapterController.php index bc57f2f..437de82 100644 --- a/controllers/ChapterController.php +++ b/controllers/ChapterController.php @@ -174,26 +174,139 @@ class ChapterController extends BaseController { public function preview() { $this->requireLogin(); - require_once 'includes/parsedown/ParsedownExtra.php'; $Parsedown = new ParsedownExtra(); - + $content = $_POST['content'] ?? ''; $title = $_POST['title'] ?? 'Предпросмотр'; $editor_type = $_POST['editor_type'] ?? 'markdown'; - + // Обрабатываем контент в зависимости от типа редактора if ($editor_type == 'markdown') { - $html_content = $Parsedown->text($content); + // Нормализуем Markdown перед преобразованием + $normalized_content = $this->normalizeMarkdownContent($content); + $html_content = $Parsedown->text($normalized_content); } else { - $html_content = $content; + // Для HTML редактора нормализуем контент + $normalized_content = $this->normalizeHtmlContent($content); + $html_content = $normalized_content; } - + $this->render('chapters/preview', [ 'content' => $html_content, 'title' => $title, 'page_title' => "Предпросмотр: " . e($title) ]); } + + private function normalizeMarkdownContent($markdown) { + // Нормализация Markdown - убеждаемся, что есть пустые строки между абзацами + $lines = explode("\n", $markdown); + $normalized = []; + $inParagraph = false; + + foreach ($lines as $line) { + $trimmed = trim($line); + + if (empty($trimmed)) { + // Пустая строка - конец абзаца + if ($inParagraph) { + $normalized[] = ''; + $inParagraph = false; + } + continue; + } + + // Проверяем, не является ли строка началом списка + if (preg_match('/^[\*\-\+] /', $line) || preg_match('/^\d+\./', $line)) { + if ($inParagraph) { + $normalized[] = ''; // Завершаем предыдущий абзац + $inParagraph = false; + } + $normalized[] = $line; + continue; + } + + // Проверяем, не является ли строка началом цитаты + if (preg_match('/^> /', $line) || preg_match('/^— /', $line)) { + if ($inParagraph) { + $normalized[] = ''; // Завершаем предыдущий абзац + $inParagraph = false; + } + $normalized[] = $line; + continue; + } + + // Проверяем, не является ли строка заголовком + if (preg_match('/^#+ /', $line)) { + if ($inParagraph) { + $normalized[] = ''; // Завершаем предыдущий абзац + $inParagraph = false; + } + $normalized[] = $line; + $normalized[] = ''; // Пустая строка после заголовка + continue; + } + + // Непустая строка - часть абзаца + if (!$inParagraph && !empty($normalized) && end($normalized) !== '') { + // Добавляем пустую строку перед новым абзацем + $normalized[] = ''; + } + + $normalized[] = $line; + $inParagraph = true; + } + + return implode("\n", $normalized); + } + + // И метод для нормализации HTML контента + private function normalizeHtmlContent($html) { + // Оборачиваем текст без тегов в

+ if (!preg_match('/<[^>]+>/', $html) && trim($html) !== '') { + $lines = explode("\n", trim($html)); + $wrapped = []; + $inParagraph = false; + + foreach ($lines as $line) { + $trimmed = trim($line); + + if (empty($trimmed)) { + if ($inParagraph) { + $wrapped[] = '

'; + $inParagraph = false; + } + continue; + } + + // Проверяем на начало списка + if (preg_match('/^[\*\-\+] /', $trimmed) || preg_match('/^\d+\./', $trimmed)) { + if ($inParagraph) { + $wrapped[] = '

'; + $inParagraph = false; + } + // Обрабатываем списки отдельно + $wrapped[] = ''; + continue; + } + + if (!$inParagraph) { + $wrapped[] = '

' . htmlspecialchars($trimmed); + $inParagraph = true; + } else { + $wrapped[] = htmlspecialchars($trimmed); + } + } + + if ($inParagraph) { + $wrapped[] = '

'; + } + + return implode("\n", $wrapped); + } + + return $html; + } } ?> \ No newline at end of file diff --git a/index.php b/index.php index b8a4886..8d0b0b5 100755 --- a/index.php +++ b/index.php @@ -2,6 +2,52 @@ // index.php - единая точка входа require_once 'config/config.php'; +// Получаем путь к запрашиваемому ресурсу +$requestUri = $_SERVER['REQUEST_URI']; +$requestPath = parse_url($requestUri, PHP_URL_PATH); + +// Убираем базовый URL (SITE_URL) из пути +$basePath = parse_url(SITE_URL, PHP_URL_PATH) ?? ''; +if ($basePath && strpos($requestPath, $basePath) === 0) { + $requestPath = substr($requestPath, strlen($basePath)); +} + +// Убираем ведущий слеш +$requestPath = ltrim($requestPath, '/'); + +// Проверяем, существует ли запрашиваемый файл +$physicalPath = __DIR__ . '/' . $requestPath; +if (file_exists($physicalPath) && !is_dir($physicalPath) && !str_contains($physicalPath, '..')) { + // Определяем MIME-тип + $mimeTypes = [ + 'css' => 'text/css', + 'js' => 'application/javascript', + 'png' => 'image/png', + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'gif' => 'image/gif', + 'webp' => 'image/webp', + 'svg' => 'image/svg+xml', + 'ico' => 'image/x-icon', + 'json' => 'application/json', + 'woff' => 'font/woff', + 'woff2' => 'font/woff2', + 'ttf' => 'font/ttf', + 'eot' => 'application/vnd.ms-fontobject', + ]; + + $extension = strtolower(pathinfo($physicalPath, PATHINFO_EXTENSION)); + if (isset($mimeTypes[$extension])) { + header('Content-Type: ' . $mimeTypes[$extension]); + } + + // Запрещаем кэширование для разработки, в продакшене можно увеличить время + header('Cache-Control: public, max-age=3600'); + + // Отправляем файл + readfile($physicalPath); + exit; +} // Простой роутер class Router { private $routes = []; diff --git a/models/Book.php b/models/Book.php index c52e6d3..4d10dd9 100755 --- a/models/Book.php +++ b/models/Book.php @@ -363,14 +363,20 @@ private function convertContent($content, $from_editor, $to_editor) { // Базовая конвертация HTML в Markdown $markdown = $html; - // Обрабатываем абзацы - заменяем на двойные переносы строк - $markdown = preg_replace('/]*>(.*?)<\/p>/is', "$1\n\n", $markdown); + // 1. Сначала обрабатываем абзацы - заменяем на двойные переносы строк + $markdown = preg_replace_callback('/]*>(.*?)<\/p>/is', function($matches) { + $content = trim($matches[1]); + if (!empty($content)) { + return $content . "\n\n"; + } + return ''; + }, $markdown); - // Обрабатываем разрывы строк + // 2. Обрабатываем разрывы строк $markdown = preg_replace('/]*>\s*<\/br[^>]*>/i', "\n", $markdown); $markdown = preg_replace('/]*>/i', " \n", $markdown); // Два пробела для Markdown разрыва - // Заголовки + // 3. Заголовки $markdown = preg_replace('/]*>(.*?)<\/h1>/is', "# $1\n\n", $markdown); $markdown = preg_replace('/]*>(.*?)<\/h2>/is', "## $1\n\n", $markdown); $markdown = preg_replace('/]*>(.*?)<\/h3>/is', "### $1\n\n", $markdown); @@ -378,49 +384,95 @@ private function convertContent($content, $from_editor, $to_editor) { $markdown = preg_replace('/]*>(.*?)<\/h5>/is', "##### $1\n\n", $markdown); $markdown = preg_replace('/]*>(.*?)<\/h6>/is', "###### $1\n\n", $markdown); - // Жирный текст + // 4. Жирный текст $markdown = preg_replace('/]*>(.*?)<\/strong>/is', '**$1**', $markdown); $markdown = preg_replace('/]*>(.*?)<\/b>/is', '**$1**', $markdown); - // Курсив + // 5. Курсив $markdown = preg_replace('/]*>(.*?)<\/em>/is', '*$1*', $markdown); $markdown = preg_replace('/]*>(.*?)<\/i>/is', '*$1*', $markdown); - // Подчеркивание (не стандартно в Markdown, но обрабатываем) - $markdown = preg_replace('/]*>(.*?)<\/u>/is', '$1', $markdown); - - // Зачеркивание + // 6. Зачеркивание $markdown = preg_replace('/]*>(.*?)<\/s>/is', '~~$1~~', $markdown); $markdown = preg_replace('/]*>(.*?)<\/strike>/is', '~~$1~~', $markdown); $markdown = preg_replace('/]*>(.*?)<\/del>/is', '~~$1~~', $markdown); - // Списки - $markdown = preg_replace('/]*>(.*?)<\/li>/is', '- $1', $markdown); - $markdown = preg_replace('/]*>(.*?)<\/ul>/is', "$1\n", $markdown); - $markdown = preg_replace('/]*>(.*?)<\/ol>/is', "$1\n", $markdown); + // 7. Списки + $markdown = preg_replace('/]*>(.*?)<\/li>/is', "- $1\n", $markdown); - // Блочные цитаты - $markdown = preg_replace('/]*>(.*?)<\/blockquote>/is', "> $1\n", $markdown); + // Обработка вложенных списков + $markdown = preg_replace('/]*>(.*?)<\/ul>/is', "\n$1\n", $markdown); + $markdown = preg_replace('/]*>(.*?)<\/ol>/is', "\n$1\n", $markdown); - // Код + // 8. Блочные цитаты + $markdown = preg_replace('/]*>(.*?)<\/blockquote>/is', "> $1\n\n", $markdown); + + // 9. Код $markdown = preg_replace('/]*>(.*?)<\/code>/is', '`$1`', $markdown); + $markdown = preg_replace('/]*>]*>(.*?)<\/code><\/pre>/is', "```\n$1\n```", $markdown); $markdown = preg_replace('/]*>(.*?)<\/pre>/is', "```\n$1\n```", $markdown); - // Ссылки + // 10. Ссылки $markdown = preg_replace('/]*href="([^"]*)"[^>]*>(.*?)<\/a>/is', '[$2]($1)', $markdown); - // Изображения + // 11. Изображения $markdown = preg_replace('/]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*>/is', '![$2]($1)', $markdown); - // Удаляем все остальные HTML-теги + // 12. Таблицы + $markdown = preg_replace_callback('/]*>(.*?)<\/table>/is', function($matches) { + $tableContent = $matches[1]; + // Простое преобразование таблицы в Markdown + $tableContent = preg_replace('/]*>(.*?)<\/th>/i', "| **$1** ", $tableContent); + $tableContent = preg_replace('/]*>(.*?)<\/td>/i', "| $1 ", $tableContent); + $tableContent = preg_replace('/]*>(.*?)<\/tr>/i', "$1|\n", $tableContent); + $tableContent = preg_replace('/]*>(.*?)<\/thead>/i', "$1", $tableContent); + $tableContent = preg_replace('/]*>(.*?)<\/tbody>/i', "$1", $tableContent); + + // Добавляем разделитель для заголовков таблицы + $tableContent = preg_replace('/\| \*\*[^\|]+\*\* [^\n]*?\|\n/', "$0| --- |\n", $tableContent, 1); + + return "\n" . $tableContent . "\n"; + }, $markdown); + + // 13. Удаляем все остальные HTML-теги $markdown = strip_tags($markdown); - // Чистим лишние пробелы и переносы - $markdown = preg_replace('/\n\s*\n\s*\n/', "\n\n", $markdown); + // 14. Чистим лишние пробелы и переносы + $markdown = preg_replace('/\n{3,}/', "\n\n", $markdown); // Более двух переносов заменяем на два $markdown = preg_replace('/^\s+|\s+$/m', '', $markdown); // Trim каждой строки + $markdown = preg_replace('/\n\s*\n/', "\n\n", $markdown); // Чистим пустые строки + $markdown = preg_replace('/^ +/m', '', $markdown); // Убираем отступы в начале строк + $markdown = trim($markdown); - return $markdown; + // 15. Дополнительная нормализация - убеждаемся, что есть пустые строки между абзацами + $lines = explode("\n", $markdown); + $normalized = []; + $inParagraph = false; + + foreach ($lines as $line) { + $trimmed = trim($line); + + if (empty($trimmed)) { + // Пустая строка - конец абзаца + if ($inParagraph) { + $normalized[] = ''; + $inParagraph = false; + } + continue; + } + + // Непустая строка + if (!$inParagraph && !empty($normalized) && end($normalized) !== '') { + // Добавляем пустую строку перед новым абзацем + $normalized[] = ''; + } + + $normalized[] = $trimmed; + $inParagraph = true; + } + + return implode("\n", $normalized); } private function normalizeHtml($html) { diff --git a/views/chapters/preview.php b/views/chapters/preview.php index 864c553..edf1652 100644 --- a/views/chapters/preview.php +++ b/views/chapters/preview.php @@ -1,7 +1,3 @@ - @@ -29,6 +25,7 @@ include 'views/layouts/header.php'; h2 { border-bottom: 1px solid var(--border-color); padding-bottom: 0.3em; } p { margin-bottom: 1em; + text-align: justify; } code { background: var(--card-background-color); @@ -46,6 +43,7 @@ include 'views/layouts/header.php'; pre code { background: none; padding: 0; + display: block; } blockquote { border-left: 4px solid var(--border-color); @@ -61,6 +59,7 @@ include 'views/layouts/header.php'; .dialogue { margin-left: 2rem; font-style: italic; + color: #2c5aa0; } table { border-collapse: collapse; @@ -77,6 +76,17 @@ include 'views/layouts/header.php'; th { background: var(--card-background-color); } + ul, ol { + margin-bottom: 1rem; + padding-left: 2rem; + } + li { + margin-bottom: 0.3rem; + } + img { + max-width: 100%; + height: auto; + } @@ -84,13 +94,11 @@ include 'views/layouts/header.php';


-
-