Skip to content

Commit 36ae6db

Browse files
authored
Merge pull request #461 from phpDocumentor/menu-4
Enable Multi level Menus
2 parents 716c8aa + 59082c3 commit 36ae6db

File tree

83 files changed

+868
-150
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

83 files changed

+868
-150
lines changed
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<div class="level-x">
22
{% for entry in node.menuEntries -%}
3-
{% spaceless %}
3+
{% apply spaceless %}
44
<a href="{{ renderLink(entry.url) }}"
55
class="nav-link {% if entry.options.active %} active {% endif %}"
66
{% if entry.options.active %} aria-current="page" {% endif %} >
77
{{ renderNode(entry.value.value) }}
88
</a>
9-
{% endspaceless %}
9+
{% endapply %}
1010
{% endfor %}
1111
</div>
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
<ul class="level-{{ node.level }}">
22
{% for entry in node.menuEntries -%}
3-
{% spaceless %}
3+
{% apply spaceless %}
44
<li class="nav-item">
55
<a href="{{ renderLink(entry.url) }}"
66
class="nav-link {% if entry.options.active %} active {% endif %}"
77
{% if entry.options.active %} aria-current="page" {% endif %} >
88
{{ renderNode(entry.value.value) }}
99
</a>
1010
</li>
11-
{% endspaceless %}
11+
{% endapply %}
1212
{%- endfor %}
1313
</ul>

packages/guides/src/Compiler/NodeTransformers/ContentMenuNodeWithSectionEntryTransformer.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@
1515

1616
use function assert;
1717

18-
use const PHP_INT_MAX;
19-
2018
/** @implements NodeTransformer<TocNode> */
2119
class ContentMenuNodeWithSectionEntryTransformer implements NodeTransformer
2220
{
21+
// Setting a default level prevents PHP errors in case of circular references
22+
private const DEFAULT_MAX_LEVELS = 10;
23+
2324
public function enterNode(Node $node, CompilerContext $compilerContext): Node
2425
{
2526
return $node;
@@ -31,7 +32,7 @@ public function leaveNode(Node $node, CompilerContext $compilerContext): Node|nu
3132
return $node;
3233
}
3334

34-
$depth = (int) $node->getOption('depth', PHP_INT_MAX);
35+
$depth = (int) $node->getOption('depth', self::DEFAULT_MAX_LEVELS);
3536
$documentEntry = $compilerContext->getDocumentNode()->getDocumentEntry();
3637

3738
$menuEntries = [];

packages/guides/src/Compiler/NodeTransformers/DocumentEntryRegistrationTransformer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public function leaveNode(Node $node, CompilerContext $compilerContext): Node|nu
3535
$this->logger->warning('Document has not title', $node->getLoggerInformation());
3636
}
3737

38-
$entry = new DocumentEntryNode($node->getFilePath(), $node->getTitle() ?? TitleNode::emptyNode());
38+
$entry = new DocumentEntryNode($node->getFilePath(), $node->getTitle() ?? TitleNode::emptyNode(), $node->isRoot());
3939
$compilerContext->getProjectNode()->addDocumentEntry($entry);
4040

4141
return $node->setDocumentEntry($entry);
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace phpDocumentor\Guides\Compiler\NodeTransformers;
6+
7+
use phpDocumentor\Guides\Compiler\CompilerContext;
8+
use phpDocumentor\Guides\Compiler\NodeTransformer;
9+
use phpDocumentor\Guides\Nodes\DocumentTree\DocumentEntryNode;
10+
use phpDocumentor\Guides\Nodes\Menu\MenuEntryNode;
11+
use phpDocumentor\Guides\Nodes\Menu\MenuNode;
12+
use phpDocumentor\Guides\Nodes\Menu\TocNode;
13+
use phpDocumentor\Guides\Nodes\Node;
14+
15+
/** @implements NodeTransformer<MenuNode> */
16+
class TocNodeSubLevelTransformer implements NodeTransformer
17+
{
18+
// Setting a default level prevents PHP errors in case of circular references
19+
private const DEFAULT_MAX_LEVELS = 10;
20+
21+
public function enterNode(Node $node, CompilerContext $compilerContext): Node
22+
{
23+
return $node;
24+
}
25+
26+
public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null
27+
{
28+
if (!$node instanceof TocNode) {
29+
return $node;
30+
}
31+
32+
$maxDepth = (int) $node->getOption('maxdepth', self::DEFAULT_MAX_LEVELS);
33+
34+
35+
foreach ($node->getMenuEntries() as $menuEntry) {
36+
$documentEntryOfMenuEntry = $compilerContext->getProjectNode()->getDocumentEntry($menuEntry->getUrl());
37+
$this->addSubEntries($menuEntry, $documentEntryOfMenuEntry, $menuEntry->getLevel() + 1, $maxDepth);
38+
}
39+
40+
return $node;
41+
}
42+
43+
private function addSubEntries(
44+
MenuEntryNode $sectionMenuEntry,
45+
DocumentEntryNode $documentEntry,
46+
int $currentLevel,
47+
int $maxDepth,
48+
): void {
49+
if ($maxDepth < $currentLevel) {
50+
return;
51+
}
52+
53+
foreach ($documentEntry->getChildren() as $subDocumentEntryNode) {
54+
$subMenuEntry = new MenuEntryNode(
55+
$subDocumentEntryNode->getFile(),
56+
$subDocumentEntryNode->getTitle(),
57+
[],
58+
false,
59+
$currentLevel,
60+
);
61+
$sectionMenuEntry->addMenuEntry($subMenuEntry);
62+
$this->addSubEntries($subMenuEntry, $subDocumentEntryNode, $currentLevel + 1, $maxDepth);
63+
}
64+
}
65+
66+
public function supports(Node $node): bool
67+
{
68+
return $node instanceof TocNode;
69+
}
70+
71+
public function getPriority(): int
72+
{
73+
// After TocNodeWithDocumentEntryTransformer
74+
return 4000;
75+
}
76+
}

