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

Possibility to export shelves #39

Closed
Closed
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
66 changes: 66 additions & 0 deletions app/Entities/Controllers/BookshelfExportController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

namespace BookStack\Entities\Controllers;

use BookStack\Entities\Queries\BookshelfQueries;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Http\Controller;
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(string $bookshelfSlug)
{
$bookshelf = $this->queries->findVisibleBySlugOrFail($bookshelfSlug);
$pdfContent = $this->exportFormatter->bookshelfToPdf($bookshelf);

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

/**
* Export a book as a contained HTML file.
*
* @throws Throwable
*/
public function html(string $bookshelfSlug)
{
$bookshelf = $this->queries->findVisibleBySlugOrFail($bookshelfSlug);
$htmlContent = $this->exportFormatter->bookshelfToContainedHtml($bookshelf);

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

/**
* Export a book as a plain text file.
*/
public function plainText(string $bookshelfSlug)
{
$bookshelf = $this->queries->findVisibleBySlugOrFail($bookshelfSlug);
$textContent = $this->exportFormatter->bookshelfToPlainText($bookshelf);

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

/**
* Export a book as a markdown file.
*/
public function markdown(string $bookshelfSlug)
{
$bookshelf = $this->queries->findVisibleBySlugOrFail($bookshelfSlug);
$textContent = $this->exportFormatter->bookshelfToMarkdown($bookshelf);

return $this->download()->directly($textContent, $bookshelfSlug . '.md');
}
}
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);
}
}
1 change: 1 addition & 0 deletions app/Util/HtmlDescriptionFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class HtmlDescriptionFilter
'strong' => [],
'em' => [],
'br' => [],
'img' => ['src', 'alt'],
];

public static function filterFromString(string $html): string
Expand Down
6 changes: 4 additions & 2 deletions resources/js/components/page-comments.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ export class PageComments extends Component {

updateCount() {
const count = this.getCommentCount();
console.log('update count', count, this.container);
this.commentsTitle.textContent = window.$trans.choice(this.countText, count, {count});
}

Expand Down Expand Up @@ -135,7 +134,10 @@ export class PageComments extends Component {
containerElement: this.formInput,
darkMode: document.documentElement.classList.contains('dark-mode'),
textDirection: this.wysiwygTextDirection,
translations: {},
translations: {
imageUploadErrorText: this.$opts.imageUploadErrorText,
serverUploadLimitText: this.$opts.serverUploadLimitText,
},
translationMap: window.editor_translations,
});

