mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-09-25 09:53:08 +02:00
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).
139 lines
4.8 KiB
PHP
139 lines
4.8 KiB
PHP
<?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(''));
|
|
}
|
|
}
|