Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added bookshelf export functionality for PDF, HTML, plain text, and markdown formats, including route definitions and test cases #40

Open
wants to merge 3 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions app/Entities/Controllers/BookshelfExportController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php

namespace BookStack\Entities\Controllers;

use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Queries\BookshelfQueries;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Http\Controller;
use BookStack\Http\Request;
use Illuminate\Support\Facades\Response;
use Throwable;

class BookshelfExportController extends Controller
{
public function __construct(
protected BookshelfQueries $queries,
protected ExportFormatter $exportFormatter,
) {
$this->middleware('can:content-export');
}

/**
* Export a book as a PDF file.
*
* @throws Throwable
*/
public function pdf(Request $request, string $bookshelfSlug)
{
$bookshelf = $this->queries->findVisibleBySlugOrFail($bookshelfSlug);
if ($request['split'] === true) {
return $this->downloadAllInZip($bookshelf, 'pdf');
} else {
$htmlContent = $this->exportFormatter->bookshelfToPdf($bookshelf);

return $this->download()->directly($htmlContent, $bookshelfSlug . '.pdf');
}
}

/**
* Export a book as a contained HTML file.
*
* @throws Throwable
*/
public function html(Request $request, string $bookshelfSlug)
{
$bookshelf = $this->queries->findVisibleBySlugOrFail($bookshelfSlug);
if ($request['split'] === true) {
return $this->downloadAllInZip($bookshelf, 'html');
} else {
$htmlContent = $this->exportFormatter->bookshelfToContainedHtml($bookshelf);

return $this->download()->directly($htmlContent, $bookshelfSlug . '.html');
}
}

/**
* Export a book as a plain text file.
*/
public function plainText(Request $request, string $bookshelfSlug)
{
$bookshelf = $this->queries->findVisibleBySlugOrFail($bookshelfSlug);
if ($request['split'] === true) {
return $this->downloadAllInZip($bookshelf, 'txt');
} else {
$htmlContent = $this->exportFormatter->bookshelfToPlainText($bookshelf);

return $this->download()->directly($htmlContent, $bookshelfSlug . '.txt');
}
}

/**
* Export a book as a markdown file.
*/
public function markdown(Request $request, string $bookshelfSlug)
{
$bookshelf = $this->queries->findVisibleBySlugOrFail($bookshelfSlug);
if ($request['split'] === true) {
return $this->downloadAllInZip($bookshelf, 'md');
} else {
$htmlContent = $this->exportFormatter->bookshelfToMarkdown($bookshelf);

return $this->download()->directly($htmlContent, $bookshelfSlug . '.md');
}
}

public function downloadAllInZip(Bookshelf $bookshelf, string $type)
{
$bookshelf->load('books');

$zip = new \ZipArchive();

$tempFilePath = storage_path('app/public/' . $bookshelf->slug . '.zip');
if ($zip->open($tempFilePath, \ZipArchive::CREATE) === true) {
foreach ($bookshelf->books as $book) {
$pdfContent = $this->getContentBasedOntype($book, $type);
$zip->addFromString($book->slug, $pdfContent);
}
$zip->close();

return Response::download($tempFilePath)->deleteFileAfterSend(true);
}
}

public function getContentBasedOntype(Book $book, string $type)
{
switch ($type) {
case 'pdf':
return $this->exportFormatter->bookToPdf($book);
break;


case 'html':
return $this->exportFormatter->bookToContainedHtml($book);
break;

case 'txt':
return $this->exportFormatter->bookToPlainText($book);
break;

case 'md':
return $this->exportFormatter->bookToMarkdown($book);
break;
default:
return "";
break;
}
}
}
23 changes: 23 additions & 0 deletions app/Entities/Tools/BookshelfContents.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace BookStack\Entities\Tools;

use BookStack\Entities\Models\Bookshelf;

class BookshelfContents
{
public function __construct(protected Bookshelf $bookshelf)
{
}

public function getTree(bool $renderPages = false)
{
$books = $this->bookshelf->books()->scopes('visible')->get();

$books->each(function ($book) use ($renderPages) {
$book->setAttribute('bookChildrens', (new BookContents($book))->getTree(false, $renderPages));
});

return collect($books);
}
}
67 changes: 67 additions & 0 deletions app/Entities/Tools/ExportFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace BookStack\Entities\Tools;

use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Tools\Markdown\HtmlToMarkdown;
Expand Down Expand Up @@ -82,6 +83,25 @@ public function bookToContainedHtml(Book $book): string
return $this->containHtml($html);
}

