add wisiwig editor TinyMCE. default content type = markdown.
This commit is contained in:
parent
d97e4d4944
commit
d7fe90a615
|
|
@ -923,3 +923,37 @@ article > header, article > footer {
|
||||||
resize: none !important;
|
resize: none !important;
|
||||||
box-shadow: 0 0 20px rgba(0,0,0,0.3) !important;
|
box-shadow: 0 0 20px rgba(0,0,0,0.3) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Стили для TinyMCE редактора */
|
||||||
|
.tox-tinymce {
|
||||||
|
border-radius: 4px !important;
|
||||||
|
border: 1px solid #ddd !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tox-tinymce:focus {
|
||||||
|
border-color: #007bff !important;
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Адаптивность для TinyMCE */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.tox-tinymce {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tox-toolbar {
|
||||||
|
flex-wrap: wrap !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
background: #d1ecf1;
|
||||||
|
color: #0c5460;
|
||||||
|
border: 1px solid #bee5eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-warning {
|
||||||
|
background: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
border: 1px solid #ffeaa7;
|
||||||
|
}
|
||||||
|
|
@ -30,6 +30,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
$title = trim($_POST['title'] ?? '');
|
$title = trim($_POST['title'] ?? '');
|
||||||
$description = trim($_POST['description'] ?? '');
|
$description = trim($_POST['description'] ?? '');
|
||||||
$genre = trim($_POST['genre'] ?? '');
|
$genre = trim($_POST['genre'] ?? '');
|
||||||
|
$editor_type = $_POST['editor_type'] ?? 'markdown';
|
||||||
|
|
||||||
if (empty($title)) {
|
if (empty($title)) {
|
||||||
$_SESSION['error'] = "Название книги обязательно";
|
$_SESSION['error'] = "Название книги обязательно";
|
||||||
|
|
@ -37,7 +38,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
$series_id = !empty($_POST['series_id']) ? (int)$_POST['series_id'] : null;
|
$series_id = !empty($_POST['series_id']) ? (int)$_POST['series_id'] : null;
|
||||||
$sort_order_in_series = !empty($_POST['sort_order_in_series']) ? (int)$_POST['sort_order_in_series'] : null;
|
$sort_order_in_series = !empty($_POST['sort_order_in_series']) ? (int)$_POST['sort_order_in_series'] : null;
|
||||||
|
|
||||||
// Если серия указана, но порядок нет - генерируем автоматически
|
|
||||||
if ($series_id && !$sort_order_in_series) {
|
if ($series_id && !$sort_order_in_series) {
|
||||||
$seriesModel = new Series($pdo);
|
$seriesModel = new Series($pdo);
|
||||||
$sort_order_in_series = $seriesModel->getNextSortOrder($series_id);
|
$sort_order_in_series = $seriesModel->getNextSortOrder($series_id);
|
||||||
|
|
@ -49,10 +49,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
'genre' => $genre,
|
'genre' => $genre,
|
||||||
'user_id' => $user_id,
|
'user_id' => $user_id,
|
||||||
'series_id' => $series_id,
|
'series_id' => $series_id,
|
||||||
'sort_order_in_series' => $sort_order_in_series
|
'sort_order_in_series' => $sort_order_in_series,
|
||||||
|
'editor_type' => $editor_type
|
||||||
];
|
];
|
||||||
$data['published'] = isset($_POST['published']) ? 1 : 0;
|
$data['published'] = isset($_POST['published']) ? 1 : 0;
|
||||||
|
|
||||||
|
// Проверяем, изменился ли тип редактора
|
||||||
|
$editor_changed = false;
|
||||||
|
$old_editor_type = null;
|
||||||
|
|
||||||
|
if ($is_edit && $book['editor_type'] !== $editor_type) {
|
||||||
|
$editor_changed = true;
|
||||||
|
$old_editor_type = $book['editor_type'];
|
||||||
|
}
|
||||||
// Обработка загрузки обложки
|
// Обработка загрузки обложки
|
||||||
if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] === UPLOAD_ERR_OK) {
|
if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] === UPLOAD_ERR_OK) {
|
||||||
$cover_result = handleCoverUpload($_FILES['cover_image'], $book_id);
|
$cover_result = handleCoverUpload($_FILES['cover_image'], $book_id);
|
||||||
|
|
@ -73,6 +82,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
|
||||||
if ($is_edit) {
|
if ($is_edit) {
|
||||||
$success = $bookModel->update($book_id, $data);
|
$success = $bookModel->update($book_id, $data);
|
||||||
|
|
||||||
|
// Конвертируем контент глав, если изменился редактор
|
||||||
|
if ($success && $editor_changed) {
|
||||||
|
$conversion_success = $bookModel->convertChaptersContent($book_id, $old_editor_type, $editor_type);
|
||||||
|
if (!$conversion_success) {
|
||||||
|
$_SESSION['warning'] = "Книга обновлена, но возникли ошибки при конвертации содержания глав";
|
||||||
|
} else {
|
||||||
|
$_SESSION['info'] = "Книга обновлена. Содержание глав сконвертировано в новый формат редактора.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$message = $success ? "Книга успешно обновлена" : "Ошибка при обновлении книги";
|
$message = $success ? "Книга успешно обновлена" : "Ошибка при обновлении книги";
|
||||||
} else {
|
} else {
|
||||||
$success = $bookModel->create($data);
|
$success = $bookModel->create($data);
|
||||||
|
|
@ -117,6 +137,41 @@ include 'views/header.php';
|
||||||
value="<?= e($book['genre'] ?? $_POST['genre'] ?? '') ?>"
|
value="<?= e($book['genre'] ?? $_POST['genre'] ?? '') ?>"
|
||||||
placeholder="Например: Фантастика, Роман, Детектив..."
|
placeholder="Например: Фантастика, Роман, Детектив..."
|
||||||
style="width: 100%; margin-bottom: 1.5rem;">
|
style="width: 100%; margin-bottom: 1.5rem;">
|
||||||
|
|
||||||
|
<label for="editor_type" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
|
||||||
|
Режим редактора
|
||||||
|
</label>
|
||||||
|
<select id="editor_type" name="editor_type" style="width: 100%; margin-bottom: 1.5rem;" onchange="showEditorWarning(this)">
|
||||||
|
<option value="markdown" <?= ($book['editor_type'] ?? 'markdown') == 'markdown' ? 'selected' : '' ?>>Markdown редактор</option>
|
||||||
|
<option value="html" <?= ($book['editor_type'] ?? '') == 'html' ? 'selected' : '' ?>>HTML редактор (TinyMCE)</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div id="editor_warning" style="display: none; background: #fff3cd; border: 1px solid #ffeaa7; padding: 10px; border-radius: 4px; margin-bottom: 1rem;">
|
||||||
|
<strong>Внимание:</strong> При смене редактора содержимое всех глав будет автоматически сконвертировано в новый формат.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function showEditorWarning(select) {
|
||||||
|
const warning = document.getElementById('editor_warning');
|
||||||
|
const currentEditor = '<?= $book['editor_type'] ?? 'markdown' ?>';
|
||||||
|
|
||||||
|
if (select.value !== currentEditor) {
|
||||||
|
warning.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
warning.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показать предупреждение при загрузке, если редактор уже отличается
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const currentEditor = '<?= $book['editor_type'] ?? 'markdown' ?>';
|
||||||
|
const selectedEditor = document.getElementById('editor_type').value;
|
||||||
|
|
||||||
|
if (currentEditor !== selectedEditor) {
|
||||||
|
document.getElementById('editor_warning').style.display = 'block';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
<label for="series_id" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
|
<label for="series_id" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
|
||||||
Серия
|
Серия
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -197,48 +252,60 @@ include 'views/header.php';
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; gap: 5px; flex-wrap: wrap; align-items: center;">
|
<div style="display: flex; gap: 5px; flex-wrap: wrap; align-items: center;">
|
||||||
<button type="submit" class="contrast compact-button">
|
<button type="submit" class="contrast button">
|
||||||
<?= $is_edit ? '💾 Сохранить изменения' : '📖 Создать книгу' ?>
|
<?= $is_edit ? '💾 Сохранить изменения' : '📖 Создать книгу' ?>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<?php if ($is_edit): ?>
|
||||||
|
<form method="post" action="book_normalize_content.php" onsubmit="return confirm('Нормализовать контент всех глав книги? Это действие нельзя отменить.')">
|
||||||
|
<input type="hidden" name="book_id" value="<?= $book_id ?>">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
|
||||||
|
<button type="submit" class="button secondary">🔄 Нормализовать контент глав</button>
|
||||||
|
<p style="margin-top: 0.5rem; font-size: 0.8em; color: #666;">
|
||||||
|
Если контент глав отображается неправильно после смены редактора, можно нормализовать его структуру.
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<?php endif; ?>
|
||||||
<?php if ($is_edit): ?>
|
<?php if ($is_edit): ?>
|
||||||
<form method="post" action="book_delete.php" style="display: inline;" onsubmit="return confirm('Вы уверены, что хотите удалить книгу «<?= e($book['title']) ?>»? Все главы также будут удалены.');">
|
<form method="post" action="book_delete.php" style="display: inline;" onsubmit="return confirm('Вы уверены, что хотите удалить книгу «<?= e($book['title']) ?>»? Все главы также будут удалены.');">
|
||||||
<input type="hidden" name="book_id" value="<?= $book['id'] ?>">
|
<input type="hidden" name="book_id" value="<?= $book['id'] ?>">
|
||||||
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
|
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
|
||||||
<button type="submit" class="compact-button secondary" style="background: #ff4444; border-color: #ff4444; color: white;" title="Удалить книгу">
|
<button type="submit" class="compact secondary" style="background: #ff4444; border-color: #ff4444; color: white;" title="Удалить книгу">
|
||||||
🗑️
|
🗑️ Удалить главу
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<?php endif ?>
|
<?php endif ?>
|
||||||
|
|
||||||
<?php if ($is_edit): ?>
|
<?php if ($is_edit): ?>
|
||||||
<div style="margin-top: 2rem; padding: 1rem; background: #f8f9fa; border-radius: 5px;">
|
<div style="margin-top: 2rem; padding: 1rem; background: #f8f9fa; border-radius: 5px;">
|
||||||
<h3>Публичная ссылка для чтения</h3>
|
<h3>Публичная ссылка для чтения</h3>
|
||||||
<p style="margin-bottom: 0.5rem;">Отправьте эту ссылку читателям для просмотра опубликованных глав:</p>
|
|
||||||
|
|
||||||
<div style="display: flex; gap: 5px; align-items: center; flex-wrap: wrap;">
|
<div style="display: flex; gap: 5px; align-items: center; flex-wrap: wrap;">
|
||||||
<input type="text"
|
<input type="text"
|
||||||
id="share-link"
|
id="share-link"
|
||||||
value="<?= e(SITE_URL . '/view_book.php?share_token=' . $book['share_token']) ?>"
|
value="<?= e(SITE_URL . '/view_book.php?share_token=' . $book['share_token']) ?>"
|
||||||
readonly
|
readonly
|
||||||
style="flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px; background: white;">
|
style="flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px; background: white; width:80%;">
|
||||||
|
<br>
|
||||||
<button type="button" onclick="copyShareLink()" class="compact-button secondary">
|
<button type="button" onclick="copyShareLink()" class="compact-button secondary" style="width: 15%;">
|
||||||
📋 Копировать
|
📋 Копировать
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<form method="post" action="book_regenerate_token.php" style="display: inline;">
|
<form method="post" action="book_regenerate_token.php" style="display: inline; margin-top: 1.5em;">
|
||||||
<input type="hidden" name="book_id" value="<?= $book_id ?>">
|
<input type="hidden" name="book_id" value="<?= $book_id ?>">
|
||||||
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
|
<input type="hidden" name="csrf_token" value="<?= generate_csrf_token() ?>">
|
||||||
<button type="submit" class="compact-button secondary" onclick="return confirm('Создать новую ссылку? Старая ссылка перестанет работать.')">
|
<button type="submit" class="compact-button secondary" onclick="return confirm('Создать новую ссылку? Старая ссылка перестанет работать.')" >
|
||||||
🔄 Обновить
|
🔄 Обновить
|
||||||
</button>
|
</button>
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p style="margin-top: 0.5rem; font-size: 0.9em; color: #666;">
|
</form>
|
||||||
|
<p style="margin-top: -1rem; font-size: 0.8em; color: #666; width: 100%;">
|
||||||
<strong>Примечание:</strong> В публичном просмотре отображаются только главы со статусом "Опубликована"
|
<strong>Примечание:</strong> В публичном просмотре отображаются только главы со статусом "Опубликована"
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
|
||||||
113
chapter_edit.php
113
chapter_edit.php
|
|
@ -186,16 +186,113 @@ include 'views/header.php';
|
||||||
|
|
||||||
<label for="content" style="display: block; margin-bottom: 0; font-weight: bold;">
|
<label for="content" style="display: block; margin-bottom: 0; font-weight: bold;">
|
||||||
Содержание главы
|
Содержание главы
|
||||||
|
<?php if (isset($book['editor_type'])): ?>
|
||||||
|
<small style="color: #666; font-weight: normal;">
|
||||||
|
(Режим: <?= $book['editor_type'] == 'markdown' ? 'Markdown' : 'HTML' ?>)
|
||||||
|
</small>
|
||||||
|
<?php endif; ?>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<?php if (($book['editor_type'] ?? 'markdown') === 'html'): ?>
|
||||||
|
<!-- HTML редактор (TinyMCE) -->
|
||||||
|
<textarea name="content" id="content" style="width: 100%; min-height: 500px;">
|
||||||
|
<?= e($chapter['content'] ?? $_POST['content'] ?? '') ?>
|
||||||
|
</textarea>
|
||||||
|
|
||||||
|
<!-- Подключаем TinyMCE -->
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/tinymce/6.8.6/tinymce.min.js" referrerpolicy="origin"></script>
|
||||||
|
<script>
|
||||||
|
tinymce.init({
|
||||||
|
selector: '#content',
|
||||||
|
plugins: 'advlist autolink lists link image charmap preview anchor pagebreak searchreplace wordcount visualblocks visualchars code fullscreen insertdatetime media table emoticons',
|
||||||
|
toolbar: 'undo redo | blocks | bold italic underline strikethrough | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image media | code preview fullscreen',
|
||||||
|
menubar: 'edit view insert format tools table',
|
||||||
|
height: 500,
|
||||||
|
language: 'ru',
|
||||||
|
branding: false,
|
||||||
|
promotion: false,
|
||||||
|
image_advtab: true,
|
||||||
|
|
||||||
|
// Важные настройки для сохранения структуры
|
||||||
|
forced_root_block: 'p', // Используем <p> вместо <div>
|
||||||
|
force_br_newlines: false, // Не использовать <br> вместо абзацев
|
||||||
|
force_p_newlines: true, // Всегда создавать новые абзацы при Enter
|
||||||
|
convert_newlines_to_brs: false, // Не конвертировать переносы в <br>
|
||||||
|
remove_trailing_brs: true, // Убирать лишние <br> в конце
|
||||||
|
|
||||||
|
// Настройки форматирования
|
||||||
|
formats: {
|
||||||
|
// Сохраняем семантическое форматирование
|
||||||
|
bold: { inline: 'strong' },
|
||||||
|
italic: { inline: 'em' },
|
||||||
|
underline: { inline: 'u', exact: true },
|
||||||
|
strikethrough: { inline: 'del' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// Настройки контента
|
||||||
|
content_style: `
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0 0 1em 0;
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
margin: 1em 0 0.5em 0;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
|
||||||
|
// Настройки для чистого HTML
|
||||||
|
valid_elements: '*[*]', // Разрешаем все элементы (можно ограничить при необходимости)
|
||||||
|
valid_children: '+body[p,div,h1,h2,h3,h4,h5,h6,blockquote,pre,ul,ol,li,table]',
|
||||||
|
|
||||||
|
// Автосохранение
|
||||||
|
setup: function (editor) {
|
||||||
|
editor.on('init', function () {
|
||||||
|
// Нормализуем контент при инициализации
|
||||||
|
var content = editor.getContent();
|
||||||
|
if (content && !content.match(/<p[^>]*>/) && content.trim().length > 0) {
|
||||||
|
// Если нет тегов абзацев, оборачиваем в <p>
|
||||||
|
editor.setContent('<p>' + content.replace(/\n/g, '</p><p>') + '</p>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.on('keydown', function (e) {
|
||||||
|
clearTimeout(window.tinymceSaveTimeout);
|
||||||
|
window.tinymceSaveTimeout = setTimeout(function() {
|
||||||
|
if (typeof autoSave === 'function') {
|
||||||
|
autoSave();
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработка вставки текста
|
||||||
|
editor.on('paste', function (e) {
|
||||||
|
// Нормализуем вставленный текст
|
||||||
|
setTimeout(function() {
|
||||||
|
var content = editor.getContent();
|
||||||
|
// Убеждаемся, что контент имеет правильную структуру абзацев
|
||||||
|
editor.setContent(content);
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php else: ?>
|
||||||
|
<!-- Markdown редактор (существующий) -->
|
||||||
<textarea name="content" id="content"
|
<textarea name="content" id="content"
|
||||||
placeholder="Начните писать вашу главу здесь..."
|
placeholder="Начните писать вашу главу здесь..."
|
||||||
rows="15"
|
rows="15"
|
||||||
style="width: 100%; font-family: monospace;"><?= e($chapter['content'] ?? $_POST['content'] ?? '') ?></textarea>
|
style="width: 100%; font-family: monospace;"><?= e($chapter['content'] ?? $_POST['content'] ?? '') ?></textarea>
|
||||||
<?php if ($is_edit && isset($chapter['word_count'])): ?>
|
|
||||||
<div style="background: #f5f5f5; padding: 10px; border-radius: 5px; margin-bottom: 1rem;">
|
<script src="/assets/js/markdown-editor.js"></script>
|
||||||
<strong>Статистика:</strong> <?= $chapter['word_count'] ?> слов
|
<?php if ($is_edit): ?>
|
||||||
| Обновлено: <?= date('d.m.Y H:i', strtotime($chapter['updated_at'])) ?>
|
<script src="/assets/js/autosave.js"></script>
|
||||||
</div>
|
<?php endif; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -218,6 +315,7 @@ include 'views/header.php';
|
||||||
<form method="post" action="preview.php" target="_blank" id="preview-form" style="display: none;">
|
<form method="post" action="preview.php" target="_blank" id="preview-form" style="display: none;">
|
||||||
<input type="hidden" name="content" id="preview-content">
|
<input type="hidden" name="content" id="preview-content">
|
||||||
<input type="hidden" name="title" id="preview-title" value="<?= e($chapter['title'] ?? 'Новая глава') ?>">
|
<input type="hidden" name="title" id="preview-title" value="<?= e($chapter['title'] ?? 'Новая глава') ?>">
|
||||||
|
<input type="hidden" name="editor_type" id="preview-editor-type" value="<?= e($book['editor_type'] ?? 'markdown') ?>">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<?php if ($is_edit): ?>
|
<?php if ($is_edit): ?>
|
||||||
|
|
@ -283,9 +381,6 @@ document.getElementById('preview-button').addEventListener('click', function() {
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="assets/js/markdown-editor.js"></script>
|
|
||||||
<?php if ($is_edit): ?>
|
|
||||||
<script src="assets/js/autosave.js"></script>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php include 'views/footer.php'; ?>
|
<?php include 'views/footer.php'; ?>
|
||||||
126
export_book.php
126
export_book.php
|
|
@ -47,14 +47,18 @@ if (!$book) {
|
||||||
redirect('books.php');
|
redirect('books.php');
|
||||||
}
|
}
|
||||||
// Получаем информацию об авторе
|
// Получаем информацию об авторе
|
||||||
$author_info = null;
|
$author_info = "Неизвестный автор";
|
||||||
if ($book) {
|
if ($book) {
|
||||||
$stmt = $pdo->prepare("SELECT display_name, username FROM users WHERE id = ?");
|
$stmt = $pdo->prepare("SELECT display_name, username FROM users WHERE id = ?");
|
||||||
$stmt->execute([$book['user_id']]);
|
$stmt->execute([$book['user_id']]);
|
||||||
$author_info = $stmt->fetch(PDO::FETCH_ASSOC);
|
$author_info = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
}
|
if ($author_info['display_name'] !=""){
|
||||||
|
$author_name = $author_info['display_name'];
|
||||||
|
}else{
|
||||||
|
$author_name = $author_info['username'] ;
|
||||||
|
}
|
||||||
|
|
||||||
$author_name = $author_info['display_name'] ?? $author_info['username'] ?? 'Неизвестный автор';
|
}
|
||||||
|
|
||||||
|
|
||||||
// Функция для преобразования Markdown в чистый текст с форматированием абзацев
|
// Функция для преобразования Markdown в чистый текст с форматированием абзацев
|
||||||
|
|
@ -354,7 +358,11 @@ function exportPDF($book, $chapters, $is_public, $author_name) {
|
||||||
|
|
||||||
// Контент главы
|
// Контент главы
|
||||||
$pdf->SetFont('dejavusans', '', 11);
|
$pdf->SetFont('dejavusans', '', 11);
|
||||||
|
if ($book['editor_type'] == 'markdown') {
|
||||||
$htmlContent = $Parsedown->text($chapter['content']);
|
$htmlContent = $Parsedown->text($chapter['content']);
|
||||||
|
} else {
|
||||||
|
$htmlContent = $chapter['content'];
|
||||||
|
}
|
||||||
$pdf->writeHTML($htmlContent, true, false, true, false, '');
|
$pdf->writeHTML($htmlContent, true, false, true, false, '');
|
||||||
|
|
||||||
$pdf->Ln(8);
|
$pdf->Ln(8);
|
||||||
|
|
@ -413,10 +421,17 @@ function exportDOCX($book, $chapters, $is_public, $author_name) {
|
||||||
|
|
||||||
// Описание
|
// Описание
|
||||||
if (!empty($book['description'])) {
|
if (!empty($book['description'])) {
|
||||||
|
if ($book['editor_type'] == 'markdown') {
|
||||||
$descriptionParagraphs = markdownToParagraphs($book['description']);
|
$descriptionParagraphs = markdownToParagraphs($book['description']);
|
||||||
|
} else {
|
||||||
|
$descriptionParagraphs = htmlToParagraphs($book['description']);
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($descriptionParagraphs as $paragraph) {
|
foreach ($descriptionParagraphs as $paragraph) {
|
||||||
|
if (!empty(trim($paragraph))) {
|
||||||
$section->addText($paragraph);
|
$section->addText($paragraph);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
$section->addTextBreak(2);
|
$section->addTextBreak(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -446,9 +461,14 @@ function exportDOCX($book, $chapters, $is_public, $author_name) {
|
||||||
$section->addText($chapter['title'], ['bold' => true, 'size' => 14]);
|
$section->addText($chapter['title'], ['bold' => true, 'size' => 14]);
|
||||||
$section->addTextBreak(1);
|
$section->addTextBreak(1);
|
||||||
|
|
||||||
// Получаем очищенный текст и разбиваем на абзацы
|
// Получаем очищенный текст и разбиваем на абзацы в зависимости от типа редактора
|
||||||
|
if ($book['editor_type'] == 'markdown') {
|
||||||
$cleanContent = cleanMarkdown($chapter['content']);
|
$cleanContent = cleanMarkdown($chapter['content']);
|
||||||
$paragraphs = markdownToParagraphs($cleanContent);
|
$paragraphs = markdownToParagraphs($cleanContent);
|
||||||
|
} else {
|
||||||
|
$cleanContent = strip_tags($chapter['content']);
|
||||||
|
$paragraphs = htmlToParagraphs($chapter['content']);
|
||||||
|
}
|
||||||
|
|
||||||
// Добавляем каждый абзац
|
// Добавляем каждый абзац
|
||||||
foreach ($paragraphs as $paragraph) {
|
foreach ($paragraphs as $paragraph) {
|
||||||
|
|
@ -479,6 +499,23 @@ function exportDOCX($book, $chapters, $is_public, $author_name) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Новая функция для разбивки HTML на абзацы
|
||||||
|
function htmlToParagraphs($html) {
|
||||||
|
// Убираем HTML теги и нормализуем пробелы
|
||||||
|
$text = strip_tags($html);
|
||||||
|
$text = preg_replace('/\s+/', ' ', $text);
|
||||||
|
|
||||||
|
// Разбиваем на абзацы по точкам и переносам строк
|
||||||
|
$paragraphs = preg_split('/(?<=[.!?])\s+/', $text);
|
||||||
|
|
||||||
|
// Фильтруем пустые абзацы
|
||||||
|
$paragraphs = array_filter($paragraphs, function($paragraph) {
|
||||||
|
return !empty(trim($paragraph));
|
||||||
|
});
|
||||||
|
|
||||||
|
return $paragraphs;
|
||||||
|
}
|
||||||
|
|
||||||
function exportHTML($book, $chapters, $is_public, $author_name) {
|
function exportHTML($book, $chapters, $is_public, $author_name) {
|
||||||
global $Parsedown;
|
global $Parsedown;
|
||||||
|
|
||||||
|
|
@ -539,7 +576,7 @@ function exportHTML($book, $chapters, $is_public, $author_name) {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
columns: 2;
|
columns: 1;
|
||||||
column-gap: 2rem;
|
column-gap: 2rem;
|
||||||
}
|
}
|
||||||
.table-of-contents h3 {
|
.table-of-contents h3 {
|
||||||
|
|
@ -657,12 +694,18 @@ function exportHTML($book, $chapters, $is_public, $author_name) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($book['description'])) {
|
if (!empty($book['description'])) {
|
||||||
$html .= '<div class="book-description">' . nl2br(htmlspecialchars($book['description'])) . '</div>';
|
$html .= '<div class="book-description">';
|
||||||
|
if ($book['editor_type'] == 'markdown') {
|
||||||
|
$html .= nl2br(htmlspecialchars($book['description']));
|
||||||
|
} else {
|
||||||
|
$html .= $book['description'];
|
||||||
|
}
|
||||||
|
$html .= '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Интерактивное оглавление
|
// Интерактивное оглавление
|
||||||
if (!empty($chapters)) {
|
if (!empty($chapters)) {
|
||||||
$html .= '<div>';
|
$html .= '<div class="table-of-contents">';
|
||||||
$html .= '<h3>Оглавление</h3>';
|
$html .= '<h3>Оглавление</h3>';
|
||||||
$html .= '<ul>';
|
$html .= '<ul>';
|
||||||
foreach ($chapters as $index => $chapter) {
|
foreach ($chapters as $index => $chapter) {
|
||||||
|
|
@ -679,7 +722,13 @@ function exportHTML($book, $chapters, $is_public, $author_name) {
|
||||||
$html .= '<div class="chapter">';
|
$html .= '<div class="chapter">';
|
||||||
$html .= '<div class="chapter-title" id="chapter-' . $chapter['id'] . '" name="chapter-' . $chapter['id'] . '">' . htmlspecialchars($chapter['title']) . '</div>';
|
$html .= '<div class="chapter-title" id="chapter-' . $chapter['id'] . '" name="chapter-' . $chapter['id'] . '">' . htmlspecialchars($chapter['title']) . '</div>';
|
||||||
|
|
||||||
|
// Обрабатываем контент в зависимости от типа редактора
|
||||||
|
if ($book['editor_type'] == 'markdown') {
|
||||||
$htmlContent = $Parsedown->text($chapter['content']);
|
$htmlContent = $Parsedown->text($chapter['content']);
|
||||||
|
} else {
|
||||||
|
$htmlContent = $chapter['content'];
|
||||||
|
}
|
||||||
|
|
||||||
$html .= '<div class="chapter-content">' . $htmlContent . '</div>';
|
$html .= '<div class="chapter-content">' . $htmlContent . '</div>';
|
||||||
$html .= '</div>';
|
$html .= '</div>';
|
||||||
|
|
||||||
|
|
@ -714,8 +763,15 @@ function exportTXT($book, $chapters, $is_public, $author_name) {
|
||||||
|
|
||||||
if (!empty($book['description'])) {
|
if (!empty($book['description'])) {
|
||||||
$content .= "ОПИСАНИЕ:\n";
|
$content .= "ОПИСАНИЕ:\n";
|
||||||
// Ширина до 144 символов
|
|
||||||
$content .= wordwrap($book['description'], 144) . "\n\n";
|
// Обрабатываем описание в зависимости от типа редактора
|
||||||
|
if ($book['editor_type'] == 'markdown') {
|
||||||
|
$descriptionText = cleanMarkdown($book['description']);
|
||||||
|
} else {
|
||||||
|
$descriptionText = strip_tags($book['description']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$content .= wordwrap($descriptionText, 144) . "\n\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Оглавление
|
// Оглавление
|
||||||
|
|
@ -735,13 +791,17 @@ function exportTXT($book, $chapters, $is_public, $author_name) {
|
||||||
$content .= $chapter['title'] . "\n";
|
$content .= $chapter['title'] . "\n";
|
||||||
$content .= str_repeat("-", 60) . "\n\n";
|
$content .= str_repeat("-", 60) . "\n\n";
|
||||||
|
|
||||||
// Получаем очищенный текст и разбиваем на абзацы
|
// Получаем очищенный текст в зависимости от типа редактора
|
||||||
|
if ($book['editor_type'] == 'markdown') {
|
||||||
$cleanContent = cleanMarkdown($chapter['content']);
|
$cleanContent = cleanMarkdown($chapter['content']);
|
||||||
$paragraphs = markdownToParagraphs($cleanContent);
|
$paragraphs = markdownToParagraphs($cleanContent);
|
||||||
|
} else {
|
||||||
|
$cleanContent = strip_tags($chapter['content']);
|
||||||
|
$paragraphs = htmlToPlainTextParagraphs($cleanContent);
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($paragraphs as $paragraph) {
|
foreach ($paragraphs as $paragraph) {
|
||||||
if (!empty(trim($paragraph))) {
|
if (!empty(trim($paragraph))) {
|
||||||
// Увеличиваем ширину до 144 символов
|
|
||||||
$content .= wordwrap($paragraph, 144) . "\n\n";
|
$content .= wordwrap($paragraph, 144) . "\n\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -762,4 +822,48 @@ function exportTXT($book, $chapters, $is_public, $author_name) {
|
||||||
echo $content;
|
echo $content;
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Новая функция для разбивки HTML на абзацы в виде простого текста
|
||||||
|
function htmlToPlainTextParagraphs($html) {
|
||||||
|
// Убираем HTML теги
|
||||||
|
$text = strip_tags($html);
|
||||||
|
|
||||||
|
// Заменяем HTML entities
|
||||||
|
$text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||||
|
|
||||||
|
// Нормализуем переносы строк
|
||||||
|
$text = str_replace(["\r\n", "\r"], "\n", $text);
|
||||||
|
|
||||||
|
// Разбиваем на строки
|
||||||
|
$lines = explode("\n", $text);
|
||||||
|
$paragraphs = [];
|
||||||
|
$currentParagraph = '';
|
||||||
|
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$trimmedLine = trim($line);
|
||||||
|
|
||||||
|
// Пустая строка - конец абзаца
|
||||||
|
if (empty($trimmedLine)) {
|
||||||
|
if (!empty($currentParagraph)) {
|
||||||
|
$paragraphs[] = $currentParagraph;
|
||||||
|
$currentParagraph = '';
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем к текущему абзацу
|
||||||
|
if (!empty($currentParagraph)) {
|
||||||
|
$currentParagraph .= ' ' . $trimmedLine;
|
||||||
|
} else {
|
||||||
|
$currentParagraph = $trimmedLine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем последний абзац
|
||||||
|
if (!empty($currentParagraph)) {
|
||||||
|
$paragraphs[] = $currentParagraph;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $paragraphs;
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
|
|
@ -61,6 +61,7 @@ CREATE TABLE `books` (
|
||||||
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
`share_token` varchar(32) DEFAULT NULL,
|
`share_token` varchar(32) DEFAULT NULL,
|
||||||
`published` tinyint(1) NOT NULL DEFAULT 0,
|
`published` tinyint(1) NOT NULL DEFAULT 0,
|
||||||
|
`editor_type` ENUM('markdown', 'html') DEFAULT 'markdown',
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `share_token` (`share_token`),
|
UNIQUE KEY `share_token` (`share_token`),
|
||||||
KEY `user_id` (`user_id`),
|
KEY `user_id` (`user_id`),
|
||||||
|
|
|
||||||
305
models/Book.php
305
models/Book.php
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
// models/Book.php
|
// models/Book.php
|
||||||
|
require_once __DIR__ . '/../includes/parsedown/ParsedownExtra.php';
|
||||||
class Book {
|
class Book {
|
||||||
private $pdo;
|
private $pdo;
|
||||||
|
|
||||||
|
|
@ -41,10 +41,11 @@ class Book {
|
||||||
public function create($data) {
|
public function create($data) {
|
||||||
$share_token = bin2hex(random_bytes(16));
|
$share_token = bin2hex(random_bytes(16));
|
||||||
$published = isset($data['published']) ? (int)$data['published'] : 0;
|
$published = isset($data['published']) ? (int)$data['published'] : 0;
|
||||||
|
$editor_type = $data['editor_type'] ?? 'markdown';
|
||||||
|
|
||||||
$stmt = $this->pdo->prepare("
|
$stmt = $this->pdo->prepare("
|
||||||
INSERT INTO books (title, description, genre, user_id, series_id, sort_order_in_series, share_token, published)
|
INSERT INTO books (title, description, genre, user_id, series_id, sort_order_in_series, share_token, published, editor_type)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
");
|
");
|
||||||
return $stmt->execute([
|
return $stmt->execute([
|
||||||
$data['title'],
|
$data['title'],
|
||||||
|
|
@ -54,16 +55,18 @@ class Book {
|
||||||
$data['series_id'] ?? null,
|
$data['series_id'] ?? null,
|
||||||
$data['sort_order_in_series'] ?? null,
|
$data['sort_order_in_series'] ?? null,
|
||||||
$share_token,
|
$share_token,
|
||||||
$published
|
$published,
|
||||||
|
$editor_type
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update($id, $data) {
|
public function update($id, $data) {
|
||||||
$published = isset($data['published']) ? (int)$data['published'] : 0;
|
$published = isset($data['published']) ? (int)$data['published'] : 0;
|
||||||
|
$editor_type = $data['editor_type'] ?? 'markdown';
|
||||||
|
|
||||||
$stmt = $this->pdo->prepare("
|
$stmt = $this->pdo->prepare("
|
||||||
UPDATE books
|
UPDATE books
|
||||||
SET title = ?, description = ?, genre = ?, series_id = ?, sort_order_in_series = ?, published = ?
|
SET title = ?, description = ?, genre = ?, series_id = ?, sort_order_in_series = ?, published = ?, editor_type = ?
|
||||||
WHERE id = ? AND user_id = ?
|
WHERE id = ? AND user_id = ?
|
||||||
");
|
");
|
||||||
return $stmt->execute([
|
return $stmt->execute([
|
||||||
|
|
@ -73,11 +76,13 @@ class Book {
|
||||||
$data['series_id'] ?? null,
|
$data['series_id'] ?? null,
|
||||||
$data['sort_order_in_series'] ?? null,
|
$data['sort_order_in_series'] ?? null,
|
||||||
$published,
|
$published,
|
||||||
|
$editor_type,
|
||||||
$id,
|
$id,
|
||||||
$data['user_id']
|
$data['user_id']
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function delete($id, $user_id) {
|
public function delete($id, $user_id) {
|
||||||
try {
|
try {
|
||||||
$this->pdo->beginTransaction();
|
$this->pdo->beginTransaction();
|
||||||
|
|
@ -200,5 +205,295 @@ class Book {
|
||||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function convertChaptersContent($book_id, $from_editor, $to_editor) {
|
||||||
|
try {
|
||||||
|
$this->pdo->beginTransaction();
|
||||||
|
|
||||||
|
$chapters = $this->getAllChapters($book_id);
|
||||||
|
|
||||||
|
foreach ($chapters as $chapter) {
|
||||||
|
$converted_content = $this->convertContent(
|
||||||
|
$chapter['content'],
|
||||||
|
$from_editor,
|
||||||
|
$to_editor
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->updateChapterContent($chapter['id'], $converted_content);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->pdo->commit();
|
||||||
|
return true;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->pdo->rollBack();
|
||||||
|
error_log("Error converting chapters: " . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getAllChapters($book_id) {
|
||||||
|
$stmt = $this->pdo->prepare("SELECT id, content FROM chapters WHERE book_id = ?");
|
||||||
|
$stmt->execute([$book_id]);
|
||||||
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function updateChapterContent($chapter_id, $content) {
|
||||||
|
$word_count = $this->countWords($content);
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
UPDATE chapters
|
||||||
|
SET content = ?, word_count = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
return $stmt->execute([$content, $word_count, $chapter_id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function countWords($text) {
|
||||||
|
$text = strip_tags($text);
|
||||||
|
$text = preg_replace('/[^\p{L}\p{N}\s]/u', ' ', $text);
|
||||||
|
$words = preg_split('/\s+/', $text);
|
||||||
|
$words = array_filter($words);
|
||||||
|
return count($words);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function convertContent($content, $from_editor, $to_editor) {
|
||||||
|
if ($from_editor === $to_editor) {
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($from_editor === 'markdown' && $to_editor === 'html') {
|
||||||
|
// Markdown to HTML с улучшенной обработкой абзацев
|
||||||
|
return $this->markdownToHtmlWithParagraphs($content);
|
||||||
|
} elseif ($from_editor === 'html' && $to_editor === 'markdown') {
|
||||||
|
// HTML to Markdown
|
||||||
|
return $this->htmlToMarkdown($content);
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("Error converting content from {$from_editor} to {$to_editor}: " . $e->getMessage());
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function markdownToHtmlWithParagraphs($markdown) {
|
||||||
|
$parsedown = new ParsedownExtra();
|
||||||
|
|
||||||
|
// Включаем разметку строк для лучшей обработки абзацев
|
||||||
|
$parsedown->setBreaksEnabled(true);
|
||||||
|
|
||||||
|
// Обрабатываем Markdown
|
||||||
|
$html = $parsedown->text($markdown);
|
||||||
|
|
||||||
|
// Дополнительная обработка для обеспечения правильной структуры абзацев
|
||||||
|
$html = $this->ensureParagraphStructure($html);
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureParagraphStructure($html) {
|
||||||
|
// Если HTML не содержит тегов абзацев или div'ов, оборачиваем в <p>
|
||||||
|
if (!preg_match('/<(p|div|h[1-6]|blockquote|pre|ul|ol|li)/i', $html)) {
|
||||||
|
// Разбиваем на строки и оборачиваем каждую непустую строку в <p>
|
||||||
|
$lines = explode("\n", trim($html));
|
||||||
|
$wrappedLines = [];
|
||||||
|
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$line = trim($line);
|
||||||
|
if (!empty($line)) {
|
||||||
|
// Пропускаем уже обернутые строки
|
||||||
|
if (!preg_match('/^<[^>]+>/', $line) || preg_match('/^<(p|div|h[1-6])/i', $line)) {
|
||||||
|
$wrappedLines[] = $line;
|
||||||
|
} else {
|
||||||
|
$wrappedLines[] = "<p>{$line}</p>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$html = implode("\n", $wrappedLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Убеждаемся, что теги правильно закрыты
|
||||||
|
$html = $this->balanceTags($html);
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function balanceTags($html) {
|
||||||
|
// Простая балансировка тегов - в реальном проекте лучше использовать DOMDocument
|
||||||
|
$tags = [
|
||||||
|
'p' => 0,
|
||||||
|
'div' => 0,
|
||||||
|
'span' => 0,
|
||||||
|
'strong' => 0,
|
||||||
|
'em' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Счетчик открывающих и закрывающих тегов
|
||||||
|
foreach ($tags as $tag => &$count) {
|
||||||
|
$open = substr_count($html, "<{$tag}>") + substr_count($html, "<{$tag} ");
|
||||||
|
$close = substr_count($html, "</{$tag}>");
|
||||||
|
$count = $open - $close;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем недостающие закрывающие теги
|
||||||
|
foreach ($tags as $tag => $count) {
|
||||||
|
if ($count > 0) {
|
||||||
|
$html .= str_repeat("</{$tag}>", $count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
private function htmlToMarkdown($html) {
|
||||||
|
// Сначала нормализуем HTML структуру
|
||||||
|
$html = $this->normalizeHtml($html);
|
||||||
|
|
||||||
|
// Базовая конвертация HTML в Markdown
|
||||||
|
$markdown = $html;
|
||||||
|
|
||||||
|
// Обрабатываем абзацы - заменяем на двойные переносы строк
|
||||||
|
$markdown = preg_replace('/<p[^>]*>(.*?)<\/p>/is', "$1\n\n", $markdown);
|
||||||
|
|
||||||
|
// Обрабатываем разрывы строк
|
||||||
|
$markdown = preg_replace('/<br[^>]*>\s*<\/br[^>]*>/i', "\n", $markdown);
|
||||||
|
$markdown = preg_replace('/<br[^>]*>/i', " \n", $markdown); // Два пробела для Markdown разрыва
|
||||||
|
|
||||||
|
// Заголовки
|
||||||
|
$markdown = preg_replace('/<h1[^>]*>(.*?)<\/h1>/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('/<h4[^>]*>(.*?)<\/h4>/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('/<strong[^>]*>(.*?)<\/strong>/is', '**$1**', $markdown);
|
||||||
|
$markdown = preg_replace('/<b[^>]*>(.*?)<\/b>/is', '**$1**', $markdown);
|
||||||
|
|
||||||
|
// Курсив
|
||||||
|
$markdown = preg_replace('/<em[^>]*>(.*?)<\/em>/is', '*$1*', $markdown);
|
||||||
|
$markdown = preg_replace('/<i[^>]*>(.*?)<\/i>/is', '*$1*', $markdown);
|
||||||
|
|
||||||
|
// Подчеркивание (не стандартно в Markdown, но обрабатываем)
|
||||||
|
$markdown = preg_replace('/<u[^>]*>(.*?)<\/u>/is', '<u>$1</u>', $markdown);
|
||||||
|
|
||||||
|
// Зачеркивание
|
||||||
|
$markdown = preg_replace('/<s[^>]*>(.*?)<\/s>/is', '~~$1~~', $markdown);
|
||||||
|
$markdown = preg_replace('/<strike[^>]*>(.*?)<\/strike>/is', '~~$1~~', $markdown);
|
||||||
|
$markdown = preg_replace('/<del[^>]*>(.*?)<\/del>/is', '~~$1~~', $markdown);
|
||||||
|
|
||||||
|
// Списки
|
||||||
|
$markdown = preg_replace('/<li[^>]*>(.*?)<\/li>/is', '- $1', $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('/<code[^>]*>(.*?)<\/code>/is', '`$1`', $markdown);
|
||||||
|
$markdown = preg_replace('/<pre[^>]*>(.*?)<\/pre>/is', "```\n$1\n```", $markdown);
|
||||||
|
|
||||||
|
// Ссылки
|
||||||
|
$markdown = preg_replace('/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/is', '[$2]($1)', $markdown);
|
||||||
|
|
||||||
|
// Изображения
|
||||||
|
$markdown = preg_replace('/<img[^>]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*>/is', '', $markdown);
|
||||||
|
|
||||||
|
// Удаляем все остальные HTML-теги
|
||||||
|
$markdown = strip_tags($markdown);
|
||||||
|
|
||||||
|
// Чистим лишние пробелы и переносы
|
||||||
|
$markdown = preg_replace('/\n\s*\n\s*\n/', "\n\n", $markdown);
|
||||||
|
$markdown = preg_replace('/^\s+|\s+$/m', '', $markdown); // Trim каждой строки
|
||||||
|
$markdown = trim($markdown);
|
||||||
|
|
||||||
|
return $markdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeHtml($html) {
|
||||||
|
// Нормализуем HTML структуру перед конвертацией
|
||||||
|
$html = preg_replace('/<div[^>]*>(.*?)<\/div>/is', "<p>$1</p>", $html);
|
||||||
|
|
||||||
|
// Убираем лишние пробелы
|
||||||
|
$html = preg_replace('/\s+/', ' ', $html);
|
||||||
|
|
||||||
|
// Восстанавливаем структуру абзацев
|
||||||
|
$html = preg_replace('/([^>])\s*<\/(p|div)>\s*([^<])/', "$1</$2>\n\n$3", $html);
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function normalizeBookContent($book_id) {
|
||||||
|
try {
|
||||||
|
$chapters = $this->getAllChapters($book_id);
|
||||||
|
$book = $this->findById($book_id);
|
||||||
|
|
||||||
|
foreach ($chapters as $chapter) {
|
||||||
|
$normalized_content = '';
|
||||||
|
|
||||||
|
if ($book['editor_type'] == 'html') {
|
||||||
|
// Нормализуем HTML контент
|
||||||
|
$normalized_content = $this->normalizeHtmlContent($chapter['content']);
|
||||||
|
} else {
|
||||||
|
// Нормализуем Markdown контент
|
||||||
|
$normalized_content = $this->normalizeMarkdownContent($chapter['content']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($normalized_content !== $chapter['content']) {
|
||||||
|
$this->updateChapterContent($chapter['id'], $normalized_content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("Error normalizing book content: " . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeHtmlContent($html) {
|
||||||
|
// Простая нормализация HTML - оборачиваем текст без тегов в <p>
|
||||||
|
if (!preg_match('/<[^>]+>/', $html) && trim($html) !== '') {
|
||||||
|
// Если нет HTML тегов, оборачиваем в <p>
|
||||||
|
$lines = explode("\n", trim($html));
|
||||||
|
$wrapped = array_map(function($line) {
|
||||||
|
$line = trim($line);
|
||||||
|
return $line ? "<p>{$line}</p>" : '';
|
||||||
|
}, $lines);
|
||||||
|
return implode("\n", array_filter($wrapped));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Непустая строка
|
||||||
|
if (!$inParagraph && !empty($normalized) && end($normalized) !== '') {
|
||||||
|
// Добавляем пустую строку перед новым абзацем
|
||||||
|
$normalized[] = '';
|
||||||
|
}
|
||||||
|
$normalized[] = $line;
|
||||||
|
$inParagraph = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode("\n", $normalized);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
10
preview.php
10
preview.php
|
|
@ -7,9 +7,15 @@ $Parsedown = new ParsedownExtra();;
|
||||||
|
|
||||||
$content = $_POST['content'] ?? '';
|
$content = $_POST['content'] ?? '';
|
||||||
$title = $_POST['title'] ?? 'Предпросмотр';
|
$title = $_POST['title'] ?? 'Предпросмотр';
|
||||||
|
$editor_type = $_POST['editor_type'] ?? 'markdown'; // Новое поле
|
||||||
|
|
||||||
|
// Обрабатываем контент в зависимости от типа редактора
|
||||||
|
if ($editor_type == 'markdown') {
|
||||||
|
$html_content = $Parsedown->text($content);
|
||||||
|
} else {
|
||||||
|
$html_content = $content;
|
||||||
|
}
|
||||||
|
|
||||||
$Parsedown = new Parsedown();
|
|
||||||
$html_content = $Parsedown->text($content);
|
|
||||||
|
|
||||||
$page_title = "Предпросмотр: " . e($title);
|
$page_title = "Предпросмотр: " . e($title);
|
||||||
?>
|
?>
|
||||||
|
|
|
||||||
|
|
@ -161,8 +161,14 @@ include 'views/header.php';
|
||||||
<a href="#start" style="text-decoration: none; color: #666; font-size: 0.8em; margin-left: 1rem;">🔗</a>
|
<a href="#start" style="text-decoration: none; color: #666; font-size: 0.8em; margin-left: 1rem;">🔗</a>
|
||||||
</h2>
|
</h2>
|
||||||
<div class="chapter-content" style="line-height: 1.6; font-size: 1.1em;">
|
<div class="chapter-content" style="line-height: 1.6; font-size: 1.1em;">
|
||||||
|
<?php if ($book['editor_type'] == 'markdown'): ?>
|
||||||
<?= $Parsedown->text($chapter['content']) ?>
|
<?= $Parsedown->text($chapter['content']) ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<?= $chapter['content'] ?>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div style="margin-top: 1rem; padding-top: 0.5rem; border-top: 1px dashed #eee; color: #666; font-size: 0.9em;">
|
<div style="margin-top: 1rem; padding-top: 0.5rem; border-top: 1px dashed #eee; color: #666; font-size: 0.9em;">
|
||||||
<small>Обновлено: <?= date('d.m.Y', strtotime($chapter['updated_at'])) ?></small>
|
<small>Обновлено: <?= date('d.m.Y', strtotime($chapter['updated_at'])) ?></small>
|
||||||
<a href="#top" style="float: right; color: #007bff; text-decoration: none;">↑ Наверх</a>
|
<a href="#top" style="float: right; color: #007bff; text-decoration: none;">↑ Наверх</a>
|
||||||
|
|
|
||||||
|
|
@ -49,3 +49,16 @@
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<main class="container">
|
<main class="container">
|
||||||
|
<?php if (isset($_SESSION['info'])): ?>
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<?= e($_SESSION['info']) ?>
|
||||||
|
<?php unset($_SESSION['info']); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (isset($_SESSION['warning'])): ?>
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<?= e($_SESSION['warning']) ?>
|
||||||
|
<?php unset($_SESSION['warning']); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
Loading…
Reference in New Issue