Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[2.x] Normalize Markdown heading identifiers #2059

Merged
merged 23 commits into from
Dec 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use Hyde\Facades\Config;
use Hyde\Markdown\Models\Markdown;
use Illuminate\Support\Str;
use Hyde\Markdown\Processing\HeadingRenderer;

/**
* Generates a nested table of contents from Markdown headings.
Expand Down Expand Up @@ -117,7 +117,7 @@ protected function createHeadingEntry(array $headingData): array
return [
'level' => $headingData['level'],
'title' => $headingData['title'],
'slug' => Str::slug($headingData['title']),
'slug' => HeadingRenderer::makeIdentifier($headingData['title']),
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public function postProcess(string $html): string

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

$this->headingRegistry[] = $identifier;

Expand All @@ -89,4 +89,10 @@ protected function ensureIdentifierIsUnique(string $slug): string

return $identifier;
}

/** @internal */
public static function makeIdentifier(string $title): string
{
return e(Str::slug(Str::transliterate(html_entity_decode($title)), dictionary: ['@' => 'at', '&' => 'and', '<' => '', '>' => '']));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,8 @@ public function testHeadingsWithSpecialCharacters()
$this->assertStringContainsString('Heading with &amp; special &lt; &gt; &quot;characters&quot;', $html);
$this->assertStringContainsString('Heading with émojis 🎉', $html);

// Todo: Try to normalize to heading-with-special-characters?
$this->assertSame(<<<'HTML'
<h2 id="heading-with-amp-special-lt-gt-quotcharactersquot" class="group w-fit scroll-mt-2">Heading with &amp; special &lt; &gt; &quot;characters&quot;<a href="#heading-with-amp-special-lt-gt-quotcharactersquot" 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></h2>
<h2 id="heading-with-and-special-characters" class="group w-fit scroll-mt-2">Heading with &amp; special &lt; &gt; &quot;characters&quot;<a href="#heading-with-and-special-characters" 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></h2>
<h3 id="heading-with-emojis" class="group w-fit scroll-mt-2">Heading with émojis 🎉<a href="#heading-with-emojis" 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></h3>

HTML, $html);
Expand Down
58 changes: 58 additions & 0 deletions packages/framework/tests/Unit/HeadingRendererUnitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class HeadingRendererUnitTest extends UnitTestCase
use UsesRealBladeInUnitTests;

protected static bool $needsConfig = true;
protected static bool $needsKernel = true;
protected static ?array $cachedConfig = null;

protected function setUp(): void
Expand Down Expand Up @@ -233,6 +234,23 @@ public function testPostProcessHandlesNoHeadingTags()
$this->assertSame('<p>Paragraph</p>', (new HeadingRenderer())->postProcess($html));
}

/**
* @dataProvider headingIdentifierProvider
*/
public function testHeadingIdentifierGeneration($input, $expected)
{
$this->assertSame($expected, HeadingRenderer::makeIdentifier($input));
}

/**
* @dataProvider headingIdentifierProvider
*/
public function testHeadingIdentifierGenerationWithEscapedInput($input, $expected)
{
$this->assertSame(HeadingRenderer::makeIdentifier($input), HeadingRenderer::makeIdentifier(e($input)));
$this->assertSame($expected, HeadingRenderer::makeIdentifier(e($input)));
}

protected function mockChildNodeRenderer(string $contents = 'Test Heading'): ChildNodeRendererInterface
{
$childRenderer = Mockery::mock(ChildNodeRendererInterface::class);
Expand All @@ -255,4 +273,44 @@ protected function validateHeadingPermalinkStates(HeadingRenderer $renderer, Chi
}
}
}

public static function headingIdentifierProvider(): array
{
return [
// Basic cases
['Hello World', 'hello-world'],
['Simple Heading', 'simple-heading'],
['Heading With Numbers 123', 'heading-with-numbers-123'],

// Special characters
['Heading with & symbol', 'heading-with-and-symbol'],
['Heading with < > symbols', 'heading-with-symbols'],
['Heading with "quotes"', 'heading-with-quotes'],
['Heading with / and \\', 'heading-with-and'],
['Heading with punctuation!?!', 'heading-with-punctuation'],
['Hyphenated-heading-name', 'hyphenated-heading-name'],

// Emojis
['Heading with emoji 🎉', 'heading-with-emoji'],
['Another emoji 🤔 test', 'another-emoji-test'],
['Multiple emojis 🎉🤔✨', 'multiple-emojis'],

// Accented and non-ASCII characters
['Accented é character', 'accented-e-character'],
['Café Crème', 'cafe-creme'],
['Łódź and święto', 'lodz-and-swieto'],
['中文标题', 'zhong-wen-biao-ti'],
['日本語の見出し', 'ri-ben-yu-nojian-chu-shi'],
['한국어 제목', 'hangugeo-jemog'],

// Edge cases
[' Leading spaces', 'leading-spaces'],
['Trailing spaces ', 'trailing-spaces'],
[' Surrounded by spaces ', 'surrounded-by-spaces'],
['----', ''],
['%%%%%%%', ''],
[' ', ''],
['1234567890', '1234567890'],
];
}
}
Loading