/**
* Convert a bookshelf to a self-contained HTML file.
*
* @throws Throwable
*/
public function bookshelfToContainedHtml(Bookshelf $bookshelf): string
{
$bookshelfTree = (new BookshelfContents($bookshelf))->getTree(true);
$html = view('exports.shelves', [
'bookshelf' => $bookshelf,
'bookshelfChildrens' => $bookshelfTree,
'format' => 'pdf',
'engine' => $this->pdfGenerator->getActiveEngine(),
'locale' => user()->getLocale(),
])->render();

return $this->containHtml($html);
}

/**
* Convert a page to a PDF file.
*
Expand Down Expand Up @@ -142,6 +162,22 @@ public function bookToPdf(Book $book): string
return $this->htmlToPdf($html);
}


public function bookshelfToPdf(Bookshelf $bookshelf): string
{
$bookshelfTree = (new BookshelfContents($bookshelf))->getTree(true);

$html = view('exports.shelves', [
'bookshelf' => $bookshelf,
'bookshelfChildrens' => $bookshelfTree,
'format' => 'pdf',
'engine' => $this->pdfGenerator->getActiveEngine(),
'locale' => user()->getLocale(),
])->render();

return $this->htmlToPdf($html);
}

/**
* Convert normal web-page HTML to a PDF.
*
Expand Down Expand Up @@ -297,6 +333,23 @@ public function bookToPlainText(Book $book): string
return $text . implode("\n\n", $parts);
}

/**
* Convert a book into a plain text string.
*/
public function bookshelfToPlainText(Bookshelf $bookshelf): string
{
$bookshelfTree = (new BookshelfContents($bookshelf))->getTree(true);
$text = $bookshelf->name . "\n" . $bookshelf->description;
$text = rtrim($text) . "\n\n";

$parts = [];
foreach ($bookshelfTree as $bookshelfChild) {
$parts[] = $this->bookToPlainText($bookshelfChild);
}

return $text . implode("\n\n", $parts);
}

/**
* Convert a page to a Markdown file.
*/
Expand Down Expand Up @@ -340,4 +393,18 @@ public function bookToMarkdown(Book $book): string

return trim($text);
}

/**
* Convert a bookshelf into a plain text string.
*/
public function bookshelfToMarkdown(Bookshelf $bookshelf): string
{
$bookshelfTree = (new BookshelfContents($bookshelf))->getTree(true);
$text = '# ' . $bookshelf->name . "\n\n";
foreach ($bookshelfTree as $bookshelfChild) {
$text .= $this->bookToMarkdown($bookshelfChild) . "\n\n";
}

return trim($text);
}
}
2 changes: 2 additions & 0 deletions lang/ar/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
'status_inactive' => 'غير نشط',
'never' => 'مطلقاً',
'none' => 'لا شَيْء',
'multiple_or_single' => "متعدد أو واحد",
'multiple_or_single_description' => "هل تريد تنزيل الملفات في ملف واحد أو أرشيف Zip (ملفات متعددة)",

// Header
'homepage' => 'الصفحة الرئيسية',
Expand Down
2 changes: 2 additions & 0 deletions lang/bg/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
'status_inactive' => 'Неактивен',
'never' => 'Никога',
'none' => 'Нищо',
'multiple_or_single' => "Множество или единично",
'multiple_or_single_description' => "Искате ли да изтеглите файлове в един файл или ZIP архив (множество файлове)",

// Header
'homepage' => 'Начална страница',
Expand Down
2 changes: 2 additions & 0 deletions lang/bs/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
'status_inactive' => 'Inactive',
'never' => 'Never',
'none' => 'None',
'multiple_or_single' => "Više ili jedno",
'multiple_or_single_description' => "Želite li preuzeti datoteke u jednoj datoteci ili ZIP arhivu (više datoteka)",

// Header
'homepage' => 'Homepage',
Expand Down
2 changes: 2 additions & 0 deletions lang/ca/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
'status_inactive' => 'Inactiu',
'never' => 'Mai',
'none' => 'Cap',
'multiple_or_single' => "Múltiple o únic",
'multiple_or_single_description' => "Voleu descarregar fitxers en un únic fitxer o en un arxiu ZIP (diversos fitxers)",

// Header
'homepage' => 'Pàgina d’inici',
Expand Down
2 changes: 2 additions & 0 deletions lang/cs/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
'status_inactive' => 'Neaktivní',
'never' => 'Nikdy',
'none' => 'Žádná',
'multiple_or_single' => "Více nebo jediné",
'multiple_or_single_description' => "Chcete stáhnout soubory do jednoho souboru nebo ZIP archivu (více souborů)",

// Header
'homepage' => 'Domovská stránka',
Expand Down
2 changes: 2 additions & 0 deletions lang/cy/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
'status_inactive' => 'Anweithredol',
'never' => 'Byth',
'none' => 'Dim un',
'multiple_or_single' => "Lluosog neu Unigol",
'multiple_or_single_description' => "Eisiau lawrlwytho ffeiliau mewn Un Ffeil neu Archif ZIP (Ffeiliau Lluosog)",

