Cleaned up messy code and moved logic to proper abstractions

This commit is contained in:
Javier Feliz 2025-08-19 20:10:05 -04:00
parent c84f0aaa7b
commit 06d22835c7
9 changed files with 617 additions and 125 deletions

13
app/Build/File.php Normal file
View File

@ -0,0 +1,13 @@
<?php
namespace App\Build;
class File
{
public string $name;
public string $slug;
public string $title;
public function __construct(
protected string $path
) {}
}

View File

@ -12,9 +12,13 @@
namespace App\Build;
use App\Models\Page;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use function Illuminate\Filesystem\join_paths;
class SiteParser
{
@ -29,48 +33,35 @@ class SiteParser
$this->targetDisk = Storage::disk('build_local');
}
protected function readSite()
/**
* Get all the pages from the source directory
*
* @return Collection<App\Models\Page>
*/
public function getPages(): Collection
{
$files = $this->sourceDisk->files(recursive: true);
foreach (self::IGNORED_DIRECTORIES as $ignore) {
$files = array_filter($files, fn($path) => !str_starts_with($path, $ignore));
}
$pages = [];
$pages = collect();
// Set up a structure
foreach ($files as $f) {
$pages[] = [
'path' => $f,
'name' => basename($f),
'source' => Storage::disk('source')->path($f),
'output_path' => $this->getOutputPath($f),
'extension' => $this->getFileExtension($f)
];
$pages->push(new Page($f));
}
return $pages;
}
public function getOutputPath(string $filename): string
public function slugFromFilename(string $filename): string
{
$indexFiles = ['index.md', 'index.blade.php'];
$baseDirectory = Str::of($filename)->remove(basename($filename))->trim('/')->toString();
if (in_array(basename($filename), $indexFiles)) {
return join_paths($baseDirectory, "index.html");
}
return join_paths($baseDirectory, "{$this->slugFromFilename($filename)}", "index.html");
return Str::of(basename($filename))->remove(".md")->remove(".blade.php")->slug()->toString();
}
public function getFileExtension($filename): string
{
// Check for .blade.php first
if (str_ends_with($filename, '.blade.php')) {
return 'blade.php';
}
public function getOutputPath(string $filename): string {}
// Fall back to regular extension
return pathinfo($filename, PATHINFO_EXTENSION);
}
/**
* Parse all content collections and sub collections.
@ -80,5 +71,5 @@ class SiteParser
*
* @return Collection
*/
public function collections(): Collection {}
// public function collections(): Collection {}
}

View File

@ -2,11 +2,13 @@
namespace App\Commands;
use App\Build\SiteParser;
use Exception;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Contracts\Cache\Store;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\View;
use LaravelZero\Framework\Commands\Command;
use Illuminate\Support\Str;
use Parsedown;
@ -38,42 +40,20 @@ class BuildCommand extends Command
// Storage::delete(Storage::disk('build_local')->files());
// Storage::delete(Storage::disk('build_local')->directories());
$ignored = [
"assets",
"_"
];
$files = Storage::disk('source')->files(recursive: true);
foreach ($ignored as $ignore) {
$files = array_filter($files, fn($path) => !str_starts_with($path, $ignore));
}
$pages = [];
// Set up a structure
foreach ($files as $f) {
$pages[] = [
'path' => $f,
'name' => basename($f),
'source' => Storage::disk('source')->path($f),
'output_path' => $this->getOutputPath($f),
'extension' => $this->getFileExtension($f)
];
}
$siteParser = new SiteParser;
$pages = $siteParser->getPages();
// Parse and output
foreach ($pages as $p) {
View::share('page', $p);
try {
if ($p['extension'] == 'blade.php') {
$rendered = $this->renderBlade($p);
} else {
$rendered = $this->renderMarkdown($p);
}
$rendered = $p->render();
} catch (Exception $e) {
$this->error("Failed on: " . $p['path']);
$this->error("Failed on: " . $p->path);
throw $e;
}
Storage::disk('build_local')->put($p['output_path'], $rendered);
Storage::disk('build_local')->put($p->outputPath(), $rendered);
}
// Copy static assets folder
@ -88,65 +68,4 @@ class BuildCommand extends Command
$this->info("Site built");
$this->notify("Site Zapped", "Your site has been built");
}
public function renderBlade(array $page): string
{
$blade = file_get_contents($page['source']);
return Blade::render($blade, []);
}
public function renderMarkdown(array $page): string
{
$parser = new Parsedown();
$md = file_get_contents($page['source']);
$parsed = $parser->text($md);
$tmpBlade = <<<HTML
<x-layouts.main>
$parsed
</x-layouts.main>
HTML;
$rendered = Blade::render($tmpBlade, []);
return $rendered;
}
public function getFileExtension($filename): string
{
// Check for .blade.php first
if (str_ends_with($filename, '.blade.php')) {
return 'blade.php';
}
// Fall back to regular extension
return pathinfo($filename, PATHINFO_EXTENSION);
}
public function slugFromFilename(string $filename): string
{
return Str::of(basename($filename))->remove(".md")->remove(".blade.php")->slug()->toString();
}
public function titleFromFilename(string $filename): string
{
return Str::of(basename($filename))->remove(".md")->remove(".blade.php")->replace('-', ' ')->title()->toString();
}
public function getOutputPath(string $filename): string
{
$indexFiles = ['index.md', 'index.blade.php'];
$baseDirectory = Str::of($filename)->remove(basename($filename))->trim('/')->toString();
if (in_array(basename($filename), $indexFiles)) {
return join_paths($baseDirectory, "index.html");
}
return join_paths($baseDirectory, "{$this->slugFromFilename($filename)}", "index.html");
}
/**
* Define the command's schedule.
*/
public function schedule(Schedule $schedule): void
{
// $schedule->command(static::class)->everyMinute();
}
}

