diff --git a/app/Api/ApiDocsController.php b/app/Api/ApiDocsController.php index d88dba3bc..9cf4c5bc3 100644 --- a/app/Api/ApiDocsController.php +++ b/app/Api/ApiDocsController.php @@ -2,10 +2,22 @@ namespace BookStack\Api; +use BookStack\Entities\Tools\Markdown\HtmlToMarkdown; use BookStack\Http\ApiController; +use Illuminate\Http\Request; +use Illuminate\Support\Str; class ApiDocsController extends ApiController { + protected static array $gettingStartedSectionsById = [ + 'authentication' => 'Authentication', + 'request-format' => 'Request Format', + 'listing-endpoints' => 'Listing Endpoints', + 'error-handling' => 'Error Handling', + 'rate-limits' => 'Rate Limits', + 'content-security' => 'Content Security', + ]; + /** * Load the docs page for the API. */ @@ -16,6 +28,7 @@ class ApiDocsController extends ApiController return view('api-docs.index', [ 'docs' => $docs, + 'gettingStartedSections' => static::$gettingStartedSectionsById, ]); } @@ -29,10 +42,36 @@ class ApiDocsController extends ApiController return response()->json($docs); } + /** + * Download the API docs as a cleaner HTML file or as JSON with embedded HTML guidance. + * Provide a ?format=json query parameter to download as JSON. + */ + public function download(Request $request) + { + $format = $request->query('format') ?? 'html'; + $docs = ApiDocsGenerator::generateConsideringCache(); + $downloadName = Str::slug(strtolower(setting('app-name')) . '-api-docs'); + + if ($format === 'json') { + $docs->prepend( + view('api-docs.parts.getting-started')->render(), + 'getting-started-guide' + ); + return $this->createDownload()->directly(json_encode($docs), "{$downloadName}.json"); + } + + $responseData = view('api-docs.download', [ + 'docs' => $docs, + 'gettingStartedSections' => static::$gettingStartedSectionsById, + ])->render(); + + return $this->createDownload()->directly($responseData, "{$downloadName}.html"); + } + /** * Redirect to the API docs page. - * Required as a controller method, instead of the Route::redirect helper, - * to ensure the URL is generated correctly. + * Required as a controller method, instead of the Route::redirect helper, + * to ensure the URL is generated correctly. */ public function redirect() { diff --git a/app/Exports/Controllers/BookExportApiController.php b/app/Exports/Controllers/BookExportApiController.php index 21f276f8a..abcddfe67 100644 --- a/app/Exports/Controllers/BookExportApiController.php +++ b/app/Exports/Controllers/BookExportApiController.php @@ -28,7 +28,7 @@ class BookExportApiController extends ApiController $book = $this->queries->findVisibleByIdOrFail($id); $pdfContent = $this->exportFormatter->bookToPdf($book); - return $this->download()->directly($pdfContent, $book->slug . '.pdf'); + return $this->createDownload()->directly($pdfContent, $book->slug . '.pdf'); } /** @@ -41,7 +41,7 @@ class BookExportApiController extends ApiController $book = $this->queries->findVisibleByIdOrFail($id); $htmlContent = $this->exportFormatter->bookToContainedHtml($book); - return $this->download()->directly($htmlContent, $book->slug . '.html'); + return $this->createDownload()->directly($htmlContent, $book->slug . '.html'); } /** @@ -52,7 +52,7 @@ class BookExportApiController extends ApiController $book = $this->queries->findVisibleByIdOrFail($id); $textContent = $this->exportFormatter->bookToPlainText($book); - return $this->download()->directly($textContent, $book->slug . '.txt'); + return $this->createDownload()->directly($textContent, $book->slug . '.txt'); } /** @@ -63,7 +63,7 @@ class BookExportApiController extends ApiController $book = $this->queries->findVisibleByIdOrFail($id); $markdown = $this->exportFormatter->bookToMarkdown($book); - return $this->download()->directly($markdown, $book->slug . '.md'); + return $this->createDownload()->directly($markdown, $book->slug . '.md'); } /** @@ -74,6 +74,6 @@ class BookExportApiController extends ApiController $book = $this->queries->findVisibleByIdOrFail($id); $zip = $builder->buildForBook($book); - return $this->download()->streamedFileDirectly($zip, $book->slug . '.zip', true); + return $this->createDownload()->streamedFileDirectly($zip, $book->slug . '.zip', true); } } diff --git a/app/Exports/Controllers/BookExportController.php b/app/Exports/Controllers/BookExportController.php index f6bb66666..dc79ca44c 100644 --- a/app/Exports/Controllers/BookExportController.php +++ b/app/Exports/Controllers/BookExportController.php @@ -30,7 +30,7 @@ class BookExportController extends Controller $book = $this->queries->findVisibleBySlugOrFail($bookSlug); $pdfContent = $this->exportFormatter->bookToPdf($book); - return $this->download()->directly($pdfContent, $bookSlug . '.pdf'); + return $this->createDownload()->directly($pdfContent, $bookSlug . '.pdf'); } /** @@ -43,7 +43,7 @@ class BookExportController extends Controller $book = $this->queries->findVisibleBySlugOrFail($bookSlug); $htmlContent = $this->exportFormatter->bookToContainedHtml($book); - return $this->download()->directly($htmlContent, $bookSlug . '.html'); + return $this->createDownload()->directly($htmlContent, $bookSlug . '.html'); } /** @@ -54,7 +54,7 @@ class BookExportController extends Controller $book = $this->queries->findVisibleBySlugOrFail($bookSlug); $textContent = $this->exportFormatter->bookToPlainText($book); - return $this->download()->directly($textContent, $bookSlug . '.txt'); + return $this->createDownload()->directly($textContent, $bookSlug . '.txt'); } /** @@ -65,7 +65,7 @@ class BookExportController extends Controller $book = $this->queries->findVisibleBySlugOrFail($bookSlug); $textContent = $this->exportFormatter->bookToMarkdown($book); - return $this->download()->directly($textContent, $bookSlug . '.md'); + return $this->createDownload()->directly($textContent, $bookSlug . '.md'); } /** @@ -77,6 +77,6 @@ class BookExportController extends Controller $book = $this->queries->findVisibleBySlugOrFail($bookSlug); $zip = $builder->buildForBook($book); - return $this->download()->streamedFileDirectly($zip, $bookSlug . '.zip', true); + return $this->createDownload()->streamedFileDirectly($zip, $bookSlug . '.zip', true); } } diff --git a/app/Exports/Controllers/ChapterExportApiController.php b/app/Exports/Controllers/ChapterExportApiController.php index 7e5a23c70..93c21b7c6 100644 --- a/app/Exports/Controllers/ChapterExportApiController.php +++ b/app/Exports/Controllers/ChapterExportApiController.php @@ -28,7 +28,7 @@ class ChapterExportApiController extends ApiController $chapter = $this->queries->findVisibleByIdOrFail($id); $pdfContent = $this->exportFormatter->chapterToPdf($chapter); - return $this->download()->directly($pdfContent, $chapter->slug . '.pdf'); + return $this->createDownload()->directly($pdfContent, $chapter->slug . '.pdf'); } /** @@ -41,7 +41,7 @@ class ChapterExportApiController extends ApiController $chapter = $this->queries->findVisibleByIdOrFail($id); $htmlContent = $this->exportFormatter->chapterToContainedHtml($chapter); - return $this->download()->directly($htmlContent, $chapter->slug . '.html'); + return $this->createDownload()->directly($htmlContent, $chapter->slug . '.html'); } /** @@ -52,7 +52,7 @@ class ChapterExportApiController extends ApiController $chapter = $this->queries->findVisibleByIdOrFail($id); $textContent = $this->exportFormatter->chapterToPlainText($chapter); - return $this->download()->directly($textContent, $chapter->slug . '.txt'); + return $this->createDownload()->directly($textContent, $chapter->slug . '.txt'); } /** @@ -63,7 +63,7 @@ class ChapterExportApiController extends ApiController $chapter = $this->queries->findVisibleByIdOrFail($id); $markdown = $this->exportFormatter->chapterToMarkdown($chapter); - return $this->download()->directly($markdown, $chapter->slug . '.md'); + return $this->createDownload()->directly($markdown, $chapter->slug . '.md'); } /** @@ -74,6 +74,6 @@ class ChapterExportApiController extends ApiController $chapter = $this->queries->findVisibleByIdOrFail($id); $zip = $builder->buildForChapter($chapter); - return $this->download()->streamedFileDirectly($zip, $chapter->slug . '.zip', true); + return $this->createDownload()->streamedFileDirectly($zip, $chapter->slug . '.zip', true); } } diff --git a/app/Exports/Controllers/ChapterExportController.php b/app/Exports/Controllers/ChapterExportController.php index fdb2bba94..ba974bea0 100644 --- a/app/Exports/Controllers/ChapterExportController.php +++ b/app/Exports/Controllers/ChapterExportController.php @@ -31,7 +31,7 @@ class ChapterExportController extends Controller $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug); $pdfContent = $this->exportFormatter->chapterToPdf($chapter); - return $this->download()->directly($pdfContent, $chapterSlug . '.pdf'); + return $this->createDownload()->directly($pdfContent, $chapterSlug . '.pdf'); } /** @@ -45,7 +45,7 @@ class ChapterExportController extends Controller $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug); $containedHtml = $this->exportFormatter->chapterToContainedHtml($chapter); - return $this->download()->directly($containedHtml, $chapterSlug . '.html'); + return $this->createDownload()->directly($containedHtml, $chapterSlug . '.html'); } /** @@ -58,7 +58,7 @@ class ChapterExportController extends Controller $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug); $chapterText = $this->exportFormatter->chapterToPlainText($chapter); - return $this->download()->directly($chapterText, $chapterSlug . '.txt'); + return $this->createDownload()->directly($chapterText, $chapterSlug . '.txt'); } /** @@ -71,7 +71,7 @@ class ChapterExportController extends Controller $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug); $chapterText = $this->exportFormatter->chapterToMarkdown($chapter); - return $this->download()->directly($chapterText, $chapterSlug . '.md'); + return $this->createDownload()->directly($chapterText, $chapterSlug . '.md'); } /** @@ -83,6 +83,6 @@ class ChapterExportController extends Controller $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug); $zip = $builder->buildForChapter($chapter); - return $this->download()->streamedFileDirectly($zip, $chapterSlug . '.zip', true); + return $this->createDownload()->streamedFileDirectly($zip, $chapterSlug . '.zip', true); } } diff --git a/app/Exports/Controllers/PageExportApiController.php b/app/Exports/Controllers/PageExportApiController.php index c5b186289..00a07e7a7 100644 --- a/app/Exports/Controllers/PageExportApiController.php +++ b/app/Exports/Controllers/PageExportApiController.php @@ -28,7 +28,7 @@ class PageExportApiController extends ApiController $page = $this->queries->findVisibleByIdOrFail($id); $pdfContent = $this->exportFormatter->pageToPdf($page); - return $this->download()->directly($pdfContent, $page->slug . '.pdf'); + return $this->createDownload()->directly($pdfContent, $page->slug . '.pdf'); } /** @@ -41,7 +41,7 @@ class PageExportApiController extends ApiController $page = $this->queries->findVisibleByIdOrFail($id); $htmlContent = $this->exportFormatter->pageToContainedHtml($page); - return $this->download()->directly($htmlContent, $page->slug . '.html'); + return $this->createDownload()->directly($htmlContent, $page->slug . '.html'); } /** @@ -52,7 +52,7 @@ class PageExportApiController extends ApiController $page = $this->queries->findVisibleByIdOrFail($id); $textContent = $this->exportFormatter->pageToPlainText($page); - return $this->download()->directly($textContent, $page->slug . '.txt'); + return $this->createDownload()->directly($textContent, $page->slug . '.txt'); } /** @@ -63,7 +63,7 @@ class PageExportApiController extends ApiController $page = $this->queries->findVisibleByIdOrFail($id); $markdown = $this->exportFormatter->pageToMarkdown($page); - return $this->download()->directly($markdown, $page->slug . '.md'); + return $this->createDownload()->directly($markdown, $page->slug . '.md'); } /** @@ -74,6 +74,6 @@ class PageExportApiController extends ApiController $page = $this->queries->findVisibleByIdOrFail($id); $zip = $builder->buildForPage($page); - return $this->download()->streamedFileDirectly($zip, $page->slug . '.zip', true); + return $this->createDownload()->streamedFileDirectly($zip, $page->slug . '.zip', true); } } diff --git a/app/Exports/Controllers/PageExportController.php b/app/Exports/Controllers/PageExportController.php index 9bc79f247..ba3aabea1 100644 --- a/app/Exports/Controllers/PageExportController.php +++ b/app/Exports/Controllers/PageExportController.php @@ -34,7 +34,7 @@ class PageExportController extends Controller $page->html = (new PageContent($page))->render(); $pdfContent = $this->exportFormatter->pageToPdf($page); - return $this->download()->directly($pdfContent, $pageSlug . '.pdf'); + return $this->createDownload()->directly($pdfContent, $pageSlug . '.pdf'); } /** @@ -49,7 +49,7 @@ class PageExportController extends Controller $page->html = (new PageContent($page))->render(); $containedHtml = $this->exportFormatter->pageToContainedHtml($page); - return $this->download()->directly($containedHtml, $pageSlug . '.html'); + return $this->createDownload()->directly($containedHtml, $pageSlug . '.html'); } /** @@ -62,7 +62,7 @@ class PageExportController extends Controller $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); $pageText = $this->exportFormatter->pageToPlainText($page); - return $this->download()->directly($pageText, $pageSlug . '.txt'); + return $this->createDownload()->directly($pageText, $pageSlug . '.txt'); } /** @@ -75,7 +75,7 @@ class PageExportController extends Controller $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); $pageText = $this->exportFormatter->pageToMarkdown($page); - return $this->download()->directly($pageText, $pageSlug . '.md'); + return $this->createDownload()->directly($pageText, $pageSlug . '.md'); } /** @@ -87,6 +87,6 @@ class PageExportController extends Controller $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); $zip = $builder->buildForPage($page); - return $this->download()->streamedFileDirectly($zip, $pageSlug . '.zip', true); + return $this->createDownload()->streamedFileDirectly($zip, $pageSlug . '.zip', true); } } diff --git a/app/Http/Controller.php b/app/Http/Controller.php index 09c38ee61..97936174d 100644 --- a/app/Http/Controller.php +++ b/app/Http/Controller.php @@ -121,7 +121,7 @@ abstract class Controller extends BaseController /** * Create and return a new download response factory using the current request. */ - protected function download(): DownloadResponseFactory + protected function createDownload(): DownloadResponseFactory { return new DownloadResponseFactory(request()); } diff --git a/app/Theming/ThemeController.php b/app/Theming/ThemeController.php index c26767803..208c8d57a 100644 --- a/app/Theming/ThemeController.php +++ b/app/Theming/ThemeController.php @@ -24,7 +24,7 @@ class ThemeController extends Controller abort(404); } - $response = $this->download()->streamedFileInline($filePath); + $response = $this->createDownload()->streamedFileInline($filePath); $response->setMaxAge(86400); return $response; diff --git a/app/Uploads/Controllers/AttachmentController.php b/app/Uploads/Controllers/AttachmentController.php index 5e128bc34..e1da0c72d 100644 --- a/app/Uploads/Controllers/AttachmentController.php +++ b/app/Uploads/Controllers/AttachmentController.php @@ -232,10 +232,10 @@ class AttachmentController extends Controller $attachmentSize = $this->attachmentService->getAttachmentFileSize($attachment); if ($request->input('open') === 'true') { - return $this->download()->streamedInline($attachmentStream, $fileName, $attachmentSize); + return $this->createDownload()->streamedInline($attachmentStream, $fileName, $attachmentSize); } - return $this->download()->streamedDirectly($attachmentStream, $fileName, $attachmentSize); + return $this->createDownload()->streamedDirectly($attachmentStream, $fileName, $attachmentSize); } /** diff --git a/resources/sass/_blocks.scss b/resources/sass/_blocks.scss index 4857c2cee..a89cfea23 100644 --- a/resources/sass/_blocks.scss +++ b/resources/sass/_blocks.scss @@ -253,6 +253,9 @@ /** * API Docs */ +.api-docs h2.list-heading { + font-size: 1.666rem; +} .api-method { font-size: 0.75rem; background-color: #888; @@ -282,11 +285,11 @@ position: sticky; top: 0; background: #F2F2F2; - background: linear-gradient(180deg,rgba(242, 242, 242, 1) 66%, rgba(242, 242, 242, 0) 100%); + background: linear-gradient(180deg,rgba(242, 242, 242, 1) 80%, rgba(242, 242, 242, 0) 100%); z-index: 4; } } .dark-mode .sticky-sidebar-header { background: #111; - background: linear-gradient(180deg,rgba(17, 17, 17, 1) 66%, rgba(17, 17, 17, 0) 100%); + background: linear-gradient(180deg,rgba(17, 17, 17, 1) 80%, rgba(17, 17, 17, 0) 100%); } diff --git a/resources/views/api-docs/download.blade.php b/resources/views/api-docs/download.blade.php new file mode 100644 index 000000000..e7bf7457f --- /dev/null +++ b/resources/views/api-docs/download.blade.php @@ -0,0 +1,65 @@ + + + + + + {{ setting('app-name') }} API Docs + + + + +

{{ setting('app-name') }} API Documentation

+ +

Contents

+ + +
+ @include('api-docs.parts.getting-started') +
+ +@foreach($docs as $model => $endpoints) +
+

{{ $model }}

+ + @if($endpoints[0]['model_description']) +

{{ $endpoints[0]['model_description'] }}

+ @endif + + @foreach($endpoints as $endpoint) +
+

{{ $endpoint['name'] }}

+

{{ $endpoint['method'] }} {{ url($endpoint['uri']) }}

+
+ @include('api-docs.parts.endpoint-details') + @endforeach +
+@endforeach + + + \ No newline at end of file diff --git a/resources/views/api-docs/index.blade.php b/resources/views/api-docs/index.blade.php index c331f0707..573dbf0b3 100644 --- a/resources/views/api-docs/index.blade.php +++ b/resources/views/api-docs/index.blade.php @@ -2,14 +2,14 @@ @section('body') -
+