From 030cfd855c9cc499cad7e4e79bf19b00a9256699 Mon Sep 17 00:00:00 2001 From: Caen De Silva Date: Sun, 1 Dec 2024 16:27:59 +0100 Subject: [PATCH 1/9] Update documentation for new table of contents system --- docs/creating-content/documentation-pages.md | 2 ++ docs/digging-deeper/troubleshooting.md | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/creating-content/documentation-pages.md b/docs/creating-content/documentation-pages.md index 64ab278358a..ea698b7e3f0 100644 --- a/docs/creating-content/documentation-pages.md +++ b/docs/creating-content/documentation-pages.md @@ -336,6 +336,8 @@ In the `config/docs.php` file you can configure the behaviour, content, and the ], ``` +To customize the markup or styles of the table of contents, you can publish the `x-hyde::docs.table-of-contents` Blade component and modify it to your liking. + ### 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. diff --git a/docs/digging-deeper/troubleshooting.md b/docs/digging-deeper/troubleshooting.md index e3c4ae9879c..9de42102756 100644 --- a/docs/digging-deeper/troubleshooting.md +++ b/docs/digging-deeper/troubleshooting.md @@ -81,7 +81,6 @@ You can read more about some of these in the [Core Concepts](core-concepts#paths | Overriding Hyde views is not working | Ensure the Blade views are in the correct directory. | Rerun `php hyde publish:views`. | | Styles not updating when deploying site | It could be a caching issue. To be honest, when dealing with styles, it's always a caching issue. | Clear your cache, and optionally complain to your site host | | Documentation sidebar items are in the wrong order | Double check the config, make sure the route keys are written correctly. Check that you are not overriding with front matter. | Check config for typos and front matter | -| Documentation table of contents is weird | The table of contents markup is generated by the [League/CommonMark extension](https://commonmark.thephpleague.com/2.3/extensions/table-of-contents/) | Make sure that your Markdown headings make sense | | Issues with date in blog post front matter | The date is parsed by the PHP `strtotime()` function. The date may be in an invalid format, or the front matter is invalid | Ensure the date is in a format that `strtotime()` can parse. Wrap the front matter value in quotes. | | RSS feed not being generated | The RSS feed requires that you have set a site URL in the Hyde config or the `.env` file. Also check that you have blog posts, and that they are enabled. | Check your configuration files. | | | Sitemap not being generated | The sitemap requires that you have set a site URL in the Hyde config or the `.env` file. | Check your configuration files. | | From c76a52cd7fc8055a2830217bf3ebcb4d5f2cf647 Mon Sep 17 00:00:00 2001 From: Caen De Silva Date: Sun, 1 Dec 2024 16:28:03 +0100 Subject: [PATCH 2/9] Cleanup code --- .../src/Framework/Actions/GeneratesTableOfContents.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php b/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php index 5ed8e632c18..acbc2f64c97 100644 --- a/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php +++ b/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php @@ -18,15 +18,14 @@ class GeneratesTableOfContents public function __construct(Markdown|string $markdown) { $this->markdown = (string) $markdown; + $this->minHeadingLevel = Config::getInt('docs.sidebar.table_of_contents.min_heading_level', 2); $this->maxHeadingLevel = Config::getInt('docs.sidebar.table_of_contents.max_heading_level', 4); } public function execute(): array { - $headings = $this->parseHeadings(); - - return $this->buildTableOfContents($headings); + return $this->buildTableOfContents($this->parseHeadings()); } protected function parseHeadings(): array From 2fc56e5d0b3f98003a6457132ee768859b62be8f Mon Sep 17 00:00:00 2001 From: Caen De Silva Date: Sun, 1 Dec 2024 16:29:55 +0100 Subject: [PATCH 3/9] Refactor to break up complex method --- .../Actions/GeneratesTableOfContents.php | 53 ++++++++++++++----- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php b/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php index acbc2f64c97..1abf2c51ff8 100644 --- a/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php +++ b/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php @@ -69,26 +69,53 @@ protected function buildTableOfContents(array $headings): array $previousLevel = $this->minHeadingLevel; foreach ($headings as $heading) { - if ($heading['level'] < $this->minHeadingLevel || $heading['level'] > $this->maxHeadingLevel) { + if (! $this->isHeadingWithinBounds($heading)) { continue; } - $item = [ - 'title' => $heading['title'], - 'slug' => $heading['slug'], - 'children' => [], - ]; - - if ($heading['level'] > $previousLevel) { - $stack[] = &$stack[count($stack) - 1][count($stack[count($stack) - 1]) - 1]['children']; - } elseif ($heading['level'] < $previousLevel) { - array_splice($stack, $heading['level'] - $this->minHeadingLevel + 1); - } - + $item = $this->createTableItem($heading); + $this->updateStackForHeadingLevel($stack, $heading['level'], $previousLevel); + $stack[count($stack) - 1][] = $item; $previousLevel = $heading['level']; } return $items; } + + protected function isHeadingWithinBounds(array $heading): bool + { + return $heading['level'] >= $this->minHeadingLevel && + $heading['level'] <= $this->maxHeadingLevel; + } + + protected function createTableItem(array $heading): array + { + return [ + 'title' => $heading['title'], + 'slug' => $heading['slug'], + 'children' => [], + ]; + } + + protected function updateStackForHeadingLevel(array &$stack, int $currentLevel, int $previousLevel): void + { + if ($currentLevel > $previousLevel) { + $this->nestNewLevel($stack); + } elseif ($currentLevel < $previousLevel) { + $this->unwindStack($stack, $currentLevel); + } + } + + protected function nestNewLevel(array &$stack): void + { + $lastStackIndex = count($stack) - 1; + $lastItemIndex = count($stack[$lastStackIndex]) - 1; + $stack[] = &$stack[$lastStackIndex][$lastItemIndex]['children']; + } + + protected function unwindStack(array &$stack, int $currentLevel): void + { + array_splice($stack, $currentLevel - $this->minHeadingLevel + 1); + } } From 90d8efa127ce046b8f0e4d1d38dc9942e4015539 Mon Sep 17 00:00:00 2001 From: Caen De Silva Date: Sun, 1 Dec 2024 16:30:34 +0100 Subject: [PATCH 4/9] Invert condition to merge up control flow --- .../Framework/Actions/GeneratesTableOfContents.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php b/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php index 1abf2c51ff8..8a9eaa19b3f 100644 --- a/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php +++ b/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php @@ -69,15 +69,13 @@ protected function buildTableOfContents(array $headings): array $previousLevel = $this->minHeadingLevel; foreach ($headings as $heading) { - if (! $this->isHeadingWithinBounds($heading)) { - continue; - } + if ($this->isHeadingWithinBounds($heading)) { + $item = $this->createTableItem($heading); + $this->updateStackForHeadingLevel($stack, $heading['level'], $previousLevel); - $item = $this->createTableItem($heading); - $this->updateStackForHeadingLevel($stack, $heading['level'], $previousLevel); - - $stack[count($stack) - 1][] = $item; - $previousLevel = $heading['level']; + $stack[count($stack) - 1][] = $item; + $previousLevel = $heading['level']; + } } return $items; From 6df1b80857164519917be0df9484eddbfff89fe9 Mon Sep 17 00:00:00 2001 From: Caen De Silva Date: Sun, 1 Dec 2024 16:32:58 +0100 Subject: [PATCH 5/9] Refactor to break up complex method --- .../Actions/GeneratesTableOfContents.php | 79 +++++++++++++------ 1 file changed, 56 insertions(+), 23 deletions(-) diff --git a/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php b/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php index 8a9eaa19b3f..bd09e78234d 100644 --- a/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php +++ b/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php @@ -30,38 +30,71 @@ public function execute(): array protected function parseHeadings(): array { - // Match both ATX-style (###) and Setext-style (===, ---) headers - $pattern = '/^(?:#{1,6}\s+(.+)|(.+)\n([=\-])\3+)$/m'; - preg_match_all($pattern, $this->markdown, $matches); - + $matches = $this->matchHeadingPatterns(); $headings = []; + foreach ($matches[0] as $index => $heading) { - // Handle ATX-style headers (###) - if (str_starts_with($heading, '#')) { - $level = substr_count($heading, '#'); - $title = $matches[1][$index]; - } - // Handle Setext-style headers (=== or ---) - else { - $title = trim($matches[2][$index]); - $level = $matches[3][$index] === '=' ? 1 : 2; - // Only add if the config level is met - if ($level < $this->minHeadingLevel) { - continue; - } + $headingData = $this->parseHeadingData($heading, $matches, $index); + + if ($headingData === null) { + continue; } - $slug = Str::slug($title); - $headings[] = [ - 'level' => $level, - 'title' => $title, - 'slug' => $slug, - ]; + $headings[] = $this->createHeadingEntry($headingData); } return $headings; } + protected function matchHeadingPatterns(): array + { + // Match both ATX-style (###) and Setext-style (===, ---) headers + $pattern = '/^(?:#{1,6}\s+(.+)|(.+)\n([=\-])\3+)$/m'; + preg_match_all($pattern, $this->markdown, $matches); + + return $matches; + } + + protected function parseHeadingData(string $heading, array $matches, int $index): ?array + { + if (str_starts_with($heading, '#')) { + return $this->parseAtxHeading($heading, $matches[1][$index]); + } + + return $this->parseSetextHeading($matches[2][$index], $matches[3][$index]); + } + + protected function parseAtxHeading(string $heading, string $title): array + { + return [ + 'level' => substr_count($heading, '#'), + 'title' => $title, + ]; + } + + protected function parseSetextHeading(string $title, string $marker): ?array + { + $level = $marker === '=' ? 1 : 2; + + if ($level < $this->minHeadingLevel) { + return null; + } + + return [ + 'level' => $level, + 'title' => trim($title), + ]; + } + + protected function createHeadingEntry(array $headingData): array + { + return [ + 'level' => $headingData['level'], + 'title' => $headingData['title'], + 'slug' => Str::slug($headingData['title']), + ]; + } + protected function buildTableOfContents(array $headings): array { $items = []; From 73c71ca67f9075b1c21e2ff38efb674bc20fc976 Mon Sep 17 00:00:00 2001 From: Caen De Silva Date: Sun, 1 Dec 2024 16:33:36 +0100 Subject: [PATCH 6/9] Break out unnecessary else prefix --- .../src/Framework/Actions/GeneratesTableOfContents.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php b/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php index bd09e78234d..145902b8666 100644 --- a/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php +++ b/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php @@ -133,7 +133,9 @@ protected function updateStackForHeadingLevel(array &$stack, int $currentLevel, { if ($currentLevel > $previousLevel) { $this->nestNewLevel($stack); - } elseif ($currentLevel < $previousLevel) { + } + + if ($currentLevel < $previousLevel) { $this->unwindStack($stack, $currentLevel); } } From deaebf015f505ccf5858c59181df577d0c29732b Mon Sep 17 00:00:00 2001 From: Caen De Silva Date: Sun, 1 Dec 2024 16:34:01 +0100 Subject: [PATCH 7/9] Add spacing --- .../framework/src/Framework/Actions/GeneratesTableOfContents.php | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php b/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php index 145902b8666..cd0e3b70012 100644 --- a/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php +++ b/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php @@ -144,6 +144,7 @@ protected function nestNewLevel(array &$stack): void { $lastStackIndex = count($stack) - 1; $lastItemIndex = count($stack[$lastStackIndex]) - 1; + $stack[] = &$stack[$lastStackIndex][$lastItemIndex]['children']; } From 3328e41c98a74c95148437c4be13b7d4a2fa86c7 Mon Sep 17 00:00:00 2001 From: Caen De Silva Date: Sun, 1 Dec 2024 16:37:10 +0100 Subject: [PATCH 8/9] Annotate generics and shapes of the arrays --- .../Actions/GeneratesTableOfContents.php | 50 ++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php b/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php index cd0e3b70012..8ea4d23791d 100644 --- a/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php +++ b/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php @@ -8,6 +8,9 @@ use Hyde\Markdown\Models\Markdown; use Illuminate\Support\Str; +/** + * Generates a nested table of contents from Markdown headings. + */ class GeneratesTableOfContents { protected string $markdown; @@ -23,11 +26,17 @@ public function __construct(Markdown|string $markdown) $this->maxHeadingLevel = Config::getInt('docs.sidebar.table_of_contents.max_heading_level', 4); } + /** + * @return array + */ public function execute(): array { return $this->buildTableOfContents($this->parseHeadings()); } + /** + * @return array + */ protected function parseHeadings(): array { $matches = $this->matchHeadingPatterns(); @@ -46,6 +55,9 @@ protected function parseHeadings(): array return $headings; } + /** + * @return array{0: array, 1: array, 2: array, 3: array} + */ protected function matchHeadingPatterns(): array { // Match both ATX-style (###) and Setext-style (===, ---) headers @@ -55,6 +67,10 @@ protected function matchHeadingPatterns(): array return $matches; } + /** + * @param array{0: array, 1: array, 2: array, 3: array} $matches + * @return array{level: int, title: string}|null + */ protected function parseHeadingData(string $heading, array $matches, int $index): ?array { if (str_starts_with($heading, '#')) { @@ -64,6 +80,9 @@ protected function parseHeadingData(string $heading, array $matches, int $index) return $this->parseSetextHeading($matches[2][$index], $matches[3][$index]); } + /** + * @return array{level: int, title: string} + */ protected function parseAtxHeading(string $heading, string $title): array { return [ @@ -72,6 +91,9 @@ protected function parseAtxHeading(string $heading, string $title): array ]; } + /** + * @return array{level: int, title: string}|null + */ protected function parseSetextHeading(string $title, string $marker): ?array { $level = $marker === '=' ? 1 : 2; @@ -86,6 +108,10 @@ protected function parseSetextHeading(string $title, string $marker): ?array ]; } + /** + * @param array{level: int, title: string} $headingData + * @return array{level: int, title: string, slug: string} + */ protected function createHeadingEntry(array $headingData): array { return [ @@ -95,6 +121,10 @@ protected function createHeadingEntry(array $headingData): array ]; } + /** + * @param array $headings + * @return array + */ protected function buildTableOfContents(array $headings): array { $items = []; @@ -114,12 +144,19 @@ protected function buildTableOfContents(array $headings): array return $items; } + /** + * @param array{level: int, title: string, slug: string} $heading + */ protected function isHeadingWithinBounds(array $heading): bool { - return $heading['level'] >= $this->minHeadingLevel && + return $heading['level'] >= $this->minHeadingLevel && $heading['level'] <= $this->maxHeadingLevel; } + /** + * @param array{level: int, title: string, slug: string} $heading + * @return array{title: string, slug: string, children: array} + */ protected function createTableItem(array $heading): array { return [ @@ -129,17 +166,23 @@ protected function createTableItem(array $heading): array ]; } + /** + * @param array> $stack + */ protected function updateStackForHeadingLevel(array &$stack, int $currentLevel, int $previousLevel): void { if ($currentLevel > $previousLevel) { $this->nestNewLevel($stack); - } + } if ($currentLevel < $previousLevel) { $this->unwindStack($stack, $currentLevel); } } + /** + * @param array> $stack + */ protected function nestNewLevel(array &$stack): void { $lastStackIndex = count($stack) - 1; @@ -148,6 +191,9 @@ protected function nestNewLevel(array &$stack): void $stack[] = &$stack[$lastStackIndex][$lastItemIndex]['children']; } + /** + * @param array> $stack + */ protected function unwindStack(array &$stack, int $currentLevel): void { array_splice($stack, $currentLevel - $this->minHeadingLevel + 1); From a2c420d0fbc90b9578608692c486339b7a6fa167 Mon Sep 17 00:00:00 2001 From: Caen De Silva Date: Sun, 1 Dec 2024 16:37:18 +0100 Subject: [PATCH 9/9] Fix formatting Co-Authored-By: StyleCI Bot --- .../src/Framework/Actions/GeneratesTableOfContents.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php b/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php index 8ea4d23791d..ae27d6af08c 100644 --- a/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php +++ b/packages/framework/src/Framework/Actions/GeneratesTableOfContents.php @@ -44,7 +44,7 @@ protected function parseHeadings(): array foreach ($matches[0] as $index => $heading) { $headingData = $this->parseHeadingData($heading, $matches, $index); - + if ($headingData === null) { continue; } @@ -76,7 +76,7 @@ protected function parseHeadingData(string $heading, array $matches, int $index) if (str_starts_with($heading, '#')) { return $this->parseAtxHeading($heading, $matches[1][$index]); } - + return $this->parseSetextHeading($matches[2][$index], $matches[3][$index]); } @@ -97,7 +97,7 @@ protected function parseAtxHeading(string $heading, string $title): array protected function parseSetextHeading(string $title, string $marker): ?array { $level = $marker === '=' ? 1 : 2; - + if ($level < $this->minHeadingLevel) { return null; }