Skip to content

Commit df85b05

Browse files
committed
Introduce menu directive
Toctrees and navigational menus need to add differently in certain aspects: - TOCS in globmode do not add the current page to the glob. This is to avoid adding the index on the level of the other files in that directory, which are regarded as children of index. TOCS change the document tree, however we want to have a menu on each and every page without having to change the document tree - The new MENU directive displays a menu (by default of the main level) without subsections (titlesonly always active) always with glob and it DOES NOT change the document tree - Fixed the bootstrap template for subpage menus, it did not work properly yet. - get rid of the warnings on rendering our own docs that were related to using toctrees on every page
1 parent be2befb commit df85b05

File tree

19 files changed

+455
-105
lines changed

19 files changed

+455
-105
lines changed

docs/include.rst.txt

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,21 @@
33

44
.. rubric:: My Header
55

6-
.. toctree::
7-
:titlesonly:
6+
.. menu::
7+
:menu: mainmenu
88

9-
/about
10-
/usage
11-
/extension/index
12-
/rst-reference/index
9+
/*
10+
/*/index
1311
1412
.. documentblock::
1513
:identifier: navbar
1614

17-
.. toctree::
18-
:titlesonly:
15+
.. menu::
16+
:menu: navbar
1917

2018
/about
2119
/usage
22-
/extension/index
23-
/rst-reference/index
20+
/configuration
2421

2522
.. documentblock::
2623
:identifier: footer

packages/guides-restructured-text/resources/config/guides-restructured-text.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use phpDocumentor\Guides\RestructuredText\Directives\IndexDirective;
2525
use phpDocumentor\Guides\RestructuredText\Directives\LaTeXMain;
2626
use phpDocumentor\Guides\RestructuredText\Directives\LiteralincludeDirective;
27+
use phpDocumentor\Guides\RestructuredText\Directives\MenuDirective;
2728
use phpDocumentor\Guides\RestructuredText\Directives\MetaDirective;
2829
use phpDocumentor\Guides\RestructuredText\Directives\NoteDirective;
2930
use phpDocumentor\Guides\RestructuredText\Directives\OptionMapper\CodeNodeOptionMapper;
@@ -176,6 +177,7 @@
176177
->set(TipDirective::class)
177178
->set(TitleDirective::class)
178179
->set(ToctreeDirective::class)
180+
->set(MenuDirective::class)
179181
->set(TodoDirective::class)
180182
->set(TopicDirective::class)
181183
->set(UmlDirective::class)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace phpDocumentor\Guides\RestructuredText\Directives;
6+
7+
use phpDocumentor\Guides\Nodes\Menu\NavMenuNode;
8+
use phpDocumentor\Guides\Nodes\Node;
9+
use phpDocumentor\Guides\RestructuredText\Parser\Directive;
10+
use phpDocumentor\Guides\RestructuredText\Parser\DirectiveOption;
11+
use phpDocumentor\Guides\RestructuredText\Parser\DocumentParserContext;
12+
use phpDocumentor\Guides\RestructuredText\Toc\ToctreeBuilder;
13+
14+
use function count;
15+
16+
/**
17+
* A menu directives displays a menu in the page. In opposition to a toctree directive the menu
18+
* is for display only. It does not change the position of document in the document tree and can therefore be included
19+
* all pages as navigation.
20+
*
21+
* By default it displays a menu of the pages on level 1 up to level 2.
22+
*/
23+
class MenuDirective extends BaseDirective
24+
{
25+
public function __construct(private readonly ToctreeBuilder $toctreeBuilder)
26+
{
27+
}
28+
29+
public function getName(): string
30+
{
31+
return 'menu';
32+
}
33+
34+
/** {@inheritDoc} */
35+
public function process(
36+
DocumentParserContext $documentParserContext,
37+
Directive $directive,
38+
): Node|null {
39+
$parserContext = $documentParserContext->getParser()->getParserContext();
40+
$options = $directive->getOptions();
41+
$options['glob'] = new DirectiveOption('glob', true);
42+
$options['titlesonly'] = new DirectiveOption('titlesonly', false);
43+
$options['globExclude'] ??= new DirectiveOption('globExclude', 'index,Index');
44+
45+
$toctreeFiles = $this->toctreeBuilder->buildToctreeFiles(
46+
$parserContext,
47+
$documentParserContext->getDocumentIterator(),
48+
$options,
49+
);
50+
if (count($toctreeFiles) === 0) {
51+
$toctreeFiles[] = '/*';
52+
}
53+
54+
return (new NavMenuNode($toctreeFiles))->withOptions($this->optionsToArray($options));
55+
}
56+
}

