Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
a58d850
Create table-of-contents.blade.php
emmadesilva Dec 1, 2024
285f7fe
Update unit test to extend feature test case
emmadesilva Dec 1, 2024
b67c4f3
Add component identifier class
emmadesilva Dec 1, 2024
e08057d
Create SidebarTableOfContentsViewTest.php
emmadesilva Dec 1, 2024
72327b1
Revert "Update unit test to extend feature test case"
emmadesilva Dec 1, 2024
2f31aac
Rewrite table of contents generator with a custom implementation
emmadesilva Dec 1, 2024
9b45af3
Breaking: GeneratesTableOfContents execute method now returns array
emmadesilva Dec 1, 2024
a051140
Fix formatting
emmadesilva Dec 1, 2024
6093c48
Remove old assertions
emmadesilva Dec 1, 2024
8b008f8
Implement the new assertions
emmadesilva Dec 1, 2024
876fff8
Add lost Setext header support
emmadesilva Dec 1, 2024
e9e3557
Move unit test to unit namespace
emmadesilva Dec 1, 2024
bbecf26
Add test crosslinks
emmadesilva Dec 1, 2024
828cf5e
Rename test for new clearer scope
emmadesilva Dec 1, 2024
586465a
Explicit test results
emmadesilva Dec 1, 2024
2609fbd
Test various config levels
emmadesilva Dec 1, 2024
d201827
Test various config level edge cases
emmadesilva Dec 1, 2024
0c4de8d
Support all heading levels
emmadesilva Dec 1, 2024
849ee8c
Refactor to extract protected class properties
emmadesilva Dec 1, 2024
3d06340
Delete table-of-contents.css
emmadesilva Dec 1, 2024
bdf5dc7
Remove import for deleted table-of-contents.css
emmadesilva Dec 1, 2024
8869386
Use Blade component to make the table of contents
emmadesilva Dec 1, 2024
e8b8749
Inline method usage
emmadesilva Dec 1, 2024
142490d
Breaking: Remove `DocumentationPage::getTableOfContents` method
emmadesilva Dec 1, 2024
0682b95
Apply fixes from StyleCI
StyleCIBot Dec 1, 2024
2538112
Update app.css
emmadesilva Dec 1, 2024
b7b00ea
Extract new method to render data for component test
emmadesilva Dec 1, 2024
654dd42
Cleanup formatting
emmadesilva Dec 1, 2024
d94ff75
Strip Tailwind from test comparisons
emmadesilva Dec 1, 2024
bc02907
Granular replacements
emmadesilva Dec 1, 2024
b898ffa
Reindent HTML for comparison
emmadesilva Dec 1, 2024
ee9259b
Unwrap extra list parent element
emmadesilva Dec 1, 2024
fed7022
Replace replacements with removals
emmadesilva Dec 1, 2024
8b189ca
Merge two initial tests
emmadesilva Dec 1, 2024
2668c96
Tweak replacements
emmadesilva Dec 1, 2024
4c19684
Better test normalization
emmadesilva Dec 1, 2024
5790d8f
Don't add class to recursed components
emmadesilva Dec 1, 2024
713cdc4
Only render items when there are any
emmadesilva Dec 1, 2024
72a5e1e
Update tests to expect the hash span
emmadesilva Dec 1, 2024
21e9d64
Fix formatting
emmadesilva Dec 1, 2024
0a1d2e1
Refactor to closer match existing styles
emmadesilva Dec 1, 2024
12dc804
Refactor to closer match old styles
emmadesilva Dec 1, 2024
9ef45dc
Update test for changed classes
emmadesilva Dec 1, 2024
ccd2942
Update RELEASE_NOTES.md
emmadesilva Dec 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ This serves two purposes:

- Breaking: Removed the build task `\Hyde\Framework\Actions\PostBuildTasks\GenerateSearch` (see upgrade guide below)
- Breaking: Removed the deprecated `\Hyde\Framework\Services\BuildService::transferMediaAssets()` method (see upgrade guide below)
- Breaking: Removed the `DocumentationPage::getTableOfContents()` method as we now use Blade to generate the table of contents in https://github.com/hydephp/develop/pull/2045
- Removed the deprecated global `unslash()` function, replaced with the namespaced `\Hyde\unslash()` function in https://github.com/hydephp/develop/pull/1754
- Removed the deprecated `BaseUrlNotSetException` class, with the `Hyde::url()` helper now throwing `BadMethodCallException` if no base URL is set in https://github.com/hydephp/develop/pull/1760
- Removed: The deprecated `PostAuthor::getName()` method is now removed (use `$author->name`) in https://github.com/hydephp/develop/pull/1782
Expand Down Expand Up @@ -500,6 +501,28 @@ Hyperlinks::isRemote($source);

This change was implemented in https://github.com/hydephp/develop/pull/1883. Make sure to update any instances of `FeaturedImage::isRemote()` in your codebase to ensure compatibility with HydePHP v2.0.

### Blade-based table of contents generator

The way we generate table of contents for documentation pages have been changed from a helper method to a Blade component.

This new system is much easier to customize and style, and is up to 40 times faster than the old system.

See https://github.com/hydephp/develop/pull/2045 for more information.

#### Scope

The likelihood of impact is low, but if any of the following are true, you may need to update your code:

- If you have used the `Hyde\Framework\Actions\GeneratesTableOfContents` class in custom code, you will likely need to update that code for the rewritten class.
- If you have called the `getTableOfContents` method of the `DocumentationPage` class in custom code, you will need to update that usage as the that message has been removed.
- If you have published the `resources/views/components/docs/sidebar-item.blade.php` component, you will need to update it to call the new component instead of the old generator rendering.

