Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
aff4388
Create HeadingRenderer.php
emmadesilva Dec 1, 2024
ed7f281
Create markdown-heading.blade.php
emmadesilva Dec 1, 2024
7e45297
Render slot literally
emmadesilva Dec 1, 2024
5607c6d
Indent code
emmadesilva Dec 1, 2024
ec6b2c6
Add an attribute for controlling permalink state
emmadesilva Dec 1, 2024
98ddab1
Set state from class
emmadesilva Dec 1, 2024
e1ccda8
Fix recursion issue
emmadesilva Dec 1, 2024
24e459f
Support extra attributes
emmadesilva Dec 1, 2024
2c3e07b
Forward node attributes
emmadesilva Dec 1, 2024
6631d86
Format props
emmadesilva Dec 1, 2024
07d5e97
Update added config setting
emmadesilva Dec 1, 2024
c430b9b
Apply fixes from StyleCI
StyleCIBot Dec 1, 2024
b0a4ef9
Register the custom heading renderer
emmadesilva Dec 1, 2024
87f03f7
Extract helper method
emmadesilva Dec 1, 2024
585764d
Create heading permalinks configuration
emmadesilva Dec 1, 2024
bfc93e3
Better option name
emmadesilva Dec 1, 2024
cb6c70e
Construct with page class
emmadesilva Dec 1, 2024
94cada0
Add permalinks only for configured pages
emmadesilva Dec 1, 2024
bcf5cdd
Format long line
emmadesilva Dec 1, 2024
22d30c5
Nullable class string
emmadesilva Dec 1, 2024
6fbb656
Remove auto-configuration for HeadingPermalinkExtension
emmadesilva Dec 1, 2024
b7f0509
Change component to match markup made by extension
emmadesilva Dec 1, 2024
539a8eb
Use more explicit assertions
emmadesilva Dec 1, 2024
53b158f
Make the permalink headings level configurable
emmadesilva Dec 1, 2024
bb9fd0a
Default to minimum level of 2
emmadesilva Dec 1, 2024
d8896ad
Introduce local variable
emmadesilva Dec 1, 2024
2c53963
Post process to normalize result to CommonMark implementation
emmadesilva Dec 1, 2024
358c799
Replace multiple replacements with single Regex
emmadesilva Dec 1, 2024
cc8cc6d
Add a test for when heading permalinks are disabled
emmadesilva Dec 1, 2024
2475010
Sync configuration files
emmadesilva Dec 1, 2024
30469f4
Mock the View factory
emmadesilva Dec 1, 2024
a6bcaa1
Fix mock bindings
emmadesilva Dec 1, 2024
567b505
Granular mocks
emmadesilva Dec 1, 2024
22a7745
Update test to test the automatic permalinks feature
emmadesilva Dec 1, 2024
1100719
Remove the `MarkdownService::withPermalinks` method
emmadesilva Dec 1, 2024
d03dbf3
Add todo
emmadesilva Dec 1, 2024
9311bd1
Create HeadingRendererUnitTest.php
emmadesilva Dec 1, 2024
80ed9d4
Create MarkdownHeadingRendererTest.php
emmadesilva Dec 1, 2024
5e243e7
Create a minimal view environment
emmadesilva Dec 1, 2024
c7eb6bb
Register finder path
emmadesilva Dec 1, 2024
bad9eb3
Create and configure the engine resolver
emmadesilva Dec 1, 2024
fd6df97
Extract helper method
emmadesilva Dec 1, 2024
4995016
Basic constructor tests
emmadesilva Dec 1, 2024
a13f1e8
Cleanup helper method code
emmadesilva Dec 1, 2024
0d08cc5
Helper to mock child node renderer
emmadesilva Dec 1, 2024
6dc0095
Improve formatting
emmadesilva Dec 1, 2024
a6b26be
Set default value
emmadesilva Dec 1, 2024
598d9c9
Implement initial unit test
emmadesilva Dec 1, 2024
5b00e79
Test can add permalink based on configuration
emmadesilva Dec 1, 2024
bc876e8
Refactor to data provider attribute
emmadesilva Dec 1, 2024
10e905a
Clean up after test
emmadesilva Dec 1, 2024
703e70c
Automatically reset to cached config default
emmadesilva Dec 1, 2024
039c11a
More extensive range testing
emmadesilva Dec 1, 2024
3a709dd
Revert "More extensive range testing"
emmadesilva Dec 1, 2024
496260c
Make protected helper methods public internal
emmadesilva Dec 1, 2024
90ff929
Remove tests for later refactor
emmadesilva Dec 1, 2024
0382947
Cleaner and extended testing
emmadesilva Dec 1, 2024
240e3dd
Test more code paths
emmadesilva Dec 2, 2024
bbd5a0b
Clarify test name
emmadesilva Dec 2, 2024
1195704
Apply fixes from StyleCI
StyleCIBot Dec 2, 2024
65cc1ff
Unit test the post processing
emmadesilva Dec 2, 2024
2dd5c32
Clarify test name to specify the reason behind it
emmadesilva Dec 2, 2024
b1b6266
Add extra test case
emmadesilva Dec 2, 2024
2f31b9b
Apply fixes from StyleCI
StyleCIBot Dec 2, 2024
3c3661a
Clean up test code to remove focus from implementation details
emmadesilva Dec 2, 2024
b00ea06
Implement the high level feature test
emmadesilva Dec 2, 2024
a7b52d6
Add some more assertions
emmadesilva Dec 2, 2024
e67a640
Assert on the full output when relevant
emmadesilva Dec 2, 2024
6fe5cb8
Expand feature testing
emmadesilva Dec 2, 2024
3c285a7
Add todo
emmadesilva Dec 2, 2024
43d82a4
Test escaping
emmadesilva Dec 2, 2024
4e80297
Document custom Markdown heading renderer
emmadesilva Dec 3, 2024
f53eb4d
Remove the enabled key from the permalinks configuration
emmadesilva Dec 3, 2024
ce21d66
Remove the unused `canEnablePermalinks` from `MarkdownService`
emmadesilva Dec 3, 2024
3d3ed48
Improve the test
emmadesilva Dec 3, 2024
7f79bee
Update RELEASE_NOTES.md
emmadesilva Dec 4, 2024
0710e95
More semantic Markdown heading permalinks
emmadesilva Dec 4, 2024
8737a52
Conditionally add element identifier
emmadesilva Dec 4, 2024
04aa325
Remove support for custom identifier when it breaks permalinks
emmadesilva Dec 4, 2024
e7be152
Expect semantic heading identifiers
emmadesilva Dec 4, 2024
3c66d44
Refactor to use Tailwind permalink styles
emmadesilva Dec 4, 2024
5f90cbc
Tweak styles to match original behaviour
emmadesilva Dec 4, 2024
af9f4fa
Update tests for semantic Markdown permalink headers
emmadesilva Dec 4, 2024
1caa631
Add scroll margin to header
emmadesilva Dec 5, 2024
9c49d46
Format long line
emmadesilva Dec 5, 2024
7ebd17f
Merge pull request #2052 from hydephp/semantic-markdown-heading-perma…
emmadesilva Dec 5, 2024
418044e
Merge attributes dynamically
emmadesilva Dec 5, 2024
4fb8e57
Unwrap unnecessary unwrapping
emmadesilva Dec 5, 2024
f9f9755
Trim empty class directives
emmadesilva Dec 5, 2024
a36760b
Allow side effect of extra space for edge case
emmadesilva Dec 5, 2024
c78ddeb
Inline local variable override
emmadesilva Dec 5, 2024
3feddd3
Revert "Inline local variable override"
emmadesilva Dec 5, 2024
0d9f3e8
Remove todo as it can be handled in Blade
emmadesilva Dec 5, 2024
99e2d1e
Add internal heading registry
emmadesilva Dec 5, 2024
7f40d01
Start duplicate suffixes at two
emmadesilva Dec 5, 2024
c4e67ff
Add suffix to heading identifiers of the same name
emmadesilva Dec 5, 2024
f0adee1
Update RELEASE_NOTES.md
emmadesilva Dec 5, 2024
155c456
Add more test values to ensure proper count
emmadesilva Dec 5, 2024
b959d2d
Cleanup and refactor code
emmadesilva Dec 5, 2024
3df7aa2
Add array types
emmadesilva Dec 5, 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
6 changes: 6 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ This serves two purposes:
- Added new `npm run build` command for compiling frontend assets with Vite
- Added a Vite HMR support for the realtime compiler in https://github.com/hydephp/develop/pull/2016
- Added Vite facade in https://github.com/hydephp/develop/pull/2016
- Added a custom Blade-based heading renderer for Markdown conversions in https://github.com/hydephp/develop/pull/2047

