From 7ba95d70cb2c8e84908f75a6bc639a6a0a5d72b0 Mon Sep 17 00:00:00 2001 From: Stuart Jones Date: Mon, 14 Aug 2023 22:45:46 +0930 Subject: [PATCH] - output path can be remapped --- README.md | 44 +++++++++ caxton | 14 +-- src/File.php | 50 ----------- src/FileList.php | 45 ---------- src/Middleware/BuildContentFiles.php | 97 +++++++++----------- src/Middleware/CopyPublicFiles.php | 49 ---------- src/Middleware/CopyStaticFiles.php | 27 ++++++ src/Middleware/Middleware.php | 4 +- src/Middleware/ScanFiles.php | 129 +++++++++++++++++++++++++++ src/Site.php | 42 +++++++++ src/SourceFile.php | 34 +++++++ 11 files changed, 329 insertions(+), 206 deletions(-) delete mode 100644 src/File.php delete mode 100644 src/FileList.php delete mode 100644 src/Middleware/CopyPublicFiles.php create mode 100644 src/Middleware/CopyStaticFiles.php create mode 100644 src/Middleware/ScanFiles.php create mode 100644 src/Site.php create mode 100644 src/SourceFile.php diff --git a/README.md b/README.md index 08ecc2b..be49ceb 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,50 @@ You can specify files for inclusion or exclusion in the `caxton.json` configurat } ``` +### Output mapping + +By default, the output URLs will follow the same structure as the folder paths within your public and content directories. + +If you like to organise your files differently, then you can use the `output.maps` configuration to map the URLs accordingly. + +For example: + +``` ++ content +|-+ blog + |-+ 2018 + |-+ 10-22-it-begins + |- index.blade.md + |- pretty-picture.png +``` + +To output this document as `/blog/2018-10-22/it-begins`, you can use this in your `caxton.json` file: + +``` +{ + "output": { + "maps": [ + { + "path": "/blog/*/*/", + "url": "/blog/{{ date }}/{{ slug }}/" + } + ] + } +} +``` + +`date` and `slug` are read from the front matter of the template file. + +``` +--- +date: 2018-10-22 +slug: it-begins +--- +``` + +Caxton will then store an internal map for all output for paths starting with `/blog/2018/10-22-it-begins/` and rewrite them as `/blog/2018-10-22/it-begins`. +This means that any resources related to the blog post (such as the `png` file) will be written to the same output URL. + ### Sitemap Caxton will generate a `sitemap.xml` and add it to the root of your output directory. diff --git a/caxton b/caxton index 429c087..8cfca9d 100755 --- a/caxton +++ b/caxton @@ -6,7 +6,7 @@ require_once 'vendor/autoload.php'; use SavvyWombat\Caxton\Config; use SavvyWombat\Caxton\ConfigFile; use SavvyWombat\Caxton\Middleware; -use SavvyWombat\Caxton\FileList; +use SavvyWombat\Caxton\Site; $config = []; $config['verbose'] = in_array('-v', $argv) || in_array('--verbose', $argv); @@ -29,6 +29,8 @@ $config['paths']['public'] = $config['paths']['base'] . ($_ENV['PUBLIC_DIR'] ?? $config['paths']['cache'] = $config['paths']['base'] . '/build' . ($_ENV['CACHE_DIR'] ?? '/cache'); $config['paths']['output'] = $config['paths']['base'] . '/build' . ($_ENV['OUTPUT_DIR'] ?? '/' . $config['environment']); +$config['blade']['extensions'] = '.blade.(md|php)'; + $config = Config::instance( $config, ConfigFile::read($config['paths']['base'] . '/caxton.json', []), @@ -57,14 +59,14 @@ if (file_exists(Config::instance()->get('paths.output'))) { } $middlewares = [ - Middleware\CopyPublicFiles::class, + Middleware\ScanFiles::class, + Middleware\CopyStaticFiles::class, Middleware\BuildContentFiles::class, - Middleware\GenerateSiteMap::class, ]; -$action = fn (FileList $files): FileList => $files; +$action = fn (Site $site): Site => $site; foreach (array_reverse($middlewares) as $m) { $middleware = new $m(); - $action = fn (FileList $files): FileList => $middleware->run($files, $action); + $action = fn (Site $site): Site => $middleware->run($site, $action); } -$action(new FileList()); +$action(Site::instance()); diff --git a/src/File.php b/src/File.php deleted file mode 100644 index f5c6e36..0000000 --- a/src/File.php +++ /dev/null @@ -1,50 +0,0 @@ -url = $filename; - } - $this->sourceFileInfo = new \SplFileInfo($source); - } - - public function filename(): string - { - return $this->filename; - } - - public function url(): string - { - return $this->url; - } - - public function fullUrl(): string - { - return Config::instance()->get('base_url') . $this->url; - } - - public function lastModified(): ?Carbon - { - if (empty($this->source)) { - return null; - } - - return Carbon::createFromTimestamp($this->sourceFileInfo->getMTime()); - } - - public function type(): string - { - return mime_content_type(Config::instance()->get('paths.output') . $this->filename); - } -} diff --git a/src/FileList.php b/src/FileList.php deleted file mode 100644 index a636b2f..0000000 --- a/src/FileList.php +++ /dev/null @@ -1,45 +0,0 @@ -files[$file->filename()] = $file; - - if (! in_array($file->filename(), $this->filenames)) { - $this->filenames[] = $file->filename(); - } - } - - public function current(): File - { - return $this->files[$this->filenames[$this->index]]; - } - - public function key(): int - { - return $this->index; - } - - public function next(): void - { - ++$this->index; - } - - public function valid(): bool - { - return isset($this->filenames[$this->index]); - } - - public function rewind(): void - { - $this->index = 0; - } -} diff --git a/src/Middleware/BuildContentFiles.php b/src/Middleware/BuildContentFiles.php index 6cee2a6..28426f9 100644 --- a/src/Middleware/BuildContentFiles.php +++ b/src/Middleware/BuildContentFiles.php @@ -4,72 +4,61 @@ use SavvyWombat\Caxton\Blade\ViewFactory; use SavvyWombat\Caxton\Config; -use SavvyWombat\Caxton\ContentFileFilter; -use SavvyWombat\Caxton\File; -use SavvyWombat\Caxton\FileList; use SavvyWombat\Caxton\Markdown\MarkdownConverter; +use SavvyWombat\Caxton\Site; +use SavvyWombat\Caxton\SourceFile; class BuildContentFiles implements Middleware { - public function run(FileList $files, callable $next): FileList - { - $contentFiles = new \RecursiveIteratorIterator( - new ContentFileFilter( - new \RecursiveDirectoryIterator( - Config::instance()->get('paths.content'), - \FilesystemIterator::SKIP_DOTS - ) - ), - \RecursiveIteratorIterator::SELF_FIRST - ); + protected $markdown = null; - foreach ($contentFiles as $contentFile) { - $subpath = str_replace( - Config::instance()->get('paths.content'), - '', - $contentFile->getRealPath() - ); + public function __construct() { + $this->markdown = new MarkdownConverter(); + } - if (!file_exists(Config::instance()->get('paths.output') . dirname($subpath))) { - // directories need the 'execute/search' bit - // permissions are subject to the umask value in the running environment - mkdir(Config::instance()->get('paths.output') . dirname($subpath), 0775, true); + public function run(Site $site, callable $next): Site + { + $extensions = str_replace('.', '\.', Config::instance()->get('blade.extensions')); + + foreach ($site->sourceFiles() as $file) { + if (preg_match('#(.*)' . $extensions . '$#', $file->sourcePath())) { + $this->buildFromTemplate($file, $file->outputPath()); } + } - $matches = []; - if (preg_match('#(.*)\.blade\.(md|php)$#', $subpath, $matches)) { - $markdown = new MarkdownConverter(); - $frontMatter = $markdown->extractFrontMatter( - file_get_contents(Config::instance()->get('paths.content') . $subpath) - ); + return $next($site); + } - $data = yaml_parse($frontMatter) ?? []; + protected function buildFromTemplate(SourceFile $sourceFile, string $outputPath) + { + $extensions = str_replace('.', '\.', Config::instance()->get('blade.extensions')); - $subpath = $matches[1]; - $output = ViewFactory::instance()->make( - str_replace('/', '.', $subpath), - [ - 'page' => null, - 'url' => Config::instance()->get('base_url') . (str_ends_with($subpath, '/index') ? substr($subpath, 0, -6) : $subpath), - ...$data, - ] - )->render(); + $templateName = str_replace( + '/', + '.', + str_replace( + Config::instance()->get('paths.content') . '/', + '', + preg_replace( + '#' . $extensions . '$#', + '', + $sourceFile->sourcePath(), + ), - file_put_contents(Config::instance()->get('paths.output') . $subpath . '.html', $output); - $files->add(new File( - filename: $subpath . '.html', - url: (str_ends_with($subpath, '/index')) ? substr($subpath, 0, -6) : $subpath, - source: $contentFile->getRealPath(), - )); - } else if (!is_dir($contentFile->getRealPath())) { - copy($contentFile->getRealPath(), Config::instance()->get('paths.output') . $subpath); - $files->add(new File( - filename: $subpath, - source: $contentFile->getRealPath(), - )); - } + ) + ); + + $output = ViewFactory::instance()->make( + str_replace('/', '.', $templateName), + $sourceFile->data() + )->render(); + + if (!file_exists(Config::instance()->get('paths.output') . dirname($outputPath))) { + // directories need the 'execute/search' bit + // permissions are subject to the umask value in the running environment + mkdir(Config::instance()->get('paths.output') . dirname($outputPath), 0775, true); } - return $next($files); + file_put_contents(Config::instance()->get('paths.output') . $outputPath, $output); } } diff --git a/src/Middleware/CopyPublicFiles.php b/src/Middleware/CopyPublicFiles.php deleted file mode 100644 index 895006c..0000000 --- a/src/Middleware/CopyPublicFiles.php +++ /dev/null @@ -1,49 +0,0 @@ -get('paths.public'), - \FilesystemIterator::SKIP_DOTS - ) - ), - \RecursiveIteratorIterator::SELF_FIRST - ); - - foreach ($publicFiles as $publicFile) { - $subpath = str_replace( - Config::instance()->get('paths.public'), - '', - $publicFile->getRealPath() - ); - - if (!file_exists(Config::instance()->get('paths.output') . dirname($subpath))) { - // directories need the 'execute/search' bit - // permissions are subject to the umask value in the running environment - mkdir(Config::instance()->get('paths.output') . dirname($subpath), 0775, true); - } - - if (!is_dir($publicFile->getRealPath())) { - copy($publicFile->getRealPath(), Config::instance()->get('paths.output') . $subpath); - - $files->add(new File( - filename: $subpath, - source: $publicFile->getRealPath(), - )); - } - } - - return $next($files); - } -} diff --git a/src/Middleware/CopyStaticFiles.php b/src/Middleware/CopyStaticFiles.php new file mode 100644 index 0000000..e5ed2dd --- /dev/null +++ b/src/Middleware/CopyStaticFiles.php @@ -0,0 +1,27 @@ +get('blade.extensions')); + + foreach ($site->sourceFiles() as $file) { + if (! preg_match('#(.*)' . $extensions . '$#', $file->sourcePath())) { + $outputPath = $file->outputPath(); + + if (! file_exists(dirname(Config::instance()->get('paths.output') . '/' . $outputPath))) { + mkdir(dirname(Config::instance()->get('paths.output') . '/' . $outputPath), 0775, true); + } + copy($file->sourcePath(), Config::instance()->get('paths.output') . '/' . $outputPath); + } + } + + return $next($site); + } +} diff --git a/src/Middleware/Middleware.php b/src/Middleware/Middleware.php index 1175593..cc12486 100644 --- a/src/Middleware/Middleware.php +++ b/src/Middleware/Middleware.php @@ -2,9 +2,9 @@ namespace SavvyWombat\Caxton\Middleware; -use SavvyWombat\Caxton\FileList; +use SavvyWombat\Caxton\Site; interface Middleware { - public function run(FileList $files, callable $next): FileList; + public function run(Site $site, callable $next): Site; } diff --git a/src/Middleware/ScanFiles.php b/src/Middleware/ScanFiles.php new file mode 100644 index 0000000..989b6a6 --- /dev/null +++ b/src/Middleware/ScanFiles.php @@ -0,0 +1,129 @@ +markdownConverter = new MarkdownConverter(); + } + + public function run(Site $site, callable $next): Site + { + $this->site = $site; + $this->scanPublicFiles(); + $this->scanContentFiles(); + + return $next($this->site); + } + + protected function scanPublicFiles(): void + { + $publicFiles = new \RecursiveIteratorIterator( + new ContentFileFilter( + new \RecursiveDirectoryIterator( + Config::instance()->get('paths.public'), + \FilesystemIterator::SKIP_DOTS + ) + ), + \RecursiveIteratorIterator::SELF_FIRST + ); + + foreach ($publicFiles as $publicFile) { + if (! is_dir($publicFile)) { + $sourcePath = $publicFile->getRealPath(); + $outputPath = str_replace( + Config::instance()->get('paths.public'), + '', + $sourcePath + ); + + $this->site->addFile( + new SourceFile($sourcePath, $outputPath) + ); + } + } + } + + protected function scanContentFiles(): void + { + $contentFiles = new \RecursiveIteratorIterator( + new ContentFileFilter( + new \RecursiveDirectoryIterator( + Config::instance()->get('paths.content'), + \FilesystemIterator::SKIP_DOTS + ) + ), + \RecursiveIteratorIterator::SELF_FIRST + ); + + foreach ($contentFiles as $contentFile) { + if (! is_dir($contentFile)) { + $sourcePath = $contentFile->getRealPath(); + $outputPath = str_replace( + Config::instance()->get('paths.content'), + '', + $sourcePath + ); + + $data = []; + + $extensions = str_replace('.', '\.', Config::instance()->get('blade.extensions')); + if (preg_match('#(.*)' . $extensions . '$#', $outputPath, $matches)) { + $frontMatter = $this->markdownConverter->extractFrontMatter( + file_get_contents($sourcePath) + ); + + $data = yaml_parse($frontMatter) ?? []; + + $outputPath = $matches[1] . '.html'; + + $this->site->addMap($this->buildMap($outputPath, $data)); + } + + $this->site->addFile( + new SourceFile($sourcePath, $outputPath, $data) + ); + } + } + } + + protected function buildMap(string $path, array $data): ?array + { + $maps = Config::instance()->get('output.maps'); + list ($map, $basepath) = (function() use ($maps, $path) { + foreach ($maps as $map) { + $pattern = str_replace('*', '.*?', $map['path']); + + if (preg_match('#^(' . $pattern . ").*$#", $path, $matches)) { + return [$map, $matches[1]]; + } + } + + return [null, $path]; + })(); + + if ($map) { + preg_match_all('#{{(.*?)}}#', $map['url'], $matches); + + $url = $map['url']; + + foreach ($matches[1] as $variable) { + $url = str_replace('{{' . $variable . '}}', $data[trim($variable)], $url); + } + + return ['path' => $basepath, 'url' => $url]; + } + + return []; + } +} \ No newline at end of file diff --git a/src/Site.php b/src/Site.php new file mode 100644 index 0000000..9eccd01 --- /dev/null +++ b/src/Site.php @@ -0,0 +1,42 @@ +sourceFiles[] = $file; + } + + public function sourceFiles() + { + return $this->sourceFiles; + } + + public function addMap(array $map): void + { + if (isset($map['url'])) { + $this->maps[] = $map; + } + } + + public function maps() + { + return $this->maps; + } +} \ No newline at end of file diff --git a/src/SourceFile.php b/src/SourceFile.php new file mode 100644 index 0000000..99031f7 --- /dev/null +++ b/src/SourceFile.php @@ -0,0 +1,34 @@ +sourcePath; + } + + public function outputPath(): string + { + foreach (Site::instance()->maps() as $map) { + if (str_starts_with($this->outputPath, $map['path'])) { + return str_replace($map['path'], $map['url'], $this->outputPath); + } + } + + return $this->outputPath; + } + + public function data(): array + { + return $this->data; + } +} \ No newline at end of file