#### Changes
- Adds a new `resources/views/components/docs/table-of-contents.blade.php` component containing the structure and styles for the table of contents
- Rewrites the `GeneratesTableOfContents` class to use a custom implementation instead of using CommonMark
- The `execute` method of the `GeneratesTableOfContents` class now returns an array of data, instead of a string of HTML. This data should be fed into the new component
- Removed the `table-of-contents.css` file as styles are now made using Tailwind

## New features

<!-- Editors note: Todo: Maybe move to the relevant docs... -->
Expand Down
2 changes: 1 addition & 1 deletion _media/app.css

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,6 @@ No description provided.
$page->getOnlineSourcePath(): string|false
```

#### `getTableOfContents()`

Generate Table of Contents as HTML from a Markdown document body.

```php
$page->getTableOfContents(): string
```

#### `getRouteKey()`

Get the route key for the page.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@props(['grouped' => false])
@use('Hyde\Framework\Actions\GeneratesTableOfContents')
@php /** @var \Hyde\Framework\Features\Navigation\NavigationItem $item */ @endphp
<li @class(['sidebar-item -ml-4 pl-4', $grouped
? 'active -ml-8 pl-8 bg-black/5 dark:bg-black/10'
Expand All @@ -14,7 +15,7 @@

@if(config('docs.sidebar.table_of_contents.enabled', true))
<span class="sr-only">Table of contents</span>
{!! $page->getTableOfContents() !!}
<x-hyde::docs.table-of-contents :items="(new GeneratesTableOfContents($page->markdown))->execute()" />
@endif
@else
<a href="{{ $item->getLink() }}" @class([$grouped
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@props(['items', 'isChild' => false])

@if(! empty($items))
<ul class="{{ ! $isChild ? 'table-of-contents pb-3' : 'pl-2' }}">
@foreach($items as $item)
<li class="my-0.5">
<a href="#{{ $item['slug'] }}" class="-ml-8 pl-8 opacity-80 hover:opacity-100 hover:bg-gray-200/20 transition-all duration-300">
<span class="text-[75%] opacity-50 hover:opacity-100 transition-opacity duration-300">#</span>
{{ $item['title'] }}
</a>

@if(! empty($item['children']))
<x-hyde::docs.table-of-contents :items="$item['children']" :isChild="true" />
@endif
</li>
@endforeach
</ul>
@endif
114 changes: 70 additions & 44 deletions packages/framework/src/Framework/Actions/GeneratesTableOfContents.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,64 +6,90 @@

use Hyde\Facades\Config;
use Hyde\Markdown\Models\Markdown;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension;
use League\CommonMark\Extension\TableOfContents\TableOfContentsExtension;
use League\CommonMark\MarkdownConverter;

use function strpos;
use function substr;

/**
* Generates a table of contents for the Markdown document, most commonly used for the sidebar.
*/
use Illuminate\Support\Str;

class GeneratesTableOfContents
{
protected string $markdown;

protected int $minHeadingLevel = 2;
protected int $maxHeadingLevel = 4;

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(): string
public function execute(): array
{
$config = [
'table_of_contents' => [
'html_class' => 'table-of-contents',
'position' => 'placeholder',
'placeholder' => '[[START_TOC]]',
'style' => 'bullet',
'min_heading_level' => Config::getInt('docs.sidebar.table_of_contents.min_heading_level', 2),
'max_heading_level' => Config::getInt('docs.sidebar.table_of_contents.max_heading_level', 4),
'normalize' => 'relative',
],
'heading_permalink' => [
'fragment_prefix' => '',
],
];

$environment = new Environment($config);
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new HeadingPermalinkExtension());
$environment->addExtension(new TableOfContentsExtension());

$converter = new MarkdownConverter($environment);
$html = $converter->convert($this->markdown."\n[[START_TOC]]")->getContent();

return $this->extractTableOfContents($html);
$headings = $this->parseHeadings();

return $this->buildTableOfContents($headings);
}

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);

$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;
}
}

$slug = Str::slug($title);
$headings[] = [
'level' => $level,
'title' => $title,
'slug' => $slug,
];
}

return $headings;
}

protected function extractTableOfContents(string $html): string
protected function buildTableOfContents(array $headings): array
{
// The table of contents is always at the end of the document, so we can just strip everything before it.
$position = strpos($html, '<ul class="table-of-contents">');
if ($position === false) {
// The document has no headings, so we'll just return an empty string.
return '';
$items = [];
$stack = [&$items];
$previousLevel = $this->minHeadingLevel;

foreach ($headings as $heading) {
if ($heading['level'] < $this->minHeadingLevel || $heading['level'] > $this->maxHeadingLevel) {
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);
}

$stack[count($stack) - 1][] = $item;
$previousLevel = $heading['level'];
}

return substr($html, $position);
return $items;
}
}
9 changes: 0 additions & 9 deletions packages/framework/src/Pages/DocumentationPage.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

use Hyde\Facades\Config;
use Hyde\Foundation\Facades\Routes;
use Hyde\Framework\Actions\GeneratesTableOfContents;
use Hyde\Pages\Concerns\BaseMarkdownPage;
use Hyde\Support\Models\Route;

Expand Down Expand Up @@ -54,14 +53,6 @@ public static function hasTableOfContents(): bool
return Config::getBool('docs.sidebar.table_of_contents.enabled', true);
}

/**
* Generate Table of Contents as HTML from a Markdown document body.
*/
public function getTableOfContents(): string
{
return (new GeneratesTableOfContents($this->markdown))->execute();
}

/**
* Get the route key for the page.
*
Expand Down
Loading