浏览代码

Implement non-static docs

shalvah 5 年之前
父节点
当前提交
2a99555f24
共有 3 个文件被更改,包括 314 次插入2 次删除
  1. 4 0
      .gitignore
  2. 5 2
      config/apidoc.php
  3. 305 0
      src/Writing/Writer.php

+ 4 - 0
.gitignore

@@ -3,9 +3,13 @@ composer.lock
 .php_cs.cache
 /vendor/
 public/
+resources/docs/
+resources/views/apidoc/
 tests/public/
 .idea/
 coverage.xml
 results.xml
 docs/_build
 docs/make.bat
+tests/public/docs/
+tests/resources/**

+ 5 - 2
config/apidoc.php

@@ -4,8 +4,8 @@ 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 Blade files,
-     * so you can add routing and authentication
+     * - "laravel" will generate the documentation as a Blade view,
+     * so you can add routing and authentication.
      */
     'type' => 'static',
 
@@ -22,6 +22,9 @@ return [
 
     /*
      * Generate a Postman collection in addition to HTML docs.
+     * For 'static' docs, the collection will be generated to public/docs/collection.json.
+     * For 'laravel' docs, it will be generated to storage/app/apidoc/collection.json.
+     * The `ApiDoc::routes()` helper will add routes for both the HTML and the Postman collection.
      */
     'postman' => [
         /*

+ 305 - 0
src/Writing/Writer.php

@@ -0,0 +1,305 @@
+<?php
+
+
+namespace Mpociot\ApiDoc\Writing;
+
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Storage;
+use Mpociot\Documentarian\Documentarian;
+use Mpociot\ApiDoc\Tools\DocumentationConfig;
+
+class Writer
+{
+    /**
+     * @var Collection
+     */
+    protected $routes;
+
+    /**
+     * @var Command
+     */
+    protected $output;
+
+    /**
+     * @var DocumentationConfig
+     */
+    private $config;
+
+    /**
+     * @var string
+     */
+    private $baseUrl;
+
+    /**
+     * @var bool
+     */
+    private $forceIt;
+
+    /**
+     * @var bool
+     */
+    private $shouldGeneratePostmanCollection = true;
+
+    /**
+     * @var Documentarian
+     */
+    private $documentarian;
+
+    public function __construct(Collection $routes, bool $forceIt, Command $output, DocumentationConfig $config = null)
+    {
+        $this->routes = $routes;
+        // If no config is injected, pull from global
+        $this->config = $config ?: new DocumentationConfig(config('apidoc'));
+        $this->baseUrl = $this->config->get('base_url') ?? config('app.url');
+        $this->forceIt = $forceIt;
+        $this->output = $output;
+        $this->shouldGeneratePostmanCollection = $this->config->get('postman.enabled', false);
+        $this->documentarian = new Documentarian();
+    }
+
+    public function writeDocs()
+    {
+        // The source files (index.md, js/, css/, and images/) always go in resources/docs/source.
+        // The static assets (js/, css/, and images/) always go in public/docs/.
+        // For 'static' docs, the output files (index.html, collection.json) go in public/docs/.
+        // For 'laravel' docs, the output files (index.blade.php, collection.json)
+        // go in resources/views/apidoc/ and storage/app/apidoc/ respectively.
+        $isStatic = $this->config->get('type') === 'static';
+
+        $sourceOutputPath = "resources/docs";
+        $outputPath = $isStatic ? "public/docs" : "resources/views/apidoc";
+
+        $this->writeMarkdownAndSourceFiles($this->routes, $sourceOutputPath);
+
+        $this->writeHtmlDocs($sourceOutputPath, $outputPath, $isStatic);
+
+        $this->writePostmanCollection($this->routes, $outputPath, $isStatic);
+    }
+
+    /**
+     * @param  Collection $parsedRoutes
+     *
+     * @return void
+     */
+    public function writeMarkdownAndSourceFiles(Collection $parsedRoutes, string $sourceOutputPath)
+    {
+        $targetFile = $sourceOutputPath . '/source/index.md';
+        $compareFile = $sourceOutputPath . '/source/.compare.md';
+
+        $infoText = view('apidoc::partials.info')
+            ->with('outputPath', 'docs')
+            ->with('showPostmanCollectionButton', $this->shouldGeneratePostmanCollection);
+
+        $settings = ['languages' => $this->config->get('example_languages')];
+        // Generate Markdown for each route
+        $parsedRouteOutput = $this->generateMarkdownOutputForEachRoute($parsedRoutes, $settings);
+
+        $frontmatter = view('apidoc::partials.frontmatter')
+            ->with('settings', $settings);
+
+        /*
+         * If the target file already exists,
+         * we check if the documentation was modified
+         * and skip the modified parts of the routes.
+         */
+        if (file_exists($targetFile) && file_exists($compareFile)) {
+            $generatedDocumentation = file_get_contents($targetFile);
+            $compareDocumentation = file_get_contents($compareFile);
+
+            $parsedRouteOutput->transform(function (Collection $routeGroup) use ($generatedDocumentation, $compareDocumentation) {
+                return $routeGroup->transform(function (array $route) use ($generatedDocumentation, $compareDocumentation) {
+                    if (preg_match('/<!-- START_' . $route['id'] . ' -->(.*)<!-- END_' . $route['id'] . ' -->/is', $generatedDocumentation, $existingRouteDoc)) {
+                        $routeDocumentationChanged = (preg_match('/<!-- START_' . $route['id'] . ' -->(.*)<!-- END_' . $route['id'] . ' -->/is', $compareDocumentation, $lastDocWeGeneratedForThisRoute) && $lastDocWeGeneratedForThisRoute[1] !== $existingRouteDoc[1]);
+                        if ($routeDocumentationChanged === false || $this->forceIt) {
+                            if ($routeDocumentationChanged) {
+                                $this->output->warn('Discarded manual changes for route [' . implode(',', $route['methods']) . '] ' . $route['uri']);
+                            }
+                        } else {
+                            $this->output->warn('Skipping modified route [' . implode(',', $route['methods']) . '] ' . $route['uri']);
+                            $route['modified_output'] = $existingRouteDoc[0];
+                        }
+                    }
+
+                    return $route;
+                });
+            });
+        }
+
+        $prependFileContents = $this->getMarkdownToPrepend($sourceOutputPath);
+        $appendFileContents = $this->getMarkdownToAppend($sourceOutputPath);
+
+        $markdown = view('apidoc::documentarian')
+            ->with('writeCompareFile', false)
+            ->with('frontmatter', $frontmatter)
+            ->with('infoText', $infoText)
+            ->with('prependMd', $prependFileContents)
+            ->with('appendMd', $appendFileContents)
+            ->with('outputPath', $this->config->get('output'))
+            ->with('showPostmanCollectionButton', $this->shouldGeneratePostmanCollection)
+            ->with('parsedRoutes', $parsedRouteOutput);
+
+        $this->output->info('Writing index.md and source files to: ' . $sourceOutputPath);
+
+        if (!is_dir($sourceOutputPath)) {
+            $documentarian = new Documentarian();
+            $documentarian->create($sourceOutputPath);
+        }
+
+        // Write output file
+        file_put_contents($targetFile, $markdown);
+
+        // Write comparable markdown file
+        $compareMarkdown = view('apidoc::documentarian')
+            ->with('writeCompareFile', true)
+            ->with('frontmatter', $frontmatter)
+            ->with('infoText', $infoText)
+            ->with('prependMd', $prependFileContents)
+            ->with('appendMd', $appendFileContents)
+            ->with('outputPath', $this->config->get('output'))
+            ->with('showPostmanCollectionButton', $this->shouldGeneratePostmanCollection)
+            ->with('parsedRoutes', $parsedRouteOutput);
+
+        file_put_contents($compareFile, $compareMarkdown);
+
+        $this->output->info('Wrote index.md and source files to: ' . $sourceOutputPath);
+    }
+
+    public function generateMarkdownOutputForEachRoute(Collection $parsedRoutes, array $settings): Collection
+    {
+        $parsedRouteOutput = $parsedRoutes->map(function (Collection $routeGroup) use ($settings) {
+            return $routeGroup->map(function (array $route) use ($settings) {
+                if (count($route['cleanBodyParameters']) && !isset($route['headers']['Content-Type'])) {
+                    // Set content type if the user forgot to set it
+                    $route['headers']['Content-Type'] = 'application/json';
+                }
+                $route['output'] = (string) view('apidoc::partials.route')
+                    ->with('route', $route)
+                    ->with('settings', $settings)
+                    ->with('baseUrl', $this->baseUrl)
+                    ->render();
+
+                return $route;
+            });
+        });
+        return $parsedRouteOutput;
+    }
+
+    protected function writePostmanCollection(Collection $parsedRoutes, string $outputPath, bool $isStatic): void
+    {
+        if ($this->shouldGeneratePostmanCollection) {
+            $this->output->info('Generating Postman collection');
+
+            $collection = $this->generatePostmanCollection($parsedRoutes);
+            if ($isStatic) {
+                $collectionPath = "{$outputPath}/collection.json";
+                file_put_contents($collectionPath, $collection);
+            } else {
+                Storage::disk('local')->put('apidoc/collection.json', $collection);
+                $collectionPath = 'storage/app/apidoc/collection.json';
+            }
+
+            $this->output->info("Wrote Postman collection to: {$collectionPath}");
+        }
+    }
+
+    /**
+     * Generate Postman collection JSON file.
+     *
+     * @param Collection $routes
+     *
+     * @return string
+     */
+    public function generatePostmanCollection(Collection $routes)
+    {
+        $writer = new PostmanCollectionWriter($routes, $this->baseUrl);
+
+        return $writer->getCollection();
+    }
+
+    /**
+     * @param string $sourceOutputPath
+     *
+     * @return string
+     */
+    protected function getMarkdownToPrepend(string $sourceOutputPath): string
+    {
+        $prependFile = $sourceOutputPath . '/source/prepend.md';
+        $prependFileContents = file_exists($prependFile)
+            ? file_get_contents($prependFile) . "\n" : '';
+        return $prependFileContents;
+}
+
+    /**
+     * @param string $sourceOutputPath
+     *
+     * @return string
+     */
+    protected function getMarkdownToAppend(string $sourceOutputPath): string
+    {
+        $appendFile = $sourceOutputPath . '/source/append.md';
+        $appendFileContents = file_exists($appendFile)
+            ? "\n" . file_get_contents($appendFile) : '';
+        return $appendFileContents;
+}
+
+    protected function copyAssetsFromSourceFolderToPublicFolder(string $sourceOutputPath, bool $isStatic = true): void
+    {
+        $publicPath = "public/docs";
+        if (!is_dir($publicPath)) {
+            mkdir($publicPath, 0777, true);
+            mkdir("{$publicPath}/css");
+            mkdir("{$publicPath}/js");
+        }
+        copy("{$sourceOutputPath}/js/all.js", "{$publicPath}/js/all.js");
+        rcopy("{$sourceOutputPath}/images", "{$publicPath}/images");
+        rcopy("{$sourceOutputPath}/css", "{$publicPath}/css");
+
+        if ($logo = $this->config->get('logo')) {
+            if ($isStatic) {
+                copy($logo, "{$publicPath}/images/logo.png");
+            }
+        }
+    }
+
+    protected function moveOutputFromSourceFolderToTargetFolder(string $sourceOutputPath, string $outputPath, bool $isStatic): void
+    {
+        if ($isStatic) {
+            // Move output (index.html, css/style.css and js/all.js) to public/docs
+            rename("{$sourceOutputPath}/index.html", "{$outputPath}/index.html");
+        } else {
+            // Move output to resources/views
+            if (!is_dir($outputPath)) {
+                mkdir($outputPath);
+            }
+            rename("{$sourceOutputPath}/index.html", "$outputPath/index.blade.php");
+            $contents = file_get_contents("$outputPath/index.blade.php");
+            //
+            $contents = str_replace('href="css/style.css"', 'href="/docs/css/style.css"', $contents);
+            $contents = str_replace('src="js/all.js"', 'src="/docs/js/all.js"', $contents);
+            $contents = str_replace('src="images/', 'src="/docs/images/', $contents);
+            $contents = preg_replace('#href="http://.+?/docs/collection.json"#', 'href="{{ route("apidoc", ["format" => ".json"]) }}"', $contents);
+            file_put_contents("$outputPath/index.blade.php", $contents);
+        }
+    }
+
+    /**
+     * @param string $sourceOutputPath
+     * @param string $outputPath
+     * @param bool $isStatic
+     */
+    protected function writeHtmlDocs(string $sourceOutputPath, string $outputPath, bool $isStatic): void
+    {
+        $this->output->info('Generating API HTML code');
+
+        $this->documentarian->generate($sourceOutputPath);
+
+        // Move assets to public folder
+        $this->copyAssetsFromSourceFolderToPublicFolder($sourceOutputPath, $isStatic);
+
+        $this->moveOutputFromSourceFolderToTargetFolder($sourceOutputPath, $outputPath, $isStatic);
+
+        $this->output->info("Wrote HTML documentation to: {$outputPath}");
+    }
+}