diff --git a/app/Entities/Tools/ExportFormatter.php b/app/Entities/Tools/ExportFormatter.php index eb8f6862f..b462abec5 100644 --- a/app/Entities/Tools/ExportFormatter.php +++ b/app/Entities/Tools/ExportFormatter.php @@ -7,7 +7,9 @@ use BookStack\Uploads\ImageService; use DomPDF; use Exception; use SnappyPDF; +use League\HTMLToMarkdown\HtmlConverter; use Throwable; +use ZipArchive; class ExportFormatter { @@ -226,4 +228,72 @@ class ExportFormatter } return $text; } + + /** + * Convert a page to a Markdown file. + * @throws Throwable + */ + public function pageToMarkdown(Page $page) + { + if (property_exists($page, 'markdown') && $page->markdown != '') { + return "# " . $page->name . "\n\n" . $page->markdown; + } else { + $converter = new HtmlConverter(); + return "# " . $page->name . "\n\n" . $converter->convert($page->html); + } + } + + /** + * Convert a chapter to a Markdown file. + * @throws Throwable + */ + public function chapterToMarkdown(Chapter $chapter) + { + $text = "# " . $chapter->name . "\n\n"; + $text .= $chapter->description . "\n\n"; + foreach ($chapter->pages as $page) { + $text .= $this->pageToMarkdown($page); + } + return $text; + } + + /** + * Convert a book into a plain text string. + */ + public function bookToMarkdown(Book $book): string + { + $bookTree = (new BookContents($book))->getTree(false, true); + $text = "# " . $book->name . "\n\n"; + foreach ($bookTree as $bookChild) { + if ($bookChild->isA('chapter')) { + $text .= $this->chapterToMarkdown($bookChild); + } else { + $text .= $this->pageToMarkdown($bookChild); + } + } + return $text; + } + + /** + * Convert a book into a zip file. + */ + public function bookToZip(Book $book): string + { + // TODO: Is not unlinking the file a security risk? + $z = new ZipArchive(); + $z->open("book.zip", \ZipArchive::CREATE | \ZipArchive::OVERWRITE); + $bookTree = (new BookContents($book))->getTree(false, true); + foreach ($bookTree as $bookChild) { + if ($bookChild->isA('chapter')) { + $z->addEmptyDir($bookChild->name); + foreach ($bookChild->pages as $page) { + $filename = $bookChild->name . "/" . $page->name . ".md"; + $z->addFromString($filename, $this->pageToMarkdown($page)); + } + } else { + $z->addFromString($bookChild->name . ".md", $this->pageToMarkdown($bookChild)); + } + } + return "book.zip"; + } } diff --git a/app/Http/Controllers/BookExportController.php b/app/Http/Controllers/BookExportController.php index 1c1f12442..58868fa5c 100644 --- a/app/Http/Controllers/BookExportController.php +++ b/app/Http/Controllers/BookExportController.php @@ -52,4 +52,24 @@ class BookExportController extends Controller $textContent = $this->exportFormatter->bookToPlainText($book); return $this->downloadResponse($textContent, $bookSlug . '.txt'); } + + /** + * Export a book as a markdown file. + */ + public function markdown(string $bookSlug) + { + $book = $this->bookRepo->getBySlug($bookSlug); + $textContent = $this->exportService->bookToMarkdown($book); + return $this->downloadResponse($textContent, $bookSlug . '.md'); + } + + /** + * Export a book as a zip file, made of markdown files. + */ + public function zip(string $bookSlug) + { + $book = $this->bookRepo->getBySlug($bookSlug); + $filename = $this->exportService->bookToZip($book); + return response()->download($filename); + } } diff --git a/app/Http/Controllers/ChapterExportController.php b/app/Http/Controllers/ChapterExportController.php index 52d087442..bc709771b 100644 --- a/app/Http/Controllers/ChapterExportController.php +++ b/app/Http/Controllers/ChapterExportController.php @@ -54,4 +54,16 @@ class ChapterExportController extends Controller $chapterText = $this->exportFormatter->chapterToPlainText($chapter); return $this->downloadResponse($chapterText, $chapterSlug . '.txt'); } + + /** + * Export a chapter to a simple markdown file. + * @throws NotFoundException + */ + public function markdown(string $bookSlug, string $chapterSlug) + { + // TODO: This should probably export to a zip file. + $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug); + $chapterText = $this->exportService->chapterToMarkdown($chapter); + return $this->downloadResponse($chapterText, $chapterSlug . '.md'); + } } diff --git a/app/Http/Controllers/PageExportController.php b/app/Http/Controllers/PageExportController.php index e5e027fe7..d9cc5ba48 100644 --- a/app/Http/Controllers/PageExportController.php +++ b/app/Http/Controllers/PageExportController.php @@ -60,4 +60,15 @@ class PageExportController extends Controller $pageText = $this->exportFormatter->pageToPlainText($page); return $this->downloadResponse($pageText, $pageSlug . '.txt'); } + + /** + * Export a page to a simple markdown .md file. + * @throws NotFoundException + */ + public function markdown(string $bookSlug, string $pageSlug) + { + $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug); + $pageText = $this->exportService->pageToMarkdown($page); + return $this->downloadResponse($pageText, $pageSlug . '.md'); + } } diff --git a/composer.json b/composer.json index 8450a2f92..8124ccbca 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ "laravel/socialite": "^5.1", "league/commonmark": "^1.5", "league/flysystem-aws-s3-v3": "^1.0.29", + "league/html-to-markdown": "^4.9", "nunomaduro/collision": "^3.1", "onelogin/php-saml": "^4.0", "predis/predis": "^1.1.6", diff --git a/dev/docker/Dockerfile b/dev/docker/Dockerfile index 895ad595a..178ea9a6c 100644 --- a/dev/docker/Dockerfile +++ b/dev/docker/Dockerfile @@ -5,9 +5,9 @@ WORKDIR /app # Install additional dependacnies and configure apache RUN apt-get update -y \ - && apt-get install -y git zip unzip libpng-dev libldap2-dev wait-for-it \ + && apt-get install -y git zip unzip libpng-dev libldap2-dev libzip-dev wait-for-it \ && docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu \ - && docker-php-ext-install pdo_mysql gd ldap \ + && docker-php-ext-install pdo_mysql gd ldap zip \ && a2enmod rewrite \ && sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \ && sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf @@ -20,4 +20,4 @@ RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \ # Use the default production configuration and update it as required RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" \ - && sed -i 's/memory_limit = 128M/memory_limit = 512M/g' "$PHP_INI_DIR/php.ini" \ No newline at end of file + && sed -i 's/memory_limit = 128M/memory_limit = 512M/g' "$PHP_INI_DIR/php.ini" diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index 462402f33..1d4632bce 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -36,6 +36,7 @@ return [ 'export_html' => 'Contained Web File', 'export_pdf' => 'PDF File', 'export_text' => 'Plain Text File', + 'export_md' => 'Markdown File', // Permissions and restrictions 'permissions' => 'Permissions', diff --git a/resources/views/partials/entity-export-menu.blade.php b/resources/views/partials/entity-export-menu.blade.php index 6d23af07c..2b0f5c19d 100644 --- a/resources/views/partials/entity-export-menu.blade.php +++ b/resources/views/partials/entity-export-menu.blade.php @@ -8,5 +8,6 @@
  • {{ trans('entities.export_html') }} .html
  • {{ trans('entities.export_pdf') }} .pdf
  • {{ trans('entities.export_text') }} .txt
  • +
  • {{ trans('entities.export_md') }} .md
  • - \ No newline at end of file + diff --git a/routes/web.php b/routes/web.php index 72d089078..2bba3e2cf 100644 --- a/routes/web.php +++ b/routes/web.php @@ -48,6 +48,8 @@ Route::group(['middleware' => 'auth'], function () { Route::put('/{bookSlug}/sort', 'BookSortController@update'); Route::get('/{bookSlug}/export/html', 'BookExportController@html'); Route::get('/{bookSlug}/export/pdf', 'BookExportController@pdf'); + Route::get('/{bookSlug}/export/markdown', 'BookExportController@markdown'); + Route::get('/{bookSlug}/export/zip', 'BookExportController@zip'); Route::get('/{bookSlug}/export/plaintext', 'BookExportController@plainText'); // Pages @@ -58,6 +60,7 @@ Route::group(['middleware' => 'auth'], function () { Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show'); Route::get('/{bookSlug}/page/{pageSlug}/export/pdf', 'PageExportController@pdf'); Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageExportController@html'); + Route::get('/{bookSlug}/page/{pageSlug}/export/markdown', 'PageExportController@markdown'); Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageExportController@plainText'); Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit'); Route::get('/{bookSlug}/page/{pageSlug}/move', 'PageController@showMove'); @@ -92,6 +95,7 @@ Route::group(['middleware' => 'auth'], function () { Route::get('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@showPermissions'); Route::get('/{bookSlug}/chapter/{chapterSlug}/export/pdf', 'ChapterExportController@pdf'); Route::get('/{bookSlug}/chapter/{chapterSlug}/export/html', 'ChapterExportController@html'); + Route::get('/{bookSlug}/chapter/{chapterSlug}/export/markdown', 'ChapterExportController@markdown'); Route::get('/{bookSlug}/chapter/{chapterSlug}/export/plaintext', 'ChapterExportController@plainText'); Route::put('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@permissions'); Route::get('/{bookSlug}/chapter/{chapterSlug}/delete', 'ChapterController@showDelete');