fixes fixes...

This commit is contained in:
mirivlad 2025-11-24 17:24:34 +08:00
parent 24b3788493
commit f093791c14
6 changed files with 272 additions and 36 deletions

View File

@ -249,6 +249,11 @@ article > header, article > footer {
color: #111; color: #111;
background-color: #fff; background-color: #fff;
resize: vertical; resize: vertical;
/* Добавляем эти свойства для правильного отображения переносов */
white-space: pre-wrap; /* Сохраняет пробелы и переносы строк, переносит текст */
word-wrap: break-word; /* Переносит длинные слова */
overflow-wrap: break-word; /* Альтернативное название word-wrap */
tab-size: 4; /* Размер табуляции */
} }
#content:focus { #content:focus {

View File

@ -8,9 +8,21 @@ document.addEventListener('DOMContentLoaded', function() {
let originalStyles = {}; let originalStyles = {};
let isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); 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(); initEditor();
function initEditor() { function initEditor() {
// Нормализуем контент при загрузке
if (contentTextarea.value) {
contentTextarea.value = normalizeContent(contentTextarea.value);
}
autoResize(); autoResize();
contentTextarea.addEventListener('input', autoResize); contentTextarea.addEventListener('input', autoResize);
contentTextarea.addEventListener('input', processDialogues); contentTextarea.addEventListener('input', processDialogues);

View File

@ -174,26 +174,139 @@ class ChapterController extends BaseController {
public function preview() { public function preview() {
$this->requireLogin(); $this->requireLogin();
require_once 'includes/parsedown/ParsedownExtra.php'; require_once 'includes/parsedown/ParsedownExtra.php';
$Parsedown = new ParsedownExtra(); $Parsedown = new ParsedownExtra();
$content = $_POST['content'] ?? ''; $content = $_POST['content'] ?? '';
$title = $_POST['title'] ?? 'Предпросмотр'; $title = $_POST['title'] ?? 'Предпросмотр';
$editor_type = $_POST['editor_type'] ?? 'markdown'; $editor_type = $_POST['editor_type'] ?? 'markdown';
// Обрабатываем контент в зависимости от типа редактора // Обрабатываем контент в зависимости от типа редактора
if ($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 { } else {
$html_content = $content; // Для HTML редактора нормализуем контент
$normalized_content = $this->normalizeHtmlContent($content);
$html_content = $normalized_content;
} }
$this->render('chapters/preview', [ $this->render('chapters/preview', [
'content' => $html_content, 'content' => $html_content,
'title' => $title, 'title' => $title,
'page_title' => "Предпросмотр: " . e($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) {
// Оборачиваем текст без тегов в <p>
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[] = '</p>';
$inParagraph = false;
}
continue;
}
// Проверяем на начало списка
if (preg_match('/^[\*\-\+] /', $trimmed) || preg_match('/^\d+\./', $trimmed)) {
if ($inParagraph) {
$wrapped[] = '</p>';
$inParagraph = false;
}
// Обрабатываем списки отдельно
$wrapped[] = '<ul><li>' . htmlspecialchars($trimmed) . '</li></ul>';
continue;
}
if (!$inParagraph) {
$wrapped[] = '<p>' . htmlspecialchars($trimmed);
$inParagraph = true;
} else {
$wrapped[] = htmlspecialchars($trimmed);
}
}
if ($inParagraph) {
$wrapped[] = '</p>';
}
return implode("\n", $wrapped);
}
return $html;
}
} }
?> ?>

View File

@ -2,6 +2,52 @@
// index.php - единая точка входа // index.php - единая точка входа
require_once 'config/config.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 { class Router {
private $routes = []; private $routes = [];

View File

@ -363,14 +363,20 @@ private function convertContent($content, $from_editor, $to_editor) {
// Базовая конвертация HTML в Markdown // Базовая конвертация HTML в Markdown
$markdown = $html; $markdown = $html;
// Обрабатываем абзацы - заменяем на двойные переносы строк // 1. Сначала обрабатываем абзацы - заменяем на двойные переносы строк
$markdown = preg_replace('/<p[^>]*>(.*?)<\/p>/is', "$1\n\n", $markdown); $markdown = preg_replace_callback('/<p[^>]*>(.*?)<\/p>/is', function($matches) {
$content = trim($matches[1]);
if (!empty($content)) {
return $content . "\n\n";
}
return '';
}, $markdown);
// Обрабатываем разрывы строк // 2. Обрабатываем разрывы строк
$markdown = preg_replace('/<br[^>]*>\s*<\/br[^>]*>/i', "\n", $markdown); $markdown = preg_replace('/<br[^>]*>\s*<\/br[^>]*>/i', "\n", $markdown);
$markdown = preg_replace('/<br[^>]*>/i', " \n", $markdown); // Два пробела для Markdown разрыва $markdown = preg_replace('/<br[^>]*>/i', " \n", $markdown); // Два пробела для Markdown разрыва
// Заголовки // 3. Заголовки
$markdown = preg_replace('/<h1[^>]*>(.*?)<\/h1>/is', "# $1\n\n", $markdown); $markdown = preg_replace('/<h1[^>]*>(.*?)<\/h1>/is', "# $1\n\n", $markdown);
$markdown = preg_replace('/<h2[^>]*>(.*?)<\/h2>/is', "## $1\n\n", $markdown); $markdown = preg_replace('/<h2[^>]*>(.*?)<\/h2>/is', "## $1\n\n", $markdown);
$markdown = preg_replace('/<h3[^>]*>(.*?)<\/h3>/is', "### $1\n\n", $markdown); $markdown = preg_replace('/<h3[^>]*>(.*?)<\/h3>/is', "### $1\n\n", $markdown);
@ -378,49 +384,95 @@ private function convertContent($content, $from_editor, $to_editor) {
$markdown = preg_replace('/<h5[^>]*>(.*?)<\/h5>/is', "##### $1\n\n", $markdown); $markdown = preg_replace('/<h5[^>]*>(.*?)<\/h5>/is', "##### $1\n\n", $markdown);
$markdown = preg_replace('/<h6[^>]*>(.*?)<\/h6>/is', "###### $1\n\n", $markdown); $markdown = preg_replace('/<h6[^>]*>(.*?)<\/h6>/is', "###### $1\n\n", $markdown);
// Жирный текст // 4. Жирный текст
$markdown = preg_replace('/<strong[^>]*>(.*?)<\/strong>/is', '**$1**', $markdown); $markdown = preg_replace('/<strong[^>]*>(.*?)<\/strong>/is', '**$1**', $markdown);
$markdown = preg_replace('/<b[^>]*>(.*?)<\/b>/is', '**$1**', $markdown); $markdown = preg_replace('/<b[^>]*>(.*?)<\/b>/is', '**$1**', $markdown);
// Курсив // 5. Курсив
$markdown = preg_replace('/<em[^>]*>(.*?)<\/em>/is', '*$1*', $markdown); $markdown = preg_replace('/<em[^>]*>(.*?)<\/em>/is', '*$1*', $markdown);
$markdown = preg_replace('/<i[^>]*>(.*?)<\/i>/is', '*$1*', $markdown); $markdown = preg_replace('/<i[^>]*>(.*?)<\/i>/is', '*$1*', $markdown);
// Подчеркивание (не стандартно в Markdown, но обрабатываем) // 6. Зачеркивание
$markdown = preg_replace('/<u[^>]*>(.*?)<\/u>/is', '<u>$1</u>', $markdown);
// Зачеркивание
$markdown = preg_replace('/<s[^>]*>(.*?)<\/s>/is', '~~$1~~', $markdown); $markdown = preg_replace('/<s[^>]*>(.*?)<\/s>/is', '~~$1~~', $markdown);
$markdown = preg_replace('/<strike[^>]*>(.*?)<\/strike>/is', '~~$1~~', $markdown); $markdown = preg_replace('/<strike[^>]*>(.*?)<\/strike>/is', '~~$1~~', $markdown);
$markdown = preg_replace('/<del[^>]*>(.*?)<\/del>/is', '~~$1~~', $markdown); $markdown = preg_replace('/<del[^>]*>(.*?)<\/del>/is', '~~$1~~', $markdown);
// Списки // 7. Списки
$markdown = preg_replace('/<li[^>]*>(.*?)<\/li>/is', '- $1', $markdown); $markdown = preg_replace('/<li[^>]*>(.*?)<\/li>/is', "- $1\n", $markdown);
$markdown = preg_replace('/<ul[^>]*>(.*?)<\/ul>/is', "$1\n", $markdown);
$markdown = preg_replace('/<ol[^>]*>(.*?)<\/ol>/is', "$1\n", $markdown);
// Блочные цитаты // Обработка вложенных списков
$markdown = preg_replace('/<blockquote[^>]*>(.*?)<\/blockquote>/is', "> $1\n", $markdown); $markdown = preg_replace('/<ul[^>]*>(.*?)<\/ul>/is', "\n$1\n", $markdown);
$markdown = preg_replace('/<ol[^>]*>(.*?)<\/ol>/is', "\n$1\n", $markdown);
// Код // 8. Блочные цитаты
$markdown = preg_replace('/<blockquote[^>]*>(.*?)<\/blockquote>/is', "> $1\n\n", $markdown);
// 9. Код
$markdown = preg_replace('/<code[^>]*>(.*?)<\/code>/is', '`$1`', $markdown); $markdown = preg_replace('/<code[^>]*>(.*?)<\/code>/is', '`$1`', $markdown);
$markdown = preg_replace('/<pre[^>]*><code[^>]*>(.*?)<\/code><\/pre>/is', "```\n$1\n```", $markdown);
$markdown = preg_replace('/<pre[^>]*>(.*?)<\/pre>/is', "```\n$1\n```", $markdown); $markdown = preg_replace('/<pre[^>]*>(.*?)<\/pre>/is', "```\n$1\n```", $markdown);
// Ссылки // 10. Ссылки
$markdown = preg_replace('/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/is', '[$2]($1)', $markdown); $markdown = preg_replace('/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/is', '[$2]($1)', $markdown);
// Изображения // 11. Изображения
$markdown = preg_replace('/<img[^>]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*>/is', '![$2]($1)', $markdown); $markdown = preg_replace('/<img[^>]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*>/is', '![$2]($1)', $markdown);
// Удаляем все остальные HTML-теги // 12. Таблицы
$markdown = preg_replace_callback('/<table[^>]*>(.*?)<\/table>/is', function($matches) {
$tableContent = $matches[1];
// Простое преобразование таблицы в Markdown
$tableContent = preg_replace('/<th[^>]*>(.*?)<\/th>/i', "| **$1** ", $tableContent);
$tableContent = preg_replace('/<td[^>]*>(.*?)<\/td>/i', "| $1 ", $tableContent);
$tableContent = preg_replace('/<tr[^>]*>(.*?)<\/tr>/i', "$1|\n", $tableContent);
$tableContent = preg_replace('/<thead[^>]*>(.*?)<\/thead>/i', "$1", $tableContent);
$tableContent = preg_replace('/<tbody[^>]*>(.*?)<\/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 = strip_tags($markdown);
// Чистим лишние пробелы и переносы // 14. Чистим лишние пробелы и переносы
$markdown = preg_replace('/\n\s*\n\s*\n/', "\n\n", $markdown); $markdown = preg_replace('/\n{3,}/', "\n\n", $markdown); // Более двух переносов заменяем на два
$markdown = preg_replace('/^\s+|\s+$/m', '', $markdown); // Trim каждой строки $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); $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) { private function normalizeHtml($html) {

View File

@ -1,7 +1,3 @@
<?php
// views/chapters/preview.php
include 'views/layouts/header.php';
?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ru"> <html lang="ru">
<head> <head>
@ -29,6 +25,7 @@ include 'views/layouts/header.php';
h2 { border-bottom: 1px solid var(--border-color); padding-bottom: 0.3em; } h2 { border-bottom: 1px solid var(--border-color); padding-bottom: 0.3em; }
p { p {
margin-bottom: 1em; margin-bottom: 1em;
text-align: justify;
} }
code { code {
background: var(--card-background-color); background: var(--card-background-color);
@ -46,6 +43,7 @@ include 'views/layouts/header.php';
pre code { pre code {
background: none; background: none;
padding: 0; padding: 0;
display: block;
} }
blockquote { blockquote {
border-left: 4px solid var(--border-color); border-left: 4px solid var(--border-color);
@ -61,6 +59,7 @@ include 'views/layouts/header.php';
.dialogue { .dialogue {
margin-left: 2rem; margin-left: 2rem;
font-style: italic; font-style: italic;
color: #2c5aa0;
} }
table { table {
border-collapse: collapse; border-collapse: collapse;
@ -77,6 +76,17 @@ include 'views/layouts/header.php';
th { th {
background: var(--card-background-color); background: var(--card-background-color);
} }
ul, ol {
margin-bottom: 1rem;
padding-left: 2rem;
}
li {
margin-bottom: 0.3rem;
}
img {
max-width: 100%;
height: auto;
}
</style> </style>
</head> </head>
<body> <body>
@ -84,13 +94,11 @@ include 'views/layouts/header.php';
<h1><?= e($title) ?></h1> <h1><?= e($title) ?></h1>
<hr> <hr>
</header> </header>
<main class="content"> <main class="content">
<?= $content ?> <?= $content ?>
</main> </main>
<footer style="margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--border-color);"> <footer style="margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--border-color);">
<small>Сгенерировано <?= date('d.m.Y H:i') ?> | Markdown Preview</small> <small>Сгенерировано <?= date('d.m.Y H:i') ?> | Предпросмотр</small>
<br> <br>
<a href="javascript:window.close()" class="button secondary">Закрыть</a> <a href="javascript:window.close()" class="button secondary">Закрыть</a>
<a href="javascript:window.print()" class="button">Печать</a> <a href="javascript:window.print()" class="button">Печать</a>