diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 998c920ef11..1bd2a7a4fcd 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -15,6 +15,9 @@ This serves two purposes: - Added a new `\Hyde\Framework\Exceptions\ParseException` exception class to handle parsing exceptions in data collection files in https://github.com/hydephp/develop/pull/1732 - Added a new `\Hyde\Framework\Exceptions\InvalidConfigurationException` exception class to handle invalid configuration exceptions in https://github.com/hydephp/develop/pull/1799 - The `\Hyde\Facades\Features` class is no longer marked as internal, and is now thus part of the public API. +- Added support for setting custom navigation items in the YAML configuration in https://github.com/hydephp/develop/pull/1818 +- Added support for setting extra attributes for navigation items in https://github.com/hydephp/develop/pull/1824 +- Introduced a new navigation config builder class to simplify navigation configuration in https://github.com/hydephp/develop/pull/1827 ### Changed - **Breaking:** The internals of the navigation system has been rewritten into a new Navigation API. This change is breaking for custom navigation implementations. For more information, see below. @@ -23,6 +26,8 @@ This serves two purposes: - **Breaking:** The `hyde.authors` config setting should now be keyed by the usernames. For more information, see below. - **Breaking:** The `Author::create()` method now returns an array instead of a `PostAuthor` instance. For more information, see below. - **Breaking:** The `Author::get()` method now returns `null` if an author is not found, rather than creating a new instance. For more information, see below. +- **Breaking:** The custom navigation item configuration now uses array inputs instead of the previous format. For more information, see the upgrade guide below. +- **Breaking:** Renamed the `hyde.navigation.subdirectories` configuration option to `hyde.navigation.subdirectory_display`. - Medium: The `route` function will now throw a `RouteNotFoundException` if the route does not exist in https://github.com/hydephp/develop/pull/1741 - Minor: Navigation menu items are now no longer filtered by duplicates (meaning two items with the same label can now exist in the same menu) in https://github.com/hydephp/develop/pull/1573 - Minor: Due to changes in the navigation system, it is possible that existing configuration files will need to be adjusted in order for menus to look the same (in terms of ordering etc.) @@ -43,6 +48,8 @@ This serves two purposes: - Calling the `DataCollection` methods will no longer create the data collections directory in https://github.com/hydephp/develop/pull/1732 - Markdown includes are now converted to HTML using the custom HydePHP Markdown service, meaning they now support full GFM spec and custom Hyde features like colored blockquotes and code block filepath labels in https://github.com/hydephp/develop/pull/1738 - Markdown returned from includes are now trimmed of trailing whitespace and newlines in https://github.com/hydephp/develop/pull/1738 +- Reorganized and cleaned up the navigation and sidebar documentation for improved clarity. +- Moved the sidebar documentation to the documentation pages section for better organization. ### Deprecated - for soon-to-be removed features. @@ -333,3 +340,78 @@ For example, an empty or malformed JSON file will now throw an exception like th In order to normalize the thrown exceptions, we now rethrow the `ParseException` from `Symfony/Yaml` as our custom `ParseException` to match the JSON and Markdown validation. Additionally, an exception will be thrown if a data file is empty, as this is unlikely to be intentional. Markdown files can have an empty body if front matter is present. + +## New features + + + +### Navigation configuration changes + +The custom navigation item configuration format has been updated to use array inputs. This change allows for more flexibility and consistency in defining navigation items. + +#### Old format: + +```php +'navigation' => [ + 'custom_items' => [ + 'Custom Item' => '/custom-page', + ], +], +``` + +#### New format: + +```php +'navigation' => [ + 'custom_items' => [ + ['label' => 'Custom Item', 'destination' => '/custom-page'], + ], +], +``` + +Additionally, the `hyde.navigation.subdirectories` configuration option has been renamed to `hyde.navigation.subdirectory_display`. Update your configuration files accordingly. + +### YAML configuration for navigation items + +You can now set custom navigation items directly in your YAML configuration files. This provides an alternative to defining them in the PHP configuration files. + +Example: + +```yaml +navigation: + custom_items: + - label: Custom Item + destination: /custom-page +``` + +### Extra attributes for navigation items + +Navigation items now support extra attributes, allowing you to add custom data or styling to your navigation elements. You can set these attributes in both PHP and YAML configurations. + +Example in PHP: + +```php +'navigation' => [ + 'custom_items' => [ + [ + 'label' => 'Custom Item', + 'destination' => '/custom-page', + 'attributes' => ['class' => 'special-link', 'target' => '_blank'], + ], + ], +], +``` + +Example in YAML: + +```yaml +navigation: + custom_items: + - label: Custom Item + destination: /custom-page + attributes: + class: special-link + target: _blank +``` + +These changes provide more flexibility and control over your site's navigation structure. Make sure to update your configuration files and any custom code that interacts with navigation items to align with these new formats and features. diff --git a/app/config.php b/app/config.php index ab5db6907b0..143a4a6dda4 100644 --- a/app/config.php +++ b/app/config.php @@ -97,6 +97,7 @@ 'Features' => \Hyde\Facades\Features::class, 'Config' => \Hyde\Facades\Config::class, 'Filesystem' => \Hyde\Facades\Filesystem::class, + 'Navigation' => \Hyde\Facades\Navigation::class, 'Routes' => \Hyde\Foundation\Facades\Routes::class, 'HtmlPage' => \Hyde\Pages\HtmlPage::class, 'BladePage' => \Hyde\Pages\BladePage::class, diff --git a/config/hyde.php b/config/hyde.php index 0898d2a80f4..67c1da3b43b 100644 --- a/config/hyde.php +++ b/config/hyde.php @@ -25,6 +25,7 @@ use Hyde\Facades\Author; use Hyde\Facades\Meta; use Hyde\Enums\Feature; +use Hyde\Facades\Navigation; return [ @@ -331,40 +332,23 @@ | */ - 'navigation' => [ - // This configuration sets the priorities used to determine the order of the menu. - // The default values have been added below for reference and easy editing. - // The array key is the page's route key, the value is the priority. - // Lower values show up first in the menu. The default is 999. - 'order' => [ + 'navigation' => Navigation::configure() + ->setPagePriorities([ 'index' => 0, 'posts' => 10, 'docs/index' => 100, - ], - - // In case you want to customize the labels for the menu items, you can do so here. - // Simply add the route key as the array key, and the label as the value. - 'labels' => [ + ]) + ->setPageLabels([ 'index' => 'Home', 'docs/index' => 'Docs', - ], - - // These are the route keys of pages that should not show up in the navigation menu. - 'exclude' => [ + ]) + ->excludePages([ '404', - ], - - // Any extra links you want to add to the navigation menu can be added here. - // To get started quickly, you can uncomment the defaults here. - // See the documentation link above for more information. - 'custom' => [ - // NavigationItem::create('https://github.com/hydephp/hyde', 'GitHub', 200), - ], - - // How should pages in subdirectories be displayed in the menu? - // You can choose between 'dropdown', 'flat', and 'hidden'. - 'subdirectories' => 'hidden', - ], + ]) + ->addNavigationItems([ + // Navigation::item('https://github.com/hydephp/hyde', 'GitHub', 200), + ]) + ->setSubdirectoryDisplayMode('hidden'), /* |-------------------------------------------------------------------------- diff --git a/docs/creating-content/documentation-pages.md b/docs/creating-content/documentation-pages.md index 38cf1616530..2238f6777d9 100644 --- a/docs/creating-content/documentation-pages.md +++ b/docs/creating-content/documentation-pages.md @@ -8,23 +8,17 @@ navigation: ## Introduction to Hyde Documentation Pages -Welcome to the Hyde Documentation Pages, where creating professional-looking documentation sites has never been easier. -Using the Hyde Documentation module, all you need to do is place standard Markdown files in the _docs/ directory, and Hyde takes care of the rest. +Welcome to the Hyde Documentation Pages, where creating professional-looking documentation sites has never been easier. Using the Hyde Documentation module, all you need to do is place standard Markdown files in the `_docs/` directory, and Hyde takes care of the rest. -Hyde compiles your Markdown content into beautiful static HTML pages using a TailwindCSS frontend, complete with a -responsive sidebar that is automatically generated based on your Markdown files. You can even customize the order, -labels, and even groups, of the sidebar items to suit your needs. +Hyde compiles your Markdown content into beautiful static HTML pages using a TailwindCSS frontend, complete with a responsive sidebar that is automatically generated based on your Markdown files. You can even customize the order, labels, and groups of the sidebar items to suit your needs. -Additionally, if you have a `_docs/index.md` file, the sidebar header will link to it, and an automatically generated -"Docs" link will be added to your site's main navigation menu, pointing to your documentation page. +Additionally, if you have an `_docs/index.md` file, the sidebar header will link to it, and an automatically generated "Docs" link will be added to your site's main navigation menu, pointing to your documentation page. -If you have a Torchlight API token in your .env file, Hyde will even enable syntax highlighting automatically, -saving you time and effort. For more information about this feature, see the [extensions page](extensions#torchlight). +If you have a Torchlight API token in your `.env` file, Hyde will even enable syntax highlighting automatically, saving you time and effort. For more information about this feature, see the [extensions page](extensions#torchlight). ### Best Practices and Hyde Expectations -Since Hyde does a lot of things automatically, there are some things you may need -to keep in mind when creating documentation pages so that you don't get unexpected results. +Since Hyde does a lot of things automatically, there are some things you may need to keep in mind when creating documentation pages so that you don't get unexpected results. #### Filenames @@ -35,7 +29,7 @@ to keep in mind when creating documentation pages so that you don't get unexpect - You should always have an `index.md` file in the `_docs/` directory - Your page will be stored in `_site/docs/.html` unless you [change it in the config](#output-directory) -### Advanced usage and customization +### Advanced Usage and Customization Like most of HydePHP, the Hyde Documentation module is highly customizable. Much of the frontend is composed using Blade templates and components, which you can customize to your heart's content. Since there are so many components, it's hard to list them all here in the documentation, so I encourage you to check out the [source code](https://github.com/hydephp/framework/tree/master/resources/views/components/docs) to see how it's all put together and find the customizations you are looking for. @@ -57,7 +51,7 @@ This will create the following file saved as `_docs/page-title.md` ``` -### Front Matter is optional +### Front Matter Is Optional You don't need to use [front matter](blog-posts#supported-front-matter-properties) to create a documentation page. @@ -77,76 +71,85 @@ navigation: ## Dynamic Content Generation -Hyde makes documentation pages easy to create by automatically generating dynamic content such as the sidebar and page title. -If you are not happy with the results you can customize them in the config or with front matter. +Hyde makes documentation pages easy to create by automatically generating dynamic content such as the sidebar and page title. If you are not happy with the results you can customize them in the config or with front matter. -### Front Matter reference +### Front Matter Reference -Before we look at how to override things, here is an overview of the relevant content Hyde generates, -and where the data is from as well as where it can be overridden. +Before we look at how to override things, here is an overview of the relevant content Hyde generates, and where the data is from as well as where it can be overridden. -| Property | Description | Dynamic Data Source | Override in | -|---------------------------------|--------------------------------------------------------|-------------------------------------|----------------------| -| `title` (string) | The title of the page used in the HTML `` tag | The first H1 heading (`# Foo`) | Front matter | -| `navigation.label` (string) | The label for the page shown in the sidebar | The page basename | Front matter, config | -| `navigation.priority` (integer) | The priority of the page used for ordering the sidebar | Defaults to 999 | Front matter, config | -| `navigation.hidden` (boolean) | Hides the page from the sidebar | _none_ | Front matter, config | -| `navigation.group` (string) | The group the page belongs to in the sidebar | Subdirectory, if nested | Front matter | +| Property | Description | Dynamic Data Source | Override in | +|---------------------------------|--------------------------------------------------------|--------------------------------|----------------------| +| `title` (string) | The title of the page used in the HTML `<title>` tag | The first H1 heading (`# Foo`) | Front matter | +| `navigation.label` (string) | The label for the page shown in the sidebar | The page basename | Front matter, config | +| `navigation.priority` (integer) | The priority of the page used for ordering the sidebar | Defaults to 999 | Front matter, config | +| `navigation.hidden` (boolean) | Hides the page from the sidebar | _none_ | Front matter, config | +| `navigation.group` (string) | The group the page belongs to in the sidebar | Subdirectory, if nested | Front matter | ## Sidebar -The sidebar is automatically generated from the files in the `_docs` directory. You will probably want to change the order -of these items. You can do this in two ways, either in the config or with front matter using the navigation array settings. +The sidebar is automatically generated from the files in the `_docs` directory. You will probably want to change the order of these items. You can do this in two ways, either in the config or with front matter using the navigation array settings. -Since this feature shares a lot of similarities and implementation details with the navigation menu, -I recommend you read the [navigation menu documentation](navigation) page as well to learn more about the fundamentals and terminology. +Since this feature shares a lot of similarities and implementation details with the navigation menu, I recommend you read the [navigation menu documentation](navigation) page as well to learn more about the fundamentals and terminology. -### Sidebar ordering +### Sidebar Ordering -The sidebar is sorted/ordered by the `priority` property. The higher the priority the further down in the sidebar it will be. -The default priority is 999. You can override the priority using the following front matter: +The sidebar is sorted/ordered by the `priority` property. The lower the priority value, the higher up in the sidebar it will be. The default priority is 999. You can override the priority using the following front matter: ```yaml navigation: priority: 5 ``` -You can also change the order in the Docs configuration file. -See [the chapter in the customization page](customization#navigation-menu--sidebar) for more details. <br> -_I personally think the config route is easier as it gives an instant overview, however the first way is nice as well._ +You can also change the order in the `config/docs.php` configuration file, which may be easier to manage for larger sites. -### Sidebar labels +#### Basic Priority Syntax -The sidebar items are labelled with the `label` property. The default label is the filename of the file. -You can change it with the following front matter: +A nice and simple way to define the order of pages is to add their route keys as a simple list array. Hyde will then match that array order. -```yaml -navigation: - label: "My Custom Sidebar Label" +It may be useful to know that Hyde internally will assign a priority calculated according to its position in the list, plus an offset of `500`. The offset is added to make it easier to place pages earlier in the list using front matter or with explicit priority settings. + +```php +// filepath: config/docs.php +'sidebar' => [ + 'order' => [ + 'readme', // Priority: 500 + 'installation', // Priority: 501 + 'getting-started', // Priority: 502 + ] +] ``` -### Sidebar grouping +#### Explicit Priority Syntax -Sidebar grouping allows you to group items in the sidebar into categories. This is useful for creating a sidebar with a lot of items. -The Hyde docs for instance use this. +You can also specify explicit priorities by adding a value to the array keys. Hyde will then use these exact values as the priorities. -The feature is enabled automatically when one or more of your documentation pages have the `navigation.group` property set -in the front matter, or when subdirectories are used. This will then switch to a slightly more compact sidebar layout with pages sorted into categories. -Any pages without the group front matter will get put in the "Other" group. +```php +// filepath: config/docs.php +'sidebar' => [ + 'order' => [ + 'readme' => 10, + 'installation' => 15, + 'getting-started' => 20, + ] +] +``` -### Sidebar footer customization +### Sidebar Labels -The sidebar footer contains, by default, a link to your site homepage. You can change this in the `config/docs.php` file. +The sidebar items are labelled with the `label` property. The default label is generated from the filename of the file. -```php -// filepath: config/docs.php +You can change it with the following front matter: -'sidebar' => [ - 'footer' => 'My **Markdown** Footer Text', -], +```yaml +navigation: + label: "My Custom Sidebar Label" ``` -You can also set the option to `false` to disable it entirely. +### Sidebar Grouping + +Sidebar grouping allows you to group items in the sidebar under category headings. This is useful for creating a sidebar with a lot of items. The official HydePHP.com docs, for instance, use this feature. + +The feature is enabled automatically when one or more of your documentation pages have the `navigation.group` property set in the front matter, or when documentation pages are organized in subdirectories. Once activated, Hyde will switch to a slightly more compact sidebar layout with pages sorted into labeled groups. Any pages without the group information will be put in the "Other" group. #### Using Front Matter @@ -157,15 +160,13 @@ navigation: group: "Getting Started" ``` -#### Automatic subdirectory-based grouping +#### Automatic Subdirectory-Based Grouping -You can also automatically group your documentation pages by placing source files in sub-directories. +You can also automatically group your documentation pages by placing source files in subdirectories. -For example, putting a Markdown file in `_docs/getting-started/`, is equivalent to adding the same front matter seen above. +For example, putting a Markdown file in `_docs/getting-started/` is equivalent to adding the same front matter seen above. ->info Note that when the [flattened output paths](#using-flattened-output-paths) setting is enabled (which it is by default), the file will still be compiled to the `_site/docs/` directory like it would be if you didn't use the subdirectories. Note that this means that you can't have two documentation pages with the same filename as they overwrite each other. - ->info Tip: When using subdirectory-based dropdowns, you can set their priority using the directory name as the array key. +>warning Note that when the [flattened output paths](#using-flattened-output-paths) setting is enabled (which it is by default), the file will still be compiled to the `_site/docs/` directory like it would be if you didn't use the subdirectories. Note that this means that you can't have two documentation pages with the same filename as they would overwrite each other. ### Hiding Items @@ -178,17 +179,16 @@ navigation: This can be useful to create redirects or other items that should not be shown in the sidebar. ->info The index page is by default not shown as a sidebar item, but instead is linked in the sidebar header. <br> +>info The index page is by default not shown as a sidebar item, but instead is linked in the sidebar header. ## Customization -Please see the [customization page](customization) for in-depth information on how to customize Hyde, -including the documentation pages. Here is a high level overview for quick reference though. +Please see the [customization page](customization) for in-depth information on how to customize Hyde, including the documentation pages. Here is a high level overview for quick reference though. + +### Output Directory -### Output directory +If you want to store the compiled documentation pages in a different directory than the default 'docs' directory, for example to specify a version like the Hyde docs does, you can specify the output directory in the Hyde configuration file. -If you want to store the compiled documentation pages in a different directory than the default 'docs' directory, -for example to specify a version like the Hyde docs does, you can specify the output directory in the Hyde configuration file. The path is relative to the site output, typically `_site`. ```php @@ -201,23 +201,35 @@ The path is relative to the site output, typically `_site`. Note that you need to take care as to not set it to something that may conflict with other parts, such as media or posts directories. -### Automatic navigation menu +### Automatic Navigation Menu By default, a link to the documentation page is added to the navigation menu when an index.md file is found in the `_docs` directory. Please see [the customization page](customization#navigation-menu--sidebar) for more information. -### Sidebar header name +### Sidebar Header Name -By default, the site title shown in the sidebar header is generated from the configured site name suffixed with "docs". -You can change this in the Docs configuration file. Tip: The header will link to the docs/index page, if it exists. +By default, the site title shown in the sidebar header is generated from the configured site name suffixed with "docs". You can change this in the Docs configuration file. Tip: The header will link to the docs/index page, if it exists. ```php 'title' => 'API Documentation', ``` -### Sidebar page order +### Sidebar Footer Customization -To quickly arrange the order of items in the sidebar, you can reorder the page identifiers in the list and the links will be sorted in that order. -Link items without an entry here will fall back to the default priority of 999, putting them last. +The sidebar footer contains, by default, a link to your site homepage. You can change this in the `config/docs.php` file. + +```php +// filepath: config/docs.php + +'sidebar' => [ + 'footer' => 'My **Markdown** Footer Text', +], +``` + +You can also set the option to `false` to disable it entirely. + +### Sidebar Page Order + +To quickly arrange the order of items in the sidebar, you can reorder the page identifiers in the list and the links will be sorted in that order. Link items without an entry here will fall back to the default priority of 999, putting them last. ```php 'sidebar' => [ @@ -231,13 +243,9 @@ Link items without an entry here will fall back to the default priority of 999, See [the chapter in the customization page](customization#navigation-menu--sidebar) for more details. <br> -### Automatic sidebar group labels +### Setting Sidebar Group Labels -When using the automatic sidebar grouping feature (based on subdirectories), the titles of the groups are generated from the directory names. -If these are not to your liking, for example if you need to use special characters, you can override them in the Docs configuration file. -The array key is the directory name, and the value is the label. - -Please note that this option is not added to the config file by default, as it's not a super common use case. No worries though, just add the following yourself! +When using the automatic sidebar grouping feature the titles of the groups are generated from the subdirectory names. If these are not to your liking, for example if you need to use special characters, you can override them in the configuration file. The array key is the directory name, and the value is the label. ```php // Filepath: config/docs.php @@ -247,12 +255,85 @@ Please note that this option is not added to the config file by default, as it's ], ``` -### Table of contents settings +Please note that this option is not added to the config file by default, as it's not a super common use case. No worries though, just add the following yourself! + +#### Setting Sidebar Group Priorities + +By default, each group will be assigned the lowest priority found inside the group. However, you can specify the order and priorities for sidebar group keys the same way you can for the sidebar items. + +Just use the sidebar group key as instead of the page identifier/route key: + +```php +// Filepath: config/docs.php +'sidebar' => [ + 'order' => [ + 'readme', + 'installation', + 'getting-started', + ], +], +``` + +### Numerical Prefix Sidebar Ordering + +HydePHP v2 introduces sidebar item ordering based on numerical prefixes in filenames. This feature works for the documentation sidebar. + +This has the great benefit of matching the sidebar layout with the file structure view. It also works especially well with subdirectory-based sidebar grouping. + +```shell +_docs/ + 01-installation.md # Priority: 1 + 02-configuration.md # Priority: 2 + 03-usage.md # Priority: 3 +``` + +As you can see, Hyde parses the number from the filename and uses it as the priority for the page in the sidebar, while stripping the prefix from the route key. + +#### Important Notes + +1. The numerical prefix remains part of the page identifier but is stripped from the route key. + For example: `_docs/01-installation.md` has route key `installation` and page identifier `01-installation`. +2. You can delimit the numerical prefix with either a dash or an underscore. + For example: Both `_docs/01-installation.md` and `_docs/01_installation.md` are valid. +3. Leading zeros are optional. `_docs/1-installation.md` is equally valid. + +#### Using Numerical Prefix Ordering in Subdirectories + +This feature integrates well with automatic subdirectory-based sidebar grouping. Here's an example of how you could organize a documentation site: + +```shell +_docs/ + 01-getting-started/ + 01-installation.md + 02-requirements.md + 03-configuration.md + 02-usage/ + 01-quick-start.md + 02-advanced-usage.md + 03-features/ + 01-feature-1.md + 02-feature-2.md +``` + +Here are two useful tips: + +1. You can use numerical prefixes in subdirectories to control the sidebar group order. +2. The numbering within a subdirectory works independently of its siblings, so you can start from one in each subdirectory. + +#### Customization + +If you're not interested in using numerical prefix ordering, you can disable it in the Hyde config file. Hyde will then no longer extract the priority and will no longer strip the prefix from the route key. + +```php +// filepath: config/hyde.php +'numerical_page_ordering' => false, +``` + +### Table of Contents Settings Hyde automatically generates a table of contents for the page and adds it to the sidebar. -In the `config/docs.php` file you can configure the behaviour, content, and the look and feel of the sidebar table of contents. -You can also disable the feature completely. +In the `config/docs.php` file you can configure the behaviour, content, and the look and feel of the sidebar table of contents. You can also disable the feature completely. ```php 'sidebar' => [ @@ -264,11 +345,9 @@ You can also disable the feature completely. ], ``` -### Using flattened output paths +### Using Flattened Output Paths -If this setting is set to true, Hyde will output all documentation pages into the same configured documentation output directory. -This means that you can use the automatic directory-based grouping feature, but still have a "flat" output structure. -Note that this means that you can't have two documentation pages with the same filename or navigation menu label as they will overwrite each other. +If this setting is set to true, Hyde will output all documentation pages into the same configured documentation output directory. This means that you can use the automatic directory-based grouping feature, but still have a "flat" output structure. Note that this means that you can't have two documentation pages with the same filename or navigation menu label as they will overwrite each other. If you set this to false, Hyde will match the directory structure of the source files (just like all other pages). @@ -281,8 +360,7 @@ If you set this to false, Hyde will match the directory structure of the source ### Introduction -The HydeSearch plugin adds a search feature to documentation pages. It consists of two parts, a search index generator -that runs during the build command, and a frontend JavaScript plugin that adds the actual search widget. +The HydeSearch plugin adds a search feature to documentation pages. It consists of two parts, a search index generator that runs during the build command, and a frontend JavaScript plugin that adds the actual search widget. >info Tip: The HydeSearch plugin is what powers the search feature on this site! Why not [try it out](search)? @@ -295,22 +373,19 @@ The search feature is enabled by default. You can disable it by removing the `Do ], ``` -### Using the search +### Using the Search The search works by generating a JSON search index which the JavaScript plugin loads asynchronously. Two ways to access the search are added, one is a full page search screen that will be saved to `docs/search.html`. -The second method is a button added to the documentation pages, similar to how Algolia DocSearch works. -Opening it will open a modal with an integrated search screen. You can also open the dialogue using the keyboard shortcut `/`. +The second method is a button added to the documentation pages, similar to how Algolia DocSearch works. Opening it will open a modal with an integrated search screen. You can also open the dialog using the keyboard shortcut `/`. >info The full page can be disabled by setting `create_search_page` to `false` in the `docs` config. -### Hiding pages from indexing +### Hiding Pages from Indexing -If you have a large page on your documentation site, like a changelog, you may want to hide it from the search index. -You can do this by adding the page identifier to the `exclude_from_search` array in the `docs` config, similar to how -navigation menu items are hidden. The page will still be accessible as normal but will be added to the search index JSON file. +If you have a large page on your documentation site, like a changelog, you may want to hide it from the search index. You can do this by adding the page identifier to the `exclude_from_search` array in the `docs` config, similar to how navigation menu items are hidden. The page will still be accessible as normal but will not be added to the search index JSON file. ```php // filepath: config/docs.php @@ -319,7 +394,7 @@ navigation menu items are hidden. The page will still be accessible as normal bu ] ``` -### Live search with the realtime compiler +### Live Search with the Realtime Compiler The Realtime Compiler that powers the `php hyde serve` command will automatically generate a fresh search index each time the browser requests it. @@ -327,19 +402,15 @@ The Realtime Compiler that powers the `php hyde serve` command will automaticall ### Introduction -Hyde can automatically add links to documentation pages that take the user -to a GitHub page (or similar) to edit the page. This makes it great for open-source projects -looking to allow others to contribute to the documentation in a quick and easy manner. +Hyde can automatically add links to documentation pages that take the user to a GitHub page (or similar) to edit the page. This makes it great for open-source projects looking to allow others to contribute to the documentation in a quick and easy manner. -The feature is automatically enabled when you specify a base URL in the Docs configuration file. -Hyde expects this to be a GitHub path, but it will probably work with other methods as well, -if not, please send a PR and/or create an issue on the [GitHub repository](https://github.com/hydephp/framework)! +The feature is automatically enabled when you specify a base URL in the Docs configuration file. Hyde expects this to be a GitHub path, but it will probably work with other methods as well. If not, please send a PR and/or create an issue on the [GitHub repository](https://github.com/hydephp/framework)! >info Tip: This documentation site uses this feature, scroll down to the bottom of this page and try it out! ### Configuration -As an example configuration, let's take a practical example of how HydePHP.com uses this feature. +Here's an example configuration from the official HydePHP.com documentation: ```php // Filepath: config/docs.php @@ -347,7 +418,7 @@ As an example configuration, let's take a practical example of how HydePHP.com u 'source_file_location_base' => 'https://github.com/hydephp/docs/blob/master/', ``` -#### Changing the button text +#### Changing the Button Text Changing the label is easy, just change the following config setting: @@ -356,26 +427,24 @@ Changing the label is easy, just change the following config setting: 'edit_source_link_text' => 'Edit Source on GitHub', ``` -#### Changing the position +#### Changing the Position -By default, the button will be shown in both the documentation page footer. -You can change this by setting the following config setting to `'header'`, `'footer'`, or `'both'` +By default, the button will be shown in the documentation page footer. You can change this by setting the following config setting to `'header'`, `'footer'`, or `'both'` ```php // Filepath: config/docs.php 'edit_source_link_position' => 'header', ``` -#### Adding a button icon +#### Adding a Button Icon -This is not included out of the box, but is easy to add with some CSS! -Just target the `.edit-page-link` class. +This is not included out of the box, but is easy to add with some CSS! Just target the `.edit-page-link` class. ```css // filepath e.g. app.css .edit-page-link::before {content: "✏ "} ``` -#### Changing the Blade view +#### Changing the Blade View You can also publish the `edit-source-button.blade.php` view and change it to your liking. diff --git a/docs/digging-deeper/customization.md b/docs/digging-deeper/customization.md index 19b78e541f7..f9c7a0f5682 100644 --- a/docs/digging-deeper/customization.md +++ b/docs/digging-deeper/customization.md @@ -283,15 +283,18 @@ Still, you will likely want to customize some parts of these menus, and thankful - To customize the navigation menu, use the setting `navigation.order` in the `hyde.php` config. - When customizing the navigation menu, you should use the [route key](core-concepts#route-keys) of the page. +- You can use either a basic list array or specify explicit priorities. Learn more in the [Navigation Menu](navigation) documentation. #### Customizing the documentation sidebar - To customize the sidebar, use the setting `sidebar.order` in the `docs.php` config. -- When customizing the sidebar, can use the route key, or just the [page identifier](core-concepts#page-identifiers) of the page. +- When customizing the sidebar, you can use the route key, or just the [page identifier](core-concepts#page-identifiers) of the page. +- Similar to the navigation menu, you can use a basic list array or specify explicit priorities. +- You can also use front matter in individual documentation pages to customize their appearance and behavior in the sidebar. -Learn more in the [Documentation Pages](documentation-pages) documentation. +Learn more in the [Documentation Pages](documentation-pages#sidebar) documentation. ## Additional Advanced Options diff --git a/docs/digging-deeper/navigation.md b/docs/digging-deeper/navigation.md index c5641903b90..ffef925624d 100644 --- a/docs/digging-deeper/navigation.md +++ b/docs/digging-deeper/navigation.md @@ -8,75 +8,63 @@ navigation: ## Introduction -A great time-saving feature of HydePHP is the automatic navigation menu and documentation sidebar generation. -Hyde is designed to automatically configure these menus for you based on the content you have in your project. +HydePHP offers automatic navigation menu generation features, designed to take the pain out of creating navigation menus. +While Hyde does its best to configure these menus automatically based on understanding your project files, you may want to customize them further. There are two types of navigation menus in Hyde: -- **Primary Navigation Menu**: This is the main navigation menu that appears on most pages of your site. -- **Documentation Sidebar**: This is a sidebar that appears on documentation pages and contains links to other documentation pages. +1. **Primary Navigation Menu**: The main navigation menu appearing on most pages of your site. Unique features include dropdowns for subdirectories, depending on configuration. +2. **Documentation Sidebar**: The sidebar on documentation pages with links to other documentation pages. Unique features include automatic grouping based on subdirectories. -HydePHP automatically generates all of these menus for you based on the content of your project, -and does its best to automatically configure them in the way that you most likely want them to be. +This documentation will guide you through the customization process of the primary navigation menu. To learn about the documentation sidebar, visit the [Documentation Pages](documentation-pages) documentation. -Of course, this won't always be perfect, so thankfully Hyde makes it a breeze to customize these menus to your liking. -Keep on reading to learn how! To learn even more about the sidebars, visit the [Documentation Pages](documentation-pages) documentation. +### Internal Structure Overview -### Quick primer on the internals +Internally, both navigation menu types extend the same base class and thus share core functionality. This means the configuration process is similar for both types, making the documentation applicable to both. -It may be beneficial to understand the internal workings of the navigation menus to take full advantage of the options. +For a deeper understanding of the internal workings, refer to the [Digging Deeper](#digging-deeper-into-the-internals) section. -In short, both navigation menu types extend the same class (meaning they share the same base code), this means that the way -they are configured is very similar, making the documentation here applicable to both types of menus. +### Understanding Priorities -See the [Digging Deeper](#digging-deeper-into-the-internals) section of this page if you want the full scoop on the internals! +All navigation menu items have an internal priority value determining their order. Lower values place items higher in the menu. The default priority for pages is `999`, placing them last unless you specify a value. Some pages, like the `index` page, are configured by default with the lowest priority of `0`. -### Primer on priorities +### Customization Options -All navigation menu items have an internal priority value that determines their order in the navigation. -Lower values mean that the item will be higher up in the menu. The default for pages is `999` which puts them last. -However, some pages are autoconfigured to have a lower priority, for example, the `index` page defaults to a priority of `0`. +Here's an overview of what you can customize in your navigation menus: -### What to customize? +- Item labels: The text displayed in menu links +- Item priorities: Control the order in which links appear +- Item visibility: Choose to hide or show pages in the menu +- Item grouping: Group pages together in dropdowns or sidebar categories -Here is a quick overview of what you might want to customize in your navigation menus: +### Customization Methods -- Navigation menu item labels - the text that appears in the menu links -- Navigation menu item priority - control the order in which the links appear -- Navigation menu item visibility - control if pages may show up in the menus -- Navigation menu item grouping - group pages together in dropdowns +Hyde provides multiple ways to customize navigation menus to suit your needs: -### How and where to customize? +1. Front matter data in Markdown and Blade page files, applicable to all menu types +2. Configuration in the `hyde` config file for main navigation items -Hyde provides a few different ways to customize the navigation menus, depending on what you prefer. +Keep in mind that front matter data overrides dynamically inferred or config-defined priorities. While useful for quick one-off changes on small sites, it can make reordering items later on more challenging as you can't see the entire structure at once. -Specifying the data in the front matter will override any dynamically inferred or config-defined priority. -While this is useful for one-offs, it can make it harder to reorder items later on as you can't see the whole picture at once. -It's up to you which method you prefer to use. - -To customize how a page is represented in navigation, you can either set the `navigation` front matter data in the page's markdown file, -or configure it in the config file. Main navigation items are in the `hyde` config file, while documentation sidebar items are in the `docs` config file. -General options for the entire navigation menus are also available in the `hyde` and `docs` config files. - -Now that you know the basics, let's dive into the details of how to customize the navigation menus! +Additionally, general options for the entire navigation menus are also available in the `hyde` config file. ## Front Matter Configuration -The front matter options allow you to customize the navigation menus on a per-page basis. -Here is a quick reference of the available options. The full documentation of each option is below. -You don't need to specify all the keys, only the ones you want to customize. +Front matter options allow per-page customization of navigation menus. Here's a quick reference of available options: ```yaml navigation: - label: string # The text to display - priority: int # Order is also supported - hidden: bool # Visible is also supported (but obviously invert the value) - group: string # Category is also supported + label: string # The displayed text in the navigation item link + priority: int # The page's priority for ordering (lower means higher up/first) + hidden: bool # Whether the page should be hidden from the navigation menu + group: string # Set main menu dropdown or sidebar group key ``` +You only need to specify the keys you want to customize. + ### `label` -The `label` option allows you to customize the text that appears in the navigation menu for the page. +Customizes the text appearing in the navigation menu link for the page. If not set anywhere else, Hyde will search for a title in the page content or generate one from the filename. ```yaml navigation: @@ -85,7 +73,7 @@ navigation: ### `priority` -The `priority` option allows you to control the order in which the page appears in the navigation menu. You can also use `order` instead of `priority`. +Controls the order in which the page appears in the navigation menu. ```yaml navigation: @@ -94,116 +82,78 @@ navigation: ### `hidden` -The `hidden` option allows you to control if the page appears in the navigation menu. You can also use `visible` instead of `hidden`, but obviously invert the value. +Determines if the page appears in the navigation menu. ```yaml navigation: hidden: true ``` +**Tip:** You can also use `visible: false` to achieve the same effect. + ### `group` -The `group` option has a slightly different meaning depending on the type of navigation menu. -For the primary navigation menu, it allows you to group pages together in dropdowns. -For the sidebar, it allows you to group pages together in the sidebar under a common heading. -You can also use `category` instead of `group`. +For the primary navigation menu, this groups pages together in dropdowns. For the sidebar, it groups pages under a common heading. ```yaml navigation: group: "My Group" ``` +**Note:** Sidebar group keys are normalized, so `My Group` and `my-group` are equivalent. + ## Config File Configuration -Next up, let's look at how to customize the navigation menus using the config files. +Let's explore how to customize the main navigation menu using configuration files: + +- For the main navigation menu, use the `navigation` setting in the `hyde.php` config file. -- To customize the navigation menu, use the setting `navigation` in the `hyde.php` config. -- When customizing the navigation menu, you should use the [route key](core-concepts#route-keys) of the page. +When customizing the main navigation menu, use the [route key](core-concepts#route-keys) of the page. -- To customize the sidebar, use the setting `sidebar` in the `docs.php` config. -- When customizing the sidebar, can use the route key, or just the [page identifier](core-concepts#page-identifiers) of the page. +### Changing Priorities -### Changing the priorities +The `navigation.order` setting allows you to customize the order of pages in the main navigation menu. -The `navigation.order` and `sidebar.order` settings allow you to customize the order of the pages in the navigation menus. +#### Basic Priority Syntax -#### Basic syntax for changing the priorities +A nice and simple way to define the order of pages is to add their route keys as a simple list array. We'll then match that array order. -The cleanest way is to use the list-style syntax where each item will get the priority calculated according to its position in the list, plus an offset of `500`. -The offset is added to make it easier to place pages earlier in the list using front matter or with explicit priority settings. +It may be useful to know that we internally will assign a priority calculated according to its position in the list, plus an offset of `500`. The offset is added to make it easier to place pages earlier in the list using front matter or with explicit priority settings. ```php // filepath: config/hyde.php - 'navigation' => [ 'order' => [ - 'home', // Gets priority 500 - 'about', // Gets priority 501 - 'contact', // Gets priority 502 - ] -] -``` - -```php -// filepath: config/docs.php - -'sidebar' => [ - 'order' => [ - 'readme', // Gets priority 500 - 'installation', // Gets priority 501 - 'getting-started', // Gets priority 502 + 'home', // Priority: 500 + 'about', // Priority: 501 + 'contact', // Priority: 502 ] ] ``` -#### Explicit syntax for changing the priorities +#### Explicit Priority Syntax -You can also specify explicit priorities by adding a value to the array key: +You can also specify explicit priorities by adding a value to the array keys. We'll then use these exact values as the priorities. ```php // filepath: config/hyde.php - 'navigation' => [ 'order' => [ - 'home' => 10, // Gets priority 10 - 'about' => 15, // Gets priority 15 - 'contact' => 20, // Gets priority 20 + 'home' => 10, + 'about' => 15, + 'contact' => 20, ] ] ``` -```php -// filepath: config/docs.php - -'sidebar' => [ - 'order' => [ - 'readme' => 10, // Gets priority 10 - 'installation' => 15, // Gets priority 15 - 'getting-started' => 20, // Gets priority 20 - ] -] -``` - -**You can of course also combine these methods if you want:** - -```php -// filepath: Applicable to both -[ - 'readme' => 10, // Gets priority 10 - 'installation', // Gets priority 500 - 'getting-started', // Gets priority 501 -] -``` - -### Changing the menu item labels +### Changing Menu Item Labels -Hyde makes a few attempts to find a suitable label for the navigation menu items to automatically create helpful titles. +Hyde makes a few attempts to find suitable labels for the navigation menu items to automatically create helpful titles. -From the Hyde config you can override the label of navigation links using the by mapping the route key to the desired title. -This is not yet supported for the sidebar, but will be in the future. +If you're not happy with these, it's easy to override navigation link labels by mapping the route key to the desired title in the Hyde config: ```php -// filepath config/hyde.php +// filepath: config/hyde.php 'navigation' => [ 'labels' => [ 'index' => 'Start', @@ -214,11 +164,12 @@ This is not yet supported for the sidebar, but will be in the future. ### Excluding Items (Blacklist) -Sometimes, especially if you have a lot of pages, you may want to prevent links from showing up in the main navigation menu. -To remove items from being automatically added, simply add the page's route key to the blacklist. +When you have many pages, it may be useful to prevent links from being added to the main navigation menu. + +To exclude items from being added, simply add the page's route key to the navigation blacklist in the Hyde config: ```php -// filepath config/hyde.php +// filepath: config/hyde.php 'navigation' => [ 'exclude' => [ '404' @@ -228,145 +179,109 @@ To remove items from being automatically added, simply add the page's route key ### Adding Custom Navigation Menu Links -You can easily add custom navigation menu links similar to how we add Authors. Simply add a `NavigationItem` model to the `navigation.custom` array. - -When linking to an external site, you should use the `NavigationItem::create()` method facade. The first two arguments are the -destination and label, both required. The third argument is the priority, which is optional, and defaults to `500`. +You can easily add custom navigation menu links in the Hyde config: ```php -// filepath config/hyde.php -use Hyde\Framework\Features\Navigation\NavigationItem; +// filepath: config/hyde.php +use Hyde\Facades\Navigation; 'navigation' => [ 'custom' => [ - NavigationItem::create('https://github.com/hydephp/hyde', 'GitHub', 200), + Navigation::item( + destination: 'https://github.com/hydephp/hyde', // Required + label: 'GitHub', // Optional, defaults to the destination value + priority: 200 // Optional, defaults to 500 + ), ] ] ``` -Simplified, this will then be rendered as follows: +**Tip:** While named arguments are used in the example for clarity, they are not required. -```html -<a href="https://github.com/hydephp/hyde">GitHub</a> -``` - -### Configure subdirectory handling +### Configure Subdirectory Display -Within the Hyde config you can configure how subdirectories should be displayed in the menu. +You can configure how subdirectories should be displayed in the menu: ```php -// filepath config/hyde.php +// filepath: config/hyde.php 'navigation' => [ - 'subdirectories' => 'dropdown' + 'subdirectory_display' => 'dropdown' ] ``` -Dropdown means that pages in subdirectories will be displayed in a dropdown menu, -while `flat` means that pages in subdirectories will be displayed as individual items in the menu. -Hidden means that pages in subdirectories will not be displayed in the menu at all. +**Supported Options:** +- `dropdown`: Pages in subdirectories are displayed in a dropdown menu +- `hidden`: Pages in subdirectories are not displayed at all in the menus +- `flat`: Pages in subdirectories are displayed as individual items -### Automatic menu groups +### Automatic Menu Groups -HydePHP has a neat feature to automatically place pages in dropdowns based on subdirectories. +A handy feature HydePHP has is that it can automatically place pages in dropdowns based on subdirectory structures. -#### Automatic navigation menu dropdowns +#### Automatic Navigation Menu Dropdowns -For pages that can be in the main site menu, this feature needs to be enabled in the `hyde.php` config file. +Enable this feature in the `hyde.php` config file by setting the `subdirectory_display` key to `dropdown`. ```php -// filepath config/hyde.php - +// filepath: config/hyde.php 'navigation' => [ - 'subdirectories' => 'dropdown', + 'subdirectory_display' => 'dropdown', ], ``` -Now if you create a page called `_pages/about/contact.md` it will automatically be placed in a dropdown called "About". - -#### Automatic documentation sidebar grouping +Now if you create a page called `_pages/about/contact.md`, it will automatically be placed in a dropdown called "About". -This feature works similarly to the automatic navigation menu dropdowns, but instead places the sidebar items in named groups. -This feature is enabled by default, so you only need to place your pages in subdirectories to have them grouped. +#### Dropdown Menu Notes -For example: `_docs/getting-started/installation.md` will be placed in a group called "Getting Started". - ->info Tip: When using subdirectory-based dropdowns, you can set their priority using the directory name as the array key. - -#### Dropdown menu notes - -Here are some things to keep in mind when using dropdown menus, regardless of the configuration: -- Dropdowns take priority over standard items. So if you have a dropdown with the key `about` and a page with the key `about`, the dropdown will be created, and the page won't be in the menu. - - For example: With this file structure: `_pages/foo.md`, `_pages/foo/bar.md`, `_pages/foo/baz.md`, the link to `foo` will be lost. +- Dropdowns take priority over standard items. If you have a dropdown with the key `about` and a page with the key `about`, the dropdown will be created, and the page won't be in the menu. +- Example: With this file structure: `_pages/foo.md`, `_pages/foo/bar.md`, `_pages/foo/baz.md`, the link to `foo` will be lost, so please keep this in mind when using this feature. ## Numerical Prefix Navigation Ordering -HydePHP v2 introduces a new feature that allows navigation items to be ordered based on a numerical prefix in the filename. -This is a great way to control the ordering of pages in both the primary navigation menu and the documentation sidebar, -as your file structure will match the order of the pages in the navigation menus. +HydePHP v2 introduces navigation item ordering based on numerical prefixes in filenames. This feature works for the primary navigation menu. -For example, the following will have the same order in the navigation menu as in a file explorer: +This has the great benefit of matching the navigation menu layout with the file structure view. It also works especially well with subdirectory-based navigation grouping. ```shell _pages/ - 01-home.md # Gets priority 1, putting it first (will be saved to _site/index.html) - 02-about.md # Gets priority 2, putting it second (will be saved to _site/about.html) - 03-contact.md # Gets priority 3, putting it third (will be saved to _site/contact.html) + 01-home.md # Priority: 1 (saved to _site/index.html) + 02-about.md # Priority: 2 (saved to _site/about.html) + 03-contact.md # Priority: 3 (saved to _site/contact.html) ``` -Hyde will then parse the number from the filename and use it as the priority for the page in the navigation menus. - -### Keep in mind +As you can see, Hyde parses the number from the filename and uses it as the priority for the page in navigation menus, while stripping the prefix from the route key. -Here are some things to keep in mind, especially if you mix numerical prefix ordering with other ordering methods: +### Important Notes -1. The numerical prefix will still be part of the page identifier, but it will be stripped from the route key. - - For example: `_pages/01-home.md` will have the route key `home` and the page identifier `01-home`. +1. The numerical prefix remains part of the page identifier but is stripped from the route key. + For example: `_pages/01-home.md` has route key `home` and page identifier `01-home`. 2. You can delimit the numerical prefix with either a dash or an underscore. - - For example: `_pages/01-home.md` and `_pages/01_home.md` are both valid. -3. The leading zeroes are optional, so `_pages/1-home.md` is also valid. + For example: Both `_pages/01-home.md` and `_pages/01_home.md` are valid. +3. Leading zeros are optional. `_pages/1-home.md` is equally valid. -### Using numerical prefix ordering in subdirectories +### Using Numerical Prefix Ordering in Subdirectories -The numerical prefix ordering feature works great when using the automatic subdirectory-based grouping for navigation menu dropdowns and documentation sidebar categories. +This feature integrates well with automatic subdirectory-based navigation grouping. Here are two useful tips: -This integration has two main features to consider: -1. You can use numerical prefixes in subdirectories to control the order of dropdowns. -2. The ordering within a subdirectory works independently of its siblings, so you can start from one in each subdirectory. - -Here is an example structure of how you may want to organize a documentation site: - -```shell -_docs/ - 01-getting-started/ - 01-installation.md - 02-requirements.md - 03-configuration.md - 02-usage/ - 01-quick-start.md - 02-advanced-usage.md - 03-features/ - 01-feature-1.md - 02-feature-2.md -``` +1. You can use numerical prefixes in subdirectories to control the dropdown order. +2. The numbering within a subdirectory works independently of its siblings, so you can start from one in each subdirectory. ### Customization -You can disable this feature by setting the `numerical_page_ordering` setting to `false` in the `hyde.php` config file. Hyde will then no longer extract the priority and will no longer strip the prefix from the route key. +If you're not interested in using numerical prefix ordering, you can disable it in the Hyde config file. Hyde will then no longer extract the priority and will no longer strip the prefix from the route key. ```php -// filepath config/hyde.php - +// filepath: config/hyde.php 'numerical_page_ordering' => false, ``` -While it's not recommended, as you lose out on the convenience of the automatic ordering, any front matter priority settings will override the numerical prefix ordering if you for some reason need to. - ## Digging Deeper Into the Internals -While not required to know, you may find it interesting to learn more about how the navigation is handled internally. Here is a high level overview, -but you can find more detailed information in the [Navigation API](navigation-api) documentation. +While not essential, understanding the internal workings of the navigation system can be as beneficial as it's interesting. Here's a quick high-level overview of the [Navigation API](navigation-api). + +### Navigation Menu Classes -The main navigation menu is the `MainNavigationMenu` class, and the documentation sidebar is the `DocumentationSidebar` class. Both extend the same base `NavigationMenu` class. +The main navigation menu uses the `MainNavigationMenu` class, while the documentation sidebar uses the `DocumentationSidebar` class. Both extend the base `NavigationMenu` class: ```php use Hyde\Framework\Features\Navigation\MainNavigationMenu; @@ -374,13 +289,9 @@ use Hyde\Framework\Features\Navigation\DocumentationSidebar; use Hyde\Framework\Features\Navigation\NavigationMenu; ``` -Within the base `NavigationMenu` class, you will find the main logic for how the menus are generated, -while the child implementations contain the extra logic tailored for their specific use cases. - -All the navigation menus store the menu items in their `$items` collection containing instances of the `NavigationItem` class. +The base `NavigationMenu` class contains the main logic for menu generation, while child implementations contain extra logic for specific use cases. -The `NavigationItem` class is a simple class that contains the label and URL of the menu item and is used to represent each item in the menu. -Dropdowns are represented by `NavigationGroup` instances, which extend the `NavigationMenu` class and contain a collection of additional `NavigationItem` instances. +All navigation menus store items in their `$items` collection, containing instances of the `NavigationItem` class. Dropdowns are represented by `NavigationGroup` instances, which extend the `NavigationMenu` class and contain additional `NavigationItem` instances: ```php use Hyde\Framework\Features\Navigation\NavigationItem; @@ -389,7 +300,6 @@ use Hyde\Framework\Features\Navigation\NavigationGroup; ## The Navigation API -If you want to interact with the site navigation programmatically, or if you want to create complex custom menus, you can do so through the new Navigation API. -For most cases you don't need this, as Hyde creates the navigation for you. But it can be useful for advanced users and package developers. +For advanced users and package developers, Hyde offers a Navigation API for programmatic interaction with site navigation. This API consists of a set of PHP classes allowing fluent interaction with navigation menus. -The Navigation API consists of a set of PHP classes, allowing you to fluently interact with the navigation menus. You can learn more about the API in the [Navigation API](navigation-api) documentation. +For more detailed information about the API, refer to the [Navigation API](navigation-api) documentation. diff --git a/packages/framework/config/hyde.php b/packages/framework/config/hyde.php index 0898d2a80f4..67c1da3b43b 100644 --- a/packages/framework/config/hyde.php +++ b/packages/framework/config/hyde.php @@ -25,6 +25,7 @@ use Hyde\Facades\Author; use Hyde\Facades\Meta; use Hyde\Enums\Feature; +use Hyde\Facades\Navigation; return [ @@ -331,40 +332,23 @@ | */ - 'navigation' => [ - // This configuration sets the priorities used to determine the order of the menu. - // The default values have been added below for reference and easy editing. - // The array key is the page's route key, the value is the priority. - // Lower values show up first in the menu. The default is 999. - 'order' => [ + 'navigation' => Navigation::configure() + ->setPagePriorities([ 'index' => 0, 'posts' => 10, 'docs/index' => 100, - ], - - // In case you want to customize the labels for the menu items, you can do so here. - // Simply add the route key as the array key, and the label as the value. - 'labels' => [ + ]) + ->setPageLabels([ 'index' => 'Home', 'docs/index' => 'Docs', - ], - - // These are the route keys of pages that should not show up in the navigation menu. - 'exclude' => [ + ]) + ->excludePages([ '404', - ], - - // Any extra links you want to add to the navigation menu can be added here. - // To get started quickly, you can uncomment the defaults here. - // See the documentation link above for more information. - 'custom' => [ - // NavigationItem::create('https://github.com/hydephp/hyde', 'GitHub', 200), - ], - - // How should pages in subdirectories be displayed in the menu? - // You can choose between 'dropdown', 'flat', and 'hidden'. - 'subdirectories' => 'hidden', - ], + ]) + ->addNavigationItems([ + // Navigation::item('https://github.com/hydephp/hyde', 'GitHub', 200), + ]) + ->setSubdirectoryDisplayMode('hidden'), /* |-------------------------------------------------------------------------- diff --git a/packages/framework/resources/views/components/navigation/navigation-link.blade.php b/packages/framework/resources/views/components/navigation/navigation-link.blade.php index 60ce617081d..5d2725b22e2 100644 --- a/packages/framework/resources/views/components/navigation/navigation-link.blade.php +++ b/packages/framework/resources/views/components/navigation/navigation-link.blade.php @@ -1,6 +1,6 @@ <a href="{{ $item }}" {{ $attributes->except('item')->class([ 'navigation-link block my-2 md:my-0 md:inline-block py-1 text-gray-700 hover:text-gray-900 dark:text-gray-100', 'navigation-link-active border-l-4 border-indigo-500 md:border-none font-medium -ml-6 pl-5 md:ml-0 md:pl-0 bg-gray-100 dark:bg-gray-800 md:bg-transparent dark:md:bg-transparent' => $item->isActive() -])->merge([ +])->merge($item->getExtraAttributes())->merge([ 'aria-current' => $item->isActive() ? 'page' : false, ]) }}>{{ $item->getLabel() }}</a> \ No newline at end of file diff --git a/packages/framework/src/Facades/Navigation.php b/packages/framework/src/Facades/Navigation.php new file mode 100644 index 00000000000..2eb846b0613 --- /dev/null +++ b/packages/framework/src/Facades/Navigation.php @@ -0,0 +1,45 @@ +<?php + +declare(strict_types=1); + +namespace Hyde\Facades; + +use Hyde\Framework\Features\Navigation\NavigationMenuConfigurationBuilder; + +use function compact; + +/** + * General facade for navigation features. + */ +class Navigation +{ + /** + * Configuration helper method to define a new navigation item, with better IDE support. + * + * The returned array will then be used by the framework to create a new NavigationItem instance. {@see \Hyde\Framework\Features\Navigation\NavigationItem} + * + * @see https://hydephp.com/docs/2.x/navigation-api + * + * @param string<\Hyde\Support\Models\RouteKey>|string $destination Route key, or an external URI. + * @param string|null $label If not provided, Hyde will try to get it from the route's connected page, or from the URL. + * @param int|null $priority If not provided, Hyde will try to get it from the route or the default priority of 500. + * @param array<string, scalar> $attributes Additional attributes for the navigation item. + * @return array{destination: string, label: ?string, priority: ?int, attributes: array<string, scalar>} + */ + public static function item(string $destination, ?string $label = null, ?int $priority = null, array $attributes = []): array + { + return compact('destination', 'label', 'priority', 'attributes'); + } + + /** + * Configuration helper method to define the navigation menu configuration with better IDE support. + * + * The builder is an array object that will be used by the framework to set the navigation menu configuration. + * + * @experimental This method is experimental and may change or be removed before the final release. + */ + public static function configure(): NavigationMenuConfigurationBuilder + { + return new NavigationMenuConfigurationBuilder(); + } +} diff --git a/packages/framework/src/Foundation/Internal/LoadYamlConfiguration.php b/packages/framework/src/Foundation/Internal/LoadYamlConfiguration.php index c574d6abfe9..cb4ddc0222d 100644 --- a/packages/framework/src/Foundation/Internal/LoadYamlConfiguration.php +++ b/packages/framework/src/Foundation/Internal/LoadYamlConfiguration.php @@ -4,13 +4,13 @@ namespace Hyde\Foundation\Internal; -use Throwable; use Illuminate\Support\Arr; use Hyde\Foundation\Application; use Illuminate\Config\Repository; use Hyde\Framework\Features\Blogging\Models\PostAuthor; use Hyde\Framework\Exceptions\InvalidConfigurationException; +use function tap; use function array_merge; /** @@ -67,14 +67,9 @@ protected function mergeConfiguration(string $namespace, array $yaml): void protected function parseAuthors(array $authors): array { return Arr::mapWithKeys($authors, function (array $author, string $username): array { - try { - return [$username => PostAuthor::create($author)]; - } catch (Throwable $exception) { - throw new InvalidConfigurationException( - 'Invalid author configuration detected in the YAML config file. Please double check the syntax.', - previous: $exception - ); - } + $message = 'Invalid author configuration detected in the YAML config file. Please double check the syntax.'; + + return InvalidConfigurationException::try(fn () => [$username => PostAuthor::create($author)], $message); }); } } diff --git a/packages/framework/src/Framework/Exceptions/InvalidConfigurationException.php b/packages/framework/src/Framework/Exceptions/InvalidConfigurationException.php index 694566ed717..306b6fd4a51 100644 --- a/packages/framework/src/Framework/Exceptions/InvalidConfigurationException.php +++ b/packages/framework/src/Framework/Exceptions/InvalidConfigurationException.php @@ -45,4 +45,18 @@ protected function findConfigLine(string $namespace, string $key): array return [$file, $line + 1]; } + + /** + * @internal + * + * @experimental + */ + public static function try(callable $callback, ?string $message = null): mixed + { + try { + return $callback(); + } catch (Throwable $exception) { + throw new static($message ?? $exception->getMessage(), previous: $exception); + } + } } diff --git a/packages/framework/src/Framework/Factories/NavigationDataFactory.php b/packages/framework/src/Framework/Factories/NavigationDataFactory.php index b7b331e6db6..61526679f33 100644 --- a/packages/framework/src/Framework/Factories/NavigationDataFactory.php +++ b/packages/framework/src/Framework/Factories/NavigationDataFactory.php @@ -274,7 +274,7 @@ private function getSubdirectoryName(): string protected function getSubdirectoryConfiguration(): string { - return Config::getString('hyde.navigation.subdirectories', 'hidden'); + return Config::getString('hyde.navigation.subdirectory_display', 'hidden'); } /** @param class-string<\Hyde\Pages\Concerns\HydePage> $class */ diff --git a/packages/framework/src/Framework/Features/Navigation/NavigationItem.php b/packages/framework/src/Framework/Features/Navigation/NavigationItem.php index 43d2cf5312b..a7fa7bb252c 100644 --- a/packages/framework/src/Framework/Features/Navigation/NavigationItem.php +++ b/packages/framework/src/Framework/Features/Navigation/NavigationItem.php @@ -23,16 +23,20 @@ class NavigationItem implements Stringable protected string $label; protected int $priority; + /** @var array<string, scalar> */ + protected array $attributes = []; + /** * Create a new navigation menu item, automatically filling in the properties from a Route instance if provided. * * @param \Hyde\Support\Models\Route|string<\Hyde\Support\Models\RouteKey>|string $destination Route instance or route key, or an external URI. * @param string|null $label If not provided, Hyde will try to get it from the route's connected page, or from the URL. * @param int|null $priority If not provided, Hyde will try to get it from the route or the default priority of 500. + * @param array<string, scalar> $attributes Additional attributes for the navigation item. */ - public function __construct(Route|string $destination, ?string $label = null, ?int $priority = null) + public function __construct(Route|string $destination, ?string $label = null, ?int $priority = null, array $attributes = []) { - [$this->destination, $this->label, $this->priority] = self::make($destination, $label, $priority); + [$this->destination, $this->label, $this->priority, $this->attributes] = self::make($destination, $label, $priority, $attributes); } /** @@ -41,10 +45,11 @@ public function __construct(Route|string $destination, ?string $label = null, ?i * @param \Hyde\Support\Models\Route|string<\Hyde\Support\Models\RouteKey>|string $destination Route instance or route key, or an external URI. * @param string|null $label If not provided, Hyde will try to get it from the route's connected page, or from the URL. * @param int|null $priority If not provided, Hyde will try to get it from the route or the default priority of 500. + * @param array<string, scalar> $attributes Additional attributes for the navigation item. */ - public static function create(Route|string $destination, ?string $label = null, ?int $priority = null): static + public static function create(Route|string $destination, ?string $label = null, ?int $priority = null, array $attributes = []): static { - return new static(...self::make($destination, $label, $priority)); + return new static(...self::make($destination, $label, $priority, $attributes)); } /** @@ -100,8 +105,8 @@ public function isActive(): bool return Hyde::currentRoute()?->getLink() === $this->getLink(); } - /** @return array{\Hyde\Support\Models\Route|string, string, int} */ - protected static function make(Route|string $destination, ?string $label = null, ?int $priority = null): array + /** @return array{\Hyde\Support\Models\Route|string, string, int, array<string, scalar>} */ + protected static function make(Route|string $destination, ?string $label = null, ?int $priority = null, array $attributes = []): array { // Automatically resolve the destination if it's a route key. if (is_string($destination) && Routes::has($destination)) { @@ -114,6 +119,12 @@ protected static function make(Route|string $destination, ?string $label = null, $priority ??= $destination->getPage()->navigationMenuPriority(); } - return [$destination, $label ?? $destination, $priority ?? NavigationMenu::DEFAULT]; + return [$destination, $label ?? $destination, $priority ?? NavigationMenu::DEFAULT, $attributes]; + } + + /** @return array<string, scalar> */ + public function getExtraAttributes(): array + { + return $this->attributes; } } diff --git a/packages/framework/src/Framework/Features/Navigation/NavigationMenuConfigurationBuilder.php b/packages/framework/src/Framework/Features/Navigation/NavigationMenuConfigurationBuilder.php new file mode 100644 index 00000000000..4f5d9957628 --- /dev/null +++ b/packages/framework/src/Framework/Features/Navigation/NavigationMenuConfigurationBuilder.php @@ -0,0 +1,128 @@ +<?php + +declare(strict_types=1); + +namespace Hyde\Framework\Features\Navigation; + +use TypeError; +use ArrayObject; +use Illuminate\Contracts\Support\Arrayable; + +/** + * Configuration helper class to define the navigation menu configuration with better IDE support. + * + * The configured data will then be used by the framework to set the navigation menu configuration. + * + * @see \Hyde\Facades\Navigation::configure() + * + * @experimental This class is experimental and may change or be removed before the final release. + */ +class NavigationMenuConfigurationBuilder extends ArrayObject implements Arrayable +{ + /** + * Set the order of the navigation items. + * + * Either define a map of route keys to priorities, or just a list of route keys and we'll try to match that order. + * + * @param array<string, int>|array<string> $order + * @return $this + */ + public function setPagePriorities(array $order): static + { + $this['order'] = $order; + + return $this; + } + + /** + * Set the labels for the navigation items. + * + * Each key should be a route key, and the value should be the label to display. + * + * @param array<string, string> $labels + * @return $this + */ + public function setPageLabels(array $labels): static + { + $this['labels'] = $labels; + + return $this; + } + + /** + * Exclude certain items from the navigation. + * + * Each item should be a route key for the page to exclude. + * + * @param array<string> $exclude + * @return $this + */ + public function excludePages(array $exclude): static + { + $this['exclude'] = $exclude; + + return $this; + } + + /** + * Add custom items to the navigation. + * + * @example `[Navigation::item('https://github.com/hydephp/hyde', 'GitHub', 200, ['target' => '_blank'])]` + * + * @param array<array{destination: string, label: ?string, priority: ?int, attributes: array<string, scalar>}> $custom + * @return $this + */ + public function addNavigationItems(array $custom): static + { + $this['custom'] = $custom; + + return $this; + } + + /** + * Set the display mode for pages in subdirectories. + * + * You can choose between 'dropdown', 'flat', and 'hidden'. The default is 'hidden'. + * + * @param 'dropdown'|'flat'|'hidden' $displayMode + * @return $this + */ + public function setSubdirectoryDisplayMode(string $displayMode): static + { + self::assertType(['dropdown', 'flat', 'hidden'], $displayMode); + + $this['subdirectory_display'] = $displayMode; + + return $this; + } + + /** + * Hide pages in subdirectories from the navigation. + * + * @experimental This method is experimental and may change or be removed before the final release. + * + * @return $this + */ + public function hideSubdirectoriesFromNavigation(): static + { + return $this->setSubdirectoryDisplayMode('hidden'); + } + + /** + * Get the instance as an array. + * + * @return array{order: array<string, int>, labels: array<string, string>, exclude: array<string>, custom: array<array{destination: string, label: ?string, priority: ?int, attributes: array<string, scalar>}>, subdirectory_display: string} + */ + public function toArray(): array + { + return $this->getArrayCopy(); + } + + /** @experimental May be moved to a separate helper class in the future. */ + protected static function assertType(array $types, string $value): void + { + if (! in_array($value, $types)) { + throw new TypeError('Value must be one of: '.implode(', ', $types)); + } + } +} diff --git a/packages/framework/src/Framework/Features/Navigation/NavigationMenuGenerator.php b/packages/framework/src/Framework/Features/Navigation/NavigationMenuGenerator.php index d2bd01921f8..02467e4c986 100644 --- a/packages/framework/src/Framework/Features/Navigation/NavigationMenuGenerator.php +++ b/packages/framework/src/Framework/Features/Navigation/NavigationMenuGenerator.php @@ -11,6 +11,7 @@ use Illuminate\Support\Collection; use Hyde\Foundation\Facades\Routes; use Hyde\Foundation\Kernel\RouteCollection; +use Hyde\Framework\Exceptions\InvalidConfigurationException; use function filled; use function assert; @@ -81,7 +82,11 @@ protected function generate(): void $this->items->push(NavigationItem::create(DocumentationPage::home())); } } else { - collect(Config::getArray('hyde.navigation.custom', []))->each(function (NavigationItem $item): void { + collect(Config::getArray('hyde.navigation.custom', []))->each(function (array $data): void { + /** @var array{destination: string, label: ?string, priority: ?int, attributes: array<string, scalar>} $data */ + $message = 'Invalid navigation item configuration detected the configuration file. Please double check the syntax.'; + $item = InvalidConfigurationException::try(fn () => NavigationItem::create(...$data), $message); + // Since these were added explicitly by the user, we can assume they should always be shown $this->items->push($item); }); @@ -96,7 +101,7 @@ protected function usesGroups(): bool return $this->routes->first(fn (Route $route): bool => filled($route->getPage()->navigationMenuGroup())) !== null; } else { - return Config::getString('hyde.navigation.subdirectories', 'hidden') === 'dropdown'; + return Config::getString('hyde.navigation.subdirectory_display', 'hidden') === 'dropdown'; } } @@ -189,6 +194,7 @@ protected function normalizeGroupLabel(string $label): string protected function searchForGroupLabelInConfig(string $groupKey): ?string { + // TODO: Normalize this: sidebar_group_labels -> docs.sidebar.labels return $this->getConfigArray($this->generatesSidebar ? 'docs.sidebar_group_labels' : 'hyde.navigation.labels')[$groupKey] ?? null; } diff --git a/packages/framework/tests/Feature/AutomaticNavigationConfigurationsTest.php b/packages/framework/tests/Feature/AutomaticNavigationConfigurationsTest.php index 1c3d0e3611d..4b48029f1bf 100644 --- a/packages/framework/tests/Feature/AutomaticNavigationConfigurationsTest.php +++ b/packages/framework/tests/Feature/AutomaticNavigationConfigurationsTest.php @@ -10,6 +10,7 @@ use Hyde\Pages\MarkdownPage; use Hyde\Pages\MarkdownPost; use Hyde\Pages\InMemoryPage; +use Hyde\Facades\Navigation; use Hyde\Foundation\HydeKernel; use JetBrains\PhpStorm\NoReturn; use Hyde\Pages\Concerns\HydePage; @@ -28,6 +29,7 @@ * * @see \Hyde\Framework\Testing\Unit\Views\NavigationHtmlLayoutsTest * + * @covers \Hyde\Facades\Navigation * @covers \Hyde\Framework\Factories\NavigationDataFactory * @covers \Hyde\Framework\Features\Navigation\NavigationMenuGenerator * @covers \Hyde\Framework\Features\Navigation\DocumentationSidebar @@ -411,7 +413,7 @@ public function testMainNavigationDropdownLabelsCanBeSetInConfig() public function testMainNavigationAutomaticSubdirectoryDropdownLabelsCanBeSetInConfig() { - config(['hyde.navigation.subdirectories' => 'dropdown']); + config(['hyde.navigation.subdirectory_display' => 'dropdown']); config(['hyde.navigation.labels' => ['hello' => 'World']]); $this->assertMenuEquals(['World'], [ @@ -443,7 +445,7 @@ public function testPagesInSubdirectoriesAreNotAddedToNavigation() public function testPagesInSubdirectoriesAreAddedToNavigationWhenNavigationSubdirectoriesIsSetToFlat() { - config(['hyde.navigation.subdirectories' => 'flat']); + config(['hyde.navigation.subdirectory_display' => 'flat']); $this->assertMenuEquals(['Foo', 'Bar', 'Baz'], [ new MarkdownPage('about/foo'), @@ -454,7 +456,7 @@ public function testPagesInSubdirectoriesAreAddedToNavigationWhenNavigationSubdi public function testPagesInSubdirectoriesAreAddedAsDropdownsWhenNavigationSubdirectoriesIsSetToDropdown() { - config(['hyde.navigation.subdirectories' => 'dropdown']); + config(['hyde.navigation.subdirectory_display' => 'dropdown']); $this->assertMenuEquals([ ['label' => 'About', 'children' => ['Foo', 'Bar', 'Baz']], @@ -501,7 +503,7 @@ public function testMainNavigationMenuItemsWithSameLabelButDifferentGroupsAreNot public function testMainNavigationMenuDropdownItemsWithSameLabelButDifferentGroupsAreNotFiltered() { - config(['hyde.navigation.subdirectories' => 'dropdown']); + config(['hyde.navigation.subdirectory_display' => 'dropdown']); $this->assertMenuEquals([ ['label' => 'One', 'children' => ['Foo']], @@ -514,7 +516,7 @@ public function testMainNavigationMenuDropdownItemsWithSameLabelButDifferentGrou public function testMainNavigationMenuAutomaticDropdownItemsWithSameLabelButDifferentGroupsAreNotFiltered() { - config(['hyde.navigation.subdirectories' => 'dropdown']); + config(['hyde.navigation.subdirectory_display' => 'dropdown']); $this->assertMenuEquals([ ['label' => 'One', 'children' => ['Foo']], @@ -531,7 +533,7 @@ public function testConflictingSubdirectoryAndFrontMatterDropdownConfigurationGi // we run into a conflicting state where we don't know what the user intended. We solve this by giving // precedence to the subdirectory configuration. This is opinionated, but allows for good grouping. - config(['hyde.navigation.subdirectories' => 'dropdown']); + config(['hyde.navigation.subdirectory_display' => 'dropdown']); $this->assertMenuEquals([ ['label' => 'Foo', 'children' => ['Child']], @@ -542,7 +544,7 @@ public function testConflictingSubdirectoryAndFrontMatterDropdownConfigurationGi public function testCanMixSubdirectoryDropdownsWithFrontMatterDropdowns() { - config(['hyde.navigation.subdirectories' => 'dropdown']); + config(['hyde.navigation.subdirectory_display' => 'dropdown']); $this->assertMenuEquals([ ['label' => 'Foo', 'children' => ['Bar', 'Baz']], @@ -554,7 +556,7 @@ public function testCanMixSubdirectoryDropdownsWithFrontMatterDropdowns() public function testMainMenuAutomaticDropdownLabelsCanBeSetInConfig() { - config(['hyde.navigation.subdirectories' => 'dropdown']); + config(['hyde.navigation.subdirectory_display' => 'dropdown']); config(['hyde.navigation.labels' => ['foo' => 'Bar']]); $this->assertMenuEquals([ @@ -1109,7 +1111,7 @@ public function testSidebarCanMixSubdirectoryGroupsWithFrontMatterGroups() public function testMainNavigationDropdownPriorityCanBeSetInConfig() { - config(['hyde.navigation.subdirectories' => 'dropdown']); + config(['hyde.navigation.subdirectory_display' => 'dropdown']); config(['hyde.navigation.order' => ['foo' => 500]]); $this->assertMenuEquals( @@ -1120,7 +1122,7 @@ public function testMainNavigationDropdownPriorityCanBeSetInConfig() public function testMainNavigationDropdownPriorityCanBeSetInConfigUsingDifferingCases() { - config(['hyde.navigation.subdirectories' => 'dropdown']); + config(['hyde.navigation.subdirectory_display' => 'dropdown']); config(['hyde.navigation.order' => ['hello-world' => 500]]); $expected = [['label' => 'Hello World', 'priority' => 500]]; @@ -1175,7 +1177,7 @@ public function testMainMenuNavigationItemCasingUsingFrontMatter() public function testMainMenuNavigationGroupCasing() { - config(['hyde.navigation.subdirectories' => 'dropdown']); + config(['hyde.navigation.subdirectory_display' => 'dropdown']); // When using subdirectory groupings, we try to format them the same way as the page titles @@ -1233,6 +1235,109 @@ public function testSidebarGroupCasingUsingFrontMatter() $this->assertSidebarEquals(['Hello World'], [new DocumentationPage('foo', ['navigation.group' => 'hello world'])]); } + // Configuration tests + + public function testCanConfigureMainMenuUsingArraySettings() + { + $config = [ + 'navigation' => [ + 'order' => [ + 'foo' => 3, + 'bar' => 2, + 'baz' => 1, + ], + + 'labels' => [ + 'foo' => 'Foo Page', + 'bar' => 'Bar Page', + 'baz' => 'Baz Page', + 'dropdown/item' => 'Dropdown Item Page', + ], + + 'exclude' => [ + 'qux', + ], + + 'custom' => [ + [ + 'label' => 'Custom', + 'destination' => 'https://example.com', + 'priority' => 120, + 'attributes' => [ + 'target' => '_blank', + ], + ], + ], + + 'subdirectory_display' => 'flat', + ], + ]; + + config(['hyde' => $config]); + + $this->assertMenuEquals([ + ['label' => 'Baz Page', 'priority' => 1], + ['label' => 'Bar Page', 'priority' => 2], + ['label' => 'Foo Page', 'priority' => 3], + ['label' => 'Custom', 'priority' => 120, 'attributes' => ['target' => '_blank']], + ['label' => 'Dropdown Item Page', 'priority' => 999], + ], [ + new MarkdownPage('foo'), + new MarkdownPage('bar'), + new MarkdownPage('baz'), + new MarkdownPage('qux'), + new MarkdownPage('dropdown/item'), + ]); + } + + public function testCanConfigureMainMenuUsingBuilderSettings() + { + $config = [ + 'navigation' => Navigation::configure() + ->setPagePriorities([ + 'foo' => 3, + 'bar' => 2, + 'baz' => 1, + ]) + ->setPageLabels([ + 'foo' => 'Foo Page', + 'bar' => 'Bar Page', + 'baz' => 'Baz Page', + 'dropdown/item' => 'Dropdown Item Page', + ]) + ->excludePages([ + 'qux', + ]) + ->addNavigationItems([ + [ + 'label' => 'Custom', + 'destination' => 'https://example.com', + 'priority' => 120, + 'attributes' => [ + 'target' => '_blank', + ], + ], + ]) + ->setSubdirectoryDisplayMode('flat'), + ]; + + config(['hyde' => $config]); + + $this->assertMenuEquals([ + ['label' => 'Baz Page', 'priority' => 1], + ['label' => 'Bar Page', 'priority' => 2], + ['label' => 'Foo Page', 'priority' => 3], + ['label' => 'Custom', 'priority' => 120, 'attributes' => ['target' => '_blank']], + ['label' => 'Dropdown Item Page', 'priority' => 999], + ], [ + new MarkdownPage('foo'), + new MarkdownPage('bar'), + new MarkdownPage('baz'), + new MarkdownPage('qux'), + new MarkdownPage('dropdown/item'), + ]); + } + // Testing helpers protected function assertSidebarEquals(array $expected, array $menuPages): AssertableNavigationMenu diff --git a/packages/framework/tests/Feature/HydePageTest.php b/packages/framework/tests/Feature/HydePageTest.php index c6adb02918f..d9ca0150311 100644 --- a/packages/framework/tests/Feature/HydePageTest.php +++ b/packages/framework/tests/Feature/HydePageTest.php @@ -1176,7 +1176,7 @@ public function testNavigationDataFactoryHidesPageFromNavigationWhenInASubdirect public function testNavigationDataFactoryHidesPageFromNavigationWhenInAAndConfigIsSetToHidden() { - config(['hyde.navigation.subdirectories' => 'hidden']); + config(['hyde.navigation.subdirectory_display' => 'hidden']); $page = MarkdownPage::make('foo/bar'); @@ -1186,7 +1186,7 @@ public function testNavigationDataFactoryHidesPageFromNavigationWhenInAAndConfig public function testNavigationDataFactoryDoesNotHidePageFromNavigationWhenInASubdirectoryAndAllowedInConfiguration() { - config(['hyde.navigation.subdirectories' => 'flat']); + config(['hyde.navigation.subdirectory_display' => 'flat']); $page = MarkdownPage::make('foo/bar'); @@ -1196,7 +1196,7 @@ public function testNavigationDataFactoryDoesNotHidePageFromNavigationWhenInASub public function testNavigationDataFactoryAllowsShowInNavigationAndSetsGroupWhenDropdownIsSelectedInConfig() { - config(['hyde.navigation.subdirectories' => 'dropdown']); + config(['hyde.navigation.subdirectory_display' => 'dropdown']); $page = MarkdownPage::make('foo/bar'); diff --git a/packages/framework/tests/Feature/NavigationMenuTest.php b/packages/framework/tests/Feature/NavigationMenuTest.php index 7f6cb8b3ac7..0b48b00c05d 100644 --- a/packages/framework/tests/Feature/NavigationMenuTest.php +++ b/packages/framework/tests/Feature/NavigationMenuTest.php @@ -5,6 +5,7 @@ namespace Hyde\Framework\Testing\Feature; use Hyde\Hyde; +use Hyde\Facades\Navigation; use Hyde\Support\Models\Route; use Hyde\Foundation\Facades\Routes; use Hyde\Framework\Features\Navigation\NavigationGroup; @@ -13,6 +14,7 @@ use Hyde\Pages\MarkdownPage; use Hyde\Testing\TestCase; use Illuminate\Support\Collection; +use Hyde\Framework\Exceptions\InvalidConfigurationException; use Hyde\Framework\Features\Navigation\NavigationMenuGenerator; /** @@ -91,9 +93,39 @@ public function testIsSortedAutomaticallyWhenUsingNavigationMenuCreate() $this->assertEquals($expected, $menu->getItems()); } + public function testCanAddCustomLinksInConfig() + { + config(['hyde.navigation.custom' => [Navigation::item('foo', 'Foo')]]); + + $menu = $this->createNavigationMenu(); + + $expected = collect([ + NavigationItem::create(Routes::get('index')), + NavigationItem::create('foo', 'Foo'), + ]); + + $this->assertCount(count($expected), $menu->getItems()); + $this->assertEquals($expected, $menu->getItems()); + } + + public function testCanAddCustomLinksInConfigAsArray() + { + config(['hyde.navigation.custom' => [['destination' => 'foo', 'label' => 'Foo']]]); + + $menu = $this->createNavigationMenu(); + + $expected = collect([ + NavigationItem::create(Routes::get('index')), + NavigationItem::create('foo', 'Foo'), + ]); + + $this->assertCount(count($expected), $menu->getItems()); + $this->assertEquals($expected, $menu->getItems()); + } + public function testExternalLinkCanBeAddedInConfig() { - config(['hyde.navigation.custom' => [NavigationItem::create('https://example.com', 'foo')]]); + config(['hyde.navigation.custom' => [Navigation::item('https://example.com', 'foo')]]); $menu = $this->createNavigationMenu(); @@ -108,7 +140,7 @@ public function testExternalLinkCanBeAddedInConfig() public function testPathLinkCanBeAddedInConfig() { - config(['hyde.navigation.custom' => [NavigationItem::create('foo', 'foo')]]); + config(['hyde.navigation.custom' => [Navigation::item('foo', 'foo')]]); $menu = $this->createNavigationMenu(); @@ -121,11 +153,26 @@ public function testPathLinkCanBeAddedInConfig() $this->assertEquals($expected, $menu->getItems()); } + public function testCanAddCustomLinksInConfigWithExtraAttributes() + { + config(['hyde.navigation.custom' => [Navigation::item('foo', 'Foo', 100, ['class' => 'foo'])]]); + + $menu = $this->createNavigationMenu(); + + $expected = collect([ + NavigationItem::create(Routes::get('index')), + NavigationItem::create('foo', 'Foo', 100, ['class' => 'foo']), + ]); + + $this->assertCount(count($expected), $menu->getItems()); + $this->assertEquals($expected, $menu->getItems()); + } + public function testDuplicatesAreNotRemovedWhenAddingInConfig() { config(['hyde.navigation.custom' => [ - NavigationItem::create('foo', 'foo'), - NavigationItem::create('foo', 'foo'), + Navigation::item('foo', 'foo'), + Navigation::item('foo', 'foo'), ]]); $menu = $this->createNavigationMenu(); @@ -143,8 +190,8 @@ public function testDuplicatesAreNotRemovedWhenAddingInConfig() public function testDuplicatesAreNotRemovedWhenAddingInConfigRegardlessOfDestination() { config(['hyde.navigation.custom' => [ - NavigationItem::create('foo', 'foo'), - NavigationItem::create('bar', 'foo'), + Navigation::item('foo', 'foo'), + Navigation::item('bar', 'foo'), ]]); $menu = $this->createNavigationMenu(); @@ -163,7 +210,7 @@ public function testConfigItemsTakePrecedenceOverGeneratedItems() { $this->file('_pages/foo.md'); - config(['hyde.navigation.custom' => [NavigationItem::create('bar', 'Foo')]]); + config(['hyde.navigation.custom' => [Navigation::item('bar', 'Foo')]]); $menu = $this->createNavigationMenu(); @@ -177,6 +224,18 @@ public function testConfigItemsTakePrecedenceOverGeneratedItems() $this->assertEquals($expected, $menu->getItems()); } + public function testInvalidCustomNavigationConfigurationThrowsException() + { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('Invalid navigation item configuration detected the configuration file. Please double check the syntax.'); + + config(['hyde.navigation.custom' => [ + ['invalid_key' => 'value'], + ]]); + + $this->createNavigationMenu(); + } + public function testDocumentationPagesThatAreNotIndexAreNotAddedToTheMenu() { $this->file('_docs/foo.md'); @@ -208,7 +267,7 @@ public function testPagesInSubdirectoriesAreNotAddedToTheNavigationMenu() public function testPagesInSubdirectoriesCanBeAddedToTheNavigationMenuWithConfigFlatSetting() { - config(['hyde.navigation.subdirectories' => 'flat']); + config(['hyde.navigation.subdirectory_display' => 'flat']); $this->directory('_pages/foo'); $this->file('_pages/foo/bar.md'); @@ -225,7 +284,7 @@ public function testPagesInSubdirectoriesCanBeAddedToTheNavigationMenuWithConfig public function testPagesInSubdirectoriesAreNotAddedToTheNavigationMenuWithConfigDropdownSetting() { - config(['hyde.navigation.subdirectories' => 'dropdown']); + config(['hyde.navigation.subdirectory_display' => 'dropdown']); $this->directory('_pages/foo'); $this->file('_pages/foo/bar.md'); @@ -244,7 +303,7 @@ public function testPagesInSubdirectoriesAreNotAddedToTheNavigationMenuWithConfi public function testPagesInDropdownsDoNotGetAddedToTheMainNavigation() { - config(['hyde.navigation.subdirectories' => 'dropdown']); + config(['hyde.navigation.subdirectory_display' => 'dropdown']); Routes::push((new MarkdownPage('foo'))->getRoute()); Routes::push((new MarkdownPage('bar/baz'))->getRoute()); diff --git a/packages/framework/tests/Feature/NumericalPageOrderingHelperTest.php b/packages/framework/tests/Feature/NumericalPageOrderingHelperTest.php index b5b54025ac0..82137389905 100644 --- a/packages/framework/tests/Feature/NumericalPageOrderingHelperTest.php +++ b/packages/framework/tests/Feature/NumericalPageOrderingHelperTest.php @@ -38,7 +38,7 @@ protected function setUp(): void $this->helper = new FilenamePrefixNavigationPriorityTestingHelper($this); - Config::set('hyde.navigation.subdirectories', 'dropdown'); + Config::set('hyde.navigation.subdirectory_display', 'dropdown'); // Todo: Replace kernel with mock class $this->withoutDefaultPages(); diff --git a/packages/framework/tests/Feature/YamlConfigurationFeatureTest.php b/packages/framework/tests/Feature/YamlConfigurationFeatureTest.php index efb6baeb5c6..4bd0a733680 100644 --- a/packages/framework/tests/Feature/YamlConfigurationFeatureTest.php +++ b/packages/framework/tests/Feature/YamlConfigurationFeatureTest.php @@ -4,10 +4,15 @@ namespace Hyde\Framework\Testing\Feature; +use Hyde\Hyde; use Hyde\Testing\TestCase; use Illuminate\Support\Env; +use Hyde\Pages\MarkdownPage; +use Hyde\Framework\Features\Navigation\NavigationItem; use Hyde\Framework\Features\Blogging\Models\PostAuthor; +use Hyde\Framework\Features\Navigation\MainNavigationMenu; use Hyde\Framework\Exceptions\InvalidConfigurationException; +use Hyde\Framework\Features\Navigation\NavigationMenuGenerator; /** * Test the Yaml configuration feature. @@ -20,6 +25,16 @@ */ class YamlConfigurationFeatureTest extends TestCase { + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + + if (file_exists('hyde.yml')) { + // Clean up if a test failed to clean up after itself. + unlink('hyde.yml'); + } + } + protected function tearDown(): void { $this->clearEnvVars(); @@ -441,6 +456,8 @@ public function testCanSetAuthorsInTheYamlConfig() public function testTypeErrorsInAuthorsYamlConfigAreRethrownMoreHelpfully() { + $exceptionThrown = false; + file_put_contents('hyde.yml', <<<'YAML' authors: wrong: @@ -450,9 +467,262 @@ public function testTypeErrorsInAuthorsYamlConfigAreRethrownMoreHelpfully() try { $this->runBootstrappers(); } catch (InvalidConfigurationException $exception) { + $exceptionThrown = true; $this->assertSame('Invalid author configuration detected in the YAML config file. Please double check the syntax.', $exception->getMessage()); } + $this->assertTrue($exceptionThrown, 'Failed asserting that the exception was thrown.'); + unlink('hyde.yml'); + } + + public function testCanSetCustomNavigationItemsInTheYamlConfig() + { + $this->file('hyde.yml', <<<'YAML' + hyde: + navigation: + custom: + - destination: 'https://example.com' + label: 'Example' + priority: 100 + - destination: 'about' + label: 'About Us' + priority: 200 + - destination: 'contact' + label: 'Contact' + priority: 300 + YAML); + + $this->runBootstrappers(); + + $configItems = config('hyde.navigation.custom'); + + $this->assertSame([ + [ + 'destination' => 'https://example.com', + 'label' => 'Example', + 'priority' => 100, + ], [ + 'destination' => 'about', + 'label' => 'About Us', + 'priority' => 200, + ], [ + 'destination' => 'contact', + 'label' => 'Contact', + 'priority' => 300, + ], + ], $configItems); + + /** @var NavigationItem[] $navigationItems */ + $navigationItems = NavigationMenuGenerator::handle(MainNavigationMenu::class)->getItems()->all(); + + $this->assertCount(4, $navigationItems); + $this->assertContainsOnlyInstancesOf(NavigationItem::class, $navigationItems); + + $this->assertSame('index.html', $navigationItems[0]->getLink()); + $this->assertSame('Home', $navigationItems[0]->getLabel()); + $this->assertSame(0, $navigationItems[0]->getPriority()); + + $this->assertSame('https://example.com', $navigationItems[1]->getLink()); + $this->assertSame('Example', $navigationItems[1]->getLabel()); + $this->assertSame(100, $navigationItems[1]->getPriority()); + + $this->assertSame('about', $navigationItems[2]->getLink()); + $this->assertSame('About Us', $navigationItems[2]->getLabel()); + $this->assertSame(200, $navigationItems[2]->getPriority()); + + $this->assertSame('contact', $navigationItems[3]->getLink()); + $this->assertSame('Contact', $navigationItems[3]->getLabel()); + $this->assertSame(300, $navigationItems[3]->getPriority()); + } + + public function testCanSetAttributesInNavigationItemsInTheYamlConfig() + { + $this->file('hyde.yml', <<<'YAML' + hyde: + navigation: + custom: + - destination: 'https://example.com' + label: 'Example' + priority: 100 + attributes: + class: 'example' + - destination: 'about' + label: 'About Us' + priority: 200 + attributes: + class: 'about' + id: 'about' + - destination: 'contact' + label: 'Contact' + priority: 300 + attributes: + target: '_blank' + rel: 'noopener noreferrer' + foo: 'bar' + YAML); + + $this->runBootstrappers(); + + $configItems = config('hyde.navigation.custom'); + + $this->assertSame([ + [ + 'destination' => 'https://example.com', + 'label' => 'Example', + 'priority' => 100, + 'attributes' => ['class' => 'example'], + ], [ + 'destination' => 'about', + 'label' => 'About Us', + 'priority' => 200, + 'attributes' => ['class' => 'about', 'id' => 'about'], + ], [ + 'destination' => 'contact', + 'label' => 'Contact', + 'priority' => 300, + 'attributes' => ['target' => '_blank', 'rel' => 'noopener noreferrer', 'foo' => 'bar'], + ], + ], $configItems); + + /** @var NavigationItem[] $navigationItems */ + $navigationItems = NavigationMenuGenerator::handle(MainNavigationMenu::class)->getItems()->all(); + + $this->assertCount(4, $navigationItems); + $this->assertContainsOnlyInstancesOf(NavigationItem::class, $navigationItems); + + $this->assertSame([], $navigationItems[0]->getExtraAttributes()); + $this->assertSame(['class' => 'example'], $navigationItems[1]->getExtraAttributes()); + $this->assertSame(['class' => 'about', 'id' => 'about'], $navigationItems[2]->getExtraAttributes()); + $this->assertSame(['target' => '_blank', 'rel' => 'noopener noreferrer', 'foo' => 'bar'], $navigationItems[3]->getExtraAttributes()); + } + + public function testOnlyNeedToAddDestinationToYamlConfiguredNavigationItems() + { + $this->file('hyde.yml', <<<'YAML' + hyde: + navigation: + custom: + - destination: 'about.html' + YAML); + + $this->runBootstrappers(); + + $configItems = config('hyde.navigation.custom'); + + $this->assertSame([ + [ + 'destination' => 'about.html', + ], + ], $configItems); + + /** @var NavigationItem[] $navigationItems */ + $navigationItems = NavigationMenuGenerator::handle(MainNavigationMenu::class)->getItems()->all(); + + $this->assertCount(2, $navigationItems); + $this->assertContainsOnlyInstancesOf(NavigationItem::class, $navigationItems); + + $this->assertSame('index.html', $navigationItems[0]->getLink()); + $this->assertSame('Home', $navigationItems[0]->getLabel()); + $this->assertSame(0, $navigationItems[0]->getPriority()); + + $this->assertSame('about.html', $navigationItems[1]->getLink()); + $this->assertSame('about.html', $navigationItems[1]->getLabel()); // The label is automatically set to the destination. + $this->assertSame(500, $navigationItems[1]->getPriority()); + } + + public function testNavigationItemsInTheYamlConfigCanBeResolvedToRoutes() + { + $this->file('hyde.yml', <<<'YAML' + hyde: + navigation: + custom: + - destination: 'about' + YAML); + + $this->runBootstrappers(); + + Hyde::routes()->addRoute((new MarkdownPage('about', ['title' => 'About Us', 'navigation' => ['priority' => 250]]))->getRoute()); + + $navigationItems = NavigationMenuGenerator::handle(MainNavigationMenu::class)->getItems()->all(); + + // The route is already automatically added to the navigation menu, so we'll have two of it. + $this->assertCount(3, $navigationItems); + $this->assertContainsOnlyInstancesOf(NavigationItem::class, $navigationItems); + + $this->assertEquals($navigationItems[1], $navigationItems[2]); + + $this->assertSame('about.html', $navigationItems[1]->getLink()); + $this->assertSame('About Us', $navigationItems[1]->getLabel()); + $this->assertSame(250, $navigationItems[1]->getPriority()); + } + + public function testTypeErrorsInNavigationYamlConfigAreRethrownMoreHelpfully() + { + $exceptionThrown = false; + + file_put_contents('hyde.yml', <<<'YAML' + hyde: + navigation: + custom: + - destination: false + YAML); + + try { + $this->runBootstrappers(); + NavigationMenuGenerator::handle(MainNavigationMenu::class)->getItems()->all(); + } catch (InvalidConfigurationException $exception) { + $exceptionThrown = true; + $this->assertSame('Invalid navigation item configuration detected the configuration file. Please double check the syntax.', $exception->getMessage()); + } + + $this->assertTrue($exceptionThrown, 'Failed asserting that the exception was thrown.'); + unlink('hyde.yml'); + } + + public function testMustAddDestinationToYamlConfiguredNavigationItems() + { + $exceptionThrown = false; + + file_put_contents('hyde.yml', <<<'YAML' + hyde: + navigation: + custom: + - label: 'About Us' + YAML); + + try { + $this->runBootstrappers(); + NavigationMenuGenerator::handle(MainNavigationMenu::class)->getItems()->all(); + } catch (InvalidConfigurationException $exception) { + $exceptionThrown = true; + $this->assertSame('Invalid navigation item configuration detected the configuration file. Please double check the syntax.', $exception->getMessage()); + } + + $this->assertTrue($exceptionThrown, 'Failed asserting that the exception was thrown.'); + unlink('hyde.yml'); + } + + public function testAddingExtraYamlNavigationItemFieldsThrowsAnException() + { + $exceptionThrown = false; + + file_put_contents('hyde.yml', <<<'YAML' + hyde: + navigation: + custom: + - destination: 'about' + extra: 'field' + YAML); + + try { + $this->runBootstrappers(); + NavigationMenuGenerator::handle(MainNavigationMenu::class)->getItems()->all(); + } catch (InvalidConfigurationException $exception) { + $exceptionThrown = true; + $this->assertSame('Invalid navigation item configuration detected the configuration file. Please double check the syntax.', $exception->getMessage()); + } + + $this->assertTrue($exceptionThrown, 'Failed asserting that the exception was thrown.'); unlink('hyde.yml'); } diff --git a/packages/framework/tests/Unit/CustomExceptionsTest.php b/packages/framework/tests/Unit/CustomExceptionsTest.php index 6009a31bc4e..af058ac7390 100644 --- a/packages/framework/tests/Unit/CustomExceptionsTest.php +++ b/packages/framework/tests/Unit/CustomExceptionsTest.php @@ -210,4 +210,45 @@ public function testInvalidConfigurationExceptionWithPreviousThrowable() $this->assertSame('Invalid configuration.', $exception->getMessage()); $this->assertSame($previous, $exception->getPrevious()); } + + public function testInvalidConfigurationExceptionTryMethodWithSuccessfulCallback() + { + $result = InvalidConfigurationException::try(function () { + return 'success'; + }); + + $this->assertSame('success', $result); + } + + public function testInvalidConfigurationExceptionTryMethodWithThrowingCallback() + { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('Custom error message'); + + InvalidConfigurationException::try(function () { + throw new RuntimeException('Original error'); + }, 'Custom error message'); + } + + public function testInvalidConfigurationExceptionTryMethodWithDefaultMessage() + { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('Original error'); + + InvalidConfigurationException::try(function () { + throw new RuntimeException('Original error'); + }); + } + + public function testInvalidConfigurationExceptionTryMethodPreservesPreviousException() + { + try { + InvalidConfigurationException::try(function () { + throw new RuntimeException('Original error'); + }, 'Custom error message'); + } catch (InvalidConfigurationException $e) { + $this->assertInstanceOf(RuntimeException::class, $e->getPrevious()); + $this->assertSame('Original error', $e->getPrevious()->getMessage()); + } + } } diff --git a/packages/framework/tests/Unit/Facades/NavigationFacadeTest.php b/packages/framework/tests/Unit/Facades/NavigationFacadeTest.php new file mode 100644 index 00000000000..fa2350fda8f --- /dev/null +++ b/packages/framework/tests/Unit/Facades/NavigationFacadeTest.php @@ -0,0 +1,97 @@ +<?php + +declare(strict_types=1); + +namespace Hyde\Framework\Testing\Unit\Facades; + +use Hyde\Facades\Navigation; +use Hyde\Testing\UnitTestCase; +use Hyde\Framework\Features\Navigation\NavigationMenuConfigurationBuilder; + +/** + * @covers \Hyde\Facades\Navigation + */ +class NavigationFacadeTest extends UnitTestCase +{ + public function testItem() + { + $item = Navigation::item('home', 'Home', 100); + + $this->assertSame([ + 'destination' => 'home', + 'label' => 'Home', + 'priority' => 100, + 'attributes' => [], + ], $item); + } + + public function testItemWithOnlyDestination() + { + $item = Navigation::item('home'); + + $this->assertSame([ + 'destination' => 'home', + 'label' => null, + 'priority' => null, + 'attributes' => [], + ], $item); + } + + public function testItemWithUrl() + { + $item = Navigation::item('https://example.com', 'External', 200); + + $this->assertSame([ + 'destination' => 'https://example.com', + 'label' => 'External', + 'priority' => 200, + 'attributes' => [], + ], $item); + } + + public function testConfigure() + { + $builder = Navigation::configure(); + + $this->assertInstanceOf(NavigationMenuConfigurationBuilder::class, $builder); + } + + public function testConfigureWithChainedMethods() + { + $config = Navigation::configure() + ->setPagePriorities(['index' => 0, 'posts' => 10]) + ->setPageLabels(['index' => 'Home']) + ->excludePages(['404']) + ->addNavigationItems([Navigation::item('https://github.com', 'GitHub', 200)]) + ->setSubdirectoryDisplayMode('dropdown') + ->toArray(); + + $this->assertIsArray($config); + $this->assertArrayHasKey('order', $config); + $this->assertArrayHasKey('labels', $config); + $this->assertArrayHasKey('exclude', $config); + $this->assertArrayHasKey('custom', $config); + $this->assertArrayHasKey('subdirectory_display', $config); + + $this->assertSame(['index' => 0, 'posts' => 10], $config['order']); + $this->assertSame(['index' => 'Home'], $config['labels']); + $this->assertSame(['404'], $config['exclude']); + $this->assertSame([Navigation::item('https://github.com', 'GitHub', 200)], $config['custom']); + $this->assertSame('dropdown', $config['subdirectory_display']); + } + + public function testConfigureWithSomeChainedMethods() + { + $config = Navigation::configure() + ->setPagePriorities(['about' => 1, 'contact' => 2]) + ->setPageLabels(['about' => 'About Us']) + ->setSubdirectoryDisplayMode('flat') + ->toArray(); + + $this->assertSame(['about' => 1, 'contact' => 2], $config['order']); + $this->assertSame(['about' => 'About Us'], $config['labels']); + $this->assertSame('flat', $config['subdirectory_display']); + $this->assertArrayNotHasKey('exclude', $config); + $this->assertArrayNotHasKey('custom', $config); + } +} diff --git a/packages/framework/tests/Unit/NavigationDataFactoryUnitTest.php b/packages/framework/tests/Unit/NavigationDataFactoryUnitTest.php index eccd5604465..bdbca3f172e 100644 --- a/packages/framework/tests/Unit/NavigationDataFactoryUnitTest.php +++ b/packages/framework/tests/Unit/NavigationDataFactoryUnitTest.php @@ -248,7 +248,7 @@ public function testMakeGroupUsesFrontMatterGroupIfSet() public function testMakeGroupUsesFrontMatterGroupIfSetRegardlessOfSubdirectoryConfiguration() { - self::mockConfig(['hyde.navigation.subdirectories' => 'hidden']); + self::mockConfig(['hyde.navigation.subdirectory_display' => 'hidden']); $frontMatter = new FrontMatter(['navigation.group' => 'Test Group']); $coreDataObject = new CoreDataObject($frontMatter, new Markdown(), MarkdownPage::class, 'test.md', '', '', ''); @@ -259,7 +259,7 @@ public function testMakeGroupUsesFrontMatterGroupIfSetRegardlessOfSubdirectoryCo public function testMakeGroupDefaultsToNullIfFrontMatterGroupNotSetAndSubdirectoriesNotUsed() { - self::mockConfig(['hyde.navigation.subdirectories' => 'hidden']); + self::mockConfig(['hyde.navigation.subdirectory_display' => 'hidden']); $coreDataObject = new CoreDataObject(new FrontMatter(), new Markdown(), MarkdownPage::class, 'test.md', '', '', ''); $factory = new NavigationConfigTestClass($coreDataObject); diff --git a/packages/framework/tests/Unit/NavigationItemTest.php b/packages/framework/tests/Unit/NavigationItemTest.php index e0a669adf9a..22742802644 100644 --- a/packages/framework/tests/Unit/NavigationItemTest.php +++ b/packages/framework/tests/Unit/NavigationItemTest.php @@ -325,4 +325,16 @@ public function testIsCurrentIsNullSafe() { $this->assertFalse(NavigationItem::create('foo', 'bar')->isActive()); } + + public function testConstructWithAttributes() + { + $item = new NavigationItem('foo', 'Test', 500, ['class' => 'active']); + $this->assertSame(['class' => 'active'], $item->getExtraAttributes()); + } + + public function testCreateWithAttributes() + { + $item = NavigationItem::create('foo', 'Test', 500, ['class' => 'active']); + $this->assertSame(['class' => 'active'], $item->getExtraAttributes()); + } } diff --git a/packages/framework/tests/Unit/NavigationMenuConfigurationBuilderTest.php b/packages/framework/tests/Unit/NavigationMenuConfigurationBuilderTest.php new file mode 100644 index 00000000000..cde1e6c4100 --- /dev/null +++ b/packages/framework/tests/Unit/NavigationMenuConfigurationBuilderTest.php @@ -0,0 +1,164 @@ +<?php + +declare(strict_types=1); + +use Hyde\Testing\UnitTestCase; +use Illuminate\Contracts\Support\Arrayable; +use Hyde\Framework\Features\Navigation\NavigationMenuConfigurationBuilder; +use Hyde\Facades\Navigation; + +/** + * @covers \Hyde\Framework\Features\Navigation\NavigationMenuConfigurationBuilder + */ +class NavigationMenuConfigurationBuilderTest extends UnitTestCase +{ + private NavigationMenuConfigurationBuilder $builder; + + protected function setUp(): void + { + parent::setUp(); + + $this->builder = new NavigationMenuConfigurationBuilder(); + } + + public function testSetPagePriorities() + { + $order = ['index' => 0, 'posts' => 10, 'docs/index' => 100]; + $result = $this->builder->setPagePriorities($order)->toArray(); + + $this->assertArrayHasKey('order', $result); + $this->assertSame($order, $result['order']); + } + + public function testSetPageLabels() + { + $labels = ['index' => 'Home', 'docs/index' => 'Docs']; + $result = $this->builder->setPageLabels($labels)->toArray(); + + $this->assertArrayHasKey('labels', $result); + $this->assertSame($labels, $result['labels']); + } + + public function testExcludePages() + { + $exclude = ['404', 'admin']; + $result = $this->builder->excludePages($exclude)->toArray(); + + $this->assertArrayHasKey('exclude', $result); + $this->assertSame($exclude, $result['exclude']); + } + + public function testAddNavigationItems() + { + $custom = [ + Navigation::item('https://example.com', 'Example', 200), + Navigation::item('https://github.com', 'GitHub', 300), + ]; + + $result = $this->builder->addNavigationItems($custom)->toArray(); + + $this->assertArrayHasKey('custom', $result); + $this->assertSame($custom, $result['custom']); + } + + public function testSetSubdirectoryDisplayMode() + { + $displayModes = ['dropdown', 'flat', 'hidden']; + + foreach ($displayModes as $mode) { + $result = $this->builder->setSubdirectoryDisplayMode($mode)->toArray(); + + $this->assertArrayHasKey('subdirectory_display', $result); + $this->assertSame($mode, $result['subdirectory_display']); + } + } + + public function testChainedMethods() + { + $result = $this->builder + ->setPagePriorities(['index' => 0, 'posts' => 10]) + ->setPageLabels(['index' => 'Home']) + ->excludePages(['404']) + ->addNavigationItems([Navigation::item('https://github.com', 'GitHub', 200)]) + ->setSubdirectoryDisplayMode('dropdown') + ->toArray(); + + $this->assertArrayHasKey('order', $result); + $this->assertArrayHasKey('labels', $result); + $this->assertArrayHasKey('exclude', $result); + $this->assertArrayHasKey('custom', $result); + $this->assertArrayHasKey('subdirectory_display', $result); + } + + public function testEmptyConfiguration() + { + $result = $this->builder->toArray(); + + $this->assertEmpty($result); + } + + public function testInvalidSubdirectoryDisplay() + { + $this->expectException(\TypeError::class); + $this->builder->setSubdirectoryDisplayMode('invalid'); + } + + public function testRealLifeUsageScenario() + { + $result = $this->builder + ->setPagePriorities([ + 'index' => 0, + 'posts' => 10, + 'docs/index' => 100, + ]) + ->setPageLabels([ + 'index' => 'Home', + 'docs/index' => 'Docs', + ]) + ->excludePages([ + '404', + ]) + ->addNavigationItems([ + Navigation::item('https://github.com/hydephp/hyde', 'GitHub', 200), + ]) + ->setSubdirectoryDisplayMode('dropdown') + ->toArray(); + + $this->assertSame([ + 'order' => ['index' => 0, 'posts' => 10, 'docs/index' => 100], + 'labels' => ['index' => 'Home', 'docs/index' => 'Docs'], + 'exclude' => ['404'], + 'custom' => [ + ['destination' => 'https://github.com/hydephp/hyde', 'label' => 'GitHub', 'priority' => 200, 'attributes' => []], + ], + 'subdirectory_display' => 'dropdown', + ], $result); + } + + public function testHideSubdirectoriesFromNavigationShorthand() + { + $result = $this->builder->hideSubdirectoriesFromNavigation()->toArray(); + + $this->assertSame('hidden', $result['subdirectory_display']); + } + + public function testArrayableInterface() + { + $this->assertInstanceOf(Arrayable::class, $this->builder); + } + + public function testArrayObjectBehavior() + { + $this->builder->setPagePriorities(['index' => 0]); + + $this->assertCount(1, $this->builder); + $this->assertSame(['order' => ['index' => 0]], $this->builder->getArrayCopy()); + } + + public function testToArrayMethodReturnsSameResultAsArrayObject() + { + $this->builder->setPagePriorities(['index' => 0]); + + $this->assertSame(['order' => ['index' => 0]], $this->builder->toArray()); + } +} diff --git a/packages/framework/tests/Unit/RelativeLinksAcrossPagesRetainsIntegrityTest.php b/packages/framework/tests/Unit/RelativeLinksAcrossPagesRetainsIntegrityTest.php index facec7a3cc6..e8f9f8c1f66 100644 --- a/packages/framework/tests/Unit/RelativeLinksAcrossPagesRetainsIntegrityTest.php +++ b/packages/framework/tests/Unit/RelativeLinksAcrossPagesRetainsIntegrityTest.php @@ -23,7 +23,7 @@ protected function setUp(): void parent::setUp(); config(['hyde.enable_cache_busting' => false]); - config(['hyde.navigation.subdirectories' => 'flat']); + config(['hyde.navigation.subdirectory_display' => 'flat']); $this->needsDirectory('_pages/nested'); $this->file('_pages/root.md'); diff --git a/packages/framework/tests/Unit/Views/NavigationHtmlLayoutsTest.php b/packages/framework/tests/Unit/Views/NavigationHtmlLayoutsTest.php index b34f0d78af2..9935ef20e84 100644 --- a/packages/framework/tests/Unit/Views/NavigationHtmlLayoutsTest.php +++ b/packages/framework/tests/Unit/Views/NavigationHtmlLayoutsTest.php @@ -515,7 +515,7 @@ protected function sidebar(array $withPages = []): RenderedDocumentationSidebarM protected function useSubdirectoryConfig(string $option): static { - config(['hyde.navigation.subdirectories' => $option]); + config(['hyde.navigation.subdirectory_display' => $option]); return $this; } diff --git a/packages/framework/tests/Unit/Views/NavigationLinkViewTest.php b/packages/framework/tests/Unit/Views/NavigationLinkViewTest.php index de571ed18df..274a71420e0 100644 --- a/packages/framework/tests/Unit/Views/NavigationLinkViewTest.php +++ b/packages/framework/tests/Unit/Views/NavigationLinkViewTest.php @@ -26,10 +26,10 @@ protected function setUp(): void $this->mockPage(); } - protected function testView(): TestView + protected function testView(array $extraAttributes = []): TestView { return $this->view(view('hyde::components.navigation.navigation-link', [ - 'item' => NavigationItem::create(new Route(new InMemoryPage('foo')), 'Foo'), + 'item' => NavigationItem::create(new Route(new InMemoryPage('foo')), 'Foo', null, $extraAttributes), 'attributes' => new ComponentAttributeBag(), ])); } @@ -88,4 +88,66 @@ public function testComponentHasActiveClassWhenActive() ->assertHasClass('navigation-link') ->assertHasClass('navigation-link-active'); } + + public function testComponentRendersExtraAttributes() + { + $this->testView(['data-test' => 'value']) + ->assertHasAttribute('data-test') + ->assertAttributeIs('data-test="value"'); + } + + public function testComponentRendersMultipleExtraAttributes() + { + $this->testView(['data-test' => 'value', 'data-foo' => 'bar']) + ->assertHasAttribute('data-test') + ->assertAttributeIs('data-test="value"') + ->assertHasAttribute('data-foo') + ->assertAttributeIs('data-foo="bar"'); + } + + public function testComponentRendersExtraAttributesWithExistingAttributes() + { + $this->mockCurrentPage('foo'); + + $view = $this->testView(['data-test' => 'value']); + + $expected = <<<'HTML' + <a href="foo.html" aria-current="page" class="navigation-link block my-2 md:my-0 md:inline-block py-1 text-gray-700 hover:text-gray-900 dark:text-gray-100 navigation-link-active border-l-4 border-indigo-500 md:border-none font-medium -ml-6 pl-5 md:ml-0 md:pl-0 bg-gray-100 dark:bg-gray-800 md:bg-transparent dark:md:bg-transparent" data-test="value">Foo</a> + HTML; + + $this->assertSame($expected, $view->getRendered()); + } + + public function testComponentMergesClassAttributeCorrectly() + { + $this->testView(['class' => 'custom-class']) + ->assertHasClass('navigation-link') + ->assertHasClass('custom-class'); + } + + public function testComponentOverridesDefaultAttributesWithExtraAttributes() + { + $this->testView(['href' => 'https://example.com']) + ->assertAttributeIs('href="https://example.com"'); + } + + public function testComponentHandlesEmptyExtraAttributes() + { + $this->testView([]) + ->assertHasElement('<a>') + ->assertTextIs('Foo'); + } + + public function testComponentState() + { + $this->mockCurrentPage('foo'); + + $view = $this->testView(); + + $expected = <<<'HTML' + <a href="foo.html" aria-current="page" class="navigation-link block my-2 md:my-0 md:inline-block py-1 text-gray-700 hover:text-gray-900 dark:text-gray-100 navigation-link-active border-l-4 border-indigo-500 md:border-none font-medium -ml-6 pl-5 md:ml-0 md:pl-0 bg-gray-100 dark:bg-gray-800 md:bg-transparent dark:md:bg-transparent">Foo</a> + HTML; + + $this->assertSame($expected, $view->getRendered()); + } } diff --git a/packages/framework/tests/Unit/Views/NavigationMenuViewTest.php b/packages/framework/tests/Unit/Views/NavigationMenuViewTest.php index 18d9bfd7cbf..e1aa632ac05 100644 --- a/packages/framework/tests/Unit/Views/NavigationMenuViewTest.php +++ b/packages/framework/tests/Unit/Views/NavigationMenuViewTest.php @@ -78,7 +78,7 @@ public function testNavigationMenuWithRootPages() public function testNavigationMenuWithDropdownPages() { - config(['hyde.navigation.subdirectories' => 'dropdown']); + config(['hyde.navigation.subdirectory_display' => 'dropdown']); $page = new MarkdownPage('page'); $bar = new MarkdownPage('foo/bar'); @@ -104,7 +104,7 @@ public function testNavigationMenuWithDropdownPages() public function testNavigationMenuWithDropdownPagesWithRootGroupPage() { - config(['hyde.navigation.subdirectories' => 'dropdown']); + config(['hyde.navigation.subdirectory_display' => 'dropdown']); $foo = new MarkdownPage('foo'); $bar = new MarkdownPage('foo/bar');