1 | <?php |
||||
2 | |||||
3 | namespace App\Docsets; |
||||
4 | |||||
5 | use Godbout\DashDocsetBuilder\Docsets\BaseDocset; |
||||
6 | use Illuminate\Support\Collection; |
||||
7 | use Illuminate\Support\Facades\Storage; |
||||
8 | use Illuminate\Support\Str; |
||||
9 | use Wa72\HtmlPageDom\HtmlPageCrawler; |
||||
10 | |||||
11 | class TailwindCSS extends BaseDocset |
||||
12 | { |
||||
13 | public const CODE = 'tailwindcss'; |
||||
14 | public const NAME = 'Tailwind CSS'; |
||||
15 | public const URL = 'tailwindcss.com'; |
||||
16 | public const INDEX = 'docs/installation.html'; |
||||
17 | public const PLAYGROUND = 'https://play.tailwindcss.com/'; |
||||
18 | public const ICON_16 = 'favicon-16x16.png'; |
||||
19 | public const ICON_32 = 'favicon-32x32.png'; |
||||
20 | public const EXTERNAL_DOMAINS = [ |
||||
21 | ]; |
||||
22 | |||||
23 | |||||
24 | public function grab(): bool |
||||
25 | { |
||||
26 | $toIgnore = implode('|', [ |
||||
27 | 'blog.tailwindcss.com', |
||||
28 | 'v1.tailwindcss.com', |
||||
29 | 'v2.tailwindcss.com' |
||||
30 | ]); |
||||
31 | |||||
32 | system( |
||||
33 | "echo; wget tailwindcss.com/docs \ |
||||
34 | --mirror \ |
||||
35 | --trust-server-names \ |
||||
36 | --reject-regex='{$toIgnore}' \ |
||||
37 | --page-requisites \ |
||||
38 | --adjust-extension \ |
||||
39 | --convert-links \ |
||||
40 | --span-hosts \ |
||||
41 | --domains={$this->externalDomains()} \ |
||||
42 | --directory-prefix=storage/{$this->downloadedDirectory()} \ |
||||
43 | -e robots=off \ |
||||
44 | --quiet \ |
||||
45 | --show-progress", |
||||
46 | $result |
||||
47 | ); |
||||
48 | |||||
49 | return $result === 0; |
||||
50 | } |
||||
51 | |||||
52 | 16 | public function entries(string $file): Collection |
|||
53 | { |
||||
54 | 16 | $crawler = HtmlPageCrawler::create(Storage::get($file)); |
|||
55 | |||||
56 | 16 | $entries = collect(); |
|||
57 | |||||
58 | 16 | $entries = $entries->union($this->resourceEntries($crawler, $file)); |
|||
59 | 16 | $entries = $entries->union($this->guideEntries($crawler, $file)); |
|||
60 | 16 | $entries = $entries->union($this->sectionEntries($crawler, $file)); |
|||
61 | |||||
62 | 16 | return $entries; |
|||
63 | } |
||||
64 | |||||
65 | 16 | protected function resourceEntries(HtmlPageCrawler $crawler, string $file) |
|||
66 | { |
||||
67 | 16 | $entries = collect(); |
|||
68 | |||||
69 | 16 | if (Str::contains($file, "{$this->url()}/resources.html")) { |
|||
70 | 16 | $crawler->filter('h2')->each(function (HtmlPageCrawler $node) use ($entries, $file) { |
|||
71 | 16 | $entries->push([ |
|||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
72 | 16 | 'name' => $this->cleanAnchorText($node->text()), |
|||
73 | 16 | 'type' => 'Resource', |
|||
74 | 16 | 'path' => Str::after($file . '#' . Str::slug($node->text()), $this->innerDirectory()), |
|||
75 | 8 | ]); |
|||
76 | 16 | }); |
|||
77 | |||||
78 | 16 | $crawler->filter('h3')->each(function (HtmlPageCrawler $node) use ($entries, $file) { |
|||
79 | 16 | $entries->push([ |
|||
0 ignored issues
–
show
array('name' => $this->c...his->innerDirectory())) of type array<string,string> is incompatible with the type Illuminate\Support\TValue expected by parameter $values of Illuminate\Support\Collection::push() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
80 | 16 | 'name' => $this->cleanAnchorText($node->text()), |
|||
81 | 16 | 'type' => 'Section', |
|||
82 | 16 | 'path' => Str::after($file . '#' . Str::slug($node->text()), $this->innerDirectory()), |
|||
83 | 8 | ]); |
|||
84 | 16 | }); |
|||
85 | |||||
86 | 16 | return $entries; |
|||
87 | } |
||||
88 | 8 | } |
|||
89 | |||||
90 | 16 | protected function guideEntries(HtmlPageCrawler $crawler, string $file) |
|||
91 | { |
||||
92 | 16 | $entries = collect(); |
|||
93 | |||||
94 | 16 | if (Str::contains($file, "{$this->url()}/docs.html")) { |
|||
95 | 8 | $itemsToIgnore = collect(['Release Notes', 'Typography', 'Forms', 'Aspect Ratio', 'Line Clamp']); |
|||
0 ignored issues
–
show
array('Release Notes', '...t Ratio', 'Line Clamp') of type array<integer,string> is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $value of collect() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
96 | |||||
97 | 4 | $crawler |
|||
98 | 8 | ->filter('nav#nav li.mt-8 a') |
|||
99 | 8 | ->each(function (HtmlPageCrawler $node) use ($entries, $itemsToIgnore) { |
|||
100 | 8 | if (! $itemsToIgnore->contains($node->text())) { |
|||
101 | 8 | $entries->push([ |
|||
0 ignored issues
–
show
array('name' => trim($no... . $node->attr('href')) of type array<string,string> is incompatible with the type Illuminate\Support\TValue expected by parameter $values of Illuminate\Support\Collection::push() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
102 | 8 | 'name' => trim($node->text()), |
|||
103 | 8 | 'type' => 'Guide', |
|||
104 | 8 | 'path' => $this->url() . '/' . $node->attr('href'), |
|||
105 | 4 | ]); |
|||
106 | } |
||||
107 | 8 | }); |
|||
108 | } |
||||
109 | |||||
110 | 16 | return $entries; |
|||
111 | } |
||||
112 | |||||
113 | 16 | protected function sectionEntries(HtmlPageCrawler $crawler, string $file) |
|||
114 | { |
||||
115 | 16 | $entries = collect(); |
|||
116 | |||||
117 | 16 | $crawler->filter('h2')->each(function (HtmlPageCrawler $node) use ($entries, $file) { |
|||
118 | 16 | $entries->push([ |
|||
0 ignored issues
–
show
array('name' => $this->c...his->innerDirectory())) of type array<string,string> is incompatible with the type Illuminate\Support\TValue expected by parameter $values of Illuminate\Support\Collection::push() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
119 | 16 | 'name' => $this->cleanAnchorText($node->text()), |
|||
120 | 16 | 'type' => 'Section', |
|||
121 | 16 | 'path' => Str::after($file . '#' . Str::slug($node->text()), $this->innerDirectory()), |
|||
122 | 8 | ]); |
|||
123 | 16 | }); |
|||
124 | |||||
125 | 16 | return $entries; |
|||
126 | } |
||||
127 | |||||
128 | 16 | public function format(string $file): string |
|||
129 | { |
||||
130 | 16 | $crawler = HtmlPageCrawler::create(Storage::get($file)); |
|||
131 | |||||
132 | 16 | $this->removeTopbar($crawler); |
|||
133 | 16 | $this->removeLeftSidebar($crawler); |
|||
134 | 16 | $this->removeRightSidebar($crawler); |
|||
135 | 16 | $this->removeClassesOverflow($crawler); |
|||
136 | |||||
137 | 16 | $this->updateContainerWidth($crawler); |
|||
138 | 16 | $this->tweakFooter($crawler); |
|||
139 | |||||
140 | 16 | $this->removeDarkModeInHTMLTag($crawler); |
|||
141 | 16 | $this->ignoreDarkModeForSomeColors($crawler); |
|||
142 | |||||
143 | 16 | $this->removeUnwantedJavaScript($crawler); |
|||
144 | |||||
145 | 16 | $this->insertOnlineRedirection($crawler, $file); |
|||
146 | 16 | $this->insertDashTableOfContents($crawler, $file); |
|||
147 | |||||
148 | 16 | return $crawler->saveHTML(); |
|||
149 | } |
||||
150 | |||||
151 | 16 | protected function removeTopbar(HtmlPageCrawler $crawler) |
|||
152 | { |
||||
153 | 16 | $crawler->filter('div.sticky.top-0')->remove(); |
|||
154 | 8 | } |
|||
155 | |||||
156 | 16 | protected function removeLeftSidebar(HtmlPageCrawler $crawler) |
|||
157 | { |
||||
158 | 16 | $crawler->filter('div.hidden.fixed.z-20.inset-0.overflow-y-auto')->remove(); |
|||
159 | 8 | } |
|||
160 | |||||
161 | 16 | protected function removeRightSidebar(HtmlPageCrawler $crawler) |
|||
162 | { |
||||
163 | 16 | $crawler->filter('div.fixed.z-20.bottom-0.py-10.px-8.overflow-y-auto.hidden')->remove(); |
|||
164 | 8 | } |
|||
165 | |||||
166 | 16 | protected function removeClassesOverflow(HtmlPageCrawler $crawler) |
|||
167 | { |
||||
168 | 16 | $crawler->filter('#class-reference + div') |
|||
169 | 16 | ->removeClass('overflow-hidden') |
|||
170 | 16 | ->removeClass('lg:overflow-auto') |
|||
171 | 16 | ->addClass('overflow-auto') |
|||
172 | 8 | ; |
|||
173 | |||||
174 | 16 | $crawler->filter('#class-reference ~ div:nth-of-type(2)') |
|||
175 | 16 | ->remove() |
|||
176 | 8 | ; |
|||
177 | 8 | } |
|||
178 | |||||
179 | 16 | protected function updateContainerWidth(HtmlPageCrawler $crawler) |
|||
180 | { |
||||
181 | 16 | $crawler->filter('div.max-w-8xl.mx-auto.px-4 > div:first-child') |
|||
182 | 16 | ->removeClass('lg:pl-[19.5rem]') |
|||
183 | 8 | ; |
|||
184 | 16 | $crawler->filter('div.max-w-3xl.mx-auto.pt-10') |
|||
185 | 16 | ->removeClass('max-w-3xl') |
|||
186 | 16 | ->removeClass('mx-auto') |
|||
187 | 16 | ->removeClass('xl:max-w-none') |
|||
188 | 16 | ->removeClass('xl:mr-[15.5rem]') |
|||
189 | 16 | ->removeClass('xl:pr-16') |
|||
190 | 8 | ; |
|||
191 | 8 | } |
|||
192 | |||||
193 | 16 | protected function tweakFooter(HtmlPageCrawler $crawler) |
|||
194 | { |
||||
195 | 16 | $crawler->filter('footer div:first-child') |
|||
196 | 16 | ->removeClass('mb-10') |
|||
197 | 16 | ->addClass('pb-10') |
|||
198 | 8 | ; |
|||
199 | |||||
200 | 16 | $crawler->filter('footer div:last-child') |
|||
201 | 16 | ->remove() |
|||
202 | 8 | ; |
|||
203 | 8 | } |
|||
204 | |||||
205 | 16 | protected function removeDarkModeInHTMLTag(HtmlPageCrawler $crawler) |
|||
206 | { |
||||
207 | 16 | $crawler->filter('html') |
|||
208 | 16 | ->removeClass('dark') |
|||
209 | 8 | ; |
|||
210 | 8 | } |
|||
211 | |||||
212 | 16 | protected function ignoreDarkModeForSomeColors(HtmlPageCrawler $crawler) |
|||
213 | { |
||||
214 | 16 | $this->ignoreDarkModeForDefaultColorPaletteSection($crawler); |
|||
215 | 16 | $this->ignoreDarkModeForVariousColorTables($crawler); |
|||
216 | 8 | } |
|||
217 | |||||
218 | 16 | protected function ignoreDarkModeForDefaultColorPaletteSection(HtmlPageCrawler $crawler) |
|||
219 | { |
||||
220 | 16 | $crawler->filter('div.h-10.w-full.rounded.ring-1.ring-inset')->addClass('dash-ignore-dark-mode'); |
|||
221 | 8 | } |
|||
222 | |||||
223 | 16 | protected function ignoreDarkModeForVariousColorTables(HtmlPageCrawler $crawler) |
|||
224 | { |
||||
225 | 16 | $crawler->filter('h2 + div td:last-child')->addClass('dash-ignore-dark-mode'); |
|||
226 | 8 | } |
|||
227 | |||||
228 | 16 | protected function removeUnwantedJavaScript(HtmlPageCrawler $crawler) |
|||
229 | { |
||||
230 | 16 | $crawler->filter('script')->remove(); |
|||
231 | 8 | } |
|||
232 | |||||
233 | 16 | protected function insertOnlineRedirection(HtmlPageCrawler $crawler, string $file) |
|||
234 | { |
||||
235 | 16 | $onlineUrl = Str::substr(Str::after($file, $this->innerDirectory()), 1, -5); |
|||
236 | |||||
237 | 16 | $crawler->filter('html')->prepend("<!-- Online page at https://$onlineUrl -->"); |
|||
238 | 8 | } |
|||
239 | |||||
240 | 16 | protected function insertDashTableOfContents(HtmlPageCrawler $crawler, string $file) |
|||
241 | { |
||||
242 | 16 | if (! Str::contains($file, "{$this->url()}/docs.html")) { |
|||
243 | 16 | $crawler->filter('body') |
|||
244 | 16 | ->before('<a name="//apple_ref/cpp/Section/Top" class="dashAnchor"></a>'); |
|||
245 | |||||
246 | 16 | $crawler->filter('h2, h3')->each(function (HtmlPageCrawler $node) { |
|||
247 | 16 | $node->prepend( |
|||
248 | 16 | '<a id="' . Str::slug($node->text()) . '" name="//apple_ref/cpp/Section/' . rawurlencode($this->cleanAnchorText($node->text())) . '" class="dashAnchor"></a>' |
|||
249 | 8 | ); |
|||
250 | 16 | }); |
|||
251 | } |
||||
252 | 8 | } |
|||
253 | |||||
254 | 24 | protected function cleanAnchorText($anchorText) |
|||
255 | { |
||||
256 | 24 | return trim(preg_replace('/\s+/', ' ', $anchorText)); |
|||
257 | } |
||||
258 | } |
||||
259 |