TOC: list a heading wrapped in a div that carries an id - #656
Draft
gordonwoodhull wants to merge 1 commit into
Draft
TOC: list a heading wrapped in a div that carries an id#656gordonwoodhull wants to merge 1 commit into
gordonwoodhull wants to merge 1 commit into
Conversation
Fixes bd-toc-skips-headings-in-id-div-1jorg679. A heading inside a fenced div that carries an id never reached the table of contents. The heading rendered, its <section> was anchorable, and a reader could scroll to it, but there was no way to navigate to it. No diagnostic; the render exited 0. Quarto 1 lists it. The id is the trigger. sectionize_blocks absorbs an anonymous wrapper into the section it holds, so a div with no attributes or with only a class is already a plain section by the time the TOC is built. An id-bearing wrapper cannot be absorbed -- its id would collide with the section's -- so the section stays one level down, and collect_toc_entries ended the walk at the first non-section Div it met. The walk now sees through a wrapper whose sole content is another Div, which is pandoc's rule (toTOCTree, Text/Pandoc/Chunks.hs, jgm/pandoc#8402). It recurses rather than unwrapping one level, so a section under stacked wrappers is reached too -- Quarto 1 lists those. A wrapper holding anything besides a lone Div still ends the walk, which is where filter-built chrome stops -- though not always at its outermost Div. A titled callout and a resolved tabset each put two blocks side by side, header beside body and nav Plain beside pane container, so the walk stops at the outer Div. An untitled callout does not: build_untitled_content emits a lone .callout-body.d-flex, so the walk descends one level and stops on the icon container beside the body container. pandoc descends that same level, so Quarto 1 stops in the same place. Both stops rest on a sibling block another transform is free to move, so each is now pinned by a test, and panel_tabset_resolve.rs records that its nav Plain is load-bearing for the TOC walk. Headings inside a tab pane, a callout, or a blockquote therefore stay out of the TOC, as does a heading with prose or a sibling section beside it inside the same wrapper -- all matching both pandoc and Quarto 1. TESTS New pipeline-level tests in tests/integration/test_toc_wrapper_divs.rs drive markdown through readers::qmd::read -> sectionize_blocks -> generate_toc, because the bug was an interaction between the absorb rule and the walk and toc.rs's unit tests hand-build the sectionized AST on both sides of it. They pin the eight-case contract, stacked wrappers, and the two ways a wrapper can hold more than one block (prose before the heading, and two sibling sections). Confirmed to fail without the new arm. test_non_section_div_terminates_the_walk pinned the old behaviour with a synthetic tabset shape (.panel-tabset directly wrapping a .tab-pane) that PanelTabsetResolveTransform does not emit -- its own unit test asserts the outer Div holds two blocks. It is now test_resolved_tabset_pane_is_not_reached and uses the real shape. test_untitled_callout_body_is_not_reached is new and pins the other stop. test_blockquote_heading_is_not_collected (bd-8yjvs3bj) is untouched and green. Workspace: 13685 passed, 199 skipped (+6, all added here). VERIFIED END TO END The eight-case repro fixture: q2 now agrees with Quarto 1 on every case, including both negative controls. Callout, tabset and single-tab-tabset shapes rendered through the binary produce TOCs identical to Quarto 1's. On the Positron website's download page, which wraps its hero in ::: {#download-hero .download-hero}, the "Recommended for you" entry is present and the page's TOC matches Quarto 1's entry for entry. extract_outline shares generate_toc and runs pre-transform, so a single-child id-bearing callout or tabset now reaches profile.outline though the rendered TOC still excludes it. That is the already-filed bd-ca17fck0 divergence, extended from anonymous wrappers to id-bearing ones; noted on that strand.
Contributor
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
Member
|
I think the omission is now intentional, though we can talk about it! |
gordonwoodhull
marked this pull request as draft
September 4, 2026 19:37
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Put an
idon a fenced div and the heading inside it vanished from the page's table of contents:::: {#download-hero} ### Recommended for you :::The heading rendered, its
<section>was anchorable, and a reader could scroll to it — but there was no way to navigate to it. No warning, exit 0. Quarto 1 lists it.The
idis what triggers this. A div with no attributes, or with only a class, is fine, because sectionization folds that wrapper into the<section>it holds and the heading ends up an ordinary top-level section. A wrapper carrying an id can't be folded — its id would collide with the section's own — so the section stays nested one level down, and the TOC walk stopped at the first non-section div it met without looking inside.The walk now sees through a wrapper whose sole content is another div, which is pandoc's rule (
toTOCTree, jgm/pandoc#8402). It recurses rather than unwrapping a single level, so a section buried under several wrappers is reached too — Quarto 1 lists those as well.What still ends the walk is a wrapper holding anything besides a lone div, which is where filter-built chrome stops — though not always at its outermost div, and the distinction is worth stating because it is thinner than it looks. A titled callout and a resolved tabset each put two blocks side by side (header beside body, nav list beside pane container), so the walk stops at the outer div. An untitled callout does not: it emits a lone
.callout-body.d-flex, so the walk descends one level and stops there instead, on the icon container beside the body container. Pandoc descends that same level, so Quarto 1 stops in the same place. Both stops rest on a sibling block that another transform is free to move, so each now has a test, andpanel_tabset_resolve.rsrecords that its navPlainis load-bearing for the TOC walk rather than just for markup.Headings inside a tab pane, a callout, or a blockquote therefore stay out of the table of contents, as does a heading with prose or a sibling section beside it inside the same wrapper — all matching both pandoc and Quarto 1.
Tests
The new tests in
crates/pampa/tests/integration/test_toc_wrapper_divs.rsdrive markdown through the real pair,readers::qmd::read→sectionize_blocks→generate_toc. That matters here: the bug was an interaction between the absorb rule and the walk, andtoc.rs's unit tests hand-build the sectionized AST on both sides of it, so either half could change without the other's tests noticing. They pin the eight wrapper shapes, stacked wrappers, and the two ways a wrapper can hold more than one block. All three positive tests fail without the new match arm; the negative controls are invariant, as they should be.test_non_section_div_terminates_the_walkpinned the old behaviour using a tabset shapePanelTabsetResolveTransformdoes not emit — that transform's own unit test asserts the outer div holds two blocks. It is nowtest_resolved_tabset_pane_is_not_reachedand uses the real shape, so it guards what the transform actually produces.test_untitled_callout_body_is_not_reachedis new and pins the other stop.Verification
Checked against a fixture covering eight wrapper shapes: q2 now agrees with Quarto 1 on every one, positive and negative. Callout, tabset and single-tab-tabset shapes rendered through the binary produce tables of contents identical to Quarto 1's. On the Positron website's download page, which wraps its hero section in
::: {#download-hero .download-hero}, the "Recommended for you" entry is back and the page's table of contents matches Quarto 1's entry for entry.One known consequence
DocumentProfile::extract_outlinesharesgenerate_tocbut runs pre-transform, before callouts and tabsets are resolved into chrome. A single-child id-bearing callout or tabset therefore now reachesprofile.outlinealthough the rendered TOC still excludes it. That is the existing bd-ca17fck0 divergence — profile outline computed pre-transform disagreeing with the post-transform TOC — extended from anonymous wrappers to id-bearing ones. Nothing outside tests consumesprofile.outlinetoday; noted on that strand rather than fixed here.Fixes bd-toc-skips-headings-in-id-div-1jorg679.