Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
36 / 36 |
|
100.00% |
14 / 14 |
CRAP | |
100.00% |
1 / 1 |
SemanticDocumentationArticle | |
100.00% |
36 / 36 |
|
100.00% |
14 / 14 |
22 | |
100.00% |
1 / 1 |
make | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
renderHeader | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
renderBody | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
renderFooter | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
process | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
tokenize | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getTokenizedDataArray | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
normalizeBody | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addDynamicHeaderContent | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
addDynamicFooterContent | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
renderSourceLink | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
canRenderSourceLink | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
hasTorchlight | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | declare(strict_types=1); |
4 | |
5 | namespace Hyde\Framework\Features\Documentation; |
6 | |
7 | use Hyde\Facades\Config; |
8 | use Hyde\Facades\Features; |
9 | use Hyde\Pages\DocumentationPage; |
10 | use Illuminate\Support\Facades\View; |
11 | use Illuminate\Support\HtmlString; |
12 | use Illuminate\Support\Str; |
13 | |
14 | use function explode; |
15 | use function in_array; |
16 | use function str_contains; |
17 | use function trim; |
18 | |
19 | /** |
20 | * Class to make Hyde documentation pages smarter by dynamically enriching them with semantic HTML. |
21 | */ |
22 | class 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 | } |