mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-09-25 01:53:03 +02:00
fix(toc): add trailing bottom spacing when the TOC scrolls
The desktop TOC is its own scroll container (overflow-y:auto + max-height), but unlike article content — which gets trailing space via .page-content > article::after — it had nothing below the last link. A long TOC scrolled to the bottom left the final link flush against the edge. Add .toc::after as a block pseudo-element inside the desktop media query, using the same technique (and the same Firefox-clips-padding-bottom reason) as the article. Shared component, so public (about/charte/licence) and admin (#admin-toc) both get it. Verified with Playwright: 0px -> ~60px gap; mobile unaffected (TOC is not a scroll container there).
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension;
|
||||
|
||||
/**
|
||||
* MarkdownHelperTest — TOC extraction from markdown headings.
|
||||
*
|
||||
* Regression: TOC labels used to show raw markdown (`*Licence*`, `**Gras**`,
|
||||
* `` `code` ``), because the raw heading text was passed straight to the
|
||||
* template. Labels must be plain text and hrefs must match the heading ids
|
||||
* CommonMark generates on the rendered page.
|
||||
*/
|
||||
class MarkdownHelperTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Render markdown exactly like the content controllers do, and return the
|
||||
* heading ids in document order.
|
||||
*/
|
||||
private function renderedHeadingIds(string $markdown): array
|
||||
{
|
||||
$converter = new CommonMarkConverter([
|
||||
'html_input' => 'strip',
|
||||
'heading_permalink' => [
|
||||
'apply_id_to_heading' => true,
|
||||
'id_prefix' => '',
|
||||
'insert' => 'before',
|
||||
'aria_hidden' => true,
|
||||
],
|
||||
]);
|
||||
$converter->getEnvironment()->addExtension(new HeadingPermalinkExtension());
|
||||
$html = $converter->convert($markdown)->getContent();
|
||||
|
||||
preg_match_all('/<h[1-3] id="([^"]+)"/', $html, $matches);
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
// ── Labels are plain text ─────────────────────────────────────────────────
|
||||
|
||||
public function testItalicMarkupIsStrippedFromLabel(): void
|
||||
{
|
||||
$toc = MarkdownHelper::extractToc('## *Licence* CC');
|
||||
|
||||
$this->assertSame('Licence CC', $toc[0]['label']);
|
||||
}
|
||||
|
||||
public function testBoldMarkupIsStrippedFromLabel(): void
|
||||
{
|
||||
$toc = MarkdownHelper::extractToc('## **Gros** gras');
|
||||
|
||||
$this->assertSame('Gros gras', $toc[0]['label']);
|
||||
}
|
||||
|
||||
public function testInlineCodeMarkupIsStrippedFromLabel(): void
|
||||
{
|
||||
$toc = MarkdownHelper::extractToc('## `code` inline');
|
||||
|
||||
$this->assertSame('code inline', $toc[0]['label']);
|
||||
}
|
||||
|
||||
public function testLinkKeepsLabelTextNotUrl(): void
|
||||
{
|
||||
$toc = MarkdownHelper::extractToc('## [lien](http://x) ici');
|
||||
|
||||
$this->assertSame('lien ici', $toc[0]['label']);
|
||||
}
|
||||
|
||||
public function testNestedMarkupIsFullyStripped(): void
|
||||
{
|
||||
$toc = MarkdownHelper::extractToc('## *[lien](http://x)* et `code`');
|
||||
|
||||
$this->assertSame('lien et code', $toc[0]['label']);
|
||||
}
|
||||
|
||||
public function testNoMarkdownSyntaxLeaksIntoAnyLabel(): void
|
||||
{
|
||||
$markdown = "## *Italique*\n\n## **Gras**\n\n## `Code`\n\n## [Lien](http://x)\n";
|
||||
$labels = array_column(MarkdownHelper::extractToc($markdown), 'label');
|
||||
|
||||
foreach ($labels as $label) {
|
||||
$this->assertDoesNotMatchRegularExpression('/[*`]|\[|\]\(/', $label);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Hrefs match rendered heading ids ──────────────────────────────────────
|
||||
|
||||
public function testHrefsMatchRenderedHeadingIdsForInlineMarkup(): void
|
||||
{
|
||||
$markdown = "## *Licence* CC\n\n## **Gros** gras\n\n## `code` inline\n\n## [lien](http://x) ici\n";
|
||||
|
||||
$toc = MarkdownHelper::extractToc($markdown);
|
||||
$headingIds = $this->renderedHeadingIds($markdown);
|
||||
|
||||
foreach ($toc as $index => $item) {
|
||||
$this->assertSame('#' . $headingIds[$index], $item['href'], "TOC href mismatch at index {$index}");
|
||||
}
|
||||
}
|
||||
|
||||
public function testHrefsMatchRenderedHeadingIdsForAccents(): void
|
||||
{
|
||||
$markdown = "## Été accents\n\n## Emoji 🎉 fin\n";
|
||||
|
||||
$toc = MarkdownHelper::extractToc($markdown);
|
||||
$headingIds = $this->renderedHeadingIds($markdown);
|
||||
|
||||
foreach ($toc as $index => $item) {
|
||||
$this->assertSame('#' . $headingIds[$index], $item['href'], "TOC href mismatch at index {$index}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Levels and structure ──────────────────────────────────────────────────
|
||||
|
||||
public function testLevelsAreCapturedFromHashes(): void
|
||||
{
|
||||
$markdown = "# Un\n\n## Deux\n\n### Trois\n";
|
||||
$toc = MarkdownHelper::extractToc($markdown);
|
||||
|
||||
$this->assertSame([1, 2, 3], array_column($toc, 'level'));
|
||||
}
|
||||
|
||||
public function testHeadingsDeeperThanThreeAreIgnored(): void
|
||||
{
|
||||
$toc = MarkdownHelper::extractToc("# Un\n\n#### Quatre\n");
|
||||
|
||||
$this->assertCount(1, $toc);
|
||||
}
|
||||
|
||||
public function testContentWithoutHeadingsYieldsEmptyToc(): void
|
||||
{
|
||||
$this->assertSame([], MarkdownHelper::extractToc("Juste du texte.\n\nEt un paragraphe."));
|
||||
}
|
||||
|
||||
public function testEmptyContentYieldsEmptyToc(): void
|
||||
{
|
||||
$this->assertSame([], MarkdownHelper::extractToc(''));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user