packages/guides-restructured-text/src/RestructuredText/Directives/ToctreeDirective.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use phpDocumentor\Guides\Nodes\Menu\TocNode;
88
use phpDocumentor\Guides\Nodes\Node;
99
use phpDocumentor\Guides\RestructuredText\Parser\Directive;
10+
use phpDocumentor\Guides\RestructuredText\Parser\DirectiveOption;
1011
use phpDocumentor\Guides\RestructuredText\Parser\DocumentParserContext;
1112
use phpDocumentor\Guides\RestructuredText\Toc\ToctreeBuilder;
1213

@@ -37,6 +38,7 @@ public function process(
3738
): Node|null {
3839
$parserContext = $documentParserContext->getParser()->getParserContext();
3940
$options = $directive->getOptions();
41+
$options['globExclude'] ??= new DirectiveOption('globExclude', 'index,Index');
4042

4143
$toctreeFiles = $this->toctreeBuilder->buildToctreeFiles(
4244
$parserContext,
Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1-
<div class="level-x">
2-
{% for entry in node.menuEntries -%}
3-
{% apply spaceless %}
4-
<a href="{{ renderLink(entry.url) }}"
5-
class="nav-link {% if entry.options.active %} active {% endif %}"
6-
{% if entry.options.active %} aria-current="page" {% endif %} >
7-
{{ renderNode(entry.value.value) }}
8-
</a>
9-
{% endapply %}
10-
{% endfor %}
11-
</div>
1+
2+
<ul class="level-{{ menuEntry.level }}">
3+
{% for entry in menuEntry.children -%}
4+
<li><a href="{{ renderLink(entry.url) }}"
5+
class="nav-link {% if entry.options.active %} active {% endif %}"
6+
{%- if entry.options.active %} aria-current="page" {% endif -%} >
7+
{{ renderNode(entry.value.value) }}
8+
</a>
9+
10+
{%- if entry.children|length %}
11+
{% include "body/menu/mainmenu/menu-level.html.twig" with {
12+
menuEntry:entry
13+
} %}
14+
{%- endif -%}
15+
</li>
16+
{% endfor %}
17+
</ul>
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11

22
<nav class="nav flex-column">
3+
<ul class="level-1">
34
{% for entry in node.menuEntries -%}
4-
<a href="{{ renderLink(entry.url) }}"
5+
<li><a href="{{ renderLink(entry.url) }}"
56
class="nav-link {% if entry.options.active %} active {% endif %}"
67
{%- if entry.options.active %} aria-current="page" {% endif -%} >
78
{{ renderNode(entry.value.value) }}
8-
</a>
9+
</a>
910

1011
{%- if entry.children|length %}
1112
{% include "body/menu/mainmenu/menu-level.html.twig" with {
12-
tocItems:tocItem.children
13+
menuEntry:entry
1314
} %}
1415
{%- endif -%}
16+
</li>
1517
{% endfor %}
18+
</ul>
19+
1620
</nav>

packages/guides-theme-bootstrap/resources/template/body/menu/table-of-content.html.twig renamed to packages/guides-theme-bootstrap/resources/template/body/menu/menu.html.twig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{% if node.options.menu == 'mainmenu' %}
2-
{% include "body/menu/mainmenu/table-of-content.html.twig" %}
2+
{% include "body/menu/mainmenu/menu.html.twig" %}
33
{% elseif node.options.menu == 'navbar' %}
44
{% include "body/menu/navbar/table-of-content.html.twig" %}
55
{% else %}

packages/guides/src/Compiler/NodeTransformers/TocNodeWithDocumentEntryTransformer.php renamed to packages/guides/src/Compiler/NodeTransformers/MenuNodeAddEntryTransformer.php

Lines changed: 82 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use phpDocumentor\Guides\Nodes\DocumentTree\SectionEntryNode;
1111
use phpDocumentor\Guides\Nodes\Menu\MenuEntryNode;
1212
use phpDocumentor\Guides\Nodes\Menu\MenuNode;
13+
use phpDocumentor\Guides\Nodes\Menu\NavMenuNode;
1314
use phpDocumentor\Guides\Nodes\Menu\TocNode;
1415
use phpDocumentor\Guides\Nodes\Node;
1516
use Psr\Log\LoggerInterface;
@@ -18,13 +19,14 @@
1819
use function assert;
1920
use function explode;
2021
use function implode;
22+
use function in_array;
2123
use function preg_match;
2224
use function sprintf;
2325
use function str_replace;
2426
use function str_starts_with;
2527

2628
/** @implements NodeTransformer<MenuNode> */
27-
class TocNodeWithDocumentEntryTransformer implements NodeTransformer
29+
class MenuNodeAddEntryTransformer implements NodeTransformer
2830
{
2931
public function __construct(
3032
private readonly LoggerInterface $logger,
@@ -38,12 +40,13 @@ public function enterNode(Node $node, CompilerContext $compilerContext): Node
3840

3941
public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null
4042
{
41-
if (!$node instanceof TocNode) {
43+
if (!$node instanceof TocNode && !$node instanceof NavMenuNode) {
4244
return $node;
4345
}
4446

4547
$files = $node->getFiles();
4648
$glob = $node->hasOption('glob');
49+
$globExclude = explode(',', $node->getOption('globExclude') . '');
4750

4851
$documentEntries = $compilerContext->getProjectNode()->getAllDocumentEntries();
4952
$currentPath = $compilerContext->getDocumentNode()->getFilePath();
@@ -53,48 +56,25 @@ public function leaveNode(Node $node, CompilerContext $compilerContext): Node|nu
5356
foreach ($files as $file) {
5457
foreach ($documentEntries as $documentEntry) {
5558
if (
56-
!self::isEqualAbsolutePath($documentEntry->getFile(), $file, $currentPath, $glob)
57-
&& !self::isEqualRelativePath($documentEntry->getFile(), $file, $currentPath, $glob)
59+
!self::isEqualAbsolutePath($documentEntry->getFile(), $file, $currentPath, $glob, $globExclude)
60+
&& !self::isEqualRelativePath($documentEntry->getFile(), $file, $currentPath, $glob, $globExclude)
5861
) {
5962
continue;
6063
}
6164

65+
if ($node instanceof TocNode && $glob && self::isCurrent($documentEntry, $currentPath)) {
66+
// TocNodes do not select the current page in glob mode. In a menu we might want to display it
67+
continue;
68+
}
69+
6270
$documentEntriesInTree[] = $documentEntry;
6371
$menuEntry = new MenuEntryNode($documentEntry->getFile(), $documentEntry->getTitle(), [], false, 1);
6472
if (!$node->hasOption('titlesonly')) {
65-
foreach ($documentEntry->getSections() as $section) {
66-
// We do not add the main section as it repeats the document title
67-
foreach ($section->getChildren() as $subSectionEntryNode) {
68-
assert($subSectionEntryNode instanceof SectionEntryNode);
69-
$currentLevel = $menuEntry->getLevel() + 1;
70-
$sectionMenuEntry = new MenuEntryNode($documentEntry->getFile(), $subSectionEntryNode->getTitle(), [], false, $currentLevel, $subSectionEntryNode->getId());
71-
$menuEntry->addSection($sectionMenuEntry);
72-
$this->addSubSections($sectionMenuEntry, $subSectionEntryNode, $documentEntry, $currentLevel);
73-
}
74-
}
73+
$this->addSubSectionsToMenuEntries($documentEntry, $menuEntry);
7574
}
7675

77-
foreach ($documentEntriesInTree as $documentEntryInToc) {
78-
if ($documentEntryInToc->isRoot()) {
79-
// The root page may not be attached to any other
80-
continue;
81-
}
82-
83-
if ($documentEntryInToc->getParent() !== null && $documentEntryInToc->getParent() !== $compilerContext->getDocumentNode()->getDocumentEntry()) {
84-
$this->logger->warning(sprintf(
85-
'Document %s has been added to parents %s and %s',
86-
$documentEntryInToc->getFile(),
87-
$documentEntryInToc->getParent()->getFile(),
88-
$compilerContext->getDocumentNode()->getDocumentEntry()->getFile(),
89-
));
90-
}
91-
92-
if ($documentEntryInToc->getParent() !== null) {
93-
continue;
94-
}
95-
96-
$documentEntryInToc->setParent($compilerContext->getDocumentNode()->getDocumentEntry());
97-
$compilerContext->getDocumentNode()->getDocumentEntry()->addChild($documentEntryInToc);
76+
if ($node instanceof TocNode) {
77+
$this->attachDocumentEntriesToParents($documentEntriesInTree, $compilerContext, $currentPath);
9878
}
9979

10080
$menuEntries[] = $menuEntry;
@@ -110,6 +90,11 @@ public function leaveNode(Node $node, CompilerContext $compilerContext): Node|nu
11090
return $node;
11191
}
11292

93+
private function isCurrent(DocumentEntryNode $documentEntry, string $currentPath): bool
94+
{
95+
return $documentEntry->getFile() === $currentPath;
96+
}
97+
11398
private function addSubSections(MenuEntryNode $sectionMenuEntry, SectionEntryNode $sectionEntryNode, DocumentEntryNode $documentEntry, int $currentLevel): void
11499
{
115100
foreach ($sectionEntryNode->getChildren() as $subSectionEntryNode) {
@@ -120,7 +105,7 @@ private function addSubSections(MenuEntryNode $sectionMenuEntry, SectionEntryNod
120105

121106
public function supports(Node $node): bool
122107
{
123-
return $node instanceof TocNode;
108+
return $node instanceof TocNode || $node instanceof NavMenuNode;
124109
}
125110

126111
public function getPriority(): int
@@ -129,7 +114,8 @@ public function getPriority(): int
129114
return 4500;
130115
}
131116

132-
private static function isEqualAbsolutePath(string $actualFile, string $expectedFile, string $currentFile, bool $glob): bool
117+
/** @param String[] $globExclude */
118+
private static function isEqualAbsolutePath(string $actualFile, string $expectedFile, string $currentFile, bool $glob, array $globExclude): bool
133119
{
134120
if (!self::isAbsoluteFile($expectedFile)) {
135121
return false;
@@ -139,10 +125,11 @@ private static function isEqualAbsolutePath(string $actualFile, string $expected
139125
return true;
140126
}
141127

142-
return self::isGlob($glob, $actualFile, $currentFile, $expectedFile, '/');
128+
return self::isGlob($glob, $actualFile, $currentFile, $expectedFile, '/', $globExclude);
143129
}
144130

145-
private static function isEqualRelativePath(string $actualFile, string $expectedFile, string $currentFile, bool $glob): bool
131+
/** @param String[] $globExclude */
132+
private static function isEqualRelativePath(string $actualFile, string $expectedFile, string $currentFile, bool $glob, array $globExclude): bool
146133
{
147134
if (self::isAbsoluteFile($expectedFile)) {
148135
return false;
@@ -157,12 +144,13 @@ private static function isEqualRelativePath(string $actualFile, string $expected
157144
return true;
158145
}
159146

160-
return self::isGlob($glob, $actualFile, $currentFile, $absoluteExpectedFile, '');
147+
return self::isGlob($glob, $actualFile, $currentFile, $absoluteExpectedFile, '', $globExclude);
161148
}
162149

163-
private static function isGlob(bool $glob, string $documentEntryFile, string $currentPath, string $file, string $prefix): bool
150+
/** @param String[] $globExclude */
151+
private static function isGlob(bool $glob, string $documentEntryFile, string $currentPath, string $file, string $prefix, array $globExclude): bool
164152
{
165-
if ($glob && $documentEntryFile !== $currentPath) {
153+
if ($glob && !in_array($documentEntryFile, $globExclude)) {
166154
$file = str_replace('*', '[^\/]*', $file);
167155
$pattern = '`^' . $file . '$`';
168156

@@ -184,4 +172,56 @@ public static function isAbsoluteFile(string $expectedFile): bool
184172
{
185173
return str_starts_with($expectedFile, '/');
186174
}
175+
176+
/** @param DocumentEntryNode[] $documentEntriesInTree */
177+
private function attachDocumentEntriesToParents(
178+
array $documentEntriesInTree,
179+
CompilerContext $compilerContext,
180+
string $currentPath,
181+
): void {
182+
foreach ($documentEntriesInTree as $documentEntryInToc) {
183+
if ($documentEntryInToc->isRoot() || $currentPath === $documentEntryInToc->getFile()) {
184+
// The root page may not be attached to any other
185+
continue;
186+
}
187+
188+
if ($documentEntryInToc->getParent() !== null && $documentEntryInToc->getParent() !== $compilerContext->getDocumentNode()->getDocumentEntry()) {
189+
$this->logger->warning(sprintf(
190+
'Document %s has been added to parents %s and %s. The `toctree` directive changes the '
191+
. 'position of documents in the document tree. Use the `menu` directive to only display a menu without changing the document tree.',
192+
$documentEntryInToc->getFile(),
193+
$documentEntryInToc->getParent()->getFile(),
194+
$compilerContext->getDocumentNode()->getDocumentEntry()->getFile(),
195+
));
196+
}
197+
198+
if ($documentEntryInToc->getParent() !== null) {
199+
continue;
200+
}
201+
202+
$documentEntryInToc->setParent($compilerContext->getDocumentNode()->getDocumentEntry());
203+
$compilerContext->getDocumentNode()->getDocumentEntry()->addChild($documentEntryInToc);
204+
}
205+
}
206+
207+
private function addSubSectionsToMenuEntries(DocumentEntryNode $documentEntry, MenuEntryNode $menuEntry): void
208+
{
209+
foreach ($documentEntry->getSections() as $section) {
210+
// We do not add the main section as it repeats the document title
211+
foreach ($section->getChildren() as $subSectionEntryNode) {
212+
assert($subSectionEntryNode instanceof SectionEntryNode);
213+
$currentLevel = $menuEntry->getLevel() + 1;
214+
$sectionMenuEntry = new MenuEntryNode(
215+
$documentEntry->getFile(),
216+
$subSectionEntryNode->getTitle(),
217+
[],
218+
false,
219+
$currentLevel,
220+
$subSectionEntryNode->getId(),
221+
);
222+
$menuEntry->addSection($sectionMenuEntry);
223+
$this->addSubSections($sectionMenuEntry, $subSectionEntryNode, $documentEntry, $currentLevel);
224+
}
225+
}
226+
}
187227
}

0 commit comments

Comments
 (0)