### Changed

Expand Down Expand Up @@ -104,6 +105,8 @@ This serves two purposes:
- Normalized default Tailwind Typography Prose code block styles to match Torchlight's theme, ensuring consistent styling across Markdown and Torchlight code blocks in https://github.com/hydephp/develop/pull/2036.
- Extracted CSS component partials in HydeFront in https://github.com/hydephp/develop/pull/2038
- Replaced HydeFront styles with Tailwind in https://github.com/hydephp/develop/pull/2024
- Markdown headings are now compiled using our custom Blade-based heading renderer in https://github.com/hydephp/develop/pull/2047
- The `id` attributes for heading permalinks have been moved from the anchor to the heading element in https://github.com/hydephp/develop/pull/2052

### Deprecated

Expand Down Expand Up @@ -132,6 +135,8 @@ This serves two purposes:
- This also removes the `<x-hyde::docs.search-input />` and `<x-hyde::docs.search-scripts />` Blade components, replaced by the new `<x-hyde::docs.hyde-search />` component.
- Removed the `.torchlight-enabled` CSS class in https://github.com/hydephp/develop/pull/2036.
- Removed The `hyde.css` file from HydeFront in https://github.com/hydephp/develop/pull/2037 as all styles were refactored to Tailwind in https://github.com/hydephp/develop/pull/2024.
- Removed the `MarkdownService::withPermalinks` method in https://github.com/hydephp/develop/pull/2047
- Removed the `MarkdownService::canEnablePermalinks` method in https://github.com/hydephp/develop/pull/2047

