diff --git a/packages/guides/src/Compiler/NodeTransformers/MenuNodeTransformers/ToctreeSortingTransformer.php b/packages/guides/src/Compiler/NodeTransformers/MenuNodeTransformers/ToctreeSortingTransformer.php index a77f700cb..46347687f 100644 --- a/packages/guides/src/Compiler/NodeTransformers/MenuNodeTransformers/ToctreeSortingTransformer.php +++ b/packages/guides/src/Compiler/NodeTransformers/MenuNodeTransformers/ToctreeSortingTransformer.php @@ -15,11 +15,18 @@ use phpDocumentor\Guides\Compiler\CompilerContext; use phpDocumentor\Guides\Compiler\NodeTransformer; +use phpDocumentor\Guides\Nodes\DocumentTree\DocumentEntryNode; +use phpDocumentor\Guides\Nodes\DocumentTree\ExternalEntryNode; +use phpDocumentor\Guides\Nodes\Menu\MenuEntryNode; use phpDocumentor\Guides\Nodes\Menu\TocNode; use phpDocumentor\Guides\Nodes\Node; +use function array_key_first; use function array_reverse; +use function array_values; +use function count; use function is_array; +use function ksort; /** @implements NodeTransformer */ final class ToctreeSortingTransformer implements NodeTransformer @@ -35,24 +42,110 @@ public function enterNode(Node $node, CompilerContext $compilerContext): Node return $node; } - if (!$node->isReversed()) { + $entries = $node->getValue(); + if (!is_array($entries)) { return $node; } - $entries = $node->getValue(); $documentEntry = $compilerContext->getDocumentNode()->getDocumentEntry(); - $documentMenuEntries = $documentEntry->getMenuEntries(); - if (is_array($entries)) { + + if ($node->isReversed()) { $entries = array_reverse($entries); - $documentMenuEntries = array_reverse($documentMenuEntries); + $node->setValue($entries); + $documentEntry->setMenuEntries(array_reverse($documentEntry->getMenuEntries())); } - $documentEntry->setMenuEntries($documentMenuEntries); - $node->setValue($entries); + // The document entry's menu entries (used to build the navigation menu) + // are attached by separate transformers for internal and external menu + // entries, each running in its own full tree traversal. As a result the + // menu entries end up grouped by type instead of following the authored + // toctree order, so the navigation menu disagrees with the order shown + // on the page. Realign them with the toctree. Globbed toctrees are + // skipped: their order is defined by the glob expansion, not by an + // authored sequence. + if (!$node->hasOption('glob')) { + $documentEntry->setMenuEntries( + $this->sortMenuEntriesByToctree($entries, $documentEntry->getMenuEntries()), + ); + } return $node; } + /** + * Reorders the menu entries that belong to this toctree so they follow the + * authored toctree order. The toctree's entries are emitted as one + * contiguous block at the position of its first entry; entries that belong + * to other toctrees of the same document keep their relative position. + * Applied per toctree in document order, this yields the global authored + * order even when a document has several toctrees. + * + * @param array $tocEntries + * @param array $menuEntries + * + * @return array + */ + private function sortMenuEntriesByToctree(array $tocEntries, array $menuEntries): array + { + // Map each authored toctree entry to its position. The key match relies + // on the entry urls having been resolved to the document file (internal) + // or external url by the attach transformers (priority 4500), which run + // before this pass. + $order = []; + $position = 0; + foreach ($tocEntries as $tocEntry) { + if (!($tocEntry instanceof MenuEntryNode)) { + continue; + } + + $order[$tocEntry->getUrl()] = $position++; + } + + $ordered = []; + $matchedIndexes = []; + foreach ($menuEntries as $index => $menuEntry) { + $key = self::menuEntryKey($menuEntry); + if (!isset($order[$key])) { + continue; + } + + $ordered[$order[$key]] = $menuEntry; + $matchedIndexes[$index] = true; + } + + // Safety: bail out when entries do not map one-to-one (e.g. the rare + // case of duplicate entries within a single toctree). + if ($matchedIndexes === [] || count($ordered) !== count($matchedIndexes)) { + return $menuEntries; + } + + ksort($ordered); + $ordered = array_values($ordered); + $firstIndex = array_key_first($matchedIndexes); + + $result = []; + foreach ($menuEntries as $index => $menuEntry) { + if ($index === $firstIndex) { + foreach ($ordered as $orderedEntry) { + $result[] = $orderedEntry; + } + } + + if (isset($matchedIndexes[$index])) { + continue; + } + + $result[] = $menuEntry; + } + + return $result; + } + + private static function menuEntryKey(DocumentEntryNode|ExternalEntryNode $entry): string + { + return $entry instanceof DocumentEntryNode ? $entry->getFile() : $entry->getValue(); + } + public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null { return $node; diff --git a/tests/Integration/tests-full/bootstrap/bootstrap-menu-multiple-toctrees-order/expected/subpage/index.html b/tests/Integration/tests-full/bootstrap/bootstrap-menu-multiple-toctrees-order/expected/subpage/index.html new file mode 100644 index 000000000..95351b403 --- /dev/null +++ b/tests/Integration/tests-full/bootstrap/bootstrap-menu-multiple-toctrees-order/expected/subpage/index.html @@ -0,0 +1,135 @@ + + + + Subpage Title + + + + + + + +
+ + +
+
+
+
+
+
+ + +
+
+ + + +
+

Subpage Title

+
+

First

+ +
+
+

Second

+ +
+
+ +
+
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/tests/Integration/tests-full/bootstrap/bootstrap-menu-multiple-toctrees-order/input/guides.xml b/tests/Integration/tests-full/bootstrap/bootstrap-menu-multiple-toctrees-order/input/guides.xml new file mode 100644 index 000000000..4889c7b2e --- /dev/null +++ b/tests/Integration/tests-full/bootstrap/bootstrap-menu-multiple-toctrees-order/input/guides.xml @@ -0,0 +1,8 @@ + + + + diff --git a/tests/Integration/tests-full/bootstrap/bootstrap-menu-multiple-toctrees-order/input/index.rst b/tests/Integration/tests-full/bootstrap/bootstrap-menu-multiple-toctrees-order/input/index.rst new file mode 100644 index 000000000..39c63df88 --- /dev/null +++ b/tests/Integration/tests-full/bootstrap/bootstrap-menu-multiple-toctrees-order/input/index.rst @@ -0,0 +1,6 @@ +Document Title +============== + +.. toctree:: + + subpage/index diff --git a/tests/Integration/tests-full/bootstrap/bootstrap-menu-multiple-toctrees-order/input/subpage/alpha.rst b/tests/Integration/tests-full/bootstrap/bootstrap-menu-multiple-toctrees-order/input/subpage/alpha.rst new file mode 100644 index 000000000..b590bae57 --- /dev/null +++ b/tests/Integration/tests-full/bootstrap/bootstrap-menu-multiple-toctrees-order/input/subpage/alpha.rst @@ -0,0 +1,5 @@ +===== +Alpha +===== + +Lorem Ipsum Dolor. diff --git a/tests/Integration/tests-full/bootstrap/bootstrap-menu-multiple-toctrees-order/input/subpage/beta.rst b/tests/Integration/tests-full/bootstrap/bootstrap-menu-multiple-toctrees-order/input/subpage/beta.rst new file mode 100644 index 000000000..5207d6fe3 --- /dev/null +++ b/tests/Integration/tests-full/bootstrap/bootstrap-menu-multiple-toctrees-order/input/subpage/beta.rst @@ -0,0 +1,5 @@ +==== +Beta +==== + +Lorem Ipsum Dolor. diff --git a/tests/Integration/tests-full/bootstrap/bootstrap-menu-multiple-toctrees-order/input/subpage/index.rst b/tests/Integration/tests-full/bootstrap/bootstrap-menu-multiple-toctrees-order/input/subpage/index.rst new file mode 100644 index 000000000..9b3c655a1 --- /dev/null +++ b/tests/Integration/tests-full/bootstrap/bootstrap-menu-multiple-toctrees-order/input/subpage/index.rst @@ -0,0 +1,14 @@ +Subpage Title +============= + +.. toctree:: + :caption: First + + alpha + External A + +.. toctree:: + :caption: Second + + beta + External B diff --git a/tests/Integration/tests-full/bootstrap/bootstrap-menu-nested-external-order/expected/subpage/index.html b/tests/Integration/tests-full/bootstrap/bootstrap-menu-nested-external-order/expected/subpage/index.html new file mode 100644 index 000000000..ff1901bf9 --- /dev/null +++ b/tests/Integration/tests-full/bootstrap/bootstrap-menu-nested-external-order/expected/subpage/index.html @@ -0,0 +1,130 @@ + + + + Subpage Title + + + + + + + +
+ + +
+
+
+
+
+
+ + +
+
+ + + +
+

Subpage Title

+
+

Nested

+ +
+
+ +
+
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/tests/Integration/tests-full/bootstrap/bootstrap-menu-nested-external-order/input/guides.xml b/tests/Integration/tests-full/bootstrap/bootstrap-menu-nested-external-order/input/guides.xml new file mode 100644 index 000000000..4889c7b2e --- /dev/null +++ b/tests/Integration/tests-full/bootstrap/bootstrap-menu-nested-external-order/input/guides.xml @@ -0,0 +1,8 @@ + + + + diff --git a/tests/Integration/tests-full/bootstrap/bootstrap-menu-nested-external-order/input/index.rst b/tests/Integration/tests-full/bootstrap/bootstrap-menu-nested-external-order/input/index.rst new file mode 100644 index 000000000..39c63df88 --- /dev/null +++ b/tests/Integration/tests-full/bootstrap/bootstrap-menu-nested-external-order/input/index.rst @@ -0,0 +1,6 @@ +Document Title +============== + +.. toctree:: + + subpage/index diff --git a/tests/Integration/tests-full/bootstrap/bootstrap-menu-nested-external-order/input/subpage/index.rst b/tests/Integration/tests-full/bootstrap/bootstrap-menu-nested-external-order/input/subpage/index.rst new file mode 100644 index 000000000..f3c03a2a4 --- /dev/null +++ b/tests/Integration/tests-full/bootstrap/bootstrap-menu-nested-external-order/input/subpage/index.rst @@ -0,0 +1,10 @@ +Subpage Title +============= + +.. toctree:: + :caption: Nested + + page1 + External A + page2 + External B diff --git a/tests/Integration/tests-full/bootstrap/bootstrap-menu-nested-external-order/input/subpage/page1.rst b/tests/Integration/tests-full/bootstrap/bootstrap-menu-nested-external-order/input/subpage/page1.rst new file mode 100644 index 000000000..36a59ef44 --- /dev/null +++ b/tests/Integration/tests-full/bootstrap/bootstrap-menu-nested-external-order/input/subpage/page1.rst @@ -0,0 +1,5 @@ +====== +Page 1 +====== + +Lorem Ipsum Dolor. diff --git a/tests/Integration/tests-full/bootstrap/bootstrap-menu-nested-external-order/input/subpage/page2.rst b/tests/Integration/tests-full/bootstrap/bootstrap-menu-nested-external-order/input/subpage/page2.rst new file mode 100644 index 000000000..fdca6b517 --- /dev/null +++ b/tests/Integration/tests-full/bootstrap/bootstrap-menu-nested-external-order/input/subpage/page2.rst @@ -0,0 +1,5 @@ +====== +Page 2 +====== + +Lorem Ipsum Dolor.