findByShareToken($share_token); // Для публичного доступа - только опубликованные главы $chapters = $bookModel->getPublishedChapters($book['id']); $is_public = true; } elseif ($book_id && $user_id) { $book = $bookModel->findById($book_id); if (!$book || $book['user_id'] != $user_id) { $_SESSION['error'] = "Доступ запрещен"; redirect('books.php'); } // Для автора - все главы $chapters = $chapterModel->findByBook($book_id); $is_public = false; } else { $_SESSION['error'] = "Книга не найдена"; redirect('books.php'); } if (!$book) { $_SESSION['error'] = "Книга не найдена"; redirect('books.php'); } // Получаем информацию об авторе $author_info = "Неизвестный автор"; if ($book) { $stmt = $pdo->prepare("SELECT display_name, username FROM users WHERE id = ?"); $stmt->execute([$book['user_id']]); $author_info = $stmt->fetch(PDO::FETCH_ASSOC); if ($author_info['display_name'] !=""){ $author_name = $author_info['display_name']; }else{ $author_name = $author_info['username'] ; } } // Функция для преобразования Markdown в чистый текст с форматированием абзацев function markdownToPlainText($markdown) { // Обрабатываем диалоги (заменяем - на —) $markdown = preg_replace('/^- (.+)$/m', "— $1", $markdown); // Убираем Markdown разметку, но сохраняем переносы строк $text = $markdown; // Убираем заголовки $text = preg_replace('/^#+\s+/m', '', $text); // Убираем жирный и курсив $text = preg_replace('/\*\*(.*?)\*\*/', '$1', $text); $text = preg_replace('/\*(.*?)\*/', '$1', $text); $text = preg_replace('/__(.*?)__/', '$1', $text); $text = preg_replace('/_(.*?)_/', '$1', $text); // Убираем зачеркивание $text = preg_replace('/~~(.*?)~~/', '$1', $text); // Убираем код (встроенный) $text = preg_replace('/`(.*?)`/', '$1', $text); // Убираем блоки кода (сохраняем содержимое) $text = preg_replace('/```.*?\n(.*?)```/s', '$1', $text); // Убираем ссылки $text = preg_replace('/\[(.*?)\]\(.*?\)/', '$1', $text); // Обрабатываем списки - заменяем маркеры на * $text = preg_replace('/^[\*\-+]\s+/m', '* ', $text); $text = preg_replace('/^\d+\.\s+/m', '* ', $text); // Обрабатываем цитаты $text = preg_replace('/^>\s+/m', '', $text); return $text; } // Функция для разбивки Markdown на абзацы с сохранением структуры function markdownToParagraphs($markdown) { // Нормализуем переносы строк $text = str_replace(["\r\n", "\r"], "\n", $markdown); // Обрабатываем диалоги (заменяем - на —) $text = preg_replace('/^- (.+)$/m', "— $1", $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 (str_starts_with($trimmedLine, '—')) { if (!empty($currentParagraph)) { $paragraphs[] = $currentParagraph; } $currentParagraph = $trimmedLine; $paragraphs[] = $currentParagraph; $currentParagraph = ''; continue; } // Заголовки (начинаются с #) всегда начинают новый абзац if (str_starts_with($trimmedLine, '#')) { if (!empty($currentParagraph)) { $paragraphs[] = $currentParagraph; } $currentParagraph = preg_replace('/^#+\s+/', '', $trimmedLine); $paragraphs[] = $currentParagraph; $currentParagraph = ''; continue; } // Обычный текст - добавляем к текущему абзацу if (!empty($currentParagraph)) { $currentParagraph .= ' ' . $trimmedLine; } else { $currentParagraph = $trimmedLine; } } // Добавляем последний абзац if (!empty($currentParagraph)) { $paragraphs[] = $currentParagraph; } return $paragraphs; } // Функция для очистки Markdown разметки function cleanMarkdown($markdown) { $text = $markdown; // Убираем заголовки $text = preg_replace('/^#+\s+/m', '', $text); // Убираем жирный и курсив $text = preg_replace('/\*\*(.*?)\*\*/', '$1', $text); $text = preg_replace('/\*(.*?)\*/', '$1', $text); $text = preg_replace('/__(.*?)__/', '$1', $text); $text = preg_replace('/_(.*?)_/', '$1', $text); // Убираем зачеркивание $text = preg_replace('/~~(.*?)~~/', '$1', $text); // Убираем код $text = preg_replace('/`(.*?)`/', '$1', $text); $text = preg_replace('/```.*?\n(.*?)```/s', '$1', $text); // Убираем ссылки $text = preg_replace('/\[(.*?)\]\(.*?\)/', '$1', $text); // Обрабатываем списки - убираем маркеры $text = preg_replace('/^[\*\-+]\s+/m', '', $text); $text = preg_replace('/^\d+\.\s+/m', '', $text); // Обрабатываем цитаты $text = preg_replace('/^>\s+/m', '', $text); return $text; } // Функция для форматирования текста с сохранением абзацев и диалогов function formatPlainText($text) { $lines = explode("\n", $text); $formatted = []; $in_paragraph = false; foreach ($lines as $line) { $line = trim($line); if (empty($line)) { if ($in_paragraph) { $formatted[] = ''; // Пустая строка для разделения абзацев $in_paragraph = false; } continue; } // Диалоги начинаются с — if (str_starts_with($line, '—')) { if ($in_paragraph) { $formatted[] = ''; // Разделяем абзацы перед диалогом } $formatted[] = $line; $formatted[] = ''; // Пустая строка после диалога $in_paragraph = false; } else { // Обычный текст $formatted[] = $line; $in_paragraph = true; } } return implode("\n", array_filter($formatted, function($line) { return $line !== '' || !empty($line); })); } // Обработка экспорта switch ($format) { case 'pdf': exportPDF($book, $chapters, $is_public, $author_name); break; case 'docx': exportDOCX($book, $chapters, $is_public, $author_name); break; case 'html': exportHTML($book, $chapters, $is_public, $author_name); break; case 'txt': exportTXT($book, $chapters, $is_public, $author_name); break; default: $_SESSION['error'] = "Неверный формат экспорта"; redirect($share_token ? "view_book.php?share_token=$share_token" : "book_edit.php?id=$book_id"); } function exportPDF($book, $chapters, $is_public, $author_name) { global $Parsedown; $pdf = new TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false); // Устанавливаем метаданные документа $pdf->SetCreator(APP_NAME); $pdf->SetAuthor($author_name); $pdf->SetTitle($book['title']); $pdf->SetSubject($book['genre'] ?? ''); // Устанавливаем margins $pdf->SetMargins(15, 25, 15); $pdf->SetHeaderMargin(10); $pdf->SetFooterMargin(10); // Устанавливаем авто разрыв страниц $pdf->SetAutoPageBreak(TRUE, 15); // Добавляем страницу $pdf->AddPage(); // Устанавливаем шрифт с поддержкой кириллицы $pdf->SetFont('dejavusans', '', 12); // Заголовок книги $pdf->SetFont('dejavusans', 'B', 18); $pdf->Cell(0, 10, $book['title'], 0, 1, 'C'); $pdf->Ln(2); // Автор $pdf->SetFont('dejavusans', 'I', 14); $pdf->Cell(0, 10, $author_name, 0, 1, 'C'); $pdf->Ln(5); // Обложка книги if (!empty($book['cover_image'])) { $cover_path = COVERS_PATH . $book['cover_image']; if (file_exists($cover_path)) { list($width, $height) = getimagesize($cover_path); $max_width = 80; $ratio = $width / $height; $new_height = $max_width / $ratio; $x = (210 - $max_width) / 2; $pdf->Image($cover_path, $x, $pdf->GetY(), $max_width, $new_height, '', '', 'N', false, 300, '', false, false, 0, false, false, false); $pdf->Ln($new_height + 5); } } // Жанр if (!empty($book['genre'])) { $pdf->SetFont('dejavusans', 'I', 12); $pdf->Cell(0, 10, 'Жанр: ' . $book['genre'], 0, 1, 'C'); $pdf->Ln(5); } // Описание if (!empty($book['description'])) { $pdf->SetFont('dejavusans', '', 11); $pdf->MultiCell(0, 6, $book['description'], 0, 'J'); $pdf->Ln(10); } // Интерактивное оглавление $chapterLinks = []; if (!empty($chapters)) { $pdf->SetFont('dejavusans', 'B', 14); $pdf->Cell(0, 10, 'Оглавление', 0, 1, 'C'); $pdf->Ln(5); $toc_page = $pdf->getPage(); $pdf->SetFont('dejavusans', '', 11); foreach ($chapters as $index => $chapter) { $chapter_number = $index + 1; $link = $pdf->AddLink(); $chapterLinks[$chapter['id']] = $link; // Сохраняем ссылку для этой главы $pdf->Cell(0, 6, "{$chapter_number}. {$chapter['title']}", 0, 1, 'L', false, $link); } $pdf->Ln(10); } // Разделитель $pdf->Line(15, $pdf->GetY(), 195, $pdf->GetY()); $pdf->Ln(10); // Главы с закладками и правильными ссылками foreach ($chapters as $index => $chapter) { // Добавляем новую страницу для каждой главы $pdf->AddPage(); // УСТАНАВЛИВАЕМ ЯКОРЬ ДЛЯ ССЫЛКИ if (isset($chapterLinks[$chapter['id']])) { $pdf->SetLink($chapterLinks[$chapter['id']]); } // Устанавливаем закладку для этой главы $pdf->Bookmark($chapter['title'], 0, 0, '', 'B', array(0, 0, 0)); // Название главы $pdf->SetFont('dejavusans', 'B', 14); $pdf->Cell(0, 8, $chapter['title'], 0, 1); $pdf->Ln(2); // Контент главы $pdf->SetFont('dejavusans', '', 11); if ($book['editor_type'] == 'markdown') { $htmlContent = $Parsedown->text($chapter['content']); } else { $htmlContent = $chapter['content']; } $pdf->writeHTML($htmlContent, true, false, true, false, ''); $pdf->Ln(8); } // Футер с информацией $pdf->SetY(-25); $pdf->SetFont('dejavusans', 'I', 8); $pdf->Cell(0, 6, 'Экспортировано из ' . APP_NAME . ' - ' . date('d.m.Y H:i'), 0, 1, 'C'); $pdf->Cell(0, 6, 'Автор: ' . $author_name . ' | Всего глав: ' . count($chapters) . ' | Всего слов: ' . array_sum(array_column($chapters, 'word_count')), 0, 1, 'C'); // Отправляем файл $filename = cleanFilename($book['title']) . '.pdf'; $pdf->Output($filename, 'D'); exit; } function exportDOCX($book, $chapters, $is_public, $author_name) { global $Parsedown; $phpWord = new PhpWord(); // Стили документа $phpWord->setDefaultFontName('Times New Roman'); $phpWord->setDefaultFontSize(12); // Секция документа $section = $phpWord->addSection(); // Заголовок книги $section->addText($book['title'], ['bold' => true, 'size' => 16], ['alignment' => 'center']); $section->addTextBreak(1); // Автор $section->addText($author_name, ['italic' => true, 'size' => 14], ['alignment' => 'center']); $section->addTextBreak(2); // Обложка книги if (!empty($book['cover_image'])) { $cover_path = COVERS_PATH . $book['cover_image']; if (file_exists($cover_path)) { $section->addImage($cover_path, [ 'width' => 150, 'height' => 225, 'alignment' => 'center' ]); $section->addTextBreak(2); } } // Жанр if (!empty($book['genre'])) { $section->addText('Жанр: ' . $book['genre'], ['italic' => true], ['alignment' => 'center']); $section->addTextBreak(1); } // Описание if (!empty($book['description'])) { if ($book['editor_type'] == 'markdown') { $descriptionParagraphs = markdownToParagraphs($book['description']); } else { $descriptionParagraphs = htmlToParagraphs($book['description']); } foreach ($descriptionParagraphs as $paragraph) { if (!empty(trim($paragraph))) { $section->addText($paragraph); } } $section->addTextBreak(2); } // Интерактивное оглавление if (!empty($chapters)) { $section->addText('Оглавление', ['bold' => true, 'size' => 14], ['alignment' => 'center']); $section->addTextBreak(1); foreach ($chapters as $index => $chapter) { $chapter_number = $index + 1; // Создаем гиперссылку на заголовок главы $section->addLink("chapter_{$chapter['id']}", "{$chapter_number}. {$chapter['title']}", null, null, true); $section->addTextBreak(1); } $section->addTextBreak(2); } // Разделитель $section->addPageBreak(); // Главы с закладками foreach ($chapters as $index => $chapter) { // Добавляем закладку для главы $section->addBookmark("chapter_{$chapter['id']}"); // Заголовок главы $section->addText($chapter['title'], ['bold' => true, 'size' => 14]); $section->addTextBreak(1); // Получаем очищенный текст и разбиваем на абзацы в зависимости от типа редактора if ($book['editor_type'] == 'markdown') { $cleanContent = cleanMarkdown($chapter['content']); $paragraphs = markdownToParagraphs($cleanContent); } else { $cleanContent = strip_tags($chapter['content']); $paragraphs = htmlToParagraphs($chapter['content']); } // Добавляем каждый абзац foreach ($paragraphs as $paragraph) { if (!empty(trim($paragraph))) { $section->addText($paragraph); $section->addTextBreak(1); } } // Добавляем разрыв страницы между главами (кроме последней) if ($index < count($chapters) - 1) { $section->addPageBreak(); } } // Футер $section->addTextBreak(2); $section->addText('Экспортировано из ' . APP_NAME . ' - ' . date('d.m.Y H:i'), ['italic' => true, 'size' => 9]); $section->addText('Автор: ' . $author_name . ' | Всего глав: ' . count($chapters) . ' | Всего слов: ' . array_sum(array_column($chapters, 'word_count')), ['italic' => true, 'size' => 9]); // Сохраняем и отправляем $filename = cleanFilename($book['title']) . '.docx'; header('Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document'); header('Content-Disposition: attachment; filename="' . $filename . '"'); $objWriter = IOFactory::createWriter($phpWord, 'Word2007'); $objWriter->save('php://output'); 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) { global $Parsedown; $html = '