Added markdown export endpoints to API

- Added tests to cover.
- Added slight extra spaces at content joins.
This commit is contained in:
Dan Brown 2021-06-22 21:32:55 +01:00
parent 57ea2e92ec
commit 992f03a3c0
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
9 changed files with 85 additions and 3 deletions

View File

@ -248,7 +248,7 @@ class ExportFormatter
$text = "# " . $chapter->name . "\n\n"; $text = "# " . $chapter->name . "\n\n";
$text .= $chapter->description . "\n\n"; $text .= $chapter->description . "\n\n";
foreach ($chapter->pages as $page) { foreach ($chapter->pages as $page) {
$text .= $this->pageToMarkdown($page); $text .= $this->pageToMarkdown($page) . "\n\n";
} }
return $text; return $text;
} }

View File

@ -53,7 +53,21 @@ class HtmlToMarkdown
*/ */
protected function getConverterEnvironment(): Environment protected function getConverterEnvironment(): Environment
{ {
$environment = new Environment(['header_style' => 'atx']); $environment = new Environment([
'header_style' => 'atx', // Set to 'atx' to output H1 and H2 headers as # Header1 and ## Header2
'suppress_errors' => true, // Set to false to show warnings when loading malformed HTML
'strip_tags' => false, // Set to true to strip tags that don't have markdown equivalents. N.B. Strips tags, not their content. Useful to clean MS Word HTML output.
'strip_placeholder_links' => false, // Set to true to remove <a> that doesn't have href.
'bold_style' => '**', // DEPRECATED: Set to '__' if you prefer the underlined style
'italic_style' => '*', // DEPRECATED: Set to '_' if you prefer the underlined style
'remove_nodes' => '', // space-separated list of dom nodes that should be removed. example: 'meta style script'
'hard_break' => false, // Set to true to turn <br> into `\n` instead of ` \n`
'list_item_style' => '-', // Set the default character for each <li> in a <ul>. Can be '-', '*', or '+'
'preserve_comments' => false, // Set to true to preserve comments, or set to an array of strings to preserve specific comments
'use_autolinks' => false, // Set to true to use simple link syntax if possible. Will always use []() if set to false
'table_pipe_escape' => '\|', // Replacement string for pipe characters inside markdown table cells
'table_caption_side' => 'top', // Set to 'top' or 'bottom' to show <caption> content before or after table, null to suppress
]);
$environment->addConverter(new BlockquoteConverter()); $environment->addConverter(new BlockquoteConverter());
$environment->addConverter(new CodeConverter()); $environment->addConverter(new CodeConverter());

View File

@ -44,4 +44,14 @@ class BookExportApiController extends ApiController
$textContent = $this->exportFormatter->bookToPlainText($book); $textContent = $this->exportFormatter->bookToPlainText($book);
return $this->downloadResponse($textContent, $book->slug . '.txt'); return $this->downloadResponse($textContent, $book->slug . '.txt');
} }
/**
* Export a book as a markdown file.
*/
public function exportMarkdown(int $id)
{
$book = Book::visible()->findOrFail($id);
$markdown = $this->exportFormatter->bookToMarkdown($book);
return $this->downloadResponse($markdown, $book->slug . '.md');
}
} }

View File

@ -2,7 +2,6 @@
use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Tools\ExportFormatter; use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Entities\Repos\BookRepo;
use Throwable; use Throwable;
class ChapterExportApiController extends ApiController class ChapterExportApiController extends ApiController
@ -48,4 +47,14 @@ class ChapterExportApiController extends ApiController
$textContent = $this->exportFormatter->chapterToPlainText($chapter); $textContent = $this->exportFormatter->chapterToPlainText($chapter);
return $this->downloadResponse($textContent, $chapter->slug . '.txt'); return $this->downloadResponse($textContent, $chapter->slug . '.txt');
} }
/**
* Export a chapter as a markdown file.
*/
public function exportMarkdown(int $id)
{
$chapter = Chapter::visible()->findOrFail($id);
$markdown = $this->exportFormatter->chapterToMarkdown($chapter);
return $this->downloadResponse($markdown, $chapter->slug . '.md');
}
} }

View File

@ -44,4 +44,14 @@ class PageExportApiController extends ApiController
$textContent = $this->exportFormatter->pageToPlainText($page); $textContent = $this->exportFormatter->pageToPlainText($page);
return $this->downloadResponse($textContent, $page->slug . '.txt'); return $this->downloadResponse($textContent, $page->slug . '.txt');
} }
/**
* Export a page as a markdown file.
*/
public function exportMarkdown(int $id)
{
$page = Page::visible()->findOrFail($id);
$markdown = $this->exportFormatter->pageToMarkdown($page);
return $this->downloadResponse($markdown, $page->slug . '.md');
}
} }

