diff --git a/.github/dev-docs/RELEASE-NOTES-DRAFT.md b/.github/dev-docs/RELEASE-NOTES-DRAFT.md index 9292cf17..c07343af 100644 --- a/.github/dev-docs/RELEASE-NOTES-DRAFT.md +++ b/.github/dev-docs/RELEASE-NOTES-DRAFT.md @@ -25,4 +25,4 @@ The deprecated option named `hyde.docs_directory` has been removed. Use `docs.output_directory` instead. -The authors.yml has been deprecated, and will be refactored in an upcoming release. \ No newline at end of file +The authors.yml and related services have been removed. Define authors in the main Hyde config instead. \ No newline at end of file diff --git a/.github/dev-docs/blog-posts.md b/.github/dev-docs/blog-posts.md index 88eba597..8846ab31 100644 --- a/.github/dev-docs/blog-posts.md +++ b/.github/dev-docs/blog-posts.md @@ -172,16 +172,16 @@ date: "2022-01-01" ```yaml author: "Mr. Hyde" # Arbitrary name displayed "as is" -author: mr_hyde # Username defined in `authors.yml` config +author: mr_hyde # Username defined in `authors` config author: # Array of author data name: "Mr. Hyde" username: mr_hyde - website: https://mrhyde.example.com + website: https://twitter.com/hyde_php ``` When specifying an array you don't need all the sub-properties. The example just shows all the supported values. Array values here -will override the values in the `authors.yml` config. +will override all the values in the `authors` config entry. ### Image diff --git a/.github/dev-docs/customization.md b/.github/dev-docs/customization.md index 975167f4..fb5a3d7a 100644 --- a/.github/dev-docs/customization.md +++ b/.github/dev-docs/customization.md @@ -31,6 +31,41 @@ With a concept directly inspired by [Laravel Jetstream](https://jetstream.larave ], ``` +### Authors +Hyde has support for adding authors in front matter, for example to +automatically add a link to your website or social media profiles. +However, it's tedious to have to add those to each and every +post you make, and keeping them updated is even harder. + +You can predefine authors in the Hyde config. +When writing posts, just specify the username in the front matter, +and the rest of the data will be pulled from a matching entry. + +#### Example +// torchlight! {"lineNumbers": false} +```php +'authors' => [ + Author::create( + username: 'mr_hyde', // Required username + name: 'Mr. Hyde', // Optional display name + website: 'https://hydephp.com' // Optional website URL + ), +], +``` + +This is equivalent to the following front matter: +```yaml +author: + username: mr_hyde + name: Mr. Hyde + website: https://hydephp.com +``` + +But you only have to specify the username: +```yaml +author: mr_hyde +``` + ### Footer The footer can be customized using Markdown, and even disabled completely. diff --git a/config/hyde.php b/config/hyde.php index 3bbd8510..3e522d77 100644 --- a/config/hyde.php +++ b/config/hyde.php @@ -17,6 +17,7 @@ | */ +use Hyde\Framework\Helpers\Author; use Hyde\Framework\Helpers\Features; use Hyde\Framework\Helpers\Meta; @@ -123,6 +124,30 @@ Features::torchlight(), ], + /* + |-------------------------------------------------------------------------- + | Blog Post Authors + |-------------------------------------------------------------------------- + | + | Hyde has support for adding authors in front matter, for example to + | automatically add a link to your website or social media profiles. + | However, it's tedious to have to add those to each and every + | post you make, and keeping them updated is even harder. + | + | Here you can add predefined authors. When writing posts, + | just specify the username in the front matter, and the + | rest of the data will be pulled from a matching entry. + | + */ + + 'authors' => [ + Author::create( + username: 'mr_hyde', // Required username + name: 'Mr. Hyde', // Optional display name + website: 'https://hydephp.com' // Optional website URL + ), + ], + /* |-------------------------------------------------------------------------- | Footer Text diff --git a/src/Concerns/GeneratesPageMetadata.php b/src/Concerns/GeneratesPageMetadata.php index 3f9ce252..628d9b21 100644 --- a/src/Concerns/GeneratesPageMetadata.php +++ b/src/Concerns/GeneratesPageMetadata.php @@ -4,7 +4,6 @@ use Hyde\Framework\Hyde; use Hyde\Framework\Models\MarkdownPost; -use Hyde\Framework\Services\AuthorService; use Tests\TestCase; /** @@ -48,7 +47,7 @@ protected function parseFrontMatterMetadata(): void } if (isset($this->matter['author'])) { - $this->metadata['author'] = AuthorService::getAuthorName($this->matter['author']); + $this->metadata['author'] = $this->getAuthorName($this->matter['author']); } if (isset($this->matter['category'])) { @@ -87,4 +86,19 @@ protected function makeOpenGraphPropertiesForArticle(): void } } } + + /** + * Parse the author name string from front matter with support for both flat and array notation. + * + * @param string|array $author + * @return string + */ + protected function getAuthorName(string|array $author): string + { + if (is_string($author)) { + return $author; + } + + return $author['name'] ?? $author['username'] ?? 'Guest'; + } } diff --git a/src/Concerns/HasAuthor.php b/src/Concerns/HasAuthor.php index 13b2897a..9de225ac 100644 --- a/src/Concerns/HasAuthor.php +++ b/src/Concerns/HasAuthor.php @@ -2,8 +2,8 @@ namespace Hyde\Framework\Concerns; +use Hyde\Framework\Helpers\Author as AuthorHelper; use Hyde\Framework\Models\Author; -use Hyde\Framework\Services\AuthorService; /** * Handle logic for Page models that have an Author. @@ -19,9 +19,14 @@ public function constructAuthor(): void { if (isset($this->matter['author'])) { if (is_string($this->matter['author'])) { + // If the author is a string, we assume it's a username + // and we'll try to find the author in the config $this->author = $this->findAuthor($this->matter['author']); } if (is_array($this->matter['author'])) { + // If the author is an array, we'll assume it's a user + // with one-off custom data, so we create a new author. + // In the future we may want to merge config data with custom data $this->author = $this->createAuthor($this->matter['author']); } } @@ -29,7 +34,7 @@ public function constructAuthor(): void protected function findAuthor(string $author): Author { - return AuthorService::find($author) ?: new Author($author); + return AuthorHelper::get($author); } protected function createAuthor(array $data): Author diff --git a/src/Helpers/Author.php b/src/Helpers/Author.php new file mode 100644 index 00000000..e4a647a1 --- /dev/null +++ b/src/Helpers/Author.php @@ -0,0 +1,31 @@ + $name, + 'website'=> $website, + ]); + } + + public static function all(): Collection + { + return new Collection(config('authors', [])); + } + + public static function get(string $username): AuthorModel + { + return static::all()->firstWhere('username', $username) + ?? static::create($username); + } +} diff --git a/src/Helpers/Image.php b/src/Helpers/Image.php new file mode 100644 index 00000000..51832a11 --- /dev/null +++ b/src/Helpers/Image.php @@ -0,0 +1,11 @@ +filepath = Hyde::path('config/authors.yml'); - - if (file_exists($this->filepath)) { - $this->yaml = $this->getYaml(); - $this->authors = $this->getAuthors(); - } else { - $this->authors = new Collection(); - } - } - - /** - * Returns the filepath of the Yaml file. - * - * If the file does not exist, it will be created. - * - * @deprecated version 0.28.0 - */ - public function publishFile(): void - { - file_put_contents($this->filepath, <<<'EOF' -# Note that this file is deprecated. You'll be able to -# define authors using the Author facade in the config - -# In this file you can declare custom authors. - -# In the default example, `mr_hyde` is the username. -# When setting the author to mr_hyde in a blog post, -# the data in the array will automatically be added. - -authors: - mr_hyde: - name: Mr Hyde - website: https://github.com/hydephp/hyde -EOF - ); - } - - /** - * Parse the Yaml file. - * - * @deprecated version 0.28.0 - * - * @return array - */ - public function getYaml(): array - { - if (! file_exists($this->filepath)) { - return []; - } - $parsed = Yaml::parse(file_get_contents($this->filepath)); - if (! is_array($parsed)) { - return []; - } - - return $parsed; - } - - /** - * Use the Yaml array to parse and collect the Authors. - * - * @return Collection - */ - public function getAuthors(): Collection - { - $collection = new Collection(); - - if (isset($this->yaml['authors'])) { - foreach ($this->yaml['authors'] as $username => $data) { - $collection->push(new Author($username, $data)); - } - } - - return $collection; - } - - /** - * Find and retrieve an Author by their username. - * - * @param string $username of the Author to search for - * @param bool $forgiving should the search be fuzzy? - * @return Author|false - */ - public static function find(string $username, bool $forgiving = true): Author|false - { - $service = new self; - if ($forgiving) { - $username = Str::snake($username); - } - - return $service->authors->firstWhere('username', $username) ?? false; - } - - /** - * Parse the author name string from front matter with support for both flat and array notation. - * - * @param string|array $author - * @return string - */ - public static function getAuthorName(string|array $author): string - { - if (is_string($author)) { - return $author; - } - - return $author['name'] ?? $author['username'] ?? 'Guest'; - } -} diff --git a/tests/Feature/AuthorHelperTest.php b/tests/Feature/AuthorHelperTest.php new file mode 100644 index 00000000..5ceb3582 --- /dev/null +++ b/tests/Feature/AuthorHelperTest.php @@ -0,0 +1,89 @@ +assertInstanceOf(AuthorModel::class, $author); + } + + public function test_create_method_accepts_all_parameters() + { + $author = AuthorHelper::create('foo', 'bar', 'https://example.com'); + + $this->assertEquals('foo', $author->username); + $this->assertEquals('bar', $author->name); + $this->assertEquals('https://example.com', $author->website); + } + + public function test_all_method_returns_empty_collection_if_no_authors_are_set_in_config() + { + Config::set('authors', []); + $authors = AuthorHelper::all(); + + $this->assertInstanceOf(Collection::class, $authors); + $this->assertCount(0, $authors); + } + + public function test_all_method_returns_collection_with_all_authors_defined_in_config() + { + Config::set('authors', [ + AuthorHelper::create('foo'), + ]); + $authors = AuthorHelper::all(); + + $this->assertInstanceOf(Collection::class, $authors); + $this->assertCount(1, $authors); + $this->assertEquals(AuthorHelper::create('foo'), $authors->first()); + } + + public function test_multiple_authors_can_be_defined_in_config() + { + Config::set('authors', [ + AuthorHelper::create('foo'), + AuthorHelper::create('bar'), + ]); + $authors = AuthorHelper::all(); + + $this->assertInstanceOf(Collection::class, $authors); + $this->assertCount(2, $authors); + $this->assertEquals(AuthorHelper::create('foo'), $authors->first()); + $this->assertEquals(AuthorHelper::create('bar'), $authors->last()); + } + + public function test_get_method_returns_config_defined_author_by_username() + { + Config::set('authors', [ + AuthorHelper::create('foo', 'bar'), + ]); + $author = AuthorHelper::get('foo'); + + $this->assertInstanceOf(AuthorModel::class, $author); + $this->assertEquals('foo', $author->username); + $this->assertEquals('bar', $author->name); + } + + public function test_get_method_returns_new_author_if_username_not_found_in_config() + { + Config::set('authors', []); + $author = AuthorHelper::get('foo'); + + $this->assertInstanceOf(AuthorModel::class, $author); + $this->assertEquals('foo', $author->username); + } +} diff --git a/tests/Feature/AuthorPostsIntegrationTest.php b/tests/Feature/AuthorPostsIntegrationTest.php index c59b7552..8e8ba730 100644 --- a/tests/Feature/AuthorPostsIntegrationTest.php +++ b/tests/Feature/AuthorPostsIntegrationTest.php @@ -3,34 +3,24 @@ namespace Tests\Feature; use Hyde\Framework\Actions\CreatesNewMarkdownPostFile; +use Hyde\Framework\Helpers\Author; use Hyde\Framework\Hyde; +use Illuminate\Support\Facades\Config; use Tests\TestCase; /** - * Test that the config/authors.yml feature works in + * Test that the Author feature works in * conjunction with the static Post generator. * - * @see AuthorServiceTest * @see StaticSiteBuilderPostModuleTest */ class AuthorPostsIntegrationTest extends TestCase { - /** - * Set up the test environment. - * - * @return void - */ - public function test_setup_integration_test_environment() + protected function setUp(): void { - // If an authors.yml file exists, back it up. - if (file_exists(Hyde::path('config/authors.yml')) && ! file_exists(Hyde::path('config/authors.yml.bak'))) { - copy(Hyde::path('config/authors.yml'), Hyde::path('config/authors.yml.bak')); - } - - // Create a new authors.yml file. - file_put_contents(Hyde::path('config/authors.yml'), "authors:\n"); + parent::setUp(); - $this->assertTrue(true); + Config::set('authors', []); } /** @@ -86,13 +76,9 @@ public function test_create_post_with_defined_author_with_name() // Check that the post was created $this->assertFileExists(Hyde::path('_posts/test-2dcbb2c-post-with-defined-author-with-name.md')); - // Add the author to the authors.yml file - file_put_contents( - Hyde::path('config/authors.yml'), - 'authors: - test_named_author: - name: Test Author' - ); + Config::set('authors', [ + Author::create('test_named_author', 'Test Author'), + ]); // Check that the post was created $this->assertFileExists(Hyde::path('_posts/test-2dcbb2c-post-with-defined-author-with-name.md')); @@ -128,15 +114,9 @@ public function test_create_post_with_defined_author_with_website() // Check that the post was created $this->assertFileExists(Hyde::path('_posts/test-2dcbb2c-post-with-defined-author-with-name.md')); - // Add the author to the authors.yml file - file_put_contents( - Hyde::path('config/authors.yml'), - 'authors: - test_author_with_website: - name: Test Author - website: https://example.org -' - ); + Config::set('authors', [ + Author::create('test_author_with_website', 'Test Author', 'https://example.org'), + ]); // Check that the post was created $this->assertFileExists(Hyde::path('_posts/test-2dcbb2c-post-with-defined-author-with-name.md')); @@ -161,23 +141,4 @@ public function test_create_post_with_defined_author_with_website() unlink(Hyde::path('_posts/test-2dcbb2c-post-with-defined-author-with-name.md')); unlink(Hyde::path('_site/posts/test-2dcbb2c-post-with-defined-author-with-name.html')); } - - /** - * Tear down the test environment. - * - * @return void - */ - public function test_teardown_integration_test_environment() - { - // Remove the test authors.yml file. - unlink(Hyde::path('config/authors.yml')); - - // If an authors.yml backup exists, restore it. - if (file_exists(Hyde::path('config/authors.yml.bak'))) { - copy(Hyde::path('config/authors.yml.bak'), Hyde::path('config/authors.yml')); - unlink(Hyde::path('config/authors.yml.bak')); - } - - $this->assertTrue(true); - } } diff --git a/tests/Feature/AuthorServiceTest.php b/tests/Feature/AuthorServiceTest.php deleted file mode 100644 index 22642f56..00000000 --- a/tests/Feature/AuthorServiceTest.php +++ /dev/null @@ -1,141 +0,0 @@ -filepath; - - backup($path); - unlinkIfExists($path); - } - - protected function tearDown(): void - { - parent::tearDown(); - - // Clean up any test files - $service = new AuthorService(); - $path = $service->filepath; - unlinkIfExists($path); - - // Restore the original authors.yml file - - restore($path); - } - - public function test_publish_file_creates_file() - { - $service = new AuthorService(); - $path = $service->filepath; - - if (file_exists($path)) { - unlink($path); - } - - $service->publishFile(); - $this->assertFileExists($path); - } - - public function test_get_authors_returns_author_collection() - { - (new AuthorService)->publishFile(); - $service = new AuthorService(); - - $collection = $service->authors; - - $this->assertInstanceOf(Collection::class, $collection); - - $author = $collection->first(); - - $this->assertInstanceOf(Author::class, $author); - - $this->assertEquals('mr_hyde', $author->username); - $this->assertEquals('Mr Hyde', $author->name); - $this->assertEquals('https://github.com/hydephp/hyde', $author->website); - } - - public function test_can_find_author_by_username() - { - $service = new AuthorService(); - $service->publishFile(); - - $author = AuthorService::find('mr_hyde'); - - $this->assertInstanceOf(Author::class, $author); - - $this->assertEquals('mr_hyde', $author->username); - $this->assertEquals('Mr Hyde', $author->name); - $this->assertEquals('https://github.com/hydephp/hyde', $author->website); - } - - public function test_get_yaml_can_parse_file() - { - $service = new AuthorService(); - - $service->publishFile(); - - $array = $service->getYaml(); - - $this->assertIsArray($array); - - $this->assertEquals([ - 'authors' => [ - 'mr_hyde' => [ - 'name' => 'Mr Hyde', - 'website' => 'https://github.com/hydephp/hyde', - ], - ], - ], $array); - } - - public function test_find_method_returns_false_if_no_author_is_found() - { - $this->assertFalse(AuthorService::find('undefined_author')); - } - - public function test_get_yaml_method_returns_empty_array_if_file_does_not_exist() - { - $service = new AuthorService(); - $this->assertFileDoesNotExist($service->filepath); - $this->assertEquals([], $service->getYaml()); - } - - public function test_get_yaml_method_returns_empty_array_if_file_does_not_contain_valid_yaml() - { - $service = new AuthorService(); - file_put_contents($service->filepath, 'invalid yaml'); - - $this->assertEquals([], $service->getYaml()); - } - - public function test_get_author_name_helper_returns_string_for_string() - { - $this->assertEquals('foo', AuthorService::getAuthorName('foo')); - } - - public function test_get_author_name_helper_returns_string_for_array() - { - $this->assertEquals('foo', AuthorService::getAuthorName(['name' => 'foo'])); - } - - public function test_get_author_name_helper_returns_string_for_array_with_proper_fallback_priorities() - { - $this->assertEquals('foo', AuthorService::getAuthorName(['name' => 'foo', 'username' => 'bar'])); - $this->assertEquals('bar', AuthorService::getAuthorName(['username' => 'bar'])); - $this->assertEquals('Guest', AuthorService::getAuthorName([])); - } -}