packages/guides/src/Compiler/NodeTransformers/TocNodeWithDocumentEntryTransformer.php

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,25 @@
1212
use phpDocumentor\Guides\Nodes\Menu\MenuNode;
1313
use phpDocumentor\Guides\Nodes\Menu\TocNode;
1414
use phpDocumentor\Guides\Nodes\Node;
15+
use Psr\Log\LoggerInterface;
1516

1617
use function array_pop;
1718
use function assert;
1819
use function explode;
1920
use function implode;
2021
use function preg_match;
22+
use function sprintf;
2123
use function str_replace;
2224
use function str_starts_with;
2325

2426
/** @implements NodeTransformer<MenuNode> */
2527
class TocNodeWithDocumentEntryTransformer implements NodeTransformer
2628
{
29+
public function __construct(
30+
private readonly LoggerInterface $logger,
31+
) {
32+
}
33+
2734
public function enterNode(Node $node, CompilerContext $compilerContext): Node
2835
{
2936
return $node;
@@ -45,7 +52,10 @@ public function leaveNode(Node $node, CompilerContext $compilerContext): Node|nu
4552

4653
foreach ($files as $file) {
4754
foreach ($documentEntries as $documentEntry) {
48-
if (!$this->isEqualAbsolutePath($documentEntry, $file, $currentPath, $glob) && !$this->isEqualRelativePath($documentEntry, $file, $currentPath, $glob)) {
55+
if (
56+
!self::isEqualAbsolutePath($documentEntry->getFile(), $file, $currentPath, $glob)
57+
&& !self::isEqualRelativePath($documentEntry->getFile(), $file, $currentPath, $glob)
58+
) {
4959
continue;
5060
}
5161

@@ -64,6 +74,29 @@ public function leaveNode(Node $node, CompilerContext $compilerContext): Node|nu
6474
}
6575
}
6676

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);
98+
}
99+
67100
$menuEntries[] = $menuEntry;
68101
if (!$glob) {
69102
// If glob is not set, there may only be one result per listed file
@@ -96,42 +129,59 @@ public function getPriority(): int
96129
return 4500;
97130
}
98131

99-
private function isEqualAbsolutePath(DocumentEntryNode $documentEntry, string $file, string $currentPath, bool $glob): bool
132+
private static function isEqualAbsolutePath(string $actualFile, string $expectedFile, string $currentFile, bool $glob): bool
100133
{
101-
if ($file === '/' . $documentEntry->getFile()) {
134+
if (!self::isAbsoluteFile($expectedFile)) {
135+
return false;
136+
}
137+
138+
if ($expectedFile === '/' . $actualFile) {
102139
return true;
103140
}
104141

105-
return $this->isGlob($glob, $documentEntry->getFile(), $currentPath, $file, '/');
142+
return self::isGlob($glob, $actualFile, $currentFile, $expectedFile, '/');
106143
}
107144

108-
private function isEqualRelativePath(DocumentEntryNode $documentEntry, string $file, string $currentPath, bool $glob): bool
145+
private static function isEqualRelativePath(string $actualFile, string $expectedFile, string $currentFile, bool $glob): bool
109146
{
110-
if (str_starts_with($file, '/')) {
147+
if (self::isAbsoluteFile($expectedFile)) {
111148
return false;
112149
}
113150

114-
$current = explode('/', $currentPath);
151+
$current = explode('/', $currentFile);
115152
array_pop($current);
116-
$current[] = $file;
117-
$absolute = implode('/', $current);
153+
$current[] = $expectedFile;
154+
$absoluteExpectedFile = implode('/', $current);
118155

119-
if ($absolute === $documentEntry->getFile()) {
156+
if ($absoluteExpectedFile === $actualFile) {
120157
return true;
121158
}
122159

123-
return $this->isGlob($glob, $documentEntry->getFile(), $currentPath, $file, '');
160+
return self::isGlob($glob, $actualFile, $currentFile, $absoluteExpectedFile, '');
124161
}
125162

126-
private function isGlob(bool $glob, string $documentEntryFile, string $currentPath, string $file, string $prefix): bool
163+
private static function isGlob(bool $glob, string $documentEntryFile, string $currentPath, string $file, string $prefix): bool
127164
{
128165
if ($glob && $documentEntryFile !== $currentPath) {
129-
$file = str_replace('*', '[a-zA-Z0-9]*', $file);
166+
$file = str_replace('*', '[^\/]*', $file);
130167
$pattern = '`^' . $file . '$`';
131168

132169
return preg_match($pattern, $prefix . $documentEntryFile) > 0;
133170
}
134171

135172
return false;
136173
}
174+
175+
public static function isPatternMatchingFile(string $absoluteExpectedFile, string $actualFile): bool
176+
{
177+
$pattern = str_replace('*', '[a-zA-Z0-9-_]*', $absoluteExpectedFile);
178+
$pattern = '`^' . $pattern . '$`';
179+
180+
return preg_match($pattern, $actualFile) > 0;
181+
}
182+
183+
public static function isAbsoluteFile(string $expectedFile): bool
184+
{
185+
return str_starts_with($expectedFile, '/');
186+
}
137187
}

packages/guides/src/Handlers/ParseDirectoryHandler.php

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public function handle(ParseDirectoryCommand $command): array
2929
$currentDirectory = $command->getDirectory();
3030
$extension = $command->getInputFormat();
3131

32-
$this->guardThatAnIndexFileExists(
32+
$indexName = $this->getDirectoryIndexFile(
3333
$origin,
3434
$currentDirectory,
3535
$extension,
@@ -39,34 +39,31 @@ public function handle(ParseDirectoryCommand $command): array
3939
$documents = [];
4040
foreach ($files as $file) {
4141
$documents[] = $this->commandBus->handle(
42-
new ParseFileCommand($origin, $currentDirectory, $file, $extension, 1, $command->getProjectNode()),
42+
new ParseFileCommand($origin, $currentDirectory, $file, $extension, 1, $command->getProjectNode(), $indexName === $file),
4343
);
4444
}
4545

4646
return $documents;
4747
}
4848

49-
private function guardThatAnIndexFileExists(
49+
private function getDirectoryIndexFile(
5050
FilesystemInterface $filesystem,
5151
string $directory,
5252
string $sourceFormat,
53-
): void {
53+
): string {
5454
$extension = $sourceFormat;
5555
$hasIndexFile = false;
5656
foreach (self::INDEX_FILE_NAMES as $indexName) {
5757
$indexFilename = sprintf('%s.%s', $indexName, $extension);
5858
if ($filesystem->has($directory . '/' . $indexFilename)) {
59-
$hasIndexFile = true;
60-
break;
59+
return $indexName;
6160
}
6261
}
6362

64-
if (!$hasIndexFile) {
65-
$indexFilename = sprintf('%s.%s', self::INDEX_FILE_NAMES[0], $extension);
63+
$indexFilename = sprintf('%s.%s', self::INDEX_FILE_NAMES[0], $extension);
6664

67-
throw new InvalidArgumentException(
68-
sprintf('Could not find index file "%s" in "%s"', $indexFilename, $directory),
69-
);
70-
}
65+
throw new InvalidArgumentException(
66+
sprintf('Could not find index file "%s" in "%s"', $indexFilename, $directory),
67+
);
7168
}
7269
}

packages/guides/src/Handlers/ParseFileCommand.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public function __construct(
2525
private readonly string $extension,
2626
private readonly int $initialHeaderLevel,
2727
private readonly ProjectNode $projectNode,
28+
private readonly bool $isRoot,
2829
) {
2930
}
3031

@@ -57,4 +58,9 @@ public function getProjectNode(): ProjectNode
5758
{
5859
return $this->projectNode;
5960
}
61+
62+
public function isRoot(): bool
63+
{
64+
return $this->isRoot;
65+
}
6066
}

packages/guides/src/Handlers/ParseFileHandler.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public function handle(ParseFileCommand $command): DocumentNode|null
4949
$command->getExtension(),
5050
$command->getInitialHeaderLevel(),
5151
$command->getProjectNode(),
52+
$command->isRoot(),
5253
);
5354
}
5455

@@ -74,6 +75,7 @@ private function createDocument(
7475
string $extension,
7576
int $initialHeaderLevel,
7677
ProjectNode $projectNode,
78+
bool $isRoot,
7779
): DocumentNode|null {
7880
$path = $this->buildPathOnFileSystem($fileName, $documentFolder, $extension);
7981
$fileContents = $this->getFileContents($origin, $path);
@@ -93,7 +95,7 @@ private function createDocument(
9395

9496
$document = null;
9597
try {
96-
$document = $this->parser->parse($preParseDocumentEvent->getContents(), $extension);
98+
$document = $this->parser->parse($preParseDocumentEvent->getContents(), $extension)->withIsRoot($isRoot);
9799
} catch (RuntimeException) {
98100
$this->logger->error(
99101
sprintf('Unable to parse %s, input format was not recognized', $path),

0 commit comments

Comments
 (0)