View File

@ -18,6 +18,7 @@ Route::delete('books/{id}', 'BookApiController@delete');
Route::get('books/{id}/export/html', 'BookExportApiController@exportHtml'); Route::get('books/{id}/export/html', 'BookExportApiController@exportHtml');
Route::get('books/{id}/export/pdf', 'BookExportApiController@exportPdf'); Route::get('books/{id}/export/pdf', 'BookExportApiController@exportPdf');
Route::get('books/{id}/export/plaintext', 'BookExportApiController@exportPlainText'); Route::get('books/{id}/export/plaintext', 'BookExportApiController@exportPlainText');
Route::get('books/{id}/export/markdown', 'BookExportApiController@exportMarkdown');
Route::get('chapters', 'ChapterApiController@list'); Route::get('chapters', 'ChapterApiController@list');
Route::post('chapters', 'ChapterApiController@create'); Route::post('chapters', 'ChapterApiController@create');
@ -28,6 +29,7 @@ Route::delete('chapters/{id}', 'ChapterApiController@delete');
Route::get('chapters/{id}/export/html', 'ChapterExportApiController@exportHtml'); Route::get('chapters/{id}/export/html', 'ChapterExportApiController@exportHtml');
Route::get('chapters/{id}/export/pdf', 'ChapterExportApiController@exportPdf'); Route::get('chapters/{id}/export/pdf', 'ChapterExportApiController@exportPdf');
Route::get('chapters/{id}/export/plaintext', 'ChapterExportApiController@exportPlainText'); Route::get('chapters/{id}/export/plaintext', 'ChapterExportApiController@exportPlainText');
Route::get('chapters/{id}/export/markdown', 'ChapterExportApiController@exportMarkdown');
Route::get('pages', 'PageApiController@list'); Route::get('pages', 'PageApiController@list');
Route::post('pages', 'PageApiController@create'); Route::post('pages', 'PageApiController@create');
@ -38,6 +40,7 @@ Route::delete('pages/{id}', 'PageApiController@delete');
Route::get('pages/{id}/export/html', 'PageExportApiController@exportHtml'); Route::get('pages/{id}/export/html', 'PageExportApiController@exportHtml');
Route::get('pages/{id}/export/pdf', 'PageExportApiController@exportPdf'); Route::get('pages/{id}/export/pdf', 'PageExportApiController@exportPdf');
Route::get('pages/{id}/export/plaintext', 'PageExportApiController@exportPlainText'); Route::get('pages/{id}/export/plaintext', 'PageExportApiController@exportPlainText');
Route::get('pages/{id}/export/markdown', 'PageExportApiController@exportMarkDown');
Route::get('shelves', 'BookshelfApiController@list'); Route::get('shelves', 'BookshelfApiController@list');
Route::post('shelves', 'BookshelfApiController@create'); Route::post('shelves', 'BookshelfApiController@create');

View File

@ -140,4 +140,17 @@ class BooksApiTest extends TestCase
$resp->assertStatus(200); $resp->assertStatus(200);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.pdf"'); $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.pdf"');
} }
public function test_export_markdown_endpoint()
{
$this->actingAsApiEditor();
$book = Book::visible()->has('pages')->has('chapters')->first();
$resp = $this->get($this->baseEndpoint . "/{$book->id}/export/markdown");
$resp->assertStatus(200);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.md"');
$resp->assertSee('# ' . $book->name);
$resp->assertSee('# ' . $book->pages()->first()->name);
$resp->assertSee('# ' . $book->chapters()->first()->name);
}
} }

View File

@ -186,4 +186,16 @@ class ChaptersApiTest extends TestCase
$resp->assertStatus(200); $resp->assertStatus(200);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.pdf"'); $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.pdf"');
} }
public function test_export_markdown_endpoint()
{
$this->actingAsApiEditor();
$chapter = Chapter::visible()->has('pages')->first();
$resp = $this->get($this->baseEndpoint . "/{$chapter->id}/export/markdown");
$resp->assertStatus(200);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $chapter->slug . '.md"');
$resp->assertSee('# ' . $chapter->name);
$resp->assertSee('# ' . $chapter->pages()->first()->name);
}
} }

View File

@ -258,4 +258,15 @@ class PagesApiTest extends TestCase
$resp->assertStatus(200); $resp->assertStatus(200);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.pdf"'); $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.pdf"');
} }
public function test_export_markdown_endpoint()
{
$this->actingAsApiEditor();
$page = Page::visible()->first();
$resp = $this->get($this->baseEndpoint . "/{$page->id}/export/markdown");
$resp->assertStatus(200);
$resp->assertSee('# ' . $page->name);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.md"');
}
} }