Skip to content

Commit f34eaab

Browse files
committed
bug #156 Fix the TOC generation with unique links (javiereguiluz)
This PR was squashed before being merged into the main branch. Discussion ---------- Fix the TOC generation with unique links Still WIP. It fixes #155. I can't find any way of getting the `id` attributes of headings without changing the vendor dependencies ... so this proposes to just parse the generated HTML file and extract the information from it. Commits ------- 87859d7 Fix the TOC generation with unique links
2 parents e74e0c6 + 87859d7 commit f34eaab

File tree

7 files changed

+122
-29
lines changed

7 files changed

+122
-29
lines changed

src/DocsKernel.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use SymfonyDocsBuilder\Listener\AdmonitionListener;
2121
use SymfonyDocsBuilder\Listener\AssetsCopyListener;
2222
use SymfonyDocsBuilder\Listener\CopyImagesListener;
23+
use SymfonyDocsBuilder\Listener\DuplicatedHeaderIdListener;
2324

2425
class DocsKernel extends Kernel
2526
{
@@ -49,6 +50,11 @@ private function initializeListeners(EventManager $eventManager, ErrorManager $e
4950
new AdmonitionListener()
5051
);
5152

53+
$eventManager->addEventListener(
54+
PreParseDocumentEvent::PRE_PARSE_DOCUMENT,
55+
new DuplicatedHeaderIdListener()
56+
);
57+
5258
$eventManager->addEventListener(
5359
PreNodeRenderEvent::PRE_NODE_RENDER,
5460
new CopyImagesListener($this->buildConfig, $errorManager)

src/Generator/JsonGenerator.php

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public function generateJson(string $masterDocument = 'index'): array
6969
$crawler = new Crawler(file_get_contents($this->buildConfig->getOutputDir().'/'.$filename.'.html'));
7070

7171
// happens when some doc is a partial included in other doc an it doesn't have any titles
72-
$toc = false === current($metaEntry->getTitles()) ? [] : $this->generateToc($metaEntry, current($metaEntry->getTitles())[1]);
72+
$toc = $this->generateToc($metaEntry, $crawler);
7373
$next = $this->determineNext($parserFilename, $flattenedTocTree, $masterDocument);
7474
$prev = $this->determinePrev($parserFilename, $flattenedTocTree);
7575
$data = [
@@ -102,26 +102,35 @@ public function setOutput(SymfonyStyle $output)
102102
$this->output = $output;
103103
}
104104

105-
private function generateToc(MetaEntry $metaEntry, ?array $titles, int $level = 1): array
105+
private function generateToc(MetaEntry $metaEntry, Crawler $crawler): array
106106
{
107-
if (null === $titles) {
108-
return [];
107+
$flatTocTree = [];
108+
109+
foreach ($crawler->filter('h2, h3') as $heading) {
110+
$headerId = $heading->getAttribute('id') ?? Environment::slugify($heading->textContent);
111+
112+
// this tocTree stores items sequentially (h2, h2, h3, h3, h2, h3, etc.)
113+
$flatTocTree[] = [
114+
'level' => 'h2' === $heading->tagName ? 1 : 2,
115+
'url' => sprintf('%s#%s', $metaEntry->getUrl(), $headerId),
116+
'page' => u($metaEntry->getUrl())->beforeLast('.html')->toString(),
117+
'fragment' => $headerId,
118+
'title' => $heading->textContent,
119+
'children' => [],
120+
];
109121
}
110122

111-
$tocTree = [];
112-
113-
foreach ($titles as $title) {
114-
$tocTree[] = [
115-
'level' => $level,
116-
'url' => sprintf('%s#%s', $metaEntry->getUrl(), Environment::slugify($title[0])),
117-
'page' => u($metaEntry->getUrl())->beforeLast('.html'),
118-
'fragment' => Environment::slugify($title[0]),
119-
'title' => $title[0],
120-
'children' => $this->generateToc($metaEntry, $title[1], $level + 1),
121-
];
123+
// this tocTree stores items nested by level (h2, h2[h3, h3], h2[h3], etc.)
124+
$nestedTocTree = [];
125+
foreach ($flatTocTree as $tocItem) {
126+
if (1 === $tocItem['level']) {
127+
$nestedTocTree[] = $tocItem;
128+
} else {
129+
$nestedTocTree[\count($nestedTocTree) - 1]['children'][] = $tocItem;
130+
}
122131
}
123132

124-
return $tocTree;
133+
return $nestedTocTree;
125134
}
126135

127136
private function determineNext(string $parserFilename, array $flattenedTocTree): ?array
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the Docs Builder package.
7+
* (c) Ryan Weaver <ryan@symfonycasts.com>
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace SymfonyDocsBuilder\Listener;
13+
14+
use Doctrine\RST\Event\PreParseDocumentEvent;
15+
use SymfonyDocsBuilder\Renderers\TitleNodeRenderer;
16+
17+
final class DuplicatedHeaderIdListener
18+
{
19+
public function preParseDocument(PreParseDocumentEvent $event): void
20+
{
21+
// needed because we only need to handle duplicated headers within
22+
// the same file, not across all the files being generated
23+
TitleNodeRenderer::resetHeaderIdCache();
24+
}
25+
}

src/Renderers/TitleNodeRenderer.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ public function __construct(TitleNode $titleNode, TemplateRenderer $templateRend
3232
$this->templateRenderer = $templateRenderer;
3333
}
3434

35+
public static function resetHeaderIdCache(): void
36+
{
37+
self::$idUsagesCountByFilename = [];
38+
}
39+
3540
public function render(): string
3641
{
3742
$filename = $this->titleNode->getEnvironment()->getCurrentFileName();

tests/IntegrationTest.php

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,6 @@
2121

2222
class IntegrationTest extends AbstractIntegrationTest
2323
{
24-
public static function setUpBeforeClass(): void
25-
{
26-
$reflection = new \ReflectionClass(TitleNodeRenderer::class);
27-
$property = $reflection->getProperty('idUsagesCountByFilename');
28-
$property->setAccessible(true);
29-
30-
$property->setValue([]);
31-
}
32-
3324
/**
3425
* @dataProvider integrationProvider
3526
*/

tests/JsonIntegrationTest.php

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
namespace SymfonyDocsBuilder\Tests;
1111

1212
use SymfonyDocsBuilder\DocBuilder;
13+
use SymfonyDocsBuilder\Renderers\TitleNodeRenderer;
1314

1415
class JsonIntegrationTest extends AbstractIntegrationTest
1516
{
@@ -26,7 +27,7 @@ public function testJsonGeneration(string $filename, array $expectedData)
2627
$actualFileData = $fJsons[$filename];
2728
foreach ($expectedData as $key => $expectedKeyData) {
2829
$this->assertArrayHasKey($key, $actualFileData, sprintf('Missing key "%s" in file "%s"', $key, $filename));
29-
$this->assertSame($expectedData[$key], $actualFileData[$key], sprintf('Invalid data for key "%s" in file "%s"', $key, $filename));
30+
$this->assertSame($expectedKeyData, $actualFileData[$key], sprintf('Invalid data for key "%s" in file "%s"', $key, $filename));
3031
}
3132
}
3233

@@ -76,9 +77,53 @@ public function getJsonTests()
7677
'title' => 'Design',
7778
'toc_options' => [
7879
'maxDepth' => 2,
79-
'numVisibleItems' => 3,
80+
'numVisibleItems' => 5,
8081
'size' => 'md'
8182
],
83+
'toc' => [
84+
[
85+
'level' => 1,
86+
'url' => 'design.html#section-1',
87+
'page' => 'design',
88+
'fragment' => 'section-1',
89+
'title' => 'Section 1',
90+
'children' => [
91+
[
92+
'level' => 2,
93+
'url' => 'design.html#some-subsection',
94+
'page' => 'design',
95+
'fragment' => 'some-subsection',
96+
'title' => 'Some subsection',
97+
'children' => [],
98+
],
99+
[
100+
'level' => 2,
101+
'url' => 'design.html#some-subsection-1',
102+
'page' => 'design',
103+
'fragment' => 'some-subsection-1',
104+
'title' => 'Some subsection',
105+
'children' => [],
106+
],
107+
],
108+
],
109+
[
110+
'level' => 1,
111+
'url' => 'design.html#section-2',
112+
'page' => 'design',
113+
'fragment' => 'section-2',
114+
'title' => 'Section 2',
115+
'children' => [
116+
[
117+
'level' => 2,
118+
'url' => 'design.html#some-subsection-2',
119+
'page' => 'design',
120+
'fragment' => 'some-subsection-2',
121+
'title' => 'Some subsection',
122+
'children' => [],
123+
],
124+
],
125+
],
126+
],
82127
],
83128
];
84129

tests/fixtures/source/json/design.rst

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,30 @@ The toctree below should affects the next/prev. The
1111
first entry is effectively ignored, as it was already
1212
included by the toctree in index.rst (which is parsed first).
1313

14-
Subsection 1
15-
~~~~~~~~~~~~
14+
Some subsection
15+
~~~~~~~~~~~~~~~
1616

1717
This is a subsection of the first section. That's all.
1818

19+
Some subsection
20+
~~~~~~~~~~~~~~~
21+
22+
This sub-section uses the same title as before to test that the tool
23+
never generated two or more headings with the same ID.
24+
1925
Section 2
2026
---------
2127

2228
However, crud (which is ALSO included in the toctree in index.rst),
2329
WILL be read here, as the "crud" in index.rst has not been read
2430
yet (design comes first). Also, design/sub-page WILL be considered.
2531

32+
Some subsection
33+
~~~~~~~~~~~~~~~
34+
35+
This sub-section also uses the same title as in the previous section
36+
to test that the tool never generated two or more headings with the same ID.
37+
2638
.. toctree::
2739
:maxdepth: 1
2840

0 commit comments

Comments
 (0)