1
|
|
|
<?php |
2
|
|
|
declare(strict_types=1); |
3
|
|
|
|
4
|
|
|
namespace Nexendrie\SiteGenerator; |
5
|
|
|
|
6
|
|
|
use cebe\markdown\GithubMarkdown, |
7
|
|
|
Nette\Utils\Finder, |
8
|
|
|
Nette\Neon\Neon, |
9
|
|
|
Nette\Utils\FileSystem, |
10
|
|
|
Symfony\Component\OptionsResolver\OptionsResolver, |
11
|
|
|
Nette\Utils\Validators; |
12
|
|
|
|
13
|
|
|
/** |
14
|
|
|
* Generator |
15
|
|
|
* |
16
|
|
|
* @author Jakub Konečný |
17
|
|
|
* @property string $source |
18
|
|
|
* @property string $output |
19
|
|
|
* @method void onBeforeGenerate() |
20
|
|
|
* @method void onCreatePage(string $html, Generator $generator, string $filename) |
21
|
|
|
* @method void onAfterGenerate() |
22
|
|
|
*/ |
23
|
1 |
|
class Generator { |
24
|
1 |
|
use \Nette\SmartObject; |
25
|
|
|
|
26
|
|
|
/** @var string */ |
27
|
|
|
protected $templateFile = __DIR__ . "/template.html"; |
28
|
|
|
/** @var string[] */ |
29
|
|
|
protected $ignoredFiles = [ |
30
|
|
|
"README.md", |
31
|
|
|
]; |
32
|
|
|
/** @var string[] */ |
33
|
|
|
protected $ignoredFolders = [ |
34
|
|
|
"vendor", ".git", "tests", |
35
|
|
|
]; |
36
|
|
|
/** @var string */ |
37
|
|
|
protected $source; |
38
|
|
|
/** @var string */ |
39
|
|
|
protected $output; |
40
|
|
|
/** @var string[] */ |
41
|
|
|
protected $assets = []; |
42
|
|
|
/** @var callable[] */ |
43
|
|
|
protected $metaNormalizers = []; |
44
|
|
|
/** @var callable[] */ |
45
|
|
|
public $onBeforeGenerate = []; |
46
|
|
|
/** @var callable[] */ |
47
|
|
|
public $onCreatePage = []; |
48
|
|
|
/** @var callable[] */ |
49
|
|
|
public $onAfterGenerate = []; |
50
|
|
|
|
51
|
|
|
public function __construct(string $source, string $output) { |
52
|
1 |
|
$this->setSource($source); |
53
|
1 |
|
FileSystem::createDir($output); |
54
|
1 |
|
$this->setOutput($output); |
55
|
1 |
|
$this->onBeforeGenerate[] = [$this, "clearOutputFolder"]; |
56
|
1 |
|
$this->onCreatePage[] = [$this, "processImages"]; |
57
|
1 |
|
$this->onAfterGenerate[] = [$this, "copyAssets"]; |
58
|
1 |
|
$this->addMetaNormalizer([$this, "normalizeTitle"]); |
59
|
1 |
|
$this->addMetaNormalizer([$this, "normalizeStyles"]); |
60
|
1 |
|
$this->addMetaNormalizer([$this, "normalizeScripts"]); |
61
|
1 |
|
} |
62
|
|
|
|
63
|
|
|
public function addMetaNormalizer(callable $callback): void { |
64
|
1 |
|
$this->metaNormalizers[] = $callback; |
65
|
1 |
|
} |
66
|
|
|
|
67
|
|
|
public function getSource(): string { |
68
|
1 |
|
return $this->source; |
69
|
|
|
} |
70
|
|
|
|
71
|
|
|
public function setSource(string $source) { |
72
|
1 |
|
if(is_dir($source)) { |
73
|
1 |
|
$this->source = realpath($source); |
74
|
|
|
} |
75
|
1 |
|
} |
76
|
|
|
|
77
|
|
|
public function getOutput(): string { |
78
|
1 |
|
return $this->output; |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
public function setOutput(string $output) { |
82
|
1 |
|
$this->output = realpath($output); |
83
|
1 |
|
} |
84
|
|
|
|
85
|
|
|
protected function createMetaResolver(): OptionsResolver { |
86
|
1 |
|
$resolver = new OptionsResolver(); |
87
|
1 |
|
$resolver->setDefaults([ |
88
|
1 |
|
"title" => "", |
89
|
|
|
"styles" => [], |
90
|
|
|
"scripts" => [], |
91
|
|
|
]); |
92
|
1 |
|
$isArrayOfStrings = function(array $value) { |
93
|
1 |
|
return Validators::everyIs($value, "string"); |
94
|
|
|
}; |
95
|
1 |
|
$resolver->setAllowedTypes("title", "string"); |
96
|
1 |
|
$resolver->setAllowedTypes("styles", "array"); |
97
|
1 |
|
$resolver->setAllowedValues("styles", $isArrayOfStrings); |
98
|
1 |
|
$resolver->setAllowedTypes("scripts", "array"); |
99
|
1 |
|
$resolver->setAllowedValues("scripts", $isArrayOfStrings); |
100
|
1 |
|
return $resolver; |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
protected function getMetafileName(string $filename): string { |
104
|
1 |
|
return str_replace(".md", ".neon", $filename); |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
protected function getMeta(string $filename, string &$html): array { |
108
|
1 |
|
$resolver = $this->createMetaResolver(); |
109
|
1 |
|
$metaFilename = $this->getMetafileName($filename); |
110
|
1 |
|
$meta = []; |
111
|
1 |
|
if(file_exists($metaFilename)) { |
112
|
1 |
|
$meta = Neon::decode(file_get_contents($metaFilename)); |
113
|
|
|
} |
114
|
1 |
|
$result = $resolver->resolve($meta); |
115
|
1 |
|
foreach($this->metaNormalizers as $normalizer) { |
116
|
1 |
|
$normalizer($result, $html, $filename); |
117
|
|
|
} |
118
|
1 |
|
return $result; |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
protected function addAsset(string $asset): void { |
122
|
1 |
|
$asset = realpath($asset); |
123
|
1 |
|
if(!in_array($asset, $this->assets)) { |
124
|
1 |
|
$this->assets[] = $asset; |
125
|
|
|
} |
126
|
1 |
|
} |
127
|
|
|
|
128
|
|
|
protected function normalizeTitle(array &$meta, string &$html, string $filename): void { |
129
|
1 |
|
if(strlen($meta["title"]) === 0) { |
130
|
1 |
|
unset($meta["title"]); |
131
|
1 |
|
$html = str_replace(" |
132
|
1 |
|
<title>%%title%%</title>", "", $html); |
133
|
|
|
} |
134
|
1 |
|
} |
135
|
|
|
|
136
|
|
|
protected function removeInvalidFiles(array &$input, string $basePath): void { |
137
|
1 |
|
$input = array_filter($input, function($value) use($basePath) { |
138
|
1 |
|
return file_exists("$basePath/$value"); |
139
|
1 |
|
}); |
140
|
1 |
|
} |
141
|
|
|
|
142
|
|
View Code Duplication |
protected function normalizeStyles(array &$meta, string &$html, string $filename): void { |
|
|
|
|
143
|
1 |
|
$basePath = dirname($filename); |
144
|
1 |
|
$this->removeInvalidFiles($meta["styles"], $basePath); |
145
|
1 |
|
if(!count($meta["styles"])) { |
146
|
1 |
|
unset($meta["styles"]); |
147
|
1 |
|
$html = str_replace(" |
148
|
1 |
|
%%styles%%", "", $html); |
149
|
1 |
|
return; |
150
|
|
|
} |
151
|
1 |
|
array_walk($meta["styles"], function(&$value) use($basePath) { |
152
|
1 |
|
$this->addAsset("$basePath/$value"); |
153
|
1 |
|
$value = "<link rel=\"stylesheet\" type=\"text/css\" href=\"$value\">"; |
154
|
1 |
|
}); |
155
|
1 |
|
$meta["styles"] = implode("\n ", $meta["styles"]); |
156
|
1 |
|
} |
157
|
|
|
|
158
|
|
View Code Duplication |
protected function normalizeScripts(array &$meta, string &$html, string $filename): void { |
|
|
|
|
159
|
1 |
|
$basePath = dirname($filename); |
160
|
1 |
|
$this->removeInvalidFiles($meta["scripts"], $basePath); |
161
|
1 |
|
if(!count($meta["scripts"])) { |
162
|
1 |
|
unset($meta["scripts"]); |
163
|
1 |
|
$html = str_replace(" |
164
|
1 |
|
%%scripts%%", "", $html); |
165
|
1 |
|
return; |
166
|
|
|
} |
167
|
1 |
|
array_walk($meta["scripts"], function(&$value) use($basePath) { |
168
|
1 |
|
$this->addAsset("$basePath/$value"); |
169
|
1 |
|
$value = "<script type=\"text/javascript\" src=\"$value\"></script>"; |
170
|
1 |
|
}); |
171
|
1 |
|
$meta["scripts"] = implode("\n ", $meta["scripts"]); |
172
|
1 |
|
} |
173
|
|
|
|
174
|
|
|
protected function createMarkdownParser(): \cebe\markdown\Markdown { |
175
|
1 |
|
$parser = new class extends GithubMarkdown { |
176
|
|
|
public function parse($text): string { |
177
|
1 |
|
$markup = parent::parse($text); |
178
|
1 |
|
if(substr($markup, -1) === PHP_EOL) { |
179
|
1 |
|
$markup = substr($markup, 0, -1); |
180
|
|
|
} |
181
|
1 |
|
return $markup; |
182
|
|
|
} |
183
|
|
|
}; |
184
|
1 |
|
$parser->html5 = $parser->keepListStartNumber = $parser->enableNewlines = true; |
185
|
1 |
|
return $parser; |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
protected function createHtml(string $filename): string { |
189
|
1 |
|
$parser = $this->createMarkdownParser(); |
190
|
1 |
|
$source = $parser->parse(file_get_contents($filename)); |
191
|
1 |
|
$html = file_get_contents($this->templateFile); |
192
|
1 |
|
$html = str_replace("%%source%%", $source, $html); |
193
|
1 |
|
return $html; |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* @internal |
198
|
|
|
*/ |
199
|
|
|
public function clearOutputFolder(): void { |
200
|
1 |
|
FileSystem::delete($this->output); |
201
|
1 |
|
} |
202
|
|
|
|
203
|
|
|
/** |
204
|
|
|
* @internal |
205
|
|
|
*/ |
206
|
|
|
public function copyAssets(): void { |
207
|
1 |
|
foreach($this->assets as $asset) { |
208
|
1 |
|
$path = str_replace($this->source, "", $asset); |
209
|
1 |
|
$target = "$this->output$path"; |
210
|
1 |
|
FileSystem::copy($asset, $target); |
211
|
1 |
|
echo "Copied $path"; |
212
|
|
|
} |
213
|
1 |
|
} |
214
|
|
|
|
215
|
|
|
/** |
216
|
|
|
* @internal |
217
|
|
|
*/ |
218
|
|
|
public function processImages(string $html, self $generator, string $filename): void { |
219
|
1 |
|
$dom = new \DOMDocument(); |
220
|
1 |
|
$dom->loadHTML($html); |
221
|
1 |
|
$images = $dom->getElementsByTagName("img"); |
222
|
|
|
/** @var \DOMElement $image */ |
223
|
1 |
|
foreach($images as $image) { |
224
|
1 |
|
$path = dirname($filename) . "/" . $image->getAttribute("src"); |
225
|
1 |
|
if(file_exists($path)) { |
226
|
1 |
|
$generator->addAsset($path); |
227
|
|
|
} |
228
|
|
|
} |
229
|
1 |
|
} |
230
|
|
|
|
231
|
|
|
/** |
232
|
|
|
* Generate the site |
233
|
|
|
*/ |
234
|
|
|
public function generate(): void { |
235
|
1 |
|
$this->onBeforeGenerate(); |
236
|
1 |
|
$files = Finder::findFiles("*.md") |
237
|
1 |
|
->exclude($this->ignoredFiles) |
238
|
1 |
|
->from($this->source) |
239
|
1 |
|
->exclude($this->ignoredFolders); |
240
|
|
|
/** @var \SplFileInfo $file */ |
241
|
1 |
|
foreach($files as $file) { |
242
|
1 |
|
$path = str_replace($this->source, "", dirname($file->getRealPath())); |
243
|
1 |
|
$html = $this->createHtml($file->getRealPath()); |
244
|
1 |
|
$meta = $this->getMeta($file->getRealPath(), $html); |
245
|
1 |
|
foreach($meta as $key => $value) { |
246
|
1 |
|
$html = str_replace("%%$key%%", $value, $html); |
247
|
|
|
} |
248
|
1 |
|
$basename = $file->getBasename(".md") . ".html"; |
249
|
1 |
|
$filename = "$this->output$path/$basename"; |
250
|
1 |
|
FileSystem::write($filename, $html); |
251
|
1 |
|
echo "Created $path/$basename\n"; |
252
|
1 |
|
$this->onCreatePage($html, $this, $file->getRealPath()); |
253
|
|
|
} |
254
|
1 |
|
$this->onAfterGenerate(); |
255
|
1 |
|
} |
256
|
|
|
} |
257
|
|
|
?> |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.