Skip to content

Commit ea6161a

Browse files
Added bookshelf export functionality for PDF, HTML, plain text, and Markdown formats, including route definitions and test cases
1 parent 9f15e88 commit ea6161a

File tree

9 files changed

+267
-0
lines changed

9 files changed

+267
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
namespace BookStack\Entities\Controllers;
4+
5+
use BookStack\Entities\Queries\BookshelfQueries;
6+
use BookStack\Entities\Tools\ExportFormatter;
7+
use BookStack\Http\Controller;
8+
use Throwable;
9+
10+
class BookshelfExportController extends Controller
11+
{
12+
public function __construct(
13+
protected BookshelfQueries $queries,
14+
protected ExportFormatter $exportFormatter,
15+
) {
16+
$this->middleware('can:content-export');
17+
}
18+
19+
/**
20+
* Export a book as a PDF file.
21+
*
22+
* @throws Throwable
23+
*/
24+
public function pdf(string $bookshelfSlug)
25+
{
26+
$bookshelf = $this->queries->findVisibleBySlugOrFail($bookshelfSlug);
27+
$pdfContent = $this->exportFormatter->bookshelfToPdf($bookshelf);
28+
29+
return $this->download()->directly($pdfContent, $bookshelfSlug . '.pdf');
30+
}
31+
32+
/**
33+
* Export a book as a contained HTML file.
34+
*
35+
* @throws Throwable
36+
*/
37+
public function html(string $bookshelfSlug)
38+
{
39+
$bookshelf = $this->queries->findVisibleBySlugOrFail($bookshelfSlug);
40+
$htmlContent = $this->exportFormatter->bookshelfToContainedHtml($bookshelf);
41+
42+
return $this->download()->directly($htmlContent, $bookshelfSlug . '.html');
43+
}
44+
45+
/**
46+
* Export a book as a plain text file.
47+
*/
48+
public function plainText(string $bookshelfSlug)
49+
{
50+
$bookshelf = $this->queries->findVisibleBySlugOrFail($bookshelfSlug);
51+
$textContent = $this->exportFormatter->bookshelfToPlainText($bookshelf);
52+
53+
return $this->download()->directly($textContent, $bookshelfSlug . '.txt');
54+
}
55+
56+
/**
57+
* Export a book as a markdown file.
58+
*/
59+
public function markdown(string $bookshelfSlug)
60+
{
61+
$bookshelf = $this->queries->findVisibleBySlugOrFail($bookshelfSlug);
62+
$textContent = $this->exportFormatter->bookshelfToMarkdown($bookshelf);
63+
64+
return $this->download()->directly($textContent, $bookshelfSlug . '.md');
65+
}
66+
}
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace BookStack\Entities\Tools;
4+
5+
use BookStack\Entities\Models\Bookshelf;
6+
7+
class BookshelfContents
8+
{
9+
public function __construct(protected Bookshelf $bookshelf)
10+
{
11+
}
12+
13+
public function getTree(bool $renderPages = false)
14+
{
15+
$books = $this->bookshelf->books()->scopes('visible')->get();
16+
17+
$books->each(function ($book) use ($renderPages) {
18+
$book->setAttribute('bookChildrens', (new BookContents($book))->getTree(false, $renderPages));
19+
});
20+
21+
return collect($books);
22+
}
23+
}

app/Entities/Tools/ExportFormatter.php

+67
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace BookStack\Entities\Tools;
44

55
use BookStack\Entities\Models\Book;
6+
use BookStack\Entities\Models\Bookshelf;
67
use BookStack\Entities\Models\Chapter;
78
use BookStack\Entities\Models\Page;
89
use BookStack\Entities\Tools\Markdown\HtmlToMarkdown;
@@ -82,6 +83,25 @@ public function bookToContainedHtml(Book $book): string
8283
return $this->containHtml($html);
8384
}
8485

