From f8bb51328eae2eebaf58ba24bb51221a18022971 Mon Sep 17 00:00:00 2001 From: Caen De Silva Date: Sun, 15 Dec 2024 16:17:37 +0100 Subject: [PATCH] Preview snapshot for https://github.com/hydephp/develop/pull/2064 --- .../Commands/ChangeSourceDirectoryCommand.php | 2 +- src/Console/Commands/ServeCommand.php | 19 ++++-- src/Facades/Author.php | 2 +- src/Facades/Filesystem.php | 19 +++++- .../Concerns/BaseFoundationCollection.php | 5 ++ .../Concerns/ForwardsHyperlinks.php | 7 ++ .../Concerns/HandlesFoundationCollections.php | 3 - src/Foundation/Facades/Files.php | 1 - src/Foundation/Facades/Pages.php | 1 - src/Foundation/Facades/Routes.php | 2 - src/Foundation/HydeKernel.php | 1 + .../Internal/LoadYamlConfiguration.php | 7 +- src/Foundation/Kernel/FileCollection.php | 11 ++-- src/Foundation/Kernel/Filesystem.php | 13 ++++ src/Foundation/Kernel/Hyperlinks.php | 16 ++++- src/Foundation/Kernel/PageCollection.php | 7 +- src/Foundation/Kernel/RouteCollection.php | 7 +- src/Framework/Actions/BladeMatterParser.php | 5 +- src/Framework/Actions/Internal/FileFinder.php | 66 +++++++++++++++++++ .../PreBuildTasks/CleanSiteDirectory.php | 3 +- .../Factories/FeaturedImageFactory.php | 3 +- .../Blogging/Models/FeaturedImage.php | 13 +++- .../Features/Blogging/Models/PostAuthor.php | 13 ++-- .../Features/Metadata/MetadataBag.php | 17 ++--- .../Features/Metadata/PageMetadataBag.php | 16 +++-- .../XmlGenerators/RssFeedGenerator.php | 2 +- src/Framework/Services/MarkdownService.php | 4 +- src/Hyde.php | 55 +--------------- .../Contracts/FrontMatter/PageSchema.php | 3 +- src/Markdown/MarkdownConverter.php | 2 - src/Pages/Concerns/HydePage.php | 7 ++ src/Support/Concerns/Serializable.php | 15 ++++- src/Support/DataCollections.php | 7 +- src/Support/Filesystem/MediaFile.php | 19 +++--- src/helpers.php | 2 +- 35 files changed, 230 insertions(+), 145 deletions(-) create mode 100644 src/Framework/Actions/Internal/FileFinder.php diff --git a/src/Console/Commands/ChangeSourceDirectoryCommand.php b/src/Console/Commands/ChangeSourceDirectoryCommand.php index 6bffde7f..2b9f8cee 100644 --- a/src/Console/Commands/ChangeSourceDirectoryCommand.php +++ b/src/Console/Commands/ChangeSourceDirectoryCommand.php @@ -27,7 +27,7 @@ class ChangeSourceDirectoryCommand extends Command { /** @var string */ - protected $signature = 'change:sourceDirectory {name : The new source directory name }'; + protected $signature = 'change:sourceDirectory {name : The new source directory name}'; /** @var string */ protected $description = 'Change the source directory for your project'; diff --git a/src/Console/Commands/ServeCommand.php b/src/Console/Commands/ServeCommand.php index 943b6528..bcb8e11a 100644 --- a/src/Console/Commands/ServeCommand.php +++ b/src/Console/Commands/ServeCommand.php @@ -101,7 +101,7 @@ protected function configureOutput(): void protected function printStartMessage(): void { $this->useBasicOutput() - ? $this->output->writeln('Starting the HydeRC server... Press Ctrl+C to stop') + ? $this->output->writeln('Starting the HydeRC server... Use Ctrl+C to stop') : $this->console->printStartMessage($this->getHostSelection(), $this->getPortSelection(), $this->getEnvironmentVariables()); } @@ -146,12 +146,7 @@ protected function checkArgvForOption(string $name): ?string protected function openInBrowser(string $path = '/'): void { - $binary = match (PHP_OS_FAMILY) { - 'Windows' => 'start', - 'Darwin' => 'open', - 'Linux' => 'xdg-open', - default => null - }; + $binary = $this->getOpenCommand(PHP_OS_FAMILY); $command = sprintf('%s http://%s:%d', $binary, $this->getHostSelection(), $this->getPortSelection()); $command = rtrim("$command/$path", '/'); @@ -164,4 +159,14 @@ protected function openInBrowser(string $path = '/'): void $this->newLine(); } } + + protected function getOpenCommand(string $osFamily): ?string + { + return match ($osFamily) { + 'Windows' => 'start', + 'Darwin' => 'open', + 'Linux' => 'xdg-open', + default => null + }; + } } diff --git a/src/Facades/Author.php b/src/Facades/Author.php index 16976036..d9bbbb89 100644 --- a/src/Facades/Author.php +++ b/src/Facades/Author.php @@ -18,7 +18,7 @@ class Author * Construct a new Post Author. For Hyde to discover this author, * you must call this method from your hyde.php config file. * - * @see https://hydephp.com/docs/1.x/customization.html#authors + * @see https://hydephp.com/docs/1.x/customization#authors * * @param string $username The username of the author. This is the key used to find authors in the config. * @param string|null $name The optional display name of the author, leave blank to use the username. diff --git a/src/Facades/Filesystem.php b/src/Facades/Filesystem.php index 3229bc58..2cbcf58b 100644 --- a/src/Facades/Filesystem.php +++ b/src/Facades/Filesystem.php @@ -69,6 +69,21 @@ public static function smartGlob(string $pattern, int $flags = 0): Collection return self::kernel()->filesystem()->smartGlob($pattern, $flags); } + /** + * Find files in the project's directory, with optional filtering by extension and recursion. + * + * The returned collection will be a list of paths relative to the project root. + * + * @param string $directory + * @param string|array|false $matchExtensions The file extension(s) to match, or false to match all files. + * @param bool $recursive Whether to search recursively or not. + * @return \Illuminate\Support\Collection + */ + public static function findFiles(string $directory, string|array|false $matchExtensions = false, bool $recursive = false): Collection + { + return self::kernel()->filesystem()->findFiles($directory, $matchExtensions, $recursive); + } + /** * Touch one or more files in the project's directory. * @@ -113,7 +128,7 @@ public static function unlinkIfExists(string $path): bool */ public static function getContents(string $path, bool $lock = false): string { - return self::get($path, $lock); + return self::get(...func_get_args()); } /** @@ -126,7 +141,7 @@ public static function getContents(string $path, bool $lock = false): string */ public static function putContents(string $path, string $contents, bool $lock = false): bool|int { - return self::put($path, $contents, $lock); + return self::put(...func_get_args()); } protected static function filesystem(): \Illuminate\Filesystem\Filesystem diff --git a/src/Foundation/Concerns/BaseFoundationCollection.php b/src/Foundation/Concerns/BaseFoundationCollection.php index c11dbe9c..0b97b626 100644 --- a/src/Foundation/Concerns/BaseFoundationCollection.php +++ b/src/Foundation/Concerns/BaseFoundationCollection.php @@ -13,6 +13,11 @@ /** * Base class for the kernel auto-discovery collections. * + * @template TKey of array-key + * @template TValue + * + * @extends \Illuminate\Support\Collection + * * These collections are the heart of the discovery process. * * They are responsible for discovering the files, pages, and routes, diff --git a/src/Foundation/Concerns/ForwardsHyperlinks.php b/src/Foundation/Concerns/ForwardsHyperlinks.php index 9d194fd5..7f5bef1d 100644 --- a/src/Foundation/Concerns/ForwardsHyperlinks.php +++ b/src/Foundation/Concerns/ForwardsHyperlinks.php @@ -5,6 +5,7 @@ namespace Hyde\Foundation\Concerns; use Hyde\Support\Models\Route; +use JetBrains\PhpStorm\Deprecated; /** * @internal Single-use trait for the HydeKernel class. @@ -23,8 +24,14 @@ public function relativeLink(string $destination): string return $this->hyperlinks->relativeLink($destination); } + /** + * @deprecated This method will be removed in v2.0. Please use `asset()` instead. + */ + #[Deprecated(reason: 'Use `asset` method instead.', replacement: '%class%::asset(%parameter0%)')] public function mediaLink(string $destination, bool $validate = false): string { + trigger_deprecation('hyde/framework', '1.8.0', 'The %s() method is deprecated, use %s() instead.', __METHOD__, 'asset'); + return $this->hyperlinks->mediaLink($destination, $validate); } diff --git a/src/Foundation/Concerns/HandlesFoundationCollections.php b/src/Foundation/Concerns/HandlesFoundationCollections.php index e9ef2625..58b137ed 100644 --- a/src/Foundation/Concerns/HandlesFoundationCollections.php +++ b/src/Foundation/Concerns/HandlesFoundationCollections.php @@ -15,7 +15,6 @@ */ trait HandlesFoundationCollections { - /** @return \Hyde\Foundation\Kernel\FileCollection */ public function files(): FileCollection { $this->needsToBeBooted(); @@ -23,7 +22,6 @@ public function files(): FileCollection return $this->files; } - /** @return \Hyde\Foundation\Kernel\PageCollection */ public function pages(): PageCollection { $this->needsToBeBooted(); @@ -31,7 +29,6 @@ public function pages(): PageCollection return $this->pages; } - /** @return \Hyde\Foundation\Kernel\RouteCollection */ public function routes(): RouteCollection { $this->needsToBeBooted(); diff --git a/src/Foundation/Facades/Files.php b/src/Foundation/Facades/Files.php index 39bbebb6..65e78467 100644 --- a/src/Foundation/Facades/Files.php +++ b/src/Foundation/Facades/Files.php @@ -13,7 +13,6 @@ */ class Files extends Facade { - /** @return \Hyde\Foundation\Kernel\FileCollection */ public static function getFacadeRoot(): FileCollection { return HydeKernel::getInstance()->files(); diff --git a/src/Foundation/Facades/Pages.php b/src/Foundation/Facades/Pages.php index a06577b7..42b37c66 100644 --- a/src/Foundation/Facades/Pages.php +++ b/src/Foundation/Facades/Pages.php @@ -13,7 +13,6 @@ */ class Pages extends Facade { - /** @return \Hyde\Foundation\Kernel\PageCollection */ public static function getFacadeRoot(): PageCollection { return HydeKernel::getInstance()->pages(); diff --git a/src/Foundation/Facades/Routes.php b/src/Foundation/Facades/Routes.php index 1a211480..f82acc26 100644 --- a/src/Foundation/Facades/Routes.php +++ b/src/Foundation/Facades/Routes.php @@ -19,7 +19,6 @@ */ class Routes extends Facade { - /** @return \Hyde\Foundation\Kernel\RouteCollection */ public static function getFacadeRoot(): RouteCollection { return HydeKernel::getInstance()->routes(); @@ -41,7 +40,6 @@ public static function getOrFail(string $routeKey): Route return static::getFacadeRoot()->getRoute($routeKey); } - /** @return \Hyde\Foundation\Kernel\RouteCollection<\Hyde\Support\Models\Route> */ public static function all(): RouteCollection { return static::getFacadeRoot()->getRoutes(); diff --git a/src/Foundation/HydeKernel.php b/src/Foundation/HydeKernel.php index 1b7d8e5a..e6789e93 100644 --- a/src/Foundation/HydeKernel.php +++ b/src/Foundation/HydeKernel.php @@ -136,6 +136,7 @@ public function hasFeature(Feature|string $feature): bool public function toArray(): array { return [ + 'version' => self::VERSION, 'basePath' => $this->basePath, 'sourceRoot' => $this->sourceRoot, 'outputDirectory' => $this->outputDirectory, diff --git a/src/Foundation/Internal/LoadYamlConfiguration.php b/src/Foundation/Internal/LoadYamlConfiguration.php index d5d75acc..aae50195 100644 --- a/src/Foundation/Internal/LoadYamlConfiguration.php +++ b/src/Foundation/Internal/LoadYamlConfiguration.php @@ -26,6 +26,8 @@ class LoadYamlConfiguration { protected YamlConfigurationRepository $yaml; + + /** @var array> */ protected array $config; public function bootstrap(Application $app): void @@ -33,7 +35,10 @@ public function bootstrap(Application $app): void $this->yaml = $app->make(YamlConfigurationRepository::class); if ($this->yaml->hasYamlConfigFile()) { - tap($app->make('config'), function (Repository $config): void { + /** @var Repository $config */ + $config = $app->make('config'); + + tap($config, function (Repository $config): void { $this->config = $config->all(); $this->mergeParsedConfiguration(); })->set($this->config); diff --git a/src/Foundation/Kernel/FileCollection.php b/src/Foundation/Kernel/FileCollection.php index 063d04b0..49c67e49 100644 --- a/src/Foundation/Kernel/FileCollection.php +++ b/src/Foundation/Kernel/FileCollection.php @@ -4,13 +4,13 @@ namespace Hyde\Foundation\Kernel; +use Hyde\Facades\Filesystem; use Hyde\Foundation\Concerns\BaseFoundationCollection; use Hyde\Framework\Exceptions\FileNotFoundException; use Hyde\Pages\Concerns\HydePage; use Hyde\Support\Filesystem\SourceFile; use function basename; -use function glob; use function str_starts_with; /** @@ -18,7 +18,7 @@ * * @template T of \Hyde\Support\Filesystem\SourceFile * - * @template-extends \Hyde\Foundation\Concerns\BaseFoundationCollection + * @extends \Hyde\Foundation\Concerns\BaseFoundationCollection * * @property array $items The files in the collection. * @@ -59,7 +59,7 @@ protected function runExtensionHandlers(): void protected function discoverFilesFor(string $pageClass): void { // Scan the source directory, and directories therein, for files that match the model's file extension. - foreach (glob($this->kernel->path($pageClass::sourcePath('{*,**/*}')), GLOB_BRACE) as $path) { + foreach (Filesystem::findFiles($pageClass::sourceDirectory(), $pageClass::fileExtension(), true) as $path) { if (! str_starts_with(basename((string) $path), '_')) { $this->addFile(SourceFile::make($path, $pageClass)); } @@ -71,10 +71,7 @@ public function getFile(string $path): SourceFile return $this->get($path) ?? throw new FileNotFoundException($path); } - /** - * @param class-string<\Hyde\Pages\Concerns\HydePage>|null $pageClass - * @return \Hyde\Foundation\Kernel\FileCollection - */ + /** @param class-string<\Hyde\Pages\Concerns\HydePage>|null $pageClass */ public function getFiles(?string $pageClass = null): FileCollection { return $pageClass ? $this->filter(function (SourceFile $file) use ($pageClass): bool { diff --git a/src/Foundation/Kernel/Filesystem.php b/src/Foundation/Kernel/Filesystem.php index a8f36a32..c4532c40 100644 --- a/src/Foundation/Kernel/Filesystem.php +++ b/src/Foundation/Kernel/Filesystem.php @@ -8,6 +8,7 @@ use Hyde\Foundation\HydeKernel; use Hyde\Foundation\PharSupport; use Illuminate\Support\Collection; +use Hyde\Framework\Actions\Internal\FileFinder; use function collect; use function Hyde\normalize_slashes; @@ -183,4 +184,16 @@ public function smartGlob(string $pattern, int $flags = 0): Collection return $files->map(fn (string $path): string => $this->pathToRelative($path)); } + + /** + * @param string|array|false $matchExtensions + * @return \Illuminate\Support\Collection + */ + public function findFiles(string $directory, string|array|false $matchExtensions = false, bool $recursive = false): Collection + { + /** @var \Hyde\Framework\Actions\Internal\FileFinder $finder */ + $finder = app(FileFinder::class); + + return $finder->handle($directory, $matchExtensions, $recursive); + } } diff --git a/src/Foundation/Kernel/Hyperlinks.php b/src/Foundation/Kernel/Hyperlinks.php index 6b2fd20e..2a94165d 100644 --- a/src/Foundation/Kernel/Hyperlinks.php +++ b/src/Foundation/Kernel/Hyperlinks.php @@ -7,6 +7,7 @@ use Hyde\Facades\Config; use Hyde\Support\Models\Route; use Hyde\Foundation\HydeKernel; +use JetBrains\PhpStorm\Deprecated; use Hyde\Framework\Exceptions\BaseUrlNotSetException; use Hyde\Framework\Exceptions\FileNotFoundException; use Illuminate\Support\Str; @@ -92,7 +93,10 @@ public function relativeLink(string $destination): string * * An exception will be thrown if the file does not exist in the _media directory, * and the second argument is set to true. + * + * @deprecated This method will be removed in v2.0. Please use `asset()` instead. */ + #[Deprecated(reason: 'Use `asset` method instead.', replacement: '%class%->asset(%parameter0%)')] public function mediaLink(string $destination, bool $validate = false): string { if ($validate && ! file_exists($sourcePath = "{$this->kernel->getMediaDirectory()}/$destination")) { @@ -111,7 +115,7 @@ public function mediaLink(string $destination, bool $validate = false): string */ public function asset(string $name, bool $preferQualifiedUrl = false): string { - if (str_starts_with($name, 'http')) { + if (static::isRemote($name)) { return $name; } @@ -147,7 +151,7 @@ public function url(string $path = ''): string { $path = $this->formatLink(trim($path, '/')); - if (str_starts_with($path, 'http')) { + if (static::isRemote($path)) { return $path; } @@ -173,4 +177,12 @@ public function route(string $key): ?Route { return $this->kernel->routes()->get($key); } + + /** + * Determine if the given URL is a remote link. + */ + public static function isRemote(string $url): bool + { + return str_starts_with($url, 'http') || str_starts_with($url, '//'); + } } diff --git a/src/Foundation/Kernel/PageCollection.php b/src/Foundation/Kernel/PageCollection.php index 4739a177..4cc4d85e 100644 --- a/src/Foundation/Kernel/PageCollection.php +++ b/src/Foundation/Kernel/PageCollection.php @@ -14,7 +14,7 @@ * * @template T of \Hyde\Pages\Concerns\HydePage * - * @template-extends \Hyde\Foundation\Concerns\BaseFoundationCollection + * @extends \Hyde\Foundation\Concerns\BaseFoundationCollection * * @property array $items The pages in the collection. * @@ -59,10 +59,7 @@ public function getPage(string $sourcePath): HydePage return $this->get($sourcePath) ?? throw new FileNotFoundException($sourcePath); } - /** - * @param class-string<\Hyde\Pages\Concerns\HydePage>|null $pageClass - * @return \Hyde\Foundation\Kernel\PageCollection - */ + /** @param class-string<\Hyde\Pages\Concerns\HydePage>|null $pageClass */ public function getPages(?string $pageClass = null): PageCollection { return $pageClass ? $this->filter(function (HydePage $page) use ($pageClass): bool { diff --git a/src/Foundation/Kernel/RouteCollection.php b/src/Foundation/Kernel/RouteCollection.php index a93b8eab..fe71995a 100644 --- a/src/Foundation/Kernel/RouteCollection.php +++ b/src/Foundation/Kernel/RouteCollection.php @@ -15,7 +15,7 @@ * * @template T of \Hyde\Support\Models\Route * - * @template-extends \Hyde\Foundation\Concerns\BaseFoundationCollection + * @extends \Hyde\Foundation\Concerns\BaseFoundationCollection * * @property array $items The routes in the collection. * @@ -53,10 +53,7 @@ public function getRoute(string $routeKey): Route return $this->get($routeKey) ?? throw new RouteNotFoundException($routeKey); } - /** - * @param class-string<\Hyde\Pages\Concerns\HydePage>|null $pageClass - * @return \Hyde\Foundation\Kernel\RouteCollection - */ + /** @param class-string<\Hyde\Pages\Concerns\HydePage>|null $pageClass */ public function getRoutes(?string $pageClass = null): RouteCollection { return $pageClass ? $this->filter(function (Route $route) use ($pageClass): bool { diff --git a/src/Framework/Actions/BladeMatterParser.php b/src/Framework/Actions/BladeMatterParser.php index 2f2dcc6e..df3c402d 100644 --- a/src/Framework/Actions/BladeMatterParser.php +++ b/src/Framework/Actions/BladeMatterParser.php @@ -4,10 +4,9 @@ namespace Hyde\Framework\Actions; -use Hyde\Hyde; use RuntimeException; +use Hyde\Facades\Filesystem; -use function file_get_contents; use function str_ends_with; use function str_starts_with; use function substr_count; @@ -53,7 +52,7 @@ class BladeMatterParser public static function parseFile(string $path): array { - return static::parseString(file_get_contents(Hyde::path($path))); + return static::parseString(Filesystem::getContents($path)); } public static function parseString(string $contents): array diff --git a/src/Framework/Actions/Internal/FileFinder.php b/src/Framework/Actions/Internal/FileFinder.php new file mode 100644 index 00000000..7647b23a --- /dev/null +++ b/src/Framework/Actions/Internal/FileFinder.php @@ -0,0 +1,66 @@ +|string|false $matchExtensions + * @return \Illuminate\Support\Collection + */ + public static function handle(string $directory, array|string|false $matchExtensions = false, bool $recursive = false): Collection + { + if (! Filesystem::isDirectory($directory)) { + return collect(); + } + + $finder = Finder::create()->files()->in(Hyde::path($directory)); + + if ($recursive === false) { + $finder->depth('== 0'); + } + + if ($matchExtensions !== false) { + $finder->name(static::buildFileExtensionPattern((array) $matchExtensions)); + } + + return collect($finder)->map(function (SplFileInfo $file): string { + return Hyde::pathToRelative($file->getPathname()); + })->sort()->values(); + } + + /** @param array $extensions */ + protected static function buildFileExtensionPattern(array $extensions): string + { + $extensions = self::expandCommaSeparatedValues($extensions); + + return '/\.('.self::normalizeExtensionForRegexPattern($extensions).')$/i'; + } + + /** @param array $extensions */ + private static function expandCommaSeparatedValues(array $extensions): array + { + return array_merge(...array_map(function (string $item): array { + return array_map(fn (string $item): string => trim($item), explode(',', $item)); + }, $extensions)); + } + + /** @param array $extensions */ + private static function normalizeExtensionForRegexPattern(array $extensions): string + { + return implode('|', array_map(function (string $extension): string { + return preg_quote(ltrim($extension, '.'), '/'); + }, $extensions)); + } +} diff --git a/src/Framework/Actions/PreBuildTasks/CleanSiteDirectory.php b/src/Framework/Actions/PreBuildTasks/CleanSiteDirectory.php index 849f077b..a0edd4d8 100644 --- a/src/Framework/Actions/PreBuildTasks/CleanSiteDirectory.php +++ b/src/Framework/Actions/PreBuildTasks/CleanSiteDirectory.php @@ -10,7 +10,6 @@ use Hyde\Framework\Features\BuildTasks\PreBuildTask; use function basename; -use function glob; use function in_array; use function sprintf; @@ -21,7 +20,7 @@ class CleanSiteDirectory extends PreBuildTask public function handle(): void { if ($this->isItSafeToCleanOutputDirectory()) { - Filesystem::unlink(glob(Hyde::sitePath('*.{html,json}'), GLOB_BRACE)); + Filesystem::unlink(Filesystem::findFiles(Hyde::sitePath(), ['html', 'json'])->all()); Filesystem::cleanDirectory(Hyde::siteMediaPath()); } } diff --git a/src/Framework/Factories/FeaturedImageFactory.php b/src/Framework/Factories/FeaturedImageFactory.php index 2ecbbf27..4c2fdb01 100644 --- a/src/Framework/Factories/FeaturedImageFactory.php +++ b/src/Framework/Factories/FeaturedImageFactory.php @@ -8,6 +8,7 @@ use RuntimeException; use Illuminate\Support\Str; use Hyde\Markdown\Models\FrontMatter; +use Hyde\Foundation\Kernel\Hyperlinks; use Hyde\Framework\Features\Blogging\Models\FeaturedImage; use Hyde\Markdown\Contracts\FrontMatter\SubSchemas\FeaturedImageSchema; @@ -72,7 +73,7 @@ protected function makeSource(): string throw new RuntimeException(sprintf('No featured image source was found in "%s"', $this->filePath ?? 'unknown file')); } - if (FeaturedImage::isRemote($value)) { + if (Hyperlinks::isRemote($value)) { return $value; } diff --git a/src/Framework/Features/Blogging/Models/FeaturedImage.php b/src/Framework/Features/Blogging/Models/FeaturedImage.php index a8dcb492..ebb3522c 100644 --- a/src/Framework/Features/Blogging/Models/FeaturedImage.php +++ b/src/Framework/Features/Blogging/Models/FeaturedImage.php @@ -9,7 +9,9 @@ use Hyde\Facades\Config; use Illuminate\Support\Str; use Hyde\Support\BuildWarnings; +use JetBrains\PhpStorm\Deprecated; use Illuminate\Support\Facades\Http; +use Hyde\Foundation\Kernel\Hyperlinks; use Hyde\Framework\Exceptions\FileNotFoundException; use Hyde\Markdown\Contracts\FrontMatter\SubSchemas\FeaturedImageSchema; @@ -19,7 +21,6 @@ use function filesize; use function sprintf; use function key; -use function str_starts_with; /** * Object representation of a blog post's featured image. @@ -63,7 +64,7 @@ public function __construct( protected readonly ?string $licenseUrl = null, protected readonly ?string $copyrightText = null ) { - $this->type = self::isRemote($source) ? self::TYPE_REMOTE : self::TYPE_LOCAL; + $this->type = Hyperlinks::isRemote($source) ? self::TYPE_REMOTE : self::TYPE_LOCAL; $this->source = $this->setSource($source); } @@ -241,8 +242,14 @@ protected function getContentLengthForRemoteImage(): int return 0; } + /** + * @codeCoverageIgnore Deprecated method. + * + * @deprecated This method will be removed in v2.0. Please use `Hyperlinks::isRemote` instead. + */ + #[Deprecated(reason: 'Replaced by the \Hyde\Foundation\Kernel\Hyperlinks::isRemote method', replacement: '\Hyde\Foundation\Kernel\Hyperlinks::isRemote(%parametersList%)', since: '1.8.0')] public static function isRemote(string $source): bool { - return str_starts_with($source, 'http') || str_starts_with($source, '//'); + return Hyperlinks::isRemote($source); } } diff --git a/src/Framework/Features/Blogging/Models/PostAuthor.php b/src/Framework/Features/Blogging/Models/PostAuthor.php index ac6b850b..35236438 100644 --- a/src/Framework/Features/Blogging/Models/PostAuthor.php +++ b/src/Framework/Features/Blogging/Models/PostAuthor.php @@ -8,6 +8,7 @@ use Hyde\Facades\Author; use Hyde\Facades\Config; use Illuminate\Support\Collection; +use JetBrains\PhpStorm\Deprecated; use function strtolower; use function is_string; @@ -26,7 +27,7 @@ class PostAuthor implements Stringable /** * The display name of the author. */ - public readonly ?string $name; + public readonly string $name; /** * The author's website URL. @@ -48,7 +49,7 @@ class PostAuthor implements Stringable public function __construct(string $username, ?string $name = null, ?string $website = null) { $this->username = $username; - $this->name = $name; + $this->name = $name ?? $this->username; $this->website = $website; } @@ -82,12 +83,16 @@ public static function all(): Collection public function __toString(): string { - return $this->getName(); + return $this->name; } + /** + * @deprecated This is not needed as the name property can be accessed directly. + */ + #[Deprecated(reason: 'Use the name property instead.', replacement: '%class%->name')] public function getName(): string { - return $this->name ?? $this->username; + return $this->name; } /** @param array{username?: string, name?: string, website?: string} $data */ diff --git a/src/Framework/Features/Metadata/MetadataBag.php b/src/Framework/Features/Metadata/MetadataBag.php index ff8a036d..51e55491 100644 --- a/src/Framework/Features/Metadata/MetadataBag.php +++ b/src/Framework/Features/Metadata/MetadataBag.php @@ -54,26 +54,19 @@ public function get(): array public function add(MetadataElementContract|string $element): static { - return match (true) { - $element instanceof LinkElement => $this->addElement('links', $element), - $element instanceof MetadataElement => $this->addElement('metadata', $element), - $element instanceof OpenGraphElement => $this->addElement('properties', $element), + match (true) { + $element instanceof LinkElement => $this->links[$element->uniqueKey()] = $element, + $element instanceof MetadataElement => $this->metadata[$element->uniqueKey()] = $element, + $element instanceof OpenGraphElement => $this->properties[$element->uniqueKey()] = $element, default => $this->addGenericElement((string) $element), }; - } - - protected function addElement(string $type, MetadataElementContract $element): static - { - $this->{$type}[$element->uniqueKey()] = $element; return $this; } - protected function addGenericElement(string $element): static + protected function addGenericElement(string $element): void { $this->generics[] = $element; - - return $this; } /** @return array */ diff --git a/src/Framework/Features/Metadata/PageMetadataBag.php b/src/Framework/Features/Metadata/PageMetadataBag.php index 3aad1667..23a764b3 100644 --- a/src/Framework/Features/Metadata/PageMetadataBag.php +++ b/src/Framework/Features/Metadata/PageMetadataBag.php @@ -7,8 +7,8 @@ use Hyde\Facades\Meta; use Hyde\Pages\Concerns\HydePage; use Hyde\Pages\MarkdownPost; +use Hyde\Foundation\Kernel\Hyperlinks; -use function str_starts_with; use function substr_count; use function str_repeat; @@ -34,6 +34,11 @@ protected function addDynamicPageMetadata(HydePage $page): void $this->add(Meta::link('canonical', $page->getCanonicalUrl())); } + if ($page->has('description')) { + $this->add(Meta::name('description', $page->data('description'))); + $this->add(Meta::property('description', $page->data('description'))); + } + if ($page->has('title')) { $this->add(Meta::name('twitter:title', $page->title())); $this->add(Meta::property('title', $page->title())); @@ -46,7 +51,6 @@ protected function addDynamicPageMetadata(HydePage $page): void protected function addMetadataForMarkdownPost(MarkdownPost $page): void { - $this->addPostMetadataIfExists($page, 'description'); $this->addPostMetadataIfExists($page, 'author'); $this->addPostMetadataIfExists($page, 'category', 'keywords'); @@ -77,7 +81,11 @@ protected function resolveImageLink(string $image): string { // Since this is run before the page is rendered, we don't have the currentPage property. // So we need to run some of the same calculations here to resolve the image path link. - return str_starts_with($image, 'http') ? $image - : str_repeat('../', substr_count(MarkdownPost::outputDirectory().'/'.$this->page->identifier, '/')).$image; + return Hyperlinks::isRemote($image) ? $image : $this->calculatePathTraversal().$image; + } + + private function calculatePathTraversal(): string + { + return str_repeat('../', substr_count(MarkdownPost::outputDirectory().'/'.$this->page->identifier, '/')); } } diff --git a/src/Framework/Features/XmlGenerators/RssFeedGenerator.php b/src/Framework/Features/XmlGenerators/RssFeedGenerator.php index 09ebc3db..b3abfab4 100644 --- a/src/Framework/Features/XmlGenerators/RssFeedGenerator.php +++ b/src/Framework/Features/XmlGenerators/RssFeedGenerator.php @@ -69,7 +69,7 @@ protected function addDynamicItemData(SimpleXMLElement $item, MarkdownPost $post } if (isset($post->author)) { - $item->addChild('dc:creator', $post->author->getName(), 'http://purl.org/dc/elements/1.1/'); + $item->addChild('dc:creator', $post->author->name, 'http://purl.org/dc/elements/1.1/'); } if (isset($post->category)) { diff --git a/src/Framework/Services/MarkdownService.php b/src/Framework/Services/MarkdownService.php index aedd9949..4c9d3270 100644 --- a/src/Framework/Services/MarkdownService.php +++ b/src/Framework/Services/MarkdownService.php @@ -225,9 +225,7 @@ protected function enableAllHtmlElements(): void ], $this->config); } - /** - * Normalize indentation for an un-compiled Markdown string. - */ + /** Normalize indentation for an un-compiled Markdown string */ public static function normalizeIndentationLevel(string $string): string { $lines = self::getNormalizedLines($string); diff --git a/src/Hyde.php b/src/Hyde.php index 01167b10..cc0df0dc 100644 --- a/src/Hyde.php +++ b/src/Hyde.php @@ -4,18 +4,8 @@ namespace Hyde; -use Hyde\Enums\Feature; -use Hyde\Facades\Features; use Hyde\Foundation\HydeKernel; -use Hyde\Foundation\Kernel\FileCollection; -use Hyde\Foundation\Kernel\Filesystem; -use Hyde\Foundation\Kernel\PageCollection; -use Hyde\Foundation\Kernel\RouteCollection; -use Hyde\Pages\Concerns\HydePage; -use Hyde\Support\Models\Route; -use Hyde\Support\Filesystem\SourceFile; use Illuminate\Support\Facades\Facade; -use Illuminate\Support\HtmlString; use JetBrains\PhpStorm\Pure; /** @@ -27,50 +17,7 @@ * @copyright 2022 Caen De Silva * @license MIT License * - * @method static string path(string $path = '') - * @method static string vendorPath(string $path = '', string $package = 'framework') - * @method static string pathToAbsolute(string $path) - * @method static string pathToRelative(string $path) - * @method static string sitePath(string $path = '') - * @method static string mediaPath(string $path = '') - * @method static string siteMediaPath(string $path = '') - * @method static string formatLink(string $destination) - * @method static string relativeLink(string $destination) - * @method static string mediaLink(string $destination, bool $validate = false) - * @method static string asset(string $name, bool $preferQualifiedUrl = false) - * @method static string url(string $path = '') - * @method static Route|null route(string $key) - * @method static string makeTitle(string $value) - * @method static string normalizeNewlines(string $string) - * @method static string stripNewlines(string $string) - * @method static string trimSlashes(string $string) - * @method static HtmlString markdown(string $text, bool $stripIndentation = false) - * @method static string currentPage() - * @method static string currentRouteKey() - * @method static string getBasePath() - * @method static string getSourceRoot() - * @method static string getOutputDirectory() - * @method static string getMediaDirectory() - * @method static string getMediaOutputDirectory() - * @method static Features features() - * @method static FileCollection files() - * @method static PageCollection pages() - * @method static RouteCollection routes() - * @method static Route|null currentRoute() - * @method static HydeKernel getInstance() - * @method static Filesystem filesystem() - * @method static array getRegisteredExtensions() - * @method static bool hasFeature(Feature $feature) - * @method static bool hasSiteUrl() - * @method static void setInstance(HydeKernel $instance) - * @method static void setBasePath(string $basePath) - * @method static void setOutputDirectory(string $outputDirectory) - * @method static void setMediaDirectory(string $mediaDirectory) - * @method static void setSourceRoot(string $sourceRoot) - * @method static void shareViewData(HydePage $page) - * @method static array toArray() - * @method static bool isBooted() - * @method static void boot() + * @mixin \Hyde\Foundation\HydeKernel * * @see \Hyde\Foundation\Concerns\ForwardsFilesystem * @see \Hyde\Foundation\Concerns\ForwardsHyperlinks diff --git a/src/Markdown/Contracts/FrontMatter/PageSchema.php b/src/Markdown/Contracts/FrontMatter/PageSchema.php index ac8eed79..2875b10d 100644 --- a/src/Markdown/Contracts/FrontMatter/PageSchema.php +++ b/src/Markdown/Contracts/FrontMatter/PageSchema.php @@ -13,7 +13,8 @@ interface PageSchema extends FrontMatterSchema { public const PAGE_SCHEMA = [ 'title' => 'string', - 'canonicalUrl' => 'string', // While not present in the page data, it is supported as a front matter key for the accessor data source. + 'description' => 'string', // For values. It is used by the automatic page metadata generator, which reads this value from the front matter. + 'canonicalUrl' => 'string', // While not present in the page data as a property, it is used by the accessor method, which reads this value from the front matter. 'navigation' => NavigationSchema::NAVIGATION_SCHEMA, ]; } diff --git a/src/Markdown/MarkdownConverter.php b/src/Markdown/MarkdownConverter.php index ef6265f1..26cb8902 100644 --- a/src/Markdown/MarkdownConverter.php +++ b/src/Markdown/MarkdownConverter.php @@ -9,8 +9,6 @@ /** * The base Markdown converter class. - * - * "Extends" \League\CommonMark\CommonMarkConverter. */ class MarkdownConverter extends \League\CommonMark\MarkdownConverter { diff --git a/src/Pages/Concerns/HydePage.php b/src/Pages/Concerns/HydePage.php index fb741358..08a730ea 100644 --- a/src/Pages/Concerns/HydePage.php +++ b/src/Pages/Concerns/HydePage.php @@ -397,6 +397,13 @@ public function navigationMenuGroup(): ?string return $this->navigation->group; } + /** + * Get the canonical URL for the page to use in the `` tag. + * + * It can be explicitly set in the front matter using the `canonicalUrl` key, + * otherwise it will be generated based on the site URL and the output path, + * unless there is no configured base URL, leading to this returning null. + */ public function getCanonicalUrl(): ?string { /** @var ?string $value */ diff --git a/src/Support/Concerns/Serializable.php b/src/Support/Concerns/Serializable.php index f76c123f..9fc66cb7 100644 --- a/src/Support/Concerns/Serializable.php +++ b/src/Support/Concerns/Serializable.php @@ -14,8 +14,11 @@ */ trait Serializable { - /** @inheritDoc */ - abstract public function toArray(): array; + /** Default implementation to dynamically serialize all public properties. Can be overridden for increased control. */ + public function toArray(): array + { + return $this->automaticallySerialize(); + } /** Recursively serialize Arrayables */ public function arraySerialize(): array @@ -34,4 +37,12 @@ public function toJson($options = 0): string { return json_encode($this->jsonSerialize(), $options); } + + /** Automatically serialize all public properties. */ + protected function automaticallySerialize(): array + { + // Calling the function from a different scope means we only get the public properties. + + return get_object_vars(...)->__invoke($this); + } } diff --git a/src/Support/DataCollections.php b/src/Support/DataCollections.php index e05596c6..91a22906 100644 --- a/src/Support/DataCollections.php +++ b/src/Support/DataCollections.php @@ -12,9 +12,8 @@ use Illuminate\Support\Collection; use Illuminate\Support\Str; -use function implode; +use function Hyde\path_join; use function json_decode; -use function sprintf; use function Hyde\unslash; use function str_starts_with; @@ -102,9 +101,7 @@ public static function json(string $name, bool $asArray = false): static protected static function findFiles(string $name, array|string $extensions): Collection { - return Filesystem::smartGlob(sprintf('%s/%s/*.{%s}', - static::$sourceDirectory, $name, implode(',', (array) $extensions) - ), GLOB_BRACE); + return Filesystem::findFiles(path_join(static::$sourceDirectory, $name), $extensions); } protected static function makeIdentifier(string $path): string diff --git a/src/Support/Filesystem/MediaFile.php b/src/Support/Filesystem/MediaFile.php index 34da4b51..09ab5247 100644 --- a/src/Support/Filesystem/MediaFile.php +++ b/src/Support/Filesystem/MediaFile.php @@ -4,6 +4,7 @@ namespace Hyde\Support\Filesystem; +use Hyde\Facades\Filesystem; use Hyde\Hyde; use Hyde\Facades\Config; use Hyde\Framework\Exceptions\FileNotFoundException; @@ -14,12 +15,9 @@ use function array_merge; use function array_keys; use function filesize; -use function implode; use function pathinfo; use function collect; use function is_file; -use function sprintf; -use function glob; /** * File abstraction for a project media file. @@ -27,7 +25,7 @@ class MediaFile extends ProjectFile { /** @var array The default extensions for media types */ - final public const EXTENSIONS = ['png', 'svg', 'jpg', 'jpeg', 'webp', 'gif', 'ico', 'css', 'js']; + final public const EXTENSIONS = ['png', 'svg', 'jpg', 'jpeg', 'gif', 'ico', 'css', 'js']; /** @return array The array keys are the filenames relative to the _media/ directory */ public static function all(): array @@ -104,15 +102,18 @@ protected static function discoverMediaAssetFiles(): array })->all(); } + /** @return array */ protected static function getMediaAssetFiles(): array { - return glob(Hyde::path(static::getMediaGlobPattern()), GLOB_BRACE) ?: []; + return Filesystem::findFiles(Hyde::getMediaDirectory(), static::getMediaFileExtensions(), true)->all(); } - protected static function getMediaGlobPattern(): string + /** @return array|string */ + protected static function getMediaFileExtensions(): array|string { - return sprintf(Hyde::getMediaDirectory().'/{*,**/*,**/*/*}.{%s}', implode(',', - Config::getArray('hyde.media_extensions', self::EXTENSIONS) - )); + /** @var array|string $config */ + $config = Config::get('hyde.media_extensions', self::EXTENSIONS); + + return $config; } } diff --git a/src/helpers.php b/src/helpers.php index 743fffb8..14e69878 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -46,7 +46,7 @@ function asset(string $name, bool $preferQualifiedUrl = false): string if (! function_exists('route')) { /** - * Get a page route by its key. + * Get a page route instance by its key. Casting it to a string will return a relative link to the page. */ function route(string $key): ?Hyde\Support\Models\Route {