diff --git a/app/Entities/Controllers/BookshelfExportController.php b/app/Entities/Controllers/BookshelfExportController.php
new file mode 100644
index 00000000000..44627539093
--- /dev/null
+++ b/app/Entities/Controllers/BookshelfExportController.php
@@ -0,0 +1,66 @@
+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');
+ }
+}
diff --git a/app/Entities/Tools/BookshelfContents.php b/app/Entities/Tools/BookshelfContents.php
new file mode 100644
index 00000000000..859a11dd50a
--- /dev/null
+++ b/app/Entities/Tools/BookshelfContents.php
@@ -0,0 +1,23 @@
+bookshelf->books()->scopes('visible')->get();
+
+ $books->each(function ($book) use ($renderPages) {
+ $book->setAttribute('bookChildrens', (new BookContents($book))->getTree(false, $renderPages));
+ });
+
+ return collect($books);
+ }
+}
diff --git a/app/Entities/Tools/ExportFormatter.php b/app/Entities/Tools/ExportFormatter.php
index beddfe8e6e0..fdd4c114df0 100644
--- a/app/Entities/Tools/ExportFormatter.php
+++ b/app/Entities/Tools/ExportFormatter.php
@@ -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;
@@ -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.
*
@@ -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.
*
@@ -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.
*/
@@ -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);
+ }
}
diff --git a/app/Util/HtmlDescriptionFilter.php b/app/Util/HtmlDescriptionFilter.php
index cb091b869f8..a4c214ae58c 100644
--- a/app/Util/HtmlDescriptionFilter.php
+++ b/app/Util/HtmlDescriptionFilter.php
@@ -27,6 +27,7 @@ class HtmlDescriptionFilter
'strong' => [],
'em' => [],
'br' => [],
+ 'img' => ['src', 'alt'],
];
public static function filterFromString(string $html): string
diff --git a/resources/js/components/page-comments.js b/resources/js/components/page-comments.js
index 1d6abfe2044..22f35882c39 100644
--- a/resources/js/components/page-comments.js
+++ b/resources/js/components/page-comments.js
@@ -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});
}
@@ -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,
});
diff --git a/resources/js/wysiwyg-tinymce/config.js b/resources/js/wysiwyg-tinymce/config.js
index 1666aa50066..fdd734c9288 100644
--- a/resources/js/wysiwyg-tinymce/config.js
+++ b/resources/js/wysiwyg-tinymce/config.js
@@ -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];
@@ -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('
@@ -37,6 +39,7 @@ class="button outline">{{ trans('entities.comment_add') }}
@if(userCan('comment-create-all') || $commentTree->canUpdateAny())
@push('body-end')
+ @include('pages.parts.image-manager', ['uploaded_to' => $page->id])
@include('form.editor-translations')
@include('entities.selector-popup')
diff --git a/resources/views/exports/parts/book-item.blade.php b/resources/views/exports/parts/book-item.blade.php
new file mode 100644
index 00000000000..83636c0712b
--- /dev/null
+++ b/resources/views/exports/parts/book-item.blade.php
@@ -0,0 +1,12 @@
+
My awesome page
']); + $this->asEditor(); + + $resp = $this->get($bookshelf->getUrl('/export/plaintext')); + $resp->assertStatus(200); + $resp->assertSee($bookshelf->name); + $resp->assertSee($book->name); + $resp->assertSee($chapter->name); + $resp->assertSee($directPage->name); + $resp->assertSee('My awesome page'); + $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $bookshelf->slug . '.txt"'); + } + + public function test_bookshelf_pdf_export() + { + $bookshelf = $this->entities->shelf(); + $this->asEditor(); + + $resp = $this->get($bookshelf->getUrl('/export/pdf')); + $resp->assertStatus(200); + $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $bookshelf->slug . '.pdf"'); + } + + public function test_bookshelf_html_export() + { + $bookshelf = $this->entities->shelf(); + $book = $bookshelf->books()->first(); + + $this->asEditor(); + + $resp = $this->get($bookshelf->getUrl('/export/html')); + $resp->assertStatus(200); + $resp->assertSee($bookshelf->name); + $resp->assertSee($book->name); + $resp->assertSee($bookshelf->description); + $resp->assertSee($book->description); + $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $bookshelf->slug . '.html"'); + } + + public function test_bookshelf_markdown_export() + { + $bookshelf = Bookshelf::query()->whereHas('books', function (Builder $query) { + $query->Has('chapters')->Has('pages'); + }) + ->with(['books.chapters', 'books.pages']) + ->first(); + $book = $bookshelf->books()->first(); + $chapter = $book->chapters()->first(); + $directPage = $book->directPages()->first(); + $resp = $this->asEditor()->get($bookshelf->getUrl('/export/markdown')); + + $resp->assertSee('# ' . $bookshelf->name); + $resp->assertSee('# ' . $book->name); + $resp->assertSee('# ' . $chapter->name); + $resp->assertSee('# ' . $directPage->name); + } }