View File

@ -2,12 +2,108 @@
namespace App\Models;
use App\Render\BladeRenderer;
use App\Render\MarkdownRenderer;
use App\Render\Renderer;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Facades\Storage;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\FrontMatter\FrontMatterExtension;
use League\CommonMark\Extension\FrontMatter\Output\RenderedContentWithFrontMatter;
use League\CommonMark\MarkdownConverter;
use function Illuminate\Filesystem\join_paths;
// TODO: Load page's frontmatter and replace things like
// title and layout from it. Also set any default,
// values for things like layout from the config
// if they're not present on the page.
class Page
{
public function __construct(
protected string $title,
protected string $path,
protected string $outputPath,
protected string $content,
) {}
public string $filename;
public string $fileExtension;
public string $directory;
public string $title;
public string $slug;
public bool $isIndex;
public string $key;
protected Filesystem $sourceDisk;
public ?string $layout;
protected string $content;
public function __construct(public string $path)
{
$this->sourceDisk = Storage::disk('source');
$this->filename = basename($this->path);
$this->fileExtension = (str_ends_with($this->filename, '.blade.php')) ? 'blade.php' : pathinfo($this->filename, PATHINFO_EXTENSION);
$this->fileExtension = str($this->fileExtension)->trim('.')->toString();
$this->title = str($this->filename)->remove("." . $this->fileExtension)->title()->toString();
$this->slug = str($this->title)->slug();
$this->isIndex = str($this->filename)->remove("." . $this->fileExtension)->toString() == "index";
$this->directory = dirname($this->path);
$this->key = str($this->path)->remove("." . $this->fileExtension)->replace("/", ".")->lower()->toString();
$this->fillAdditionalData();
}
/**
* Grab data from the file's front matter
* as well as the config and overwrite
* or fill in any missing attributes.
*
* @return void
*/
public function fillAdditionalData()
{
// Grab front matter data if any
if ($this->fileExtension == 'md') {
// Build the markdown parser
$mdEnvironment = new Environment();
$mdEnvironment->addExtension(new CommonMarkCoreExtension);
$mdEnvironment->addExtension(new FrontMatterExtension);
$parser = new MarkdownConverter($mdEnvironment);
$parsed = $parser->convert($this->content());
if ($parsed instanceof RenderedContentWithFrontMatter) {
$frontMatter = $parsed->getFrontMatter();
foreach ($frontMatter as $field => $value) {
$this->$field = $value;
}
}
}
}
public function renderer(): Renderer
{
return ($this->fileExtension == 'blade.php') ? new BladeRenderer : new MarkdownRenderer;
}
public function render(): string
{
return $this->renderer()->render($this);
}
public function outputPath(): string
{
$baseDirectory = $this->directory;
if ($this->isIndex) {
return join_paths($baseDirectory, "index.html");
}
return join_paths($baseDirectory, "{$this->slug}", "index.html");
}
public function content(): string
{
if (!empty($this->content)) {
return $this->content;
}
$content = Storage::disk('source')->get($this->path);
$this->content = $content;
return $content;
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Render;
use App\Models\Page;
use Illuminate\Support\Facades\Blade;
class BladeRenderer implements Renderer
{
public static function render(Page $p): string
{
return Blade::render($p->content(), []);
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Render;
use App\Models\Page;
use Illuminate\Support\Facades\Blade;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
use League\CommonMark\MarkdownConverter;
// TODO: Use commonmark instead of parsedown
class MarkdownRenderer implements Renderer
{
public static function render(Page $p): string
{
// Build the markdown parser
$mdEnvironment = new Environment();
$mdEnvironment->addExtension(new CommonMarkCoreExtension);
$mdEnvironment->addExtension(new GithubFlavoredMarkdownExtension);
$parser = new MarkdownConverter($mdEnvironment);
$parsed = $parser->convert($p->content());
$tmpBlade = <<<HTML
<x-layouts.main>
$parsed
</x-layouts.main>
HTML;
$rendered = Blade::render($tmpBlade, []);
return $rendered;
}
}

10
app/Render/Renderer.php Normal file
View File

@ -0,0 +1,10 @@
<?php
namespace App\Render;
use App\Models\Page;
interface Renderer
{
public static function render(Page $page): string;
}

View File

@ -26,6 +26,7 @@
"erusev/parsedown": "^1.7",
"illuminate/view": "^12.17",
"laravel-zero/framework": "^12.0",
"league/commonmark": "^2.7",
"symfony/yaml": "^7.3"
},
"require-dev": {

417
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "29b7d753ffefb1c7312b35324587e774",
"content-hash": "21709f889a4d6dcee8bb6118909486a8",
"packages": [
{
"name": "brick/math",
@ -135,6 +135,81 @@
],
"time": "2024-02-09T16:56:22+00:00"
},
{
"name": "dflydev/dot-access-data",
"version": "v3.0.3",
"source": {
"type": "git",
"url": "https://github.com/dflydev/dflydev-dot-access-data.git",
"reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f",
"reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^0.12.42",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.3",
"scrutinizer/ocular": "1.6.0",
"squizlabs/php_codesniffer": "^3.5",
"vimeo/psalm": "^4.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Dflydev\\DotAccessData\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Dragonfly Development Inc.",
"email": "info@dflydev.com",
"homepage": "http://dflydev.com"
},
{
"name": "Beau Simensen",
"email": "beau@dflydev.com",
"homepage": "http://beausimensen.com"
},
{
"name": "Carlos Frutos",
"email": "carlos@kiwing.it",
"homepage": "https://github.com/cfrutos"
},
{
"name": "Colin O'Dell",
"email": "colinodell@gmail.com",
"homepage": "https://www.colinodell.com"
}
],
"description": "Given a deep data structure, access data by dot notation.",
"homepage": "https://github.com/dflydev/dflydev-dot-access-data",
"keywords": [
"access",
"data",
"dot",
"notation"
],
"support": {
"issues": "https://github.com/dflydev/dflydev-dot-access-data/issues",
"source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3"
},
"time": "2024-07-08T12:26:09+00:00"
},
{
"name": "doctrine/inflector",
"version": "2.1.0",
@ -2043,6 +2118,195 @@
},
"time": "2025-07-07T14:17:42+00:00"
},
{
"name": "league/commonmark",
"version": "2.7.1",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/commonmark.git",
"reference": "10732241927d3971d28e7ea7b5712721fa2296ca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/10732241927d3971d28e7ea7b5712721fa2296ca",
"reference": "10732241927d3971d28e7ea7b5712721fa2296ca",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"league/config": "^1.1.1",
"php": "^7.4 || ^8.0",
"psr/event-dispatcher": "^1.0",
"symfony/deprecation-contracts": "^2.1 || ^3.0",
"symfony/polyfill-php80": "^1.16"
},
"require-dev": {
"cebe/markdown": "^1.0",
"commonmark/cmark": "0.31.1",
"commonmark/commonmark.js": "0.31.1",
"composer/package-versions-deprecated": "^1.8",
"embed/embed": "^4.4",
"erusev/parsedown": "^1.0",
"ext-json": "*",
"github/gfm": "0.29.0",
"michelf/php-markdown": "^1.4 || ^2.0",
"nyholm/psr7": "^1.5",
"phpstan/phpstan": "^1.8.2",
"phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0",
"scrutinizer/ocular": "^1.8.1",
"symfony/finder": "^5.3 | ^6.0 | ^7.0",
"symfony/process": "^5.4 | ^6.0 | ^7.0",
"symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0",
"unleashedtech/php-coding-standard": "^3.1.1",
"vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0"
},
"suggest": {
"symfony/yaml": "v2.3+ required if using the Front Matter extension"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "2.8-dev"
}
},
"autoload": {
"psr-4": {
"League\\CommonMark\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Colin O'Dell",
"email": "colinodell@gmail.com",
"homepage": "https://www.colinodell.com",
"role": "Lead Developer"
}
],
"description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)",
"homepage": "https://commonmark.thephpleague.com",
"keywords": [
"commonmark",
"flavored",
"gfm",
"github",
"github-flavored",
"markdown",
"md",
"parser"
],
"support": {
"docs": "https://commonmark.thephpleague.com/",
"forum": "https://github.com/thephpleague/commonmark/discussions",
"issues": "https://github.com/thephpleague/commonmark/issues",
"rss": "https://github.com/thephpleague/commonmark/releases.atom",
"source": "https://github.com/thephpleague/commonmark"
},
"funding": [
{
"url": "https://www.colinodell.com/sponsor",
"type": "custom"
},
{
"url": "https://www.paypal.me/colinpodell/10.00",
"type": "custom"
},
{
"url": "https://github.com/colinodell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/league/commonmark",
"type": "tidelift"
}
],
"time": "2025-07-20T12:47:49+00:00"
},
{
"name": "league/config",
"version": "v1.2.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/config.git",
"reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3",
"reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3",
"shasum": ""
},
"require": {
"dflydev/dot-access-data": "^3.0.1",
"nette/schema": "^1.2",
"php": "^7.4 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^1.8.2",
"phpunit/phpunit": "^9.5.5",
"scrutinizer/ocular": "^1.8.1",
"unleashedtech/php-coding-standard": "^3.1",
"vimeo/psalm": "^4.7.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.2-dev"
}
},
"autoload": {
"psr-4": {
"League\\Config\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Colin O'Dell",
"email": "colinodell@gmail.com",
"homepage": "https://www.colinodell.com",
"role": "Lead Developer"
}
],
"description": "Define configuration arrays with strict schemas and access values with dot notation",
"homepage": "https://config.thephpleague.com",
"keywords": [
"array",
"config",
"configuration",
"dot",
"dot-access",
"nested",
"schema"
],
"support": {
"docs": "https://config.thephpleague.com/",
"issues": "https://github.com/thephpleague/config/issues",
"rss": "https://github.com/thephpleague/config/releases.atom",
"source": "https://github.com/thephpleague/config"
},
"funding": [
{
"url": "https://www.colinodell.com/sponsor",
"type": "custom"
},
{
"url": "https://www.paypal.me/colinpodell/10.00",
"type": "custom"
},
{
"url": "https://github.com/colinodell",
"type": "github"
}
],
"time": "2022-12-11T20:36:23+00:00"
},
{
"name": "league/flysystem",
"version": "3.30.0",
@ -2336,6 +2600,157 @@
],
"time": "2025-08-02T09:36:06+00:00"
},
{
"name": "nette/schema",
"version": "v1.3.2",
"source": {
"type": "git",
"url": "https://github.com/nette/schema.git",
"reference": "da801d52f0354f70a638673c4a0f04e16529431d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d",
"reference": "da801d52f0354f70a638673c4a0f04e16529431d",
"shasum": ""
},
"require": {
"nette/utils": "^4.0",
"php": "8.1 - 8.4"
},
"require-dev": {
"nette/tester": "^2.5.2",
"phpstan/phpstan-nette": "^1.0",
"tracy/tracy": "^2.8"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.3-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause",
"GPL-2.0-only",
"GPL-3.0-only"
],
"authors": [
{
"name": "David Grudl",
"homepage": "https://davidgrudl.com"
},
{
"name": "Nette Community",
"homepage": "https://nette.org/contributors"
}
],
"description": "📐 Nette Schema: validating data structures against a given Schema.",
"homepage": "https://nette.org",
"keywords": [
"config",
"nette"
],
"support": {
"issues": "https://github.com/nette/schema/issues",
"source": "https://github.com/nette/schema/tree/v1.3.2"
},
"time": "2024-10-06T23:10:23+00:00"
},
{
"name": "nette/utils",
"version": "v4.0.8",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
"reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/utils/zipball/c930ca4e3cf4f17dcfb03037703679d2396d2ede",
"reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede",
"shasum": ""
},
"require": {
"php": "8.0 - 8.5"
},
"conflict": {
"nette/finder": "<3",
"nette/schema": "<1.2.2"
},
"require-dev": {
"jetbrains/phpstorm-attributes": "^1.2",
"nette/tester": "^2.5",
"phpstan/phpstan-nette": "^2.0@stable",
"tracy/tracy": "^2.9"
},
"suggest": {
"ext-gd": "to use Image",
"ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()",
"ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()",
"ext-json": "to use Nette\\Utils\\Json",
"ext-mbstring": "to use Strings::lower() etc...",
"ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.0-dev"
}
},
"autoload": {
"psr-4": {
"Nette\\": "src"
},
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause",
"GPL-2.0-only",
"GPL-3.0-only"
],
"authors": [
{
"name": "David Grudl",
"homepage": "https://davidgrudl.com"
},
{
"name": "Nette Community",
"homepage": "https://nette.org/contributors"
}
],
"description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.",
"homepage": "https://nette.org",
"keywords": [
"array",
"core",
"datetime",
"images",
"json",
"nette",
"paginator",
"password",
"slugify",
"string",
"unicode",
"utf-8",
"utility",
"validation"
],
"support": {
"issues": "https://github.com/nette/utils/issues",
"source": "https://github.com/nette/utils/tree/v4.0.8"
},
"time": "2025-08-06T21:43:34+00:00"
},
{
"name": "nunomaduro/collision",
"version": "v8.8.2",