Expand Down
18 changes: 14 additions & 4 deletions resources/js/wysiwyg-tinymce/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,9 @@ export function buildForInput(options) {
// Set language
window.tinymce.addI18n(options.language, options.translationMap);

// Add IMage Manager Plugin
window.tinymce.PluginManager.add('imagemanager', getImagemanagerPlugin());

// BookStack Version
const version = document.querySelector('script[src*="/dist/app.js"]').getAttribute('src').split('?version=')[1];

Expand All @@ -343,13 +346,20 @@ export function buildForInput(options) {
remove_trailing_brs: false,
statusbar: false,
menubar: false,
plugins: 'link autolink lists',
plugins: 'link autolink lists imagemanager',
contextmenu: false,
toolbar: 'bold italic link bullist numlist',
toolbar: 'bold italic link bullist numlist imagemanager-insert',
content_style: getContentStyle(options),
file_picker_types: 'file',
valid_elements: 'p,a[href|title|target],ol,ul,li,strong,em,br',
file_picker_types: 'file image',
automatic_uploads: false,
valid_elements: 'p,a[href|title|target],ol,ul,li,strong,em,br,+div[pre|img],img[src|alt|width|height|class|style]',
file_picker_callback: filePickerCallback,
paste_preprocess(plugin, args) {
const {content} = args;
if (content.indexOf('<img src="file://') !== -1) {
args.content = '';
}
},
init_instance_callback(editor) {
addCustomHeadContent(editor.getDoc());

Expand Down
3 changes: 3 additions & 0 deletions resources/views/comments/comments.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
option:page-comments:count-text="{{ trans('entities.comment_count') }}"
option:page-comments:wysiwyg-language="{{ $locale->htmlLang() }}"
option:page-comments:wysiwyg-text-direction="{{ $locale->htmlDirection() }}"
option:page-comments:image-upload-error-text="{{ trans('errors.image_upload_error') }}"
option:page-comments:server-upload-limit-text="{{ trans('errors.server_upload_limit') }}"
class="comments-list"
aria-label="{{ trans('entities.comments') }}">

Expand Down Expand Up @@ -37,6 +39,7 @@ class="button outline">{{ trans('entities.comment_add') }}</button>

@if(userCan('comment-create-all') || $commentTree->canUpdateAny())
@push('body-end')
@include('pages.parts.image-manager', ['uploaded_to' => $page->id])
<script src="{{ versioned_asset('libs/tinymce/tinymce.min.js') }}" nonce="{{ $cspNonce }}" defer></script>
@include('form.editor-translations')
@include('entities.selector-popup')
Expand Down
12 changes: 12 additions & 0 deletions resources/views/exports/parts/book-item.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<div class="page-break"></div>
<h1 id="book-{{$book->id}}">{{ $book->name }}</h1>

<div>{!! $book->descriptionHtml() !!}</div>

@foreach($bookChildren as $bookChild)
@if($bookChild->isA('chapter'))
@include('exports.parts.chapter-item', ['chapter' => $bookChild])
@else
@include('exports.parts.page-item', ['page' => $bookChild, 'chapter' => null])
@endif
@endforeach
8 changes: 8 additions & 0 deletions resources/views/exports/parts/shelves-contents-menu.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@if(count($bookshelfBooks) > 0)
<ul class="contents">
@foreach($bookshelfBooks as $bookshelfbook)
<li><a href="#{{$bookshelfbook->getType()}}-{{$bookshelfbook->id}}">{{ $bookshelfbook->name }}</a></li>
@include('exports.parts.book-contents-menu', ['children' => $bookshelfbook->bookChildrens])
@endforeach
</ul>
@endif
17 changes: 17 additions & 0 deletions resources/views/exports/shelves.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@extends('layouts.export')

@section('title', $bookshelf->name)

@section('content')

<h1 style="font-size: 4.8em">{{$bookshelf->name}}</h1>
<div>{!! $bookshelf->descriptionHtml() !!}</div>

@include('exports.parts.shelves-contents-menu',['bookshelfBooks' => $bookshelfChildrens])

@foreach($bookshelfChildrens as $bookshelfChildren)
@if($bookshelfChildren->isA('book'))
@include('exports.parts.book-item',['bookChildren'=>$bookshelfChildren->bookChildrens,'book'=>$bookshelfChildren])
@endif
@endforeach
@endsection
4 changes: 4 additions & 0 deletions resources/views/shelves/show.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@
@include('entities.favourite-action', ['entity' => $shelf])
@endif

@if(userCan('content-export'))
@include('entities.export-menu', ['entity' => $shelf])
@endif

</div>
</div>
@stop
Expand Down
4 changes: 4 additions & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@
Route::put('/shelves/{slug}/permissions', [PermissionsController::class, 'updateForShelf']);
Route::post('/shelves/{slug}/copy-permissions', [PermissionsController::class, 'copyShelfPermissionsToBooks']);
Route::get('/shelves/{slug}/references', [ReferenceController::class, 'shelf']);
Route::get('/shelves/{slug}/export/pdf', [EntityControllers\BookshelfExportController::class, 'pdf']);
Route::get('/shelves/{slug}/export/html', [EntityControllers\BookshelfExportController::class, 'html']);
Route::get('/shelves/{slug}/export/plaintext', [EntityControllers\BookshelfExportController::class, 'plainText']);
Route::get('/shelves/{slug}/export/markdown', [EntityControllers\BookshelfExportController::class, 'markdown']);

// Book Creation
Route::get('/shelves/{shelfSlug}/create-book', [EntityControllers\BookController::class, 'create']);
Expand Down
12 changes: 12 additions & 0 deletions tests/Entity/CommentTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -214,4 +214,16 @@ public function test_comment_editor_js_loaded_with_create_or_edit_permissions()
$resp->assertSee('window.editor_translations', false);
$resp->assertSee('component="entity-selector"', false);
}

public function test_images_can_add_in_comment()
{
$this->asEditor();
$page = $this->entities->page();

$this->postJson("/comment/$page->id", [
'html' => '<p><a href="http://localhost:8080/uploads/images/gallery/2024-10/4-sm.webp" target="_blank" rel="noopener" data-mce-href="http://localhost:8080/uploads/images/gallery/2024-10/4-sm.webp" data-mce-selected="inline-boundary"><img src="http://localhost:8080/uploads/images/gallery/2024-10/scaled-1680-/4-sm.webp" alt="4.sm.webp" data-mce-src="http://localhost:8080/uploads/images/gallery/2024-10/scaled-1680-/4-sm.webp" data-mce-selected="1"></a></p>',
]);

$this->assertStringMatchesFormat('%A<p%A><a%A><img src="http://localhost:8080/uploads/images/gallery/%A.webp">%A</p>%A', $page->comments()->first()->html);
}
}
Loading
Loading