Browse Source

Support external docs

Shalvah 1 year ago
parent
commit
5714e3f143

+ 3 - 0
config/scribe.php

@@ -42,8 +42,11 @@ return [
     // The type of documentation output to generate.
     // - "static" will generate a static HTMl page in the /public/docs folder,
     // - "laravel" will generate the documentation as a Blade view, so you can add routing and authentication.
+    // - "external_static" and "external_laravel" do the same as above, but generate a basic template,
+    // passing the OpenAPI spec as a URL, allowing you to easily use the docs with an external generator
     'type' => 'static',
 
+    // See https://scribe.knuckles.wtf/laravel/reference/config#theme for supported options
     'theme' => 'default',
 
     'static' => [

+ 13 - 0
resources/views/external/rapidoc.blade.php

@@ -0,0 +1,13 @@
+
+<!doctype html> <!-- Important: must specify -->
+<html>
+<head>
+    <meta charset="utf-8"> <!-- Important: rapi-doc uses utf8 characters -->
+    <script type="module" src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
+</head>
+<body>
+<rapi-doc spec-url="{!! $metadata['openapi_spec_url'] !!}"
+          render-style="read"
+> </rapi-doc>
+</body>
+</html>

+ 23 - 0
resources/views/external/scalar.blade.php

@@ -0,0 +1,23 @@
+<!doctype html>
+<html>
+<head>
+    <title>{!! $metadata['title'] !!}</title>
+    <meta charset="utf-8" />
+    <meta
+            name="viewport"
+            content="width=device-width, initial-scale=1" />
+    <style>
+        body {
+            margin: 0;
+        }
+    </style>
+</head>
+<body>
+
+<script
+        id="api-reference"
+        data-url="{!! $metadata['openapi_spec_url'] !!}">
+</script>
+<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
+</body>
+</html>

+ 1 - 1
src/Commands/GenerateDocumentation.php

@@ -212,7 +212,7 @@ class GenerateDocumentation extends Command
     protected function sayGoodbye(bool $errored = false): void
     {
         $message = 'All done. ';
-        if ($this->docConfig->get('type') == 'laravel') {
+        if ($this->docConfig->outputRoutedThroughLaravel()) {
             if ($this->docConfig->get('laravel.add_routes')) {
                 $message .= 'Visit your docs at ' . url($this->docConfig->get('laravel.docs_url'));
             }

+ 16 - 0
src/Config/Output.php

@@ -59,6 +59,22 @@ class Output
         return ['static', get_defined_vars()];
     }
 
+    public static function externalStaticType(
+        string $outputPath = 'public/docs',
+    ): array
+    {
+        return ['external_static', get_defined_vars()];
+    }
+
+    public static function externalLaravelType(
+        bool   $addRoutes = true,
+        string $docsUrl = '/docs',
+        array  $middleware = [],
+    ): array
+    {
+        return ['external_laravel', get_defined_vars()];
+    }
+
     public static function postman(
         bool  $enabled = true,
         array $overrides = [],

+ 4 - 2
src/ScribeServiceProvider.php

@@ -3,6 +3,7 @@
 namespace Knuckles\Scribe;
 
 use Illuminate\Support\ServiceProvider;
+use Illuminate\Support\Str;
 use Knuckles\Scribe\Commands\DiffConfig;
 use Knuckles\Scribe\Commands\GenerateDocumentation;
 use Knuckles\Scribe\Commands\MakeStrategy;
@@ -44,7 +45,7 @@ class ScribeServiceProvider extends ServiceProvider
     protected function bootRoutes()
     {
         if (
-            config('scribe.type', 'static') === 'laravel' &&
+            Str::endsWith(config('scribe.type', 'static'), 'laravel') &&
             config('scribe.laravel.add_routes', false)
         ) {
             $routesPath = Utils::isLumen() ? __DIR__ . '/../routes/lumen.php' : __DIR__ . '/../routes/laravel.php';
@@ -55,7 +56,7 @@ class ScribeServiceProvider extends ServiceProvider
     protected function configureTranslations(): void
     {
         $this->publishes([
-            __DIR__.'/../lang/' => $this->app->langPath(),
+            __DIR__ . '/../lang/' => $this->app->langPath(),
         ], 'scribe-translations');
 
         $this->loadTranslationsFrom($this->app->langPath('scribe.php'), 'scribe');
@@ -77,6 +78,7 @@ class ScribeServiceProvider extends ServiceProvider
             'examples' => 'partials/example-requests',
             'themes' => 'themes',
             'markdown' => 'markdown',
+            'external' => 'external',
         ];
         foreach ($viewGroups as $group => $path) {
             $this->publishes([

+ 16 - 0
src/Tools/DocumentationConfig.php

@@ -2,6 +2,7 @@
 
 namespace Knuckles\Scribe\Tools;
 
+use Illuminate\Support\Str;
 use Knuckles\Scribe\Tools\ConsoleOutputUtils as c;
 
 class DocumentationConfig
@@ -45,4 +46,19 @@ class DocumentationConfig
         return 'laravel';
 
     }
+
+    public function outputIsStatic(): bool
+    {
+        return !$this->outputRoutedThroughLaravel();
+    }
+
+    public function outputRoutedThroughLaravel(): bool
+    {
+        return Str::is(['laravel', 'external_laravel'], $this->get('type'));
+    }
+
+    public function outputIsExternal(): bool
+    {
+        return Str::is(['external_static', 'external_laravel'], $this->get('type'));
+    }
 }

+ 47 - 0
src/Writing/ExternalHtmlWriter.php

@@ -0,0 +1,47 @@
+<?php
+
+namespace Knuckles\Scribe\Writing;
+
+use Illuminate\Support\Facades\View;
+
+/**
+ * Writes a basic, mostly empty template, passing the OpenAPI spec URL in for an external client-side renderer.
+ */
+class ExternalHtmlWriter extends HtmlWriter
+{
+    public function generate(array $groupedEndpoints, string $sourceFolder, string $destinationFolder)
+    {
+        $template = $this->config->get('theme');
+        $output = View::make("scribe::external.$template", [
+            'metadata' => $this->getMetadata(),
+            'baseUrl' => $this->baseUrl,
+            'tryItOut' => $this->config->get('try_it_out'),
+        ])->render();
+
+        if (!is_dir($destinationFolder)) {
+            mkdir($destinationFolder, 0777, true);
+        }
+
+        file_put_contents($destinationFolder . '/index.html', $output);
+    }
+
+    public function getMetadata(): array
+    {
+        // NB:These paths are wrong for laravel type but will be set correctly by the Writer class
+        if ($this->config->get('postman.enabled', true)) {
+            $postmanCollectionUrl = "{$this->assetPathPrefix}collection.json";
+        }
+        if ($this->config->get('openapi.enabled', false)) {
+            $openApiSpecUrl = "{$this->assetPathPrefix}openapi.yaml";
+        }
+        return [
+            'title' => $this->config->get('title') ?: config('app.name', '') . ' Documentation',
+            'example_languages' => $this->config->get('example_languages'), // may be useful
+            'logo' => $this->config->get('logo') ?? false,
+            'last_updated' => $this->getLastUpdated(), // may be useful
+            'try_it_out' => $this->config->get('try_it_out'), // may be useful
+            "postman_collection_url" => $postmanCollectionUrl ?? null,
+            "openapi_spec_url" => $openApiSpecUrl ?? null,
+        ];
+    }
+}

+ 41 - 7
src/Writing/Writer.php

@@ -3,6 +3,7 @@
 namespace Knuckles\Scribe\Writing;
 
 use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Str;
 use Knuckles\Scribe\Tools\ConsoleOutputUtils as c;
 use Knuckles\Scribe\Tools\DocumentationConfig;
 use Knuckles\Scribe\Tools\Globals;
@@ -13,6 +14,7 @@ use Symfony\Component\Yaml\Yaml;
 class Writer
 {
     private bool $isStatic;
+    private bool $isExternal;
 
     private ?string $staticTypeOutputPath;
 
@@ -33,7 +35,9 @@ class Writer
 
     public function __construct(protected DocumentationConfig $config, public PathConfig $paths)
     {
-        $this->isStatic = $this->config->get('type') === 'static';
+        $this->isStatic = $this->config->outputIsStatic();
+        $this->isExternal = $this->config->outputIsExternal();
+
         $this->laravelTypeOutputPath = $this->getLaravelTypeOutputPath();
         $this->staticTypeOutputPath = rtrim($this->config->get('static.output_path', 'public/docs'), '/');
 
@@ -52,11 +56,15 @@ class Writer
         // For 'laravel' docs, the output files (index.blade.php, collection.json)
         // go in resources/views/scribe/ and storage/app/scribe/ respectively.
 
-        $this->writeHtmlDocs($groupedEndpoints);
-
-        $this->writePostmanCollection($groupedEndpoints);
-
-        $this->writeOpenAPISpec($groupedEndpoints);
+        if ($this->isExternal) {
+            $this->writeOpenAPISpec($groupedEndpoints);
+            $this->writePostmanCollection($groupedEndpoints);
+            $this->writeExternalHtmlDocs();
+        } else {
+            $this->writeHtmlDocs($groupedEndpoints);
+            $this->writePostmanCollection($groupedEndpoints);
+            $this->writeOpenAPISpec($groupedEndpoints);
+        }
 
         $this->runAfterGeneratingHook();
     }
@@ -83,7 +91,7 @@ class Writer
 
     protected function writeOpenAPISpec(array $parsedRoutes): void
     {
-        if ($this->config->get('openapi.enabled', false)) {
+        if ($this->config->get('openapi.enabled', false) || $this->isExternal) {
             c::info('Generating OpenAPI specification');
 
             $spec = $this->generateOpenAPISpec($parsedRoutes);
@@ -169,6 +177,7 @@ class Writer
         $contents = preg_replace('#src="\.\./docs/(js|images)/(.+?)"#', 'src="{{ asset("' . $this->laravelAssetsPath . '/$1/$2") }}"', $contents);
         $contents = str_replace('href="../docs/collection.json"', 'href="{{ route("' . $this->paths->outputPath('postman', '.') . '") }}"', $contents);
         $contents = str_replace('href="../docs/openapi.yaml"', 'href="{{ route("' . $this->paths->outputPath('openapi', '.') . '") }}"', $contents);
+        $contents = str_replace('url="../docs/openapi.yaml"', 'url="{{ route("' . $this->paths->outputPath('openapi', '.') . '") }}"', $contents);
 
         file_put_contents("$this->laravelTypeOutputPath/index.blade.php", $contents);
     }
@@ -203,6 +212,31 @@ class Writer
         $this->generatedFiles['assets']['images'] = realpath("{$assetsOutputPath}images");
     }
 
+    public function writeExternalHtmlDocs(): void
+    {
+        c::info('Writing client-side HTML docs...');
+
+        /** @var ExternalHtmlWriter $writer */
+        $writer = app()->makeWith(ExternalHtmlWriter::class, ['config' => $this->config]);
+        $writer->generate([], $this->paths->intermediateOutputPath(), $this->staticTypeOutputPath);
+
+       if (!$this->isStatic) {
+           $this->performFinalTasksForLaravelType();
+       }
+
+        if ($this->isStatic) {
+            $outputPath = rtrim($this->staticTypeOutputPath, '/') . '/';
+            c::success("Wrote client-side HTML docs and assets to: $outputPath");
+            $this->generatedFiles['html'] = realpath("{$outputPath}index.html");
+        } else {
+            $outputPath = rtrim($this->laravelTypeOutputPath, '/') . '/';
+            c::success("Wrote Blade docs to: " . $this->makePathFriendly($outputPath));
+            $this->generatedFiles['blade'] = realpath("{$outputPath}index.blade.php");
+            $assetsOutputPath = public_path() . $this->laravelAssetsPath . '/';
+            c::success("Wrote Laravel assets to: " . $this->makePathFriendly($assetsOutputPath));
+        }
+    }
+
     protected function runAfterGeneratingHook()
     {
         if (is_callable(Globals::$__afterGenerating)) {