diff --git a/packages/guides/src/Compiler/NodeTransformers/MenuNodeTransformers/ExternalMenuEntryNodeTransformer.php b/packages/guides/src/Compiler/NodeTransformers/MenuNodeTransformers/ExternalMenuEntryNodeTransformer.php new file mode 100644 index 000000000..da464c2b2 --- /dev/null +++ b/packages/guides/src/Compiler/NodeTransformers/MenuNodeTransformers/ExternalMenuEntryNodeTransformer.php @@ -0,0 +1,66 @@ + */ + protected function handleMenuEntry(MenuNode $currentMenu, MenuEntryNode $entryNode, CompilerContext $compilerContext): array + { + assert($entryNode instanceof ExternalMenuEntryNode); + + $newEntryNode = new ExternalEntryNode( + $entryNode->getUrl(), + ($entryNode->getValue() ?? TitleNode::emptyNode())->toString(), + ); + + if ($currentMenu instanceof TocNode) { + $this->attachDocumentEntriesToParents([$newEntryNode], $compilerContext, ''); + } + + return [$entryNode]; + } + + public function getPriority(): int + { + // After DocumentEntryTransformer + return 4500; + } +} diff --git a/packages/guides/src/Compiler/NodeTransformers/MenuNodeTransformers/MenuEntryManagement.php b/packages/guides/src/Compiler/NodeTransformers/MenuNodeTransformers/MenuEntryManagement.php index c2a9262a2..a1ee0b245 100644 --- a/packages/guides/src/Compiler/NodeTransformers/MenuNodeTransformers/MenuEntryManagement.php +++ b/packages/guides/src/Compiler/NodeTransformers/MenuNodeTransformers/MenuEntryManagement.php @@ -15,40 +15,43 @@ use phpDocumentor\Guides\Compiler\CompilerContext; use phpDocumentor\Guides\Nodes\DocumentTree\DocumentEntryNode; +use phpDocumentor\Guides\Nodes\DocumentTree\ExternalEntryNode; use function sprintf; use function str_starts_with; trait MenuEntryManagement { - /** @param DocumentEntryNode[] $documentEntriesInTree */ + /** @param array $entryNodes */ private function attachDocumentEntriesToParents( - array $documentEntriesInTree, + array $entryNodes, CompilerContext $compilerContext, string $currentPath, ): void { - foreach ($documentEntriesInTree as $documentEntryInToc) { - if ($documentEntryInToc->isRoot() || $currentPath === $documentEntryInToc->getFile()) { - // The root page may not be attached to any other - continue; - } + foreach ($entryNodes as $entryNode) { + if ($entryNode instanceof DocumentEntryNode) { + if (($entryNode->isRoot() || $currentPath === $entryNode->getFile())) { + // The root page may not be attached to any other + continue; + } - if ($documentEntryInToc->getParent() !== null && $documentEntryInToc->getParent() !== $compilerContext->getDocumentNode()->getDocumentEntry()) { - $this->logger->warning(sprintf( - 'Document %s has been added to parents %s and %s. The `toctree` directive changes the ' - . 'position of documents in the document tree. Use the `menu` directive to only display a menu without changing the document tree.', - $documentEntryInToc->getFile(), - $documentEntryInToc->getParent()->getFile(), - $compilerContext->getDocumentNode()->getDocumentEntry()->getFile(), - ), $compilerContext->getLoggerInformation()); - } + if ($entryNode->getParent() !== null && $entryNode->getParent() !== $compilerContext->getDocumentNode()->getDocumentEntry()) { + $this->logger->warning(sprintf( + 'Document %s has been added to parents %s and %s. The `toctree` directive changes the ' + . 'position of documents in the document tree. Use the `menu` directive to only display a menu without changing the document tree.', + $entryNode->getFile(), + $entryNode->getParent()->getFile(), + $compilerContext->getDocumentNode()->getDocumentEntry()->getFile(), + ), $compilerContext->getLoggerInformation()); + } - if ($documentEntryInToc->getParent() !== null) { - continue; + if ($entryNode->getParent() !== null) { + continue; + } } - $documentEntryInToc->setParent($compilerContext->getDocumentNode()->getDocumentEntry()); - $compilerContext->getDocumentNode()->getDocumentEntry()->addChild($documentEntryInToc); + $entryNode->setParent($compilerContext->getDocumentNode()->getDocumentEntry()); + $compilerContext->getDocumentNode()->getDocumentEntry()->addChild($entryNode); } } diff --git a/packages/guides/src/Compiler/NodeTransformers/MenuNodeTransformers/SubInternalMenuEntryNodeTransformer.php b/packages/guides/src/Compiler/NodeTransformers/MenuNodeTransformers/SubInternalMenuEntryNodeTransformer.php index d0555af72..e36270513 100644 --- a/packages/guides/src/Compiler/NodeTransformers/MenuNodeTransformers/SubInternalMenuEntryNodeTransformer.php +++ b/packages/guides/src/Compiler/NodeTransformers/MenuNodeTransformers/SubInternalMenuEntryNodeTransformer.php @@ -16,10 +16,13 @@ use phpDocumentor\Guides\Compiler\CompilerContext; use phpDocumentor\Guides\Exception\DocumentEntryNotFound; use phpDocumentor\Guides\Nodes\DocumentTree\DocumentEntryNode; +use phpDocumentor\Guides\Nodes\DocumentTree\ExternalEntryNode; +use phpDocumentor\Guides\Nodes\Menu\ExternalMenuEntryNode; use phpDocumentor\Guides\Nodes\Menu\InternalMenuEntryNode; use phpDocumentor\Guides\Nodes\Menu\MenuEntryNode; use phpDocumentor\Guides\Nodes\Menu\MenuNode; use phpDocumentor\Guides\Nodes\Node; +use phpDocumentor\Guides\Nodes\TitleNode; use function assert; use function sprintf; @@ -74,24 +77,38 @@ private function addSubEntries( return; } - foreach ($documentEntry->getChildren() as $subDocumentEntryNode) { - $subMenuEntry = new InternalMenuEntryNode( - $subDocumentEntryNode->getFile(), - $subDocumentEntryNode->getTitle(), - [], - false, - $currentLevel, - '', - self::isInRootline($subDocumentEntryNode, $compilerContext->getDocumentNode()->getDocumentEntry()), - self::isCurrent($subDocumentEntryNode, $compilerContext->getDocumentNode()->getFilePath()), - ); + foreach ($documentEntry->getMenuEntries() as $subEntryNode) { + if ($subEntryNode instanceof DocumentEntryNode) { + $subMenuEntry = new InternalMenuEntryNode( + $subEntryNode->getFile(), + $subEntryNode->getTitle(), + [], + false, + $currentLevel, + '', + self::isInRootline($subEntryNode, $compilerContext->getDocumentNode()->getDocumentEntry()), + self::isCurrent($subEntryNode, $compilerContext->getDocumentNode()->getFilePath()), + ); + + if (!$currentMenu->hasOption('titlesonly') && $maxDepth - $currentLevel + 1 > 1) { + $this->addSubSectionsToMenuEntries($subEntryNode, $subMenuEntry, $maxDepth - $currentLevel + 2); + } + + $sectionMenuEntry->addMenuEntry($subMenuEntry); + $this->addSubEntries($currentMenu, $compilerContext, $subMenuEntry, $subEntryNode, $currentLevel + 1, $maxDepth); + continue; + } - if (!$currentMenu->hasOption('titlesonly') && $maxDepth - $currentLevel + 1 > 1) { - $this->addSubSectionsToMenuEntries($subDocumentEntryNode, $subMenuEntry, $maxDepth - $currentLevel + 2); + if (!($subEntryNode instanceof ExternalEntryNode)) { + continue; } + $subMenuEntry = new ExternalMenuEntryNode( + $subEntryNode->getValue(), + TitleNode::fromString($subEntryNode->getTitle()), + $currentLevel, + ); $sectionMenuEntry->addMenuEntry($subMenuEntry); - $this->addSubEntries($currentMenu, $compilerContext, $subMenuEntry, $subDocumentEntryNode, $currentLevel + 1, $maxDepth); } } } diff --git a/packages/guides/src/Compiler/Passes/GlobalMenuPass.php b/packages/guides/src/Compiler/Passes/GlobalMenuPass.php index 11d6c88c7..a74163a98 100644 --- a/packages/guides/src/Compiler/Passes/GlobalMenuPass.php +++ b/packages/guides/src/Compiler/Passes/GlobalMenuPass.php @@ -17,10 +17,14 @@ use phpDocumentor\Guides\Compiler\CompilerPass; use phpDocumentor\Guides\Nodes\DocumentNode; use phpDocumentor\Guides\Nodes\DocumentTree\DocumentEntryNode; +use phpDocumentor\Guides\Nodes\DocumentTree\EntryNode; +use phpDocumentor\Guides\Nodes\DocumentTree\ExternalEntryNode; +use phpDocumentor\Guides\Nodes\Menu\ExternalMenuEntryNode; use phpDocumentor\Guides\Nodes\Menu\InternalMenuEntryNode; use phpDocumentor\Guides\Nodes\Menu\MenuEntryNode; use phpDocumentor\Guides\Nodes\Menu\NavMenuNode; use phpDocumentor\Guides\Nodes\Menu\TocNode; +use phpDocumentor\Guides\Nodes\TitleNode; use phpDocumentor\Guides\Settings\SettingsManager; use Throwable; @@ -115,10 +119,11 @@ private function getMenuEntryWithChildren(CompilerContext $compilerContext, Menu return $newMenuEntry; } + /** @param EntryNode|ExternalEntryNode $entryNode */ private function addSubEntries( CompilerContext $compilerContext, MenuEntryNode $sectionMenuEntry, - DocumentEntryNode $documentEntry, + EntryNode $entryNode, int $currentLevel, int $maxDepth, ): void { @@ -130,17 +135,45 @@ private function addSubEntries( return; } - foreach ($documentEntry->getChildren() as $subDocumentEntryNode) { - $subMenuEntry = new InternalMenuEntryNode( - $subDocumentEntryNode->getFile(), - $subDocumentEntryNode->getTitle(), - [], - false, - $currentLevel, - '', - ); + if (!$entryNode instanceof DocumentEntryNode) { + return; + } + + foreach ($entryNode->getMenuEntries() as $subEntryNode) { + $subMenuEntry = match ($subEntryNode::class) { + DocumentEntryNode::class => $this->createInternalMenuEntry($subEntryNode, $currentLevel), + ExternalEntryNode::class => $this->createExternalMenuEntry($subEntryNode, $currentLevel), + }; + $sectionMenuEntry->addMenuEntry($subMenuEntry); - $this->addSubEntries($compilerContext, $subMenuEntry, $subDocumentEntryNode, $currentLevel + 1, $maxDepth); + $this->addSubEntries( + $compilerContext, + $subMenuEntry, + $subEntryNode, + $currentLevel + 1, + $maxDepth, + ); } } + + private function createInternalMenuEntry(DocumentEntryNode $subEntryNode, int $currentLevel): InternalMenuEntryNode + { + return new InternalMenuEntryNode( + $subEntryNode->getFile(), + $subEntryNode->getTitle(), + [], + false, + $currentLevel, + '', + ); + } + + private function createExternalMenuEntry(ExternalEntryNode $subEntryNode, int $currentLevel): ExternalMenuEntryNode + { + return new ExternalMenuEntryNode( + $subEntryNode->getValue(), + TitleNode::fromString($subEntryNode->getTitle()), + $currentLevel, + ); + } } diff --git a/packages/guides/src/Nodes/DocumentTree/DocumentEntryNode.php b/packages/guides/src/Nodes/DocumentTree/DocumentEntryNode.php index baf51a780..0bb498b9c 100644 --- a/packages/guides/src/Nodes/DocumentTree/DocumentEntryNode.php +++ b/packages/guides/src/Nodes/DocumentTree/DocumentEntryNode.php @@ -13,18 +13,18 @@ namespace phpDocumentor\Guides\Nodes\DocumentTree; -use phpDocumentor\Guides\Nodes\AbstractNode; -use phpDocumentor\Guides\Nodes\DocumentNode; use phpDocumentor\Guides\Nodes\TitleNode; -/** @extends AbstractNode */ -final class DocumentEntryNode extends AbstractNode +use function array_filter; +use function array_values; + +/** @extends EntryNode */ +final class DocumentEntryNode extends EntryNode { - /** @var DocumentEntryNode[] */ + /** @var array */ private array $entries = []; /** @var SectionEntryNode[] */ private array $sections = []; - private DocumentEntryNode|null $parent = null; public function __construct( private readonly string $file, @@ -38,25 +38,27 @@ public function getTitle(): TitleNode return $this->titleNode; } - public function addChild(DocumentEntryNode $child): void + public function addChild(DocumentEntryNode|ExternalEntryNode $child): void { $this->entries[] = $child; } - /** @return DocumentEntryNode[] */ + /** @return array */ public function getChildren(): array { - return $this->entries; - } + // Filter the entries array to only include DocumentEntryNode instances + $documentEntries = array_filter($this->entries, static function ($entry) { + return $entry instanceof DocumentEntryNode; + }); - public function getParent(): DocumentEntryNode|null - { - return $this->parent; + // Re-index the array to maintain numeric keys + return array_values($documentEntries); } - public function setParent(DocumentEntryNode|null $parent): void + /** @return array */ + public function getMenuEntries(): array { - $this->parent = $parent; + return $this->entries; } /** @return SectionEntryNode[] */ diff --git a/packages/guides/src/Nodes/DocumentTree/EntryNode.php b/packages/guides/src/Nodes/DocumentTree/EntryNode.php new file mode 100644 index 000000000..2ae0daad5 --- /dev/null +++ b/packages/guides/src/Nodes/DocumentTree/EntryNode.php @@ -0,0 +1,35 @@ + + */ +abstract class EntryNode extends AbstractNode +{ + private DocumentEntryNode|null $parent = null; + + public function getParent(): DocumentEntryNode|null + { + return $this->parent; + } + + public function setParent(DocumentEntryNode|null $parent): void + { + $this->parent = $parent; + } +} diff --git a/packages/guides/src/Nodes/DocumentTree/ExternalEntryNode.php b/packages/guides/src/Nodes/DocumentTree/ExternalEntryNode.php new file mode 100644 index 000000000..cc8076888 --- /dev/null +++ b/packages/guides/src/Nodes/DocumentTree/ExternalEntryNode.php @@ -0,0 +1,28 @@ + */ +final class ExternalEntryNode extends EntryNode +{ + public function __construct(string $value, public readonly string $title) + { + $this->value = $value; + } + + public function getTitle(): string + { + return $this->title; + } +} diff --git a/packages/guides/src/Nodes/DocumentTree/SectionEntryNode.php b/packages/guides/src/Nodes/DocumentTree/SectionEntryNode.php index ebc162238..7876c05ab 100644 --- a/packages/guides/src/Nodes/DocumentTree/SectionEntryNode.php +++ b/packages/guides/src/Nodes/DocumentTree/SectionEntryNode.php @@ -13,12 +13,11 @@ namespace phpDocumentor\Guides\Nodes\DocumentTree; -use phpDocumentor\Guides\Nodes\AbstractNode; use phpDocumentor\Guides\Nodes\DocumentNode; use phpDocumentor\Guides\Nodes\TitleNode; -/** @extends AbstractNode */ -final class SectionEntryNode extends AbstractNode +/** @extends EntryNode */ +final class SectionEntryNode extends EntryNode { /** @var SectionEntryNode[] */ private array $children = []; diff --git a/packages/guides/src/Nodes/Menu/InternalMenuEntryNode.php b/packages/guides/src/Nodes/Menu/InternalMenuEntryNode.php index b81eba96b..dededf72a 100644 --- a/packages/guides/src/Nodes/Menu/InternalMenuEntryNode.php +++ b/packages/guides/src/Nodes/Menu/InternalMenuEntryNode.php @@ -57,7 +57,7 @@ public function getEntries(): array return $this->children; } - public function addMenuEntry(InternalMenuEntryNode $menuEntryNode): void + public function addMenuEntry(MenuEntryNode $menuEntryNode): void { $this->children[] = $menuEntryNode; } diff --git a/packages/guides/src/Renderer/DocumentTreeIterator.php b/packages/guides/src/Renderer/DocumentTreeIterator.php index aa46bae65..30266415d 100644 --- a/packages/guides/src/Renderer/DocumentTreeIterator.php +++ b/packages/guides/src/Renderer/DocumentTreeIterator.php @@ -77,6 +77,8 @@ public function hasChildren(): bool public function getChildren(): self|null { - return new self($this->levelNodes[$this->position]->getChildren(), $this->documents); + $children = $this->levelNodes[$this->position]->getChildren(); + + return new self($children, $this->documents); } } diff --git a/tests/Integration/tests-full/bootstrap/bootstrap-menu-external-on-subpage/expected/index.html b/tests/Integration/tests-full/bootstrap/bootstrap-menu-external-on-subpage/expected/index.html new file mode 100644 index 000000000..86897e927 --- /dev/null +++ b/tests/Integration/tests-full/bootstrap/bootstrap-menu-external-on-subpage/expected/index.html @@ -0,0 +1,135 @@ + + + + Document Title + + + + + + + +
+ + +
+
+
+
+
+ +
+ + + + +
+

Document Title

+ +
+

Some Caption

+ +
+ +
+ + +
+
+
+
+
+ + + + + + + + + + diff --git a/tests/Integration/tests-full/bootstrap/bootstrap-menu-external-on-subpage/input/guides.xml b/tests/Integration/tests-full/bootstrap/bootstrap-menu-external-on-subpage/input/guides.xml new file mode 100644 index 000000000..4889c7b2e --- /dev/null +++ b/tests/Integration/tests-full/bootstrap/bootstrap-menu-external-on-subpage/input/guides.xml @@ -0,0 +1,8 @@ + + + + diff --git a/tests/Integration/tests-full/bootstrap/bootstrap-menu-external-on-subpage/input/index.rst b/tests/Integration/tests-full/bootstrap/bootstrap-menu-external-on-subpage/input/index.rst new file mode 100644 index 000000000..78d334414 --- /dev/null +++ b/tests/Integration/tests-full/bootstrap/bootstrap-menu-external-on-subpage/input/index.rst @@ -0,0 +1,9 @@ +Document Title +============== + +.. toctree:: + :caption: Some Caption + + Title + https://example.org + Some Page diff --git a/tests/Integration/tests-full/bootstrap/bootstrap-menu-external-on-subpage/input/page1.rst b/tests/Integration/tests-full/bootstrap/bootstrap-menu-external-on-subpage/input/page1.rst new file mode 100644 index 000000000..687dd293d --- /dev/null +++ b/tests/Integration/tests-full/bootstrap/bootstrap-menu-external-on-subpage/input/page1.rst @@ -0,0 +1,10 @@ +====== +Page 1 +====== + +Lorem Ipsum Dolor. + +.. toctree:: + :caption: Some Caption + + Title 2 \ No newline at end of file