PageRenderTime 27ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Generator.php

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