Skip to content

Commit

Permalink
Merge pull request #2059 from hydephp/normalize-markdown-heading-iden…
Browse files Browse the repository at this point in the history
…tifiers

[2.x] Normalize Markdown heading identifiers
  • Loading branch information
caendesilva authored Dec 7, 2024
2 parents 3cc57cb + 50f89f8 commit 0a6bbfa
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 5 deletions.
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'],
];
}
}

0 comments on commit 0a6bbfa

Please sign in to comment.