86+
/**
87+
* Convert a bookshelf to a self-contained HTML file.
88+
*
89+
* @throws Throwable
90+
*/
91+
public function bookshelfToContainedHtml(Bookshelf $bookshelf): string
92+
{
93+
$bookshelfTree = (new BookshelfContents($bookshelf))->getTree(true);
94+
$html = view('exports.shelves', [
95+
'bookshelf' => $bookshelf,
96+
'bookshelfChildrens' => $bookshelfTree,
97+
'format' => 'pdf',
98+
'engine' => $this->pdfGenerator->getActiveEngine(),
99+
'locale' => user()->getLocale(),
100+
])->render();
101+
102+
return $this->containHtml($html);
103+
}
104+
85105
/**
86106
* Convert a page to a PDF file.
87107
*
@@ -142,6 +162,22 @@ public function bookToPdf(Book $book): string
142162
return $this->htmlToPdf($html);
143163
}
144164

165+
166+
public function bookshelfToPdf(Bookshelf $bookshelf): string
167+
{
168+
$bookshelfTree = (new BookshelfContents($bookshelf))->getTree(true);
169+
170+
$html = view('exports.shelves', [
171+
'bookshelf' => $bookshelf,
172+
'bookshelfChildrens' => $bookshelfTree,
173+
'format' => 'pdf',
174+
'engine' => $this->pdfGenerator->getActiveEngine(),
175+
'locale' => user()->getLocale(),
176+
])->render();
177+
178+
return $this->htmlToPdf($html);
179+
}
180+
145181
/**
146182
* Convert normal web-page HTML to a PDF.
147183
*
@@ -297,6 +333,23 @@ public function bookToPlainText(Book $book): string
297333
return $text . implode("\n\n", $parts);
298334
}
299335

336+
/**
337+
* Convert a book into a plain text string.
338+
*/
339+
public function bookshelfToPlainText(Bookshelf $bookshelf): string
340+
{
341+
$bookshelfTree = (new BookshelfContents($bookshelf))->getTree(true);
342+
$text = $bookshelf->name . "\n" . $bookshelf->description;
343+
$text = rtrim($text) . "\n\n";
344+
345+
$parts = [];
346+
foreach ($bookshelfTree as $bookshelfChild) {
347+
$parts[] = $this->bookToPlainText($bookshelfChild);
348+
}
349+
350+
return $text . implode("\n\n", $parts);
351+
}
352+
300353
/**
301354
* Convert a page to a Markdown file.
302355
*/
@@ -340,4 +393,18 @@ public function bookToMarkdown(Book $book): string
340393

341394
return trim($text);
342395
}
396+
397+
/**
398+
* Convert a bookshelf into a plain text string.
399+
*/
400+
public function bookshelfToMarkdown(Bookshelf $bookshelf): string
401+
{
402+
$bookshelfTree = (new BookshelfContents($bookshelf))->getTree(true);
403+
$text = '# ' . $bookshelf->name . "\n\n";
404+
foreach ($bookshelfTree as $bookshelfChild) {
405+
$text .= $this->bookToMarkdown($bookshelfChild) . "\n\n";
406+
}
407+
408+
return trim($text);
409+
}
343410
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<div class="page-break"></div>
2+
<h1 id="book-{{$book->id}}">{{ $book->name }}</h1>
3+
4+
<div>{!! $book->descriptionHtml() !!}</div>
5+
6+
@foreach($bookChildren as $bookChild)
7+
@if($bookChild->isA('chapter'))
8+
@include('exports.parts.chapter-item', ['chapter' => $bookChild])
9+
@else
10+
@include('exports.parts.page-item', ['page' => $bookChild, 'chapter' => null])
11+
@endif
12+
@endforeach
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
@if(count($bookshelfBooks) > 0)
2+
<ul class="contents">
3+
@foreach($bookshelfBooks as $bookshelfbook)
4+
<li><a href="#{{$bookshelfbook->getType()}}-{{$bookshelfbook->id}}">{{ $bookshelfbook->name }}</a></li>
5+
@include('exports.parts.book-contents-menu', ['children' => $bookshelfbook->bookChildrens])
6+
@endforeach
7+
</ul>
8+
@endif
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
@extends('layouts.export')
2+
3+
@section('title', $bookshelf->name)
4+
5+
@section('content')
6+
7+
<h1 style="font-size: 4.8em">{{$bookshelf->name}}</h1>
8+
<div>{!! $bookshelf->descriptionHtml() !!}</div>
9+
10+
@include('exports.parts.shelves-contents-menu',['bookshelfBooks' => $bookshelfChildrens])
11+
12+
@foreach($bookshelfChildrens as $bookshelfChildren)
13+
@if($bookshelfChildren->isA('book'))
14+
@include('exports.parts.book-item',['bookChildren'=>$bookshelfChildren->bookChildrens,'book'=>$bookshelfChildren])
15+
@endif
16+
@endforeach
17+
@endsection

resources/views/shelves/show.blade.php

+4
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@
148148
@include('entities.favourite-action', ['entity' => $shelf])
149149
@endif
150150

151+
@if(userCan('content-export'))
152+
@include('entities.export-menu', ['entity' => $shelf])
153+
@endif
154+
151155
</div>
152156
</div>
153157
@stop