### Fixed

Expand Down Expand Up @@ -522,6 +527,7 @@ The likelihood of impact is low, but if any of the following are true, you may n
- 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
- Removed the `heading-permalinks.css` file as styles are now made using Tailwind

## New features

Expand Down
2 changes: 1 addition & 1 deletion _media/app.css

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions config/markdown.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,22 @@
*/

'prose_classes' => 'prose dark:prose-invert',

/*
|--------------------------------------------------------------------------
| Heading Permalinks Configuration
|--------------------------------------------------------------------------
|
| Here you can specify which page classes should have heading permalinks.
| By default, only documentation pages have permalinks enabled, but you
| are free to enable it for any kind of page by adding the page class.
|
*/

'permalinks' => [
'pages' => [
\Hyde\Pages\DocumentationPage::class,
],
],

];
30 changes: 30 additions & 0 deletions docs/digging-deeper/advanced-markdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,36 @@ anything within the path label will be rendered as HTML. This means you can add
The filepaths are hidden on mobile devices using CSS to prevent them from overlapping with the code block.


## Heading Permalinks

Hyde automatically adds clickable permalink anchors to headings in documentation pages. When you hover over a heading, a `#` link appears that you can click to get a direct link to that section.

### Usage & Configuration

The feature is enabled by default for documentation pages. When enabled, Hyde will automatically add permalink anchors to headings between levels 2-4 (h2-h4). The permalinks are hidden by default and appear when hovering over the heading.

You can enable it for other page types by adding the page class to the `permalinks.pages` array in the `config/markdown.php` file, or disable it for all pages by setting the array to an empty array.

```php
// filepath: config/markdown.php
'permalinks' => [
'pages' => [
\Hyde\Pages\DocumentationPage::class,
],
],
```

### Advanced Customization

Under the hood, Hyde uses a custom Blade-based heading renderer when converting Markdown to HTML. This allows for more flexibility and customization compared to standard Markdown parsers. You can also publish and customize the Blade component used to render the headings:

