Generator::createHtml()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 3
Bugs 0 Features 2
Metric Value
cc 1
eloc 5
c 3
b 0
f 2
nc 1
nop 1
dl 0
loc 6
ccs 5
cts 5
cp 1
crap 1
rs 10
1
<?php
2
declare(strict_types=1);
3
4
namespace Nexendrie\SiteGenerator;
5
6
use Nette\Utils\Finder;
7
use Nette\Neon\Neon;
8
use Nette\Utils\FileSystem;
9
use Symfony\Component\OptionsResolver\OptionsResolver;
10
use Nette\Utils\Validators;
11
use Nette\Utils\Strings;
12
13
/**
14
 * Generator
15
 *
16
 * @author Jakub Konečný
17
 * @property string $source
18
 * @property string $output
19
 * @property-read Finder|\SplFileInfo[] $filesToProcess
20
 * @property string[] $ignoredFiles
21
 * @property string[] $ignoredFolders
22
 * @method void onBeforeGenerate()
23
 * @method void onCreatePage(string $html, Generator $generator, string $filename)
24
 * @method void onAfterGenerate()
25
 */
26 1
final class Generator {
27
  use \Nette\SmartObject;
28
29
  private string $templateFile = __DIR__ . "/template.html";
30
  /** @var string[] */
31
  private array $ignoredFiles = [];
32
  /** @var string[] */
33
  private array $ignoredFolders = [
34
    "vendor", ".git", "tests",
35
  ];
36
  private string $source;
37
  private string $output;
38
  /** @var Finder|\SplFileInfo[] */
39
  private $filesToProcess;
40
  /** @var string[] */
41
  private array $assets = [];
42
  /** @var callable[] */
43
  private array $metaNormalizers = [];
44
  /** @var callable[] */
45
  public array $onBeforeGenerate = [];
46
  /** @var callable[] */
47
  public array $onCreatePage = [];
48
  /** @var callable[] */
49
  public array $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, "getFilesToProcess"];
56 1
    $this->onBeforeGenerate[] = [$this, "clearOutputFolder"];
57 1
    $this->onCreatePage[] = [$this, "processImages"];
58 1
    $this->onAfterGenerate[] = [$this, "copyAssets"];
59 1
    $this->addMetaNormalizer([$this, "normalizeTitle"]);
60 1
    $this->addMetaNormalizer([$this, "normalizeStyles"]);
61 1
    $this->addMetaNormalizer([$this, "normalizeScripts"]);
62 1
    $this->addMetaNormalizer([$this, "updateLinks"]);
63 1
    $this->addMetaNormalizer([$this, "addHtmlLanguage"]);
64 1
  }
65
  
66
  public function addMetaNormalizer(callable $callback): void {
67 1
    $this->metaNormalizers[] = $callback;
68 1
  }
69
  
70
  protected function getSource(): string {
71 1
    return $this->source;
72
  }
73
  
74
  protected function setSource(string $source): void {
75 1
    if(is_dir($source)) {
76 1
      $this->source = (string) realpath($source);
77
    }
78 1
  }
79
80
  protected function getOutput(): string {
81 1
    return $this->output;
82
  }
83
84
  protected function setOutput(string $output): void {
85 1
    $this->output = (string) realpath($output);
86 1
  }
87
  
88
  /**
89
   * @return string[]
90
   */
91
  protected function getIgnoredFiles(): array {
92 1
    return $this->ignoredFiles;
93
  }
94
95
  /**
96
   * @param string[] $ignoredFiles
97
   */
98
  protected function setIgnoredFiles(array $ignoredFiles): void {
99 1
    $this->ignoredFiles = [];
100 1
    foreach($ignoredFiles as $ignoredFile) {
101
      $this->ignoredFiles[] = (string) $ignoredFile;
102
    }
103 1
  }
104
  
105
  /**
106
   * @return string[]
107
   */
108
  protected function getIgnoredFolders(): array {
109 1
    return $this->ignoredFolders;
110
  }
111
112
  /**
113
   * @param string[] $ignoredFolders
114
   */
115
  protected function setIgnoredFolders(array $ignoredFolders): void {
116 1
    $this->ignoredFolders = [];
117 1
    foreach($ignoredFolders as $ignoredFolder) {
118 1
      $this->ignoredFolders[] = (string) $ignoredFolder;
119
    }
120 1
  }
