Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<TocNode> */
final class ToctreeSortingTransformer implements NodeTransformer
Expand All @@ -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<MenuEntryNode> $tocEntries
* @param array<DocumentEntryNode|ExternalEntryNode> $menuEntries
*
* @return array<DocumentEntryNode|ExternalEntryNode>
*/
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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<!DOCTYPE html>
<html class="no-js" lang="en">
<head>
<title>Subpage Title</title>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
</head>
<body>
<header class="">

<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">

<a class="navbar-brand" href="#">Navbar</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">

<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a href="/subpage/index.html" class="nav-link current active" aria-current="page">
Subpage Title
</a>
</li>
<ul class="level-">
<li class="nav-item">
<a href="/subpage/index.html"
class="nav-link current active" aria-current="page">Subpage Title</a>
</li>
</ul>
</ul>

</div>
</div>
</nav>
</header>
<main id="main-content">
<div class="container">
<div class="container">
<div class="row">
<div class="col-lg-3">
<nav class="nav flex-column">
<ul class="menu-level-main">
<li>
<a href="/subpage/index.html"
class="nav-link current active" aria-current="page">Subpage Title</a>
<ul class="level-1">
<li>
<a href="/subpage/alpha.html"
class="nav-link">Alpha</a>
</li>
<li>
<a href="https://example.com/a"
class="nav-link">External A</a>
</li>
<li>
<a href="/subpage/beta.html"
class="nav-link">Beta</a>
</li>
<li>
<a href="https://example.org/b"
class="nav-link">External B</a>
</li>
</ul>

</li>
</ul>
</nav>

</div>
<div class="col-lg-9">

<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/index.html">Document Title</a></li>
<li class="breadcrumb-item"><a href="/subpage/index.html">Subpage Title</a></li>
</ol>
</nav>
<!-- content start -->
<div class="section" id="subpage-title">
<h1>Subpage Title</h1>
<div class="toc">
<p class="caption">First</p>
<ul class="menu-level">
<li class="toc-item">
<a href="/subpage/alpha.html#alpha">Alpha</a>


</li>
<li class="toc-item">
<a href="https://example.com/a">External A</a>


</li>
</ul>
</div>
<div class="toc">
<p class="caption">Second</p>
<ul class="menu-level">
<li class="toc-item">
<a href="/subpage/beta.html#beta">Beta</a>


</li>
<li class="toc-item">
<a href="https://example.org/b">External B</a>


</li>
</ul>
</div>
</div>
<!-- content end -->
</div>
</div>
</div>
</div>
</main>

<!-- Optional JavaScript; choose one of the two! -->

<!-- Option 1: Bootstrap Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>

<!-- Option 2: Separate Popper and Bootstrap JS -->
<!--
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js" integrity="sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.min.js" integrity="sha384-cVKIPhGWiC2Al4u+LWgxfKTRIcfu0JTxR+EQDz/bgldoEyl4H0zUF0QKbrJ0EcQF" crossorigin="anonymous"></script>
-->
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<guides xmlns="https://www.phpdoc.org/guides"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://www.phpdoc.org/guides packages/guides-cli/resources/schema/guides.xsd"
theme="bootstrap"
>
<extension class="phpDocumentor\Guides\Bootstrap"/>
</guides>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Document Title
==============

.. toctree::

subpage/index
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
=====
Alpha
=====

Lorem Ipsum Dolor.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
====
Beta
====

Lorem Ipsum Dolor.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Subpage Title
=============

.. toctree::
:caption: First

alpha
External A <https://example.com/a>

.. toctree::
:caption: Second

beta
External B <https://example.org/b>
Loading
Loading