From a34a07c610390746e1160c6b57b0b190a3c772d5 Mon Sep 17 00:00:00 2001 From: Nikhil Jha Date: Tue, 12 May 2020 21:12:26 -0700 Subject: [PATCH 1/4] basic markdown export --- app/Entities/ExportService.php | 45 +++++++++++++++++++ app/Http/Controllers/BookExportController.php | 11 +++++ .../Controllers/ChapterExportController.php | 12 +++++ app/Http/Controllers/PageExportController.php | 11 +++++ resources/lang/en/entities.php | 1 + .../partials/entity-export-menu.blade.php | 1 + routes/web.php | 3 ++ 7 files changed, 84 insertions(+) diff --git a/app/Entities/ExportService.php b/app/Entities/ExportService.php index f945dfbe4..29df1e82d 100644 --- a/app/Entities/ExportService.php +++ b/app/Entities/ExportService.php @@ -225,4 +225,49 @@ class ExportService } 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 { + // TODO: Implement this feature. + return "# Unimplemented Feature\nidk how to turn html into markdown"; + } + } + + /** + * 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; + } } diff --git a/app/Http/Controllers/BookExportController.php b/app/Http/Controllers/BookExportController.php index cfa3d6a3a..40eec69fe 100644 --- a/app/Http/Controllers/BookExportController.php +++ b/app/Http/Controllers/BookExportController.php @@ -53,4 +53,15 @@ class BookExportController extends Controller $textContent = $this->exportService->bookToPlainText($book); return $this->downloadResponse($textContent, $bookSlug . '.txt'); } + + /** + * Export a book as a markdown file. + */ + public function markdown(string $bookSlug) + { + // TODO: This should probably export to a zip file. + $book = $this->bookRepo->getBySlug($bookSlug); + $textContent = $this->exportService->bookToMarkdown($book); + return $this->downloadResponse($textContent, $bookSlug . '.md'); + } } diff --git a/app/Http/Controllers/ChapterExportController.php b/app/Http/Controllers/ChapterExportController.php index 0c86f8548..c0fa9fad9 100644 --- a/app/Http/Controllers/ChapterExportController.php +++ b/app/Http/Controllers/ChapterExportController.php @@ -55,4 +55,16 @@ class ChapterExportController extends Controller $chapterText = $this->exportService->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 3b02ea224..037f84e3b 100644 --- a/app/Http/Controllers/PageExportController.php +++ b/app/Http/Controllers/PageExportController.php @@ -63,4 +63,15 @@ class PageExportController extends Controller $pageText = $this->exportService->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/resources/lang/en/entities.php b/resources/lang/en/entities.php index 6bbc723b0..b459c3d4b 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -33,6 +33,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 630d640bf..42c2eb79a 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 3e05e394d..f2c4f432e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -47,6 +47,7 @@ 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/plaintext', 'BookExportController@plainText'); // Pages @@ -57,6 +58,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'); @@ -91,6 +93,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'); From a7d9646b19cd4549849e56bec1afedb1c5987f3d Mon Sep 17 00:00:00 2001 From: Nikhil Jha Date: Wed, 13 May 2020 18:34:22 -0700 Subject: [PATCH 2/4] support exporting WYSIWYG pages as Markdown --- app/Entities/ExportService.php | 13 +++---- composer.json | 1 + composer.lock | 66 +++++++++++++++++++++++++++++++++- 3 files changed, 73 insertions(+), 7 deletions(-) diff --git a/app/Entities/ExportService.php b/app/Entities/ExportService.php index 29df1e82d..1b294d8b1 100644 --- a/app/Entities/ExportService.php +++ b/app/Entities/ExportService.php @@ -6,6 +6,7 @@ use BookStack\Uploads\ImageService; use DomPDF; use Exception; use SnappyPDF; +use League\HTMLToMarkdown\HtmlConverter; use Throwable; class ExportService @@ -232,11 +233,11 @@ class ExportService */ public function pageToMarkdown(Page $page) { - if (property_exists($page, 'markdown') || $page->markdown != '') { - return "#" . $page->name . "\n\n" . $page->markdown; + if (property_exists($page, 'markdown') && $page->markdown != '') { + return "# " . $page->name . "\n\n" . $page->markdown; } else { - // TODO: Implement this feature. - return "# Unimplemented Feature\nidk how to turn html into markdown"; + $converter = new HtmlConverter(); + return "# " . $page->name . "\n\n" . $converter->convert($page->html); } } @@ -246,7 +247,7 @@ class ExportService */ public function chapterToMarkdown(Chapter $chapter) { - $text = "#" . $chapter->name . "\n\n"; + $text = "# " . $chapter->name . "\n\n"; $text .= $chapter->description . "\n\n"; foreach ($chapter->pages as $page) { $text .= $this->pageToMarkdown($page); @@ -260,7 +261,7 @@ class ExportService public function bookToMarkdown(Book $book): string { $bookTree = (new BookContents($book))->getTree(false, true); - $text = "#" . $book->name . "\n\n"; + $text = "# " . $book->name . "\n\n"; foreach ($bookTree as $bookChild) { if ($bookChild->isA('chapter')) { $text .= $this->chapterToMarkdown($bookChild); diff --git a/composer.json b/composer.json index 80e8b0a61..68802e935 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,7 @@ "laravel/socialite": "^4.3.2", "league/commonmark": "^1.4", "league/flysystem-aws-s3-v3": "^1.0", + "league/html-to-markdown": "^4.9", "nunomaduro/collision": "^3.0", "onelogin/php-saml": "^3.3", "predis/predis": "^1.1", diff --git a/composer.lock b/composer.lock index 3ddd28e5a..7f58ffaee 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bbe47cff4f167fd6ce7047dff4602a78", + "content-hash": "280a7e2fe2a6f65812594d73df2ccc0f", "packages": [ { "name": "aws/aws-sdk-php", @@ -2004,6 +2004,70 @@ "description": "Flysystem adapter for the AWS S3 SDK v3.x", "time": "2020-02-23T13:31:58+00:00" }, + { + "name": "league/html-to-markdown", + "version": "4.9.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/html-to-markdown.git", + "reference": "1dcd0f85de786f46a7f224a27cc3d709ddd2a68c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/1dcd0f85de786f46a7f224a27cc3d709ddd2a68c", + "reference": "1dcd0f85de786f46a7f224a27cc3d709ddd2a68c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xml": "*", + "php": ">=5.3.3" + }, + "require-dev": { + "mikehaertl/php-shellcommand": "~1.1.0", + "phpunit/phpunit": "^4.8|^5.7", + "scrutinizer/ocular": "~1.1" + }, + "bin": [ + "bin/html-to-markdown" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.10-dev" + } + }, + "autoload": { + "psr-4": { + "League\\HTMLToMarkdown\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + }, + { + "name": "Nick Cernis", + "email": "nick@cern.is", + "homepage": "http://modernnerd.net", + "role": "Original Author" + } + ], + "description": "An HTML-to-markdown conversion helper for PHP", + "homepage": "https://github.com/thephpleague/html-to-markdown", + "keywords": [ + "html", + "markdown" + ], + "time": "2019-12-28T01:32:28+00:00" + }, { "name": "league/oauth1-client", "version": "1.7.0", From ea82c2f61b00231cdbcffd0463361c5b41832062 Mon Sep 17 00:00:00 2001 From: Nikhil Jha Date: Wed, 13 May 2020 19:57:59 -0700 Subject: [PATCH 3/4] support exporting books as zip files --- app/Http/Controllers/BookExportController.php | 26 ++++++++++++++++++- dev/docker/Dockerfile | 4 +-- routes/web.php | 1 + 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/BookExportController.php b/app/Http/Controllers/BookExportController.php index 40eec69fe..0414b7250 100644 --- a/app/Http/Controllers/BookExportController.php +++ b/app/Http/Controllers/BookExportController.php @@ -2,9 +2,11 @@ namespace BookStack\Http\Controllers; +use BookStack\Entities\Managers\BookContents; use BookStack\Entities\ExportService; use BookStack\Entities\Repos\BookRepo; use Throwable; +use ZipArchive; class BookExportController extends Controller { @@ -59,9 +61,31 @@ class BookExportController extends Controller */ public function markdown(string $bookSlug) { - // TODO: This should probably export to a zip file. $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); + $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) { + $z->addFromString($bookChild->name . "/" . $page->name . ".md", $this->exportService->pageToMarkdown($page)); + } + } else { + $z->addFromString($bookChild->name . ".md", $this->exportService->pageToMarkdown($bookChild)); + } + } + return response()->download('book.zip'); + // TODO: Is not unlinking it a security issue? + } } diff --git a/dev/docker/Dockerfile b/dev/docker/Dockerfile index 8816615cf..be5af9ed9 100644 --- a/dev/docker/Dockerfile +++ b/dev/docker/Dockerfile @@ -4,9 +4,9 @@ ENV APACHE_DOCUMENT_ROOT /app/public WORKDIR /app RUN apt-get update -y \ - && apt-get install -y git zip unzip libtidy-dev libpng-dev libldap2-dev libxml++2.6-dev wait-for-it \ + && apt-get install -y git zip unzip libtidy-dev libpng-dev libldap2-dev libxml++2.6-dev wait-for-it libzip-dev \ && docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu \ - && docker-php-ext-install pdo pdo_mysql tidy dom xml mbstring gd ldap \ + && docker-php-ext-install pdo pdo_mysql tidy dom xml mbstring 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 \ diff --git a/routes/web.php b/routes/web.php index f2c4f432e..4d00b5ff6 100644 --- a/routes/web.php +++ b/routes/web.php @@ -48,6 +48,7 @@ Route::group(['middleware' => 'auth'], function () { 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 From e287d965f5ed6d72bb5e83fa655207f96e3257df Mon Sep 17 00:00:00 2001 From: Nikhil Jha Date: Wed, 13 May 2020 20:07:19 -0700 Subject: [PATCH 4/4] move zip export into exportservice --- app/Entities/ExportService.php | 24 +++++++++++++++++++ app/Http/Controllers/BookExportController.php | 18 ++------------ 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/app/Entities/ExportService.php b/app/Entities/ExportService.php index 1b294d8b1..b0e88b18b 100644 --- a/app/Entities/ExportService.php +++ b/app/Entities/ExportService.php @@ -8,6 +8,7 @@ use Exception; use SnappyPDF; use League\HTMLToMarkdown\HtmlConverter; use Throwable; +use ZipArchive; class ExportService { @@ -271,4 +272,27 @@ class ExportService } 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 0414b7250..a92d94cc9 100644 --- a/app/Http/Controllers/BookExportController.php +++ b/app/Http/Controllers/BookExportController.php @@ -6,7 +6,6 @@ use BookStack\Entities\Managers\BookContents; use BookStack\Entities\ExportService; use BookStack\Entities\Repos\BookRepo; use Throwable; -use ZipArchive; class BookExportController extends Controller { @@ -72,20 +71,7 @@ class BookExportController extends Controller public function zip(string $bookSlug) { $book = $this->bookRepo->getBySlug($bookSlug); - $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) { - $z->addFromString($bookChild->name . "/" . $page->name . ".md", $this->exportService->pageToMarkdown($page)); - } - } else { - $z->addFromString($bookChild->name . ".md", $this->exportService->pageToMarkdown($bookChild)); - } - } - return response()->download('book.zip'); - // TODO: Is not unlinking it a security issue? + $filename = $this->exportService->bookToZip($book); + return response()->download($filename); } }