121
  
122
  protected function createMetaResolver(): OptionsResolver {
123 1
    $resolver = new OptionsResolver();
124 1
    $resolver->setDefaults([
125 1
      "title" => "",
126
      "htmlLang" => "",
127
      "styles" => [],
128
      "scripts" => [],
129
    ]);
130 1
    $isArrayOfStrings = function(array $value): bool {
131 1
      return Validators::everyIs($value, "string");
132 1
    };
133 1
    $resolver->setAllowedTypes("title", "string");
134 1
    $resolver->setAllowedTypes("htmlLang", "string");
135 1
    $resolver->setAllowedTypes("styles", "array");
136 1
    $resolver->setAllowedValues("styles", $isArrayOfStrings);
137 1
    $resolver->setAllowedTypes("scripts", "array");
138 1
    $resolver->setAllowedValues("scripts", $isArrayOfStrings);
139 1
    return $resolver;
140
  }
141
  
142
  protected function getMetafileName(string $filename): string {
143 1
    return str_replace(".md", ".neon", $filename);
144
  }
145
  
146
  protected function getMeta(string $filename, string &$html): array {
147 1
    $resolver = $this->createMetaResolver();
148 1
    $metaFilename = $this->getMetafileName($filename);
149 1
    $meta = [];
150 1
    if(file_exists($metaFilename)) {
151 1
      $meta = Neon::decode(file_get_contents($metaFilename));
152
    }
153 1
    $result = $resolver->resolve($meta);
154 1
    foreach($this->metaNormalizers as $normalizer) {
155 1
      $normalizer($result, $html, $filename);
156
    }
157 1
    return $result;
158
  }
159
  
160
  protected function addAsset(string $asset): void {
161 1
    $asset = realpath($asset);
162 1
    if(is_string($asset) && !in_array($asset, $this->assets, true)) {
163 1
      $this->assets[] = $asset;
164
    }
165 1
  }
166
  
