| Code Coverage | ||||||||||
| Lines | Functions and Methods | Classes and Traits | ||||||||
| Total |  | 100.00% | 24 / 24 |  | 100.00% | 11 / 11 | CRAP |  | 100.00% | 1 / 1 | 
| GeneratesDocumentationSearchIndexFile |  | 100.00% | 24 / 24 |  | 100.00% | 11 / 11 | 15 |  | 100.00% | 1 / 1 | 
| run |  | 100.00% | 1 / 1 |  | 100.00% | 1 / 1 | 1 | |||
| __construct |  | 100.00% | 2 / 2 |  | 100.00% | 1 / 1 | 1 | |||
| execute |  | 100.00% | 2 / 2 |  | 100.00% | 1 / 1 | 1 | |||
| generate |  | 100.00% | 4 / 4 |  | 100.00% | 1 / 1 | 2 | |||
| generatePageObject |  | 100.00% | 5 / 5 |  | 100.00% | 1 / 1 | 1 | |||
| getSourceFileSlugs |  | 100.00% | 1 / 1 |  | 100.00% | 1 / 1 | 1 | |||
| getObject |  | 100.00% | 1 / 1 |  | 100.00% | 1 / 1 | 1 | |||
| getJson |  | 100.00% | 1 / 1 |  | 100.00% | 1 / 1 | 1 | |||
| save |  | 100.00% | 2 / 2 |  | 100.00% | 1 / 1 | 1 | |||
| getSearchContentForDocument |  | 100.00% | 1 / 1 |  | 100.00% | 1 / 1 | 1 | |||
| getDestinationForSlug |  | 100.00% | 4 / 4 |  | 100.00% | 1 / 1 | 4 | |||
| 1 | <?php | 
| 2 | |
| 3 | namespace Hyde\Framework\Actions; | 
| 4 | |
| 5 | use Hyde\Framework\Contracts\ActionContract; | 
| 6 | use Hyde\Framework\Hyde; | 
| 7 | use Hyde\Framework\Models\DocumentationPage; | 
| 8 | use Hyde\Framework\Models\Parsers\DocumentationPageParser; | 
| 9 | use Hyde\Framework\Services\CollectionService; | 
| 10 | use Illuminate\Support\Collection; | 
| 11 | use Illuminate\Support\Str; | 
| 12 | |
| 13 | /** | 
| 14 | * Generate a JSON file that can be used as a search index for documentation pages. | 
| 15 | * | 
| 16 | * @todo Convert into Service, and add more strategies, such as slug-only (no file parsing) | 
| 17 | * search which while dumber, would be much faster to compile and take way less space. | 
| 18 | * | 
| 19 | * @see \Tests\Feature\Actions\GeneratesDocumentationSearchIndexFileTest | 
| 20 | */ | 
| 21 | class GeneratesDocumentationSearchIndexFile implements ActionContract | 
| 22 | { | 
| 23 | public Collection $searchIndex; | 
| 24 | public static string $filePath = '_site/docs/search.json'; | 
| 25 | |
| 26 | public static function run(): void | 
| 27 | { | 
| 28 | (new static())->execute(); | 
| 29 | } | 
| 30 | |
| 31 | public function __construct() | 
| 32 | { | 
| 33 | $this->searchIndex = new Collection(); | 
| 34 | |
| 35 | static::$filePath = '_site/'.config('docs.output_directory', 'docs').'/search.json'; | 
| 36 | } | 
| 37 | |
| 38 | public function execute(): void | 
| 39 | { | 
| 40 | $this->generate(); | 
| 41 | $this->save(); | 
| 42 | } | 
| 43 | |
| 44 | public function generate(): self | 
| 45 | { | 
| 46 | foreach ($this->getSourceFileSlugs() as $page) { | 
| 47 | $this->searchIndex->push( | 
| 48 | $this->generatePageObject($page) | 
| 49 | ); | 
| 50 | } | 
| 51 | |
| 52 | return $this; | 
| 53 | } | 
| 54 | |
| 55 | public function generatePageObject(string $slug): object | 
| 56 | { | 
| 57 | $page = (new DocumentationPageParser($slug))->get(); | 
| 58 | |
| 59 | return (object) [ | 
| 60 | 'slug' => $page->slug, | 
| 61 | 'title' => trim($page->findTitleForDocument()), | 
| 62 | 'content' => trim($this->getSearchContentForDocument($page)), | 
| 63 | 'destination' => $this->getDestinationForSlug($page->slug), | 
| 64 | ]; | 
| 65 | } | 
| 66 | |
| 67 | public function getSourceFileSlugs(): array | 
| 68 | { | 
| 69 | return CollectionService::getDocumentationPageList(); | 
| 70 | } | 
| 71 | |
| 72 | public function getObject(): object | 
| 73 | { | 
| 74 | return (object) $this->searchIndex; | 
| 75 | } | 
| 76 | |
| 77 | public function getJson(): string | 
| 78 | { | 
| 79 | return json_encode($this->getObject()); | 
| 80 | } | 
| 81 | |
| 82 | public function save(): self | 
| 83 | { | 
| 84 | file_put_contents(Hyde::path(static::$filePath), $this->getJson()); | 
| 85 | |
| 86 | return $this; | 
| 87 | } | 
| 88 | |
| 89 | /** | 
| 90 | * There are a few ways we could go about this. The goal is to allow the user | 
| 91 | * to run a free-text search to find relevant documentation pages. | 
| 92 | * | 
| 93 | * The easiest way to do this is by adding the Markdown body to the search index. | 
| 94 | * But this is of course not ideal as it may take an incredible amount of space | 
| 95 | * for large documentation sites. The Hyde docs weight around 80kb of JSON. | 
| 96 | * | 
| 97 | * Another option is to assemble all the headings in a document and use that | 
| 98 | * for the search basis. A truncated version of the body could also be included. | 
| 99 | * | 
| 100 | * A third option which might be the most space efficient (besides from just | 
| 101 | * adding titles, which doesn't offer much help to the user since it is just | 
| 102 | * a filterable sidebar at that point), would be to search for keywords | 
| 103 | * in the document. This would however add complexity as well as extra | 
| 104 | * computing time. | 
| 105 | * | 
| 106 | * Benchmarks: (for official Hyde docs) | 
| 107 | * | 
| 108 | * Returning $document->body as is: 500ms | 
| 109 | * Returning $document->body as Str::markdown(): 920ms + 10ms for regex | 
| 110 | */ | 
| 111 | public function getSearchContentForDocument(DocumentationPage $document): string | 
| 112 | { | 
| 113 | // This is compiles the Markdown body into HTML, and then strips out all | 
| 114 | // HTML tags to get a plain text version of the body. This takes a long | 
| 115 | // site, but is the simplest implementation I've found so far. | 
| 116 | return preg_replace('/<(.|\n)*?>/', ' ', Str::markdown($document->body)); | 
| 117 | } | 
| 118 | |
| 119 | public function getDestinationForSlug(string $slug): string | 
| 120 | { | 
| 121 | if ($slug === 'index' && config('hyde.pretty_urls', false)) { | 
| 122 | $slug = ''; | 
| 123 | } | 
| 124 | |
| 125 | return (config('hyde.pretty_urls', false) === true) | 
| 126 | ? $slug : $slug.'.html'; | 
| 127 | } | 
| 128 | } |