routes/web.php

+4
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@
5151
Route::put('/shelves/{slug}/permissions', [PermissionsController::class, 'updateForShelf']);
5252
Route::post('/shelves/{slug}/copy-permissions', [PermissionsController::class, 'copyShelfPermissionsToBooks']);
5353
Route::get('/shelves/{slug}/references', [ReferenceController::class, 'shelf']);
54+
Route::get('/shelves/{slug}/export/pdf', [EntityControllers\BookshelfExportController::class, 'pdf']);
55+
Route::get('/shelves/{slug}/export/html', [EntityControllers\BookshelfExportController::class, 'html']);
56+
Route::get('/shelves/{slug}/export/plaintext', [EntityControllers\BookshelfExportController::class, 'plainText']);
57+
Route::get('/shelves/{slug}/export/markdown', [EntityControllers\BookshelfExportController::class, 'markdown']);
5458

5559
// Book Creation
5660
Route::get('/shelves/{shelfSlug}/create-book', [EntityControllers\BookController::class, 'create']);

tests/Entity/ExportTest.php

+66
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
namespace Tests\Entity;
44

55
use BookStack\Entities\Models\Book;
6+
use BookStack\Entities\Models\Bookshelf;
67
use BookStack\Entities\Models\Chapter;
78
use BookStack\Entities\Models\Page;
89
use BookStack\Entities\Tools\PdfGenerator;
910
use BookStack\Exceptions\PdfExportException;
11+
use Illuminate\Database\Eloquent\Builder;
1012
use Illuminate\Support\Facades\Storage;
1113
use Tests\TestCase;
1214

@@ -566,4 +568,68 @@ public function test_html_exports_contain_body_classes_for_export_identification
566568
$resp = $this->asEditor()->get($page->getUrl('/export/html'));
567569
$this->withHtml($resp)->assertElementExists('body.export.export-format-html.export-engine-none');
568570
}
571+
572+
public function test_bookshelf_text_export()
573+
{
574+
$bookshelf = $this->entities->shelf();
575+
$book = $bookshelf->books()->first();
576+
$directPage = $book->directPages()->first();
577+
$chapter = $book->chapters()->first();
578+
579+
$this->entities->updatePage($directPage, ['html' => '<p>My awesome page</p>']);
580+
$this->asEditor();
581+
582+
$resp = $this->get($bookshelf->getUrl('/export/plaintext'));
583+
$resp->assertStatus(200);
584+
$resp->assertSee($bookshelf->name);
585+
$resp->assertSee($book->name);
586+
$resp->assertSee($chapter->name);
587+
$resp->assertSee($directPage->name);
588+
$resp->assertSee('My awesome page');
589+
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $bookshelf->slug . '.txt"');
590+
}
591+
592+
public function test_bookshelf_pdf_export()
593+
{
594+
$bookshelf = $this->entities->shelf();
595+
$this->asEditor();
596+
597+
$resp = $this->get($bookshelf->getUrl('/export/pdf'));
598+
$resp->assertStatus(200);
599+
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $bookshelf->slug . '.pdf"');
600+
}
601+
602+
public function test_bookshelf_html_export()
603+
{
604+
$bookshelf = $this->entities->shelf();
605+
$book = $bookshelf->books()->first();
606+
607+
$this->asEditor();
608+
609+
$resp = $this->get($bookshelf->getUrl('/export/html'));
610+
$resp->assertStatus(200);
611+
$resp->assertSee($bookshelf->name);
612+
$resp->assertSee($book->name);
613+
$resp->assertSee($bookshelf->description);
614+
$resp->assertSee($book->description);
615+
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $bookshelf->slug . '.html"');
616+
}
617+
618+
public function test_bookshelf_markdown_export()
619+
{
620+
$bookshelf = Bookshelf::query()->whereHas('books', function (Builder $query) {
621+
$query->Has('chapters')->Has('pages');
622+
})
623+
->with(['books.chapters', 'books.pages'])
624+
->first();
625+
$book = $bookshelf->books()->first();
626+
$chapter = $book->chapters()->first();
627+
$directPage = $book->directPages()->first();
628+
$resp = $this->asEditor()->get($bookshelf->getUrl('/export/markdown'));
629+
630+
$resp->assertSee('# ' . $bookshelf->name);
631+
$resp->assertSee('# ' . $book->name);
632+
$resp->assertSee('# ' . $chapter->name);
633+
$resp->assertSee('# ' . $directPage->name);
634+
}
569635
}

0 commit comments

Comments
 (0)