```bash
php hyde publish:components
```

This will copy the `markdown-heading.blade.php` component to your views directory where you can modify its markup and behavior.


## Dynamic Markdown Links

HydePHP provides a powerful feature for automatically converting Markdown links to source files to the corresponding routes in the built site.
Expand Down
18 changes: 18 additions & 0 deletions packages/framework/config/markdown.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,22 @@
*/

'prose_classes' => 'prose dark:prose-invert',

/*
|--------------------------------------------------------------------------
| Heading Permalinks Configuration
|--------------------------------------------------------------------------
|
| Here you can specify which page classes should have heading permalinks.
| By default, only documentation pages have permalinks enabled, but you
| are free to enable it for any kind of page by adding the page class.
|
*/

'permalinks' => [
'pages' => [
\Hyde\Pages\DocumentationPage::class,
],
],

];
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
@props([
'level' => 1,
'id' => null,
'extraAttributes' => [],
'addPermalink' => config('markdown.permalinks.enabled', true),
])

@php
$tag = 'h' . $level;
$id = $id ?? \Illuminate\Support\Str::slug($slot);

$extraAttributes = array_merge($extraAttributes, [
'id' => $addPermalink ? $id : ($extraAttributes['id'] ?? null),
'class' => trim(($extraAttributes['class'] ?? '') . ($addPermalink ? ' group w-fit scroll-mt-2' : '')),
]);
@endphp

<{{ $tag }} {{ $attributes->merge($extraAttributes) }}>
{!! $slot !!}
@if($addPermalink === true)
<a href="#{{ $id }}" class="heading-permalink opacity-0 ml-1 transition-opacity duration-300 ease-linear px-1 group-hover:opacity-100 focus:opacity-100 group-hover:grayscale-0 focus:grayscale-0" title="Permalink">
#
</a>
@endif
</{{ $tag }}>
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,6 @@ trait SetsUpMarkdownConverter
{
protected function enableDynamicExtensions(): void
{
if ($this->canEnablePermalinks()) {
$this->configurePermalinksExtension();
}

if ($this->canEnableTorchlight()) {
$this->addExtension(TorchlightExtension::class);
}
Expand Down
50 changes: 13 additions & 37 deletions packages/framework/src/Framework/Services/MarkdownService.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
use Hyde\Facades\Config;
use Hyde\Facades\Features;
use Hyde\Markdown\Models\MarkdownDocument;
use Hyde\Markdown\Processing\HeadingRenderer;
use Hyde\Framework\Concerns\Internal\SetsUpMarkdownConverter;
use Hyde\Pages\DocumentationPage;
use Hyde\Markdown\MarkdownConverter;
use Hyde\Markdown\Contracts\MarkdownPreProcessorContract as PreProcessor;
use Hyde\Markdown\Contracts\MarkdownPostProcessorContract as PostProcessor;
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension;
use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
use League\CommonMark\Extension\CommonMark\Node\Block\Heading;

use function str_contains;
use function str_replace;
Expand Down Expand Up @@ -54,6 +55,9 @@ class MarkdownService
/** @var array<class-string<\Hyde\Markdown\Contracts\MarkdownPostProcessorContract>> */
protected array $postprocessors = [];

/** @var array<string> */
protected array $headingRegistry = [];

public function __construct(string $markdown, ?string $pageClass = null)
{
$this->pageClass = $pageClass;
Expand Down Expand Up @@ -87,6 +91,8 @@ protected function setupConverter(): void
$this->initializeExtension($extension);
}

$this->configureCustomHeadingRenderer();

$this->registerPreProcessors();
$this->registerPostProcessors();
}
Expand Down Expand Up @@ -141,13 +147,6 @@ public function addFeature(string $feature): static
return $this;
}

public function withPermalinks(): static
{
$this->addFeature('permalinks');

return $this;
}

public function isDocumentationPage(): bool
{
return isset($this->pageClass) && $this->pageClass === DocumentationPage::class;
Expand All @@ -166,19 +165,6 @@ public function canEnableTorchlight(): bool
Features::hasTorchlight();
}