// Header
'homepage' => 'Tudalen cartref',
Expand Down
2 changes: 2 additions & 0 deletions lang/da/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
'status_inactive' => 'Inaktiv',
'never' => 'Aldrig',
'none' => 'Ingen',
'multiple_or_single' => "Flere eller enkelt",
'multiple_or_single_description' => "Vil du downloade filer i en enkelt fil eller en ZIP-arkiv (flere filer)",

// Header
'homepage' => 'Forside',
Expand Down
2 changes: 2 additions & 0 deletions lang/de/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
'status_inactive' => 'Inaktiv',
'never' => 'Niemals',
'none' => 'Nichts',
'multiple_or_single' => "Mehrfach oder Einzel",
'multiple_or_single_description' => "Möchten Sie Dateien in einer einzelnen Datei oder einem ZIP-Archiv (mehrere Dateien) herunterladen?",

// Header
'homepage' => 'Startseite',
Expand Down
2 changes: 2 additions & 0 deletions lang/de_informal/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
'status_inactive' => 'Inaktiv',
'never' => 'Niemals',
'none' => 'Keine',
'multiple_or_single' => "Mehrfach oder Einzel",
'multiple_or_single_description' => "Möchtest du Dateien in einer einzelnen Datei oder einem ZIP-Archiv (mehrere Dateien) herunterladen?",

// Header
'homepage' => 'Startseite',
Expand Down
2 changes: 2 additions & 0 deletions lang/el/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
'status_inactive' => 'Αδρανής',
'never' => 'Ποτέ',
'none' => 'Κανένας',
'multiple_or_single' => "Πολλαπλές ή Μονές",
'multiple_or_single_description' => "Θέλετε να κατεβάσετε αρχεία σε ένα μόνο αρχείο ή σε ZIP αρχείο (πολλαπλά αρχεία)",

// Header
'homepage' => 'Αρχική σελίδα',
Expand Down
2 changes: 2 additions & 0 deletions lang/en/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
'status_inactive' => 'Inactive',
'never' => 'Never',
'none' => 'None',
'multiple_or_single' => "Multiple or Single",
'multiple_or_single_description' => "Want to Download Files in Single File or Zip Archive(Multiple Files)",

// Header
'homepage' => 'Homepage',
Expand Down
2 changes: 2 additions & 0 deletions lang/es/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
'status_inactive' => 'Inactive',
'never' => 'Nunca',
'none' => 'Ninguno',
'multiple_or_single' => "Múltiple o único",
'multiple_or_single_description' => "¿Deseas descargar archivos en un solo archivo o en un archivo ZIP (múltiples archivos)?",

// Header
'homepage' => 'Página de Inicio',
Expand Down
2 changes: 2 additions & 0 deletions lang/es_AR/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
'status_inactive' => 'Inactivo',
'never' => 'Nunca',
'none' => 'Ninguno',
'multiple_or_single' => "Múltiple o único",
'multiple_or_single_description' => "¿Deseas descargar archivos en un solo archivo o en un archivo ZIP (múltiples archivos)?",

// Header
'homepage' => 'Página de Inicio',
Expand Down
2 changes: 2 additions & 0 deletions lang/et/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
'status_inactive' => 'Mitteaktiivne',
'never' => 'Mitte kunagi',
'none' => 'Puudub',
'multiple_or_single' => "Mitu või üksik",
'multiple_or_single_description' => "Kas soovite failid alla laadida ühte faili või ZIP-arhiivi (mitu faili)?",

// Header
'homepage' => 'Avaleht',
Expand Down
2 changes: 2 additions & 0 deletions lang/eu/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
'status_inactive' => 'Inaktibo',
'never' => 'Inoiz ez',
'none' => 'Bat ere ez',
'multiple_or_single' => "Anitz edo bakar",
'multiple_or_single_description' => "Fitxategiak fitxategi bakar batean edo ZIP artxibo batean (fitxategi anitzak) deskargatu nahi dituzu?",

// Header
'homepage' => 'Homepage',
Expand Down
2 changes: 2 additions & 0 deletions lang/fa/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
'status_inactive' => 'غیر فعال',
'never' => 'هرگز',
'none' => 'هیچکدام',
'multiple_or_single' => "چندگانه یا تکی",
'multiple_or_single_description' => "آیا می‌خواهید فایل‌ها را در یک فایل واحد یا آرشیو ZIP (چندین فایل) دانلود کنید؟",

// Header
'homepage' => 'صفحه اصلی',
Expand Down
Loading
Loading