Skip to content

Commit c4574c1

Browse files
authored
Merge pull request #496 from linawolf/navmenu
Introduce menu directive
2 parents be2befb + df85b05 commit c4574c1

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)