public function canEnablePermalinks(): bool
{
if ($this->hasFeature('permalinks')) {
return true;
}

if ($this->isDocumentationPage() && DocumentationPage::hasTableOfContents()) {
return true;
}

return false;
}

public function hasFeature(string $feature): bool
{
return in_array($feature, $this->features);
Expand All @@ -200,22 +186,6 @@ protected function injectTorchlightAttribution(): string
));
}

protected function configurePermalinksExtension(): void
{
$this->addExtension(HeadingPermalinkExtension::class);

$this->config = array_merge([
'heading_permalink' => [
'id_prefix' => '',
'fragment_prefix' => '',
'symbol' => '',
'insert' => 'after',
'min_heading_level' => 2,
'aria_hidden' => false,
],
], $this->config);
}

protected function enableAllHtmlElements(): void
{
$this->addExtension(DisallowedRawHtmlExtension::class);
Expand Down Expand Up @@ -272,4 +242,10 @@ protected static function findLineContentPositions(array $lines): array

return [0, 0];
}

protected function configureCustomHeadingRenderer(): void
{
$environment = $this->converter->getEnvironment();
$environment->addRenderer(Heading::class, new HeadingRenderer($this->pageClass, $this->headingRegistry));
}
}
92 changes: 92 additions & 0 deletions packages/framework/src/Markdown/Processing/HeadingRenderer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

declare(strict_types=1);

namespace Hyde\Markdown\Processing;

use Hyde\Pages\DocumentationPage;
use Illuminate\Support\Str;
use League\CommonMark\Extension\CommonMark\Node\Block\Heading;
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;

/**
* Renders a heading node, and supports built-in permalink generation.
*
* @see \League\CommonMark\Extension\CommonMark\Renderer\Block\HeadingRenderer
*/
class HeadingRenderer implements NodeRendererInterface
{
/** @var ?class-string<\Hyde\Pages\Concerns\HydePage> */
protected ?string $pageClass = null;

/** @var array<string> */
protected array $headingRegistry = [];

/** @param ?class-string<\Hyde\Pages\Concerns\HydePage> $pageClass */
public function __construct(string $pageClass = null, array &$headingRegistry = [])
{
$this->pageClass = $pageClass;
$this->headingRegistry = &$headingRegistry;
}

public function render(Node $node, ChildNodeRendererInterface $childRenderer): string
{
if (! ($node instanceof Heading)) {
throw new \InvalidArgumentException('Incompatible node type: '.get_class($node));
}

$content = $childRenderer->renderNodes($node->children());

$rendered = view('hyde::components.markdown-heading', [
'level' => $node->getLevel(),
'slot' => $content,
'id' => $this->makeHeadingId($content),
'addPermalink' => $this->canAddPermalink($content, $node->getLevel()),
'extraAttributes' => $node->data->get('attributes'),
])->render();

return $this->postProcess($rendered);
}

/** @internal */
public function canAddPermalink(string $content, int $level): bool
{
return config('markdown.permalinks.enabled', true)
&& $level >= config('markdown.permalinks.min_level', 2)
&& $level <= config('markdown.permalinks.max_level', 6)
&& ! str_contains($content, 'class="heading-permalink"')
&& in_array($this->pageClass, config('markdown.permalinks.pages', [DocumentationPage::class]));
}

/** @internal */
public function postProcess(string $html): string
{
$html = str_replace('class=""', '', $html);
$html = preg_replace('/<h([1-6]) >/', '<h$1>', $html);

return implode('', array_map('trim', explode("\n", $html)));
}

protected function makeHeadingId(string $contents): string
{
$identifier = $this->ensureIdentifierIsUnique(Str::slug($contents));

$this->headingRegistry[] = $identifier;

return $identifier;
}

protected function ensureIdentifierIsUnique(string $slug): string
{
$identifier = $slug;
$suffix = 2;

while (in_array($identifier, $this->headingRegistry)) {
$identifier = $slug.'-'.$suffix++;
}

return $identifier;
}
}
Loading
Loading