Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
14 / 14
CRAP
100.00% covered (success)
100.00%
1 / 1
SemanticDocumentationArticle
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
14 / 14
22
100.00% covered (success)
100.00%
1 / 1
 make
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 renderHeader
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 renderBody
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 renderFooter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 process
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 tokenize
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getTokenizedDataArray
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 normalizeBody
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addDynamicHeaderContent
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addDynamicFooterContent
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 renderSourceLink
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 canRenderSourceLink
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 hasTorchlight
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Hyde\Framework\Features\Documentation;
6
7use Hyde\Facades\Config;
8use Hyde\Facades\Features;
9use Hyde\Pages\DocumentationPage;
10use Illuminate\Support\Facades\View;
11use Illuminate\Support\HtmlString;
12use Illuminate\Support\Str;
13
14use function explode;
15use function in_array;
16use function str_contains;
17use function trim;
18
19/**
20 * Class to make Hyde documentation pages smarter by dynamically enriching them with semantic HTML.
21 */
22class SemanticDocumentationArticle
23{
24    protected DocumentationPage $page;
25    protected string $html;
26
27    protected string $header = '';
28    protected string $body;
29    protected string $footer = '';
30
31    /**
32     * Create a new SemanticDocumentationArticle instance, process, and return it.
33     *
34     * @param  \Hyde\Pages\DocumentationPage  $page  The source page object to process.
35     * @return static new processed instance
36     */
37    public static function make(DocumentationPage $page): static
38    {
39        return new self($page);
40    }
41
42    protected function __construct(DocumentationPage $page)
43    {
44        $this->page = $page;
45        $this->html = $page->markdown->compile($page::class);
46
47        $this->process();
48    }
49
50    public function renderHeader(): HtmlString
51    {
52        return new HtmlString($this->header);
53    }
54
55    public function renderBody(): HtmlString
56    {
57        return new HtmlString($this->body);
58    }
59
60    public function renderFooter(): HtmlString
61    {
62        return new HtmlString($this->footer);
63    }
64
65    protected function process(): static
66    {
67        $this->tokenize();
68
69        $this->addDynamicHeaderContent();
70        $this->addDynamicFooterContent();
71
72        return $this;
73    }
74
75    protected function tokenize(): static
76    {
77        // The HTML content is expected to be two parts. To create semantic HTML,
78        // we need to split the content into header and body. We do this by
79        // extracting the first <h1> tag and everything before it.
80
81        [$this->header, $this->body] = $this->getTokenizedDataArray();
82
83        $this->normalizeBody();
84
85        return $this;
86    }
87
88    protected function getTokenizedDataArray(): array
89    {
90        // Split the HTML content by the first newline, which is always after the <h1> tag
91        if (str_contains($this->html, '<h1>')) {
92            return explode("\n", $this->html, 2);
93        }
94
95        return ['', $this->html];
96    }
97
98    protected function normalizeBody(): void
99    {
100        // Remove possible trailing newlines added by the Markdown compiler to normalize the body.
101
102        $this->body = trim($this->body, "\n");
103    }
104
105    protected function addDynamicHeaderContent(): static
106    {
107        // Hook to add dynamic content to the header.
108        // This is where we can add TOC, breadcrumbs, etc.
109
110        if ($this->canRenderSourceLink('header')) {
111            $this->header .= $this->renderSourceLink();
112        }
113
114        return $this;
115    }
116
117    protected function addDynamicFooterContent(): static
118    {
119        // Hook to add dynamic content to the footer.
120        // This is where we can add copyright, attributions, info, etc.
121
122        if (Config::getBool('torchlight.attribution.enabled', true) && $this->hasTorchlight()) {
123            $this->footer .= Str::markdown(Config::getString(
124                'torchlight.attribution.markdown',
125                'Syntax highlighted by torchlight.dev'
126            ));
127        }
128
129        if ($this->canRenderSourceLink('footer')) {
130            $this->footer .= $this->renderSourceLink();
131        }
132
133        return $this;
134    }
135
136    protected function renderSourceLink(): string
137    {
138        return View::make('hyde::components.docs.edit-source-button', [
139            'href' => $this->page->getOnlineSourcePath(),
140        ])->render();
141    }
142
143    /** Do we satisfy the requirements to render an edit source button in the supplied position? */
144    protected function canRenderSourceLink(string $inPosition): bool
145    {
146        $config = Config::getString('docs.edit_source_link_position', 'both');
147        $positions = $config === 'both' ? ['header', 'footer'] : [$config];
148
149        return ($this->page->getOnlineSourcePath() !== false) && in_array($inPosition, $positions);
150    }
151
152    /** Does the current document use Torchlight? */
153    public function hasTorchlight(): bool
154    {
155        return Features::hasTorchlight() && str_contains($this->html, 'Syntax highlighted by torchlight.dev');
156    }
157}