[BUGFIX] Keep navigation menu in toctree order when mixing internal and external entries - #1344
[BUGFIX] Keep navigation menu in toctree order when mixing internal and external entries#1344CybotTM wants to merge 6 commits into
Conversation
80f7e7f to
6358e65
Compare
c45ea55 to
c4fba4f
Compare
c4fba4f to
200f1ad
Compare
…1291) Fixes #1175 ## Problem When a `toctree` mixes internal pages and external links (e.g. interlinks to other manuals), the **sidebar navigation** renders all external entries grouped at the top, even though the on-page `toctree` keeps the authored order. On the reported page (`reference-coreapi` → Fluid) the sidebar shows `Fluid Syntax` and `ViewHelper reference` (both external) above the internal pages. ## Root cause The guides compiler attaches a document's menu entries via **separate transformers for internal and external entries** (`InternalMenuEntryNodeTransformer` / `ExternalMenuEntryNodeTransformer`), and the compiler runs **one full tree traversal per transformer** at each priority. So every external entry is attached to the document's menu-entry list in one pass and every internal entry in another — the list ends up grouped by type instead of following the authored toctree. The on-page toctree renders from the `TocNode` (authored order) and stays correct; the sidebar renders from `DocumentEntryNode::getMenuEntries()` and does not. This is an engine-level issue in `phpdocumentor/guides`. The proper fix belongs upstream (phpDocumentor/guides#1344); this PR adds a **downstream compiler pass** so docs.typo3.org is fixed now, and it can be removed after a future `guides` bump. ## Fix `SortMenuEntriesByToctreeTransformer` realigns each document's menu entries with the authored order of its `toctree`s, matching entries by file/URL. It emits each toctree's entries as one contiguous block at the position of its first entry, so a document with **several** mixed toctrees is ordered correctly too. **Globbed toctrees are skipped** (their order comes from the glob expansion, which the sitemap relies on). ## Before / after Sidebar of a subpage whose toctree mixes internal pages with two external links: **Before**  **After**  ## Verification - Two integration fixtures: `tests-full/menu-external-order` (one nested mixed toctree) and `tests-full/menu-multiple-toctrees-order` (two mixed toctrees on one page). Each **passes with the fix and fails without it**. - Full integration suite green (116 tests); the change touches only mixed/explicit toctrees — `external-menu`, `two-toctrees`, `menu-subpages`, `sitemap` (glob) and `main-menu-json` are unaffected. - PHPStan (repo config) and php-cs-fixer clean on the changed files. Signed-off-by: Sebastian Mendel <info@sebastianmendel.de> Co-authored-by: Lina Wolf <48202465+linawolf@users.noreply.github.com>
…YPO3-Documentation#1291) Fixes TYPO3-Documentation#1175 ## Problem When a `toctree` mixes internal pages and external links (e.g. interlinks to other manuals), the **sidebar navigation** renders all external entries grouped at the top, even though the on-page `toctree` keeps the authored order. On the reported page (`reference-coreapi` → Fluid) the sidebar shows `Fluid Syntax` and `ViewHelper reference` (both external) above the internal pages. ## Root cause The guides compiler attaches a document's menu entries via **separate transformers for internal and external entries** (`InternalMenuEntryNodeTransformer` / `ExternalMenuEntryNodeTransformer`), and the compiler runs **one full tree traversal per transformer** at each priority. So every external entry is attached to the document's menu-entry list in one pass and every internal entry in another — the list ends up grouped by type instead of following the authored toctree. The on-page toctree renders from the `TocNode` (authored order) and stays correct; the sidebar renders from `DocumentEntryNode::getMenuEntries()` and does not. This is an engine-level issue in `phpdocumentor/guides`. The proper fix belongs upstream (phpDocumentor/guides#1344); this PR adds a **downstream compiler pass** so docs.typo3.org is fixed now, and it can be removed after a future `guides` bump. ## Fix `SortMenuEntriesByToctreeTransformer` realigns each document's menu entries with the authored order of its `toctree`s, matching entries by file/URL. It emits each toctree's entries as one contiguous block at the position of its first entry, so a document with **several** mixed toctrees is ordered correctly too. **Globbed toctrees are skipped** (their order comes from the glob expansion, which the sitemap relies on). ## Before / after Sidebar of a subpage whose toctree mixes internal pages with two external links: **Before**  **After**  ## Verification - Two integration fixtures: `tests-full/menu-external-order` (one nested mixed toctree) and `tests-full/menu-multiple-toctrees-order` (two mixed toctrees on one page). Each **passes with the fix and fails without it**. - Full integration suite green (116 tests); the change touches only mixed/explicit toctrees — `external-menu`, `two-toctrees`, `menu-subpages`, `sitemap` (glob) and `main-menu-json` are unaffected. - PHPStan (repo config) and php-cs-fixer clean on the changed files. Signed-off-by: Sebastian Mendel <info@sebastianmendel.de> Co-authored-by: Lina Wolf <48202465+linawolf@users.noreply.github.com>
|
Reviewed 1. A duplicate external link silently reverts the whole fix (
2. The glob-toctree guard is unreachable dead code (
$q = new SplPriorityQueue();
$q->insert("low", 3200);
$q->insert("high", 4000);
foreach ($q as $item) { echo $item . "\n"; }
// prints: high, lowSo by the time |
a2b7108 to
246a66c
Compare
|
Thanks — both findings are correct. I reproduced each one rather than reading the code back, and both are fixed as of f04526e. Details below, including one place where your second finding cannot be turned into a test and why. 1. The duplicated external link. Confirmed exactly as described. 2. The glob guard. Also confirmed, and your reading of One consequence worth stating plainly: there is no test that fails without that removal, and there cannot be one. The guard was unreachable, so output with and without it is identical. The PR body's "Fix" section has been corrected in place — the sentence "globbed toctrees are skipped" was wrong and is struck through there now. 3. A gap your review made me look for. The The wholesale Verified on the final tree: PHPUnit 836 tests green (integration 235), PHPStan level max clean, PHPCS clean. Rebased onto Assisted by claude-code:claude-opus-5 — Session |
…al entries A toctree that mixes internal pages and external links built the navigation menu (the document entry's menu entries) with all external entries grouped first, even though the on-page toctree kept the authored order. The sidebar/navbar therefore disagreed with the page; this was visible for nested toctrees. Internal and external menu entries are attached to the document entry by two separate transformers (InternalMenuEntryNodeTransformer and ExternalMenuEntryNodeTransformer), and the compiler runs one full tree traversal per transformer, so every external entry is appended in one pass and every internal entry in another. The result is grouped by type instead of following the toctree. ToctreeSortingTransformer now realigns the document entry's menu entries with the authored toctree order, emitting each toctree's entries as a contiguous block at the position of its first entry. Applied per toctree in document order, this also yields the correct order when a document has several mixed toctrees. It already ran at the right point to handle the reversed option; globbed toctrees are skipped, since their order comes from the glob expansion rather than an authored sequence. Reported downstream at TYPO3-Documentation/render-guides#1175
The previous commit of this branch anchored each toctree's block at the lowest
index it matched in the menu entries. Those entries are grouped by type, so
that index is not a position in the document: with one toctree holding only
internal entries and the next only an external one - an ordinary "chapters"
and "external links" split - the navigation still contradicted the page, which
is the very defect this branch exists to fix. Reproduced by rendering, then
pinned as a test.
Take the order from every toctree of the document at once, in document order,
and sort the whole menu entry list by it. The toctrees are collected from the
document tree: `DocumentNode::getTocNodes()` is filled at priority 1000, after
this pass, and `getNodes()` sees direct children only while a toctree usually
sits inside a section.
Three further corrections fall out of that:
The `:reversed:` handling no longer reverses the document's whole menu entry
list when the order can be derived; reversing the toctree's own values is
enough and does not displace entries of other toctrees. The wholesale reversal
remains for glob toctrees, where there is no authored sequence to sort by.
The glob guard tested `hasOption('glob')`, but `ToctreeBuilder` creates a
`GlobMenuEntryNode` from a `*` in the reference and never reads that option, so
the guard was wrong in both directions. Guard on the node type instead.
An entry listed twice now keeps its first authored position, where the last one
used to win while the counts still matched, so nothing noticed.
Assisted-by: claude-code:claude-opus-5
Agent-Session: https://claude.ai/code/session_015QXXkquh2eQNBiTYA39Wss
Signed-off-by: Sebastian Mendel <sebastian.mendel@netresearch.de>
An external link listed twice in a toctree is attached to the document entry twice: ExternalMenuEntryNodeTransformer builds a fresh ExternalEntryNode per occurrence, and the dedup guard in attachDocumentEntriesToParents only covers DocumentEntryNode. The position map keyed both of them to the same slot, so the second overwrote the first, the count check tripped, and the sorting bailed out for the whole page - back to the type-grouped order this branch exists to fix, and with `:reversed:` on that toctree the fallback then reversed the document's whole menu entry list. Record one position per occurrence instead and hand them out in turn. An internal entry is deduplicated on attach, so its single menu entry takes the first occurrence and the later ones stay unused; the two menu entries of a duplicated external link take the two occurrences, and the menu shows the link where the page shows it. Every menu entry now consumes a distinct position, so the count check has nothing left to catch and is gone. Drop the GlobMenuEntryNode guard. It could never fire: Compiler drives its passes off an SplPriorityQueue, which is a max-heap, so GlobMenuEntryNodeTransformer (priority 4000) has replaced every GlobMenuEntryNode with the entries it expands to before this pass runs at 3200. Instrumenting the guard across the integration suite recorded zero hits against 138 sortings. The expanded entries carry the urls this matches on and sort correctly, so the guard protected nothing and claimed to protect something. Both cases are pinned as tests. bootstrap-menu-duplicate-external-order fails on the previous commit of this branch with the type-grouped order; both new tests fail against main. Reported downstream at TYPO3-Documentation/render-guides#1175 Assisted-by: claude-code:claude-opus-5 Agent-Session: https://claude.ai/code/session_014H1xwaAmrQRWUA3vx8bJcD Agent-Host: 0493f0 Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
The two fixtures of the previous commit and the pre-existing `:reversed:` ones cover disjoint halves: the new ones never set `:reversed:`, and toctree-level-2-reversed and toctree-glob-reverse never mix in an external link. The combination is where this branch changed behaviour - the wholesale reversal of the document's menu entries now only runs when the order cannot be derived - so it is the combination that was uncovered. bootstrap-menu-reversed-external-order pins it: page and navigation menu both read Beta, External A, Alpha. It fails against main, where the menu comes out reversed on top of the type-grouped order, and passes on the previous commit of this branch, which already had this path right. Assisted-by: claude-code:claude-opus-5 Agent-Session: https://claude.ai/code/session_014H1xwaAmrQRWUA3vx8bJcD Agent-Host: 0493f0 Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
The comment named a concrete trigger for the branch - a menu entry that no toctree of the document accounts for - which is the condition the code tests, not a case anyone has produced. Instrumenting the branch across the integration suite records no hits, and all three call sites of attachDocumentEntriesToParents attach only under a TocNode of the current document, so there is reason to think nothing reaches it. That is not a proof, and the branch stays: dropping it would silently drop `:reversed:` for whatever does reach it. Say that instead, so the next reader does not take the old wording for a case that was observed. Assisted-by: claude-code:claude-opus-5 Agent-Session: https://claude.ai/code/session_014H1xwaAmrQRWUA3vx8bJcD Agent-Host: 0493f0 Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
The pass had no unit test at all, and the integration fixtures can only reach states an author can write. The fallback branch is not one of them: every menu entry is attached from a toctree of its own document, so nothing renders a document whose menu entries the toctrees do not account for. The previous commit therefore described that branch in a comment instead of testing it. ToctreeSortingTransformerTest builds the state directly and pins five things: the untouched list when an entry cannot be placed, the wholesale reversal of the same case under `:reversed:`, both positions of a duplicated external link, the first position of a duplicated internal one, and that the order comes from every toctree of the document rather than the visited one. Each was seen to fail on its own defect before it was kept - fallback removed, one position per url instead of per occurrence, skipping an unplaceable entry instead of bailing out, the last position instead of the first, and only the visited toctree. Five mutations, five distinct failures, no test green through all of them. The comment on the fallback branch now points at the test rather than arguing from the call sites. Assisted-by: claude-code:claude-opus-5 Agent-Session: https://claude.ai/code/session_014H1xwaAmrQRWUA3vx8bJcD Agent-Host: 0493f0 Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
a14f4d5 to
cc6ad63
Compare
Problem
A
toctreethat mixes internal pages and external links builds the navigation menu (the document entry's menu entries, used for the sidebar/navbar) with all external entries grouped first, even though the on-pagetoctreekeeps the authored order. The navigation therefore disagrees with the page. It shows up for nested toctrees.Root cause
Internal and external menu entries are attached to the document entry by two separate transformers —
InternalMenuEntryNodeTransformerandExternalMenuEntryNodeTransformer— and the compiler runs one full tree traversal per transformer at a given priority (both attach at 4500). So every external entry is appended toDocumentEntryNode::getMenuEntries()in one pass and every internal entry in another, and the list ends up grouped by type instead of following the toctree. The on-page toctree renders from theTocNodevalue (authored order) and stays correct.Fix
ToctreeSortingTransformer(priority 3200, after the attach passes) now realigns the document entry's menu entries with the authored toctree order, matching entries by file/URL. It emits each toctree's entries as one contiguous block at the position of its first entry, so a document with several mixed toctrees is ordered correctly too. It already ran at the right point to handle thereversedoption.Globbed toctrees are skipped — their order comes from the glob expansion, not an authored sequence.That sentence was wrong and the code it described is gone; see the last section.Before / after
Sidebar of a nested subpage whose toctree interleaves internal pages and external links (Bootstrap theme):
Tests
Seven integration fixtures under
tests-full/bootstrap/, one per toctree shape, each failing againstmain:bootstrap-menu-nested-external-order(one toctree interleaving internal and external),bootstrap-menu-multiple-toctrees-order(two mixed toctrees),bootstrap-menu-separate-toctrees-order(an internal-only toctree followed by an external-only one),bootstrap-menu-glob-option-and-duplicate(the:glob:option without a*, plus a duplicated internal entry),bootstrap-menu-duplicate-external-order(a duplicated external link),bootstrap-menu-glob-external-order(a real*glob followed by an external toctree) andbootstrap-menu-reversed-external-order(:reversed:mixing both kinds).ToctreeSortingTransformerTestcovers what no.rstinput can produce, including the fallback branch. Each of its five cases was seen failing on its own injected defect before it was kept — fallback removed, one position per url instead of per occurrence, skipping an unplaceable entry instead of bailing out, the last position instead of the first, and only the visited toctree — five mutations, five distinct failures.Verified on the final tree: PHPUnit 841 tests green, PHPStan (level max + baseline), PHPCS (Doctrine standard) and deptrac clean.
Context
Reported downstream at TYPO3-Documentation/render-guides#1175.
Update: the first commit did not fix the case it set out to fix
A review of this branch found that anchoring each toctree's block at the lowest index it matched in the menu entries uses a position in the type-grouped list, not a position in the document. With one toctree holding only internal entries and the next only an external one — the ordinary "chapters" plus "external links" split — the navigation still contradicted the page:
Both fixtures above use toctrees that mix internal and external entries, which is precisely the shape that hides this. Reproduced by rendering, then pinned as
bootstrap-menu-separate-toctrees-order.The second commit takes the order from all toctrees of the document at once, in document order, and sorts the whole menu entry list by it; when the entries cannot be mapped one to one the list is left untouched rather than partially reordered. The toctrees are collected from the document tree, because
DocumentNode::getTocNodes()is filled at priority 1000 — after this pass — andgetNodes()only sees direct children while a toctree usually sits inside a section.Three further corrections fall out of that. The
:reversed:handling no longer reverses the document's whole menu entry list when the order can be derived, so it stops displacing entries of other toctrees; the wholesale reversal remains for glob toctrees, where there is no authored sequence to sort by. The glob guard testedhasOption('glob'), butToctreeBuildercreates aGlobMenuEntryNodefrom a*in the reference and never reads that option, so the guard was wrong in both directions — it now looks at the node type. And an entry listed twice keeps its first authored position, where the last one used to win while the counts still matched, so nothing noticed.Two more fixtures cover the separate-toctrees case and the glob-option-plus-duplicate case; both fail against the previous state of this branch.
Update: two review findings from @linawolf, both confirmed and fixed
A duplicated external link reverted the whole fix.
ExternalMenuEntryNodeTransformerbuilds a freshExternalEntryNodeper occurrence, and the dedup guard inMenuEntryManagement::attachDocumentEntriesToParentsonly coversDocumentEntryNode— so a link listed twice is attached twice while the position map collapsed it to one slot. The second entry overwrote the first, the count check tripped, and the sorting bailed out for the whole page, back to the type-grouped order this branch exists to fix. Reproduced by rendering a toctree ofalpha,External A,beta,External A: sidebarExternal A, External A, Alpha, Betaagainst pageAlpha, External A, Beta, External A. The map now records one position per occurrence and hands them out in turn; an internal entry is deduplicated on attach so it takes the first occurrence, the two menu entries of a duplicated external link take both. Every menu entry now consumes a distinct position, so the count check has nothing left to catch and is gone. Pinned asbootstrap-menu-duplicate-external-order, which fails on the previous commit of this branch with exactly the order above.The glob guard was unreachable.
Compilerdrives its passes off anSplPriorityQueue, a max-heap, soGlobMenuEntryNodeTransformer(4000) has already replaced everyGlobMenuEntryNodewith the entries it expands to before this pass runs at 3200. Instrumenting the guard across the integration suite recorded zero hits against 138 sortings — the protection claimed in the "Fix" section above never existed. The guard is removed rather than relocated: the expanded entries carry the urls this matches on, and they sort correctly, which is what the suite was already showing.bootstrap-menu-glob-external-orderpins that a glob toctree followed by a toctree with an external link produces a navigation menu matching the page; it fails againstmain.So the sentence "globbed toctrees are skipped" in the Fix section is wrong and no longer describes the code — glob toctrees are sorted like any other, on the entries the expansion produced. The remaining
nullreturn, and with it the wholesale:reversed:fallback, now fires only when a menu entry is attached that no toctree of the document accounts for — a condition nothing in the test suite reaches. The branch stays because its unreachability is not proven, and its comment now says that rather than naming a case as if it had been observed.Rebased onto
mainat 382485e. Verified on the final tree: full PHPUnit suite 835 tests green (integration 234), PHPStan level max clean, PHPCS clean.Assisted by claude-code:claude-opus-5 — Session