167
  protected function normalizeTitle(array &$meta, string &$html, string $filename): void {
168 1
    if(strlen($meta["title"]) === 0) {
169 1
      unset($meta["title"]);
170 1
      $html = str_replace("
171 1
  <title>%%title%%</title>", "", $html);
172
    }
173 1
  }
174
  
175
  protected function removeInvalidFiles(array &$input, string $basePath): void {
176 1
    $input = array_filter($input, function($value) use($basePath): bool {
177 1
      return file_exists("$basePath/$value");
178 1
    });
179 1
  }
180
  
181
  protected function normalizeStyles(array &$meta, string &$html, string $filename): void {
182 1
    $basePath = dirname($filename);
183 1
    $this->removeInvalidFiles($meta["styles"], $basePath);
184 1
    if(count($meta["styles"]) === 0) {
185 1
      unset($meta["styles"]);
186 1
      $html = str_replace("
187 1
  %%styles%%", "", $html);
188 1
      return;
189
    }
190 1
    array_walk($meta["styles"], function(&$value) use($basePath): void {
191 1
      $this->addAsset("$basePath/$value");
192 1
      $value = "<link rel=\"stylesheet\" type=\"text/css\" href=\"$value\">";
193 1
    });
194 1
    $meta["styles"] = implode("\n  ", $meta["styles"]);
195 1
  }
196
  
197
  protected function normalizeScripts(array &$meta, string &$html, string $filename): void {
198 1
    $basePath = dirname($filename);
199 1
    $this->removeInvalidFiles($meta["scripts"], $basePath);
200 1
    if(count($meta["scripts"]) === 0) {
201 1
      unset($meta["scripts"]);
202 1
      $html = str_replace("
203 1
  %%scripts%%", "", $html);
204 1
      return;
205
    }
206 1
    array_walk($meta["scripts"], function(&$value) use($basePath): void {
207 1
      $this->addAsset("$basePath/$value");
208 1
      $value = "<script type=\"text/javascript\" src=\"$value\"></script>";
209 1
    });
210 1
    $meta["scripts"] = implode("\n  ", $meta["scripts"]);
211 1
  }
212
  
213
  protected function updateLinks(array &$meta, string &$html, string $filename): void {
214 1
    $dom = new \DOMDocument();
215 1
    set_error_handler(function($errno): bool {
216 1
      return $errno === E_WARNING;
217 1
    });
218 1
    $dom->loadHTML($html);
219 1
    restore_error_handler();
220 1
    $links = $dom->getElementsByTagName("a");
221
    /** @var \DOMElement $link */
222 1
    foreach($links as $link) {
223 1
      $oldContent = $dom->saveHTML($link);
224 1
      $needsUpdate = false;
225 1
      $target = $link->getAttribute("href");
226 1
      $target = dirname($filename) . "/" . $target;
227 1
      foreach($this->filesToProcess as $file) {
228 1
        if($target === $file->getRealPath() && Strings::endsWith($target, ".md")) {
229 1
          $needsUpdate = true;
230 1
          continue;
231
        }
232
      }
233 1
      if(!$needsUpdate) {
234 1
        continue;
235
      }
236 1
      $link->setAttribute("href", str_replace(".md", ".html", $link->getAttribute("href")));
237 1
      $newContent = $dom->saveHTML($link);
238 1
      $html = str_replace($oldContent, $newContent, $html);
239
    }
240 1
  }
241
242
  protected function addHtmlLanguage(array &$meta, string &$html, string $filename): void {
243 1
    if(strlen($meta["htmlLang"]) > 0) {
244 1
      $html = str_replace("<html>", "<html lang=\"{$meta["htmlLang"]}\">", $html);
245
    }
246 1
  }
247
  
248
  protected function createMarkdownParser(): \cebe\markdown\Markdown {
249 1
    return new MarkdownParser();
250
  }
251
  
252
  protected function createHtml(string $filename): string {
253 1
    $parser = $this->createMarkdownParser();
254 1
    $source = $parser->parse(file_get_contents($filename));
255 1
    $html = file_get_contents($this->templateFile);
256 1
    $html = str_replace("%%source%%", $source, $html);
257 1
    return $html;
258
  }
259
  
260
  /**
261
   * @internal
262
   * @return Finder|\SplFileInfo[]
263
   * @todo make protected when we drop support for nette/utils 2.5
264
   */
265
  public function getFilesToProcess(): Finder {
266 1
    $this->filesToProcess = Finder::findFiles("*.md")
267 1
      ->exclude($this->ignoredFiles)
268 1
      ->from($this->source)
269 1
      ->exclude($this->ignoredFolders);
270 1
    return $this->filesToProcess;
271
  }
272
  
273
  /**
274
   * @internal
275
   */
276
  public function clearOutputFolder(): void {
277 1
    FileSystem::delete($this->output);
278 1
  }
279
  
280
  /**
281
   * @internal
282
   */
283
  public function copyAssets(): void {
284 1
    foreach($this->assets as $asset) {
285 1
      $path = str_replace($this->source, "", $asset);
286 1
      $target = "$this->output$path";
287 1
      FileSystem::copy($asset, $target);
288 1
      echo "Copied $path";
289
    }
290 1
  }
291
  
292
  /**
293
   * @internal
294
   */
295
  public function processImages(string $html, self $generator, string $filename): void {
296 1
    $dom = new \DOMDocument();
297 1
    $dom->loadHTML($html);
298 1
    $images = $dom->getElementsByTagName("img");
299
    /** @var \DOMElement $image */
300 1
    foreach($images as $image) {
301 1
      $path = dirname($filename) . "/" . $image->getAttribute("src");
302 1
      if(file_exists($path)) {
303 1
        $generator->addAsset($path);
304
      }
305
    }
306 1
  }
307
  
308
  /**
309
   * Generate the site
310
   */
311
  public function generate(): void {
312 1
    $this->onBeforeGenerate();
313 1
    foreach($this->filesToProcess as $file) {
314 1
      $path = str_replace($this->source, "", dirname($file->getRealPath()));
315 1
      $html = $this->createHtml($file->getRealPath());
316 1
      $meta = $this->getMeta($file->getRealPath(), $html);
317 1
      foreach($meta as $key => $value) {
318 1
        $html = str_replace("%%$key%%", $value, $html);
319
      }
320 1
      $basename = $file->getBasename(".md") . ".html";
321 1
      $filename = "$this->output$path/$basename";
322 1
      FileSystem::write($filename, $html);
323 1
      echo "Created $path/$basename\n";
324 1
      $this->onCreatePage($html, $this, $file->getRealPath());
325
    }
326 1
    $this->onAfterGenerate();
327 1
  }
328
}
329
?>