Skip to content
Merged
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
304 changes: 304 additions & 0 deletions claude-notes/plans/2026-08-14-toc-location.md

Large diffs are not rendered by default.

185 changes: 185 additions & 0 deletions claude-notes/plans/toc-location-investigation/q1-mechanism-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# Q1 `toc-location` mechanism notes (bd-e2kpwy7n investigation)

Exploration of `external-sources/quarto-cli` (1.10.15), 2026-08-14. All
paths below are relative to `external-sources/quarto-cli/` unless noted.

## 1. Declaration / read sites

- Constant: `src/config/constants.ts:603` — `kTocLocation = "toc-location"`.
- Schema: `src/resources/schema/document-toc.yml:36-52` —
`enum: ["body", "left", "right", "left-body", "right-body"]`, default
`right`, `formats: [$html-doc]`.
- Readers (all six in the tree):
- `src/format/html/format-html-bootstrap.ts:127-129` — standalone/article path
- `src/format/html/format-html-bootstrap.ts:349` — double-TOC (`*-body`) detection
- `src/format/html/format-html-title.ts:171` — banner header class
- `src/project/types/website/website-navigation.ts:252-256, 302` — website path
- `src/project/types/manuscript/manuscript.ts:454-455` — manuscripts default to `left`
- `src/project/types/website/about/website-about.ts:106` — about pages force `right` (and `toc: false`)
- No runtime normalization beyond the YAML enum; the raw string is passed to
EJS and compared with `===`.

## 2. Placement mechanism

Two template paths emit a `div#quarto-toc-target` placeholder; one shared
DOM postprocessor moves `nav[role="doc-toc"]` into it.

### 2a. Standalone / non-website article

`src/format/html/format-html-bootstrap.ts:126-142` renders
`src/resources/formats/html/templates/before-body-article.ejs`:

```ejs
<%
const navbarTocLeft = tocLocation === "left" || tocLocation === "left-body";
const navbarTocRight = tocLocation === "right" || tocLocation === "right-body";
%>
<div id="quarto-content" class="page-columns page-rows-contents page-layout-<%- pageLayout %><%- (navbarTocLeft) ? " toc-left" : ""%>">
<% if (navbarTocLeft) { %>
<div id="quarto-sidebar-toc-left" class="sidebar toc-left">
<div id="quarto-toc-target"></div>
</div>
<% } %>
<div id="quarto-margin-sidebar" class="sidebar margin-sidebar">
<% if (navbarTocRight) { %>
<div id="quarto-toc-target"></div>
<% } %>
</div>
<main class="content" id="quarto-document-content">
```

`body` produces **no** target at all. `#quarto-margin-sidebar` is always
emitted (it may hold margin content even without a TOC).

### 2b. Website projects

`src/project/types/website/website-navigation.ts:252-256, 302, 379` →
`src/resources/projects/website/templates/nav-before-body.ejs:3-4, 111-125`:

```ejs
const navbarTocLeft = nav['toc-location'] === "left" || nav['toc-location'] === "left-body";
...
<!-- sidebar -->
<% if (nav.sidebar || navbarTocLeft) { %>
<% partial('sidebar.ejs', { sidebar: nav.sidebar, sidebarStyle: nav.sidebarStyle, navbar: !!nav.navbar, toc: navbarTocLeft, ... }) %>
<% } %>
<!-- margin-sidebar -->
<% if (nav.layout === "article" || nav.layout === "full") { %>
<div id="quarto-margin-sidebar" class="sidebar margin-sidebar">
<% if (nav.hasToc && navbarTocRight) { %>
<div id="quarto-toc-target"></div>
<% } %>
</div>
<% } %>
```

`src/resources/projects/website/templates/sidebar.ejs:1, 95-97`:

```ejs
<nav id="quarto-sidebar" class="sidebar collapse collapse-horizontal quarto-sidebar-collapse-item sidebar-navigation <%- sidebarStyle || (toc ? "floating" : "") %> overflow-auto">
...
<% if (toc) { %>
<div id="quarto-toc-target"></div>
<% } %>
</nav>
<div id="quarto-sidebar-glass" class="quarto-sidebar-collapse-item" ...></div>
```

Asymmetries vs. the standalone path: no `toc-left` class on
`#quarto-content`; the left target lives inside `nav#quarto-sidebar`, not a
separate `#quarto-sidebar-toc-left`.

- **Nav sidebar + left TOC** → one `nav#quarto-sidebar`, nav items first,
TOC appended after — merged, no second container.
- **No nav sidebar + left TOC (in a website)** → `sidebar.ejs` renders with
`sidebar === undefined`; the wrapper holds only the TOC target and gets
class `floating` (ternary fallback). `website-navigation.ts:538-543` then
latches `floating` onto `<body>`, so the `page-columns-float-*` grids
apply — **not** the `toc-left` grids, which are gated on
`body:not(.floating):not(.docked)`.

### 2c. The mover (shared DOM postprocessor)

`src/format/html/format-html-bootstrap.ts:342-412`
(`bootstrapHtmlPostprocessor`):

- `right`/`left` → `toc.remove(); tocTarget.replaceWith(toc)`; adds
`.toc-active`, `nav-link` classes, `data-scroll-target`, `.collapse` on
nested `ul`s, `data-toc-expanded`.
- `body` → `tocTarget` is null → TOC left in `main`, **no** decorations
(plain static list; smoke test
`tests/docs/smoke-all/issues/3473-toc-side-body/body.qmd`).
- `left-body`/`right-body` → clone with `id="TOC-body"` (`.toc-actions`
stripped) inserted before the original, original moved to the sidebar.

## 3. Layout / CSS

SCSS (already ported into q2's `resources/scss/bootstrap/`):

| What | Q1 file:line |
|---|---|
| `.page-columns.toc-left` wide grid | `_bootstrap-rules.scss:62-70` |
| `.page-columns.toc-left` mid grid | `_bootstrap-rules.scss:145-152` |
| narrow collapse + `nav[role="doc-toc"] { display:none }` | `_bootstrap-rules.scss:219-232` |
| `.sidebar.toc-left` grid placement (`page-start / body-start`) | `_bootstrap-rules.scss:303-306` |
| toc-left margin-element reflow into body column | `_bootstrap-rules.scss:545-557` |
| `#quarto-margin-sidebar` / `#quarto-sidebar-toc-left` hidden below md | `_bootstrap-rules.scss:574-581` |
| sticky sidebar rules | `_bootstrap-rules.scss:1075-1087` |
| sidebar-TOC typography/active/border | `_bootstrap-rules.scss:1201-1330` |
| `page-columns-tocleft-wide/-mid` mixins | `_bootstrap-mixins.scss:1302-1334` |
| grid track vars | `_bootstrap-mixins.scss:846-895` |

`.sidebar.quarto-banner-title-block-sidebar` (`_bootstrap-rules.scss:1090-1094`)
appears dead — nothing assigns the class.

## 4. Banner interaction

`src/format/html/format-html-title.ts:169-175`: when banner (or manuscript)
and `toc-location === "left"` **exactly** (`left-body` misses it — latent Q1
inconsistency), set `templateParams["banner-header-class"] = "toc-left"`.
Consumed by `templates/banner/title-block.html:2` and
`templates/manuscript/title-block.html:1` — same `$if$` hook q2 already
carries at `crates/quarto-core/src/template.rs:337`. Why needed: Q1 moves
the banner header out of `#quarto-content`
(`format-html-title.ts:262-268`), so it loses the inherited `toc-left` grid
and must carry the class itself for its `column-body` to align.

## 5. Standalone `left` output shape

```html
<div id="quarto-content" class="page-columns page-rows-contents page-layout-article toc-left">
<div id="quarto-sidebar-toc-left" class="sidebar toc-left">
<nav id="TOC" role="doc-toc" class="toc-active" data-toc-expanded="…"> … </nav>
</div>
<div id="quarto-margin-sidebar" class="sidebar margin-sidebar"></div>
<main class="content" id="quarto-document-content"> … </main>
</div>
```

No `collapse`/`sidebar-navigation`/`floating`/`#quarto-sidebar-glass` —
those are website-only. The empty margin sidebar survives
(`bootstrapHtmlFinalizer` only removes it when there is no TOC and no
margin content, `format-html-bootstrap.ts:1030-1032`) and gets
`zindex-bottom` when empty (`:1084-1091`).

Other consumers of these ids:

- `format-html-bootstrap.ts:857-865` — "Other Formats/Links" target chain:
`nav[role=doc-toc]` → `#quarto-sidebar-toc-left` → `#quarto-margin-sidebar`.
- `src/resources/formats/html/quarto.js:53-58, 556-572` — runtime toggles:
`#quarto-margin-sidebar` → `quarto-toc-toggle`, `#quarto-sidebar` →
`quarto-sidebarnav-toggle`, `#quarto-sidebar-toc-left` →
`quarto-lefttoc-toggle`. (So in the website left case the TOC gets the
*sidebarnav* toggle, another behavioral difference between the paths.)

## 6. Gotchas for a q2 implementation

1. `toc-left` as a grid class only exists on the standalone path; websites
ride `body.floating`/`body.docked` grids. Two layout regimes, one option.
2. `body` = absence of a target = plain undecorated list (no scroll-spy).
3. `*-body` = clone `id="TOC-body"` with `.toc-actions` removed.
4. `banner-header-class: toc-left` gated on `=== "left"` exactly.
5. The article path emits `#quarto-margin-sidebar` unconditionally even for
`left`; downstream code (`getLinkTarget`, `zindex-bottom`,
`fullcontent`/`slimcontent` heuristics at
`format-html-bootstrap.ts:1036-1073`) depends on its presence.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
_site/
.quarto/
29 changes: 29 additions & 0 deletions claude-notes/plans/toc-location-investigation/repro/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# toc-location: left repro (bd-e2kpwy7n)

Local copy of the committed external repro from the Connect-docs port
(`/Users/cscheid/repos/github/cscheid/q2-connect-docs/llms-info/repros/toc-location-left/`).

Run:

```bash
cargo run --bin q2 -- render .
```

Observed at HEAD (2026-08-14, main after PR #530):

- `grep -c 'id="quarto-sidebar"' _site/index.html` → `0`
- `nav#TOC` renders inside `<div id="quarto-margin-sidebar" class="sidebar margin-sidebar">` (the right margin)

Expected (Q1 1.10.15, same input — see `_site-q1/` in the external repro):

- `nav#TOC` inside `<nav id="quarto-sidebar" class="sidebar collapse collapse-horizontal quarto-sidebar-collapse-item sidebar-navigation floating overflow-auto">`
- empty `<div id="quarto-margin-sidebar" class="sidebar margin-sidebar zindex-bottom">`
- `<body class="floating quarto-light">`

`_site/` is gitignored (generated).

**Status:** fixed on branch `braid/bd-e2kpwy7n-toc-location`
(2026-08-14) — q2 now renders `nav#TOC` inside a synthesized
`nav#quarto-sidebar … sidebar-floating` with `body.floating`, matching
the Q1 mechanism with q2's own class idiom. The "Observed at HEAD"
section above records the pre-fix behavior.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
project:
type: website
website:
title: "TOC Location Repro"
13 changes: 13 additions & 0 deletions claude-notes/plans/toc-location-investigation/repro/index.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
title: "Home"
toc: true
toc-location: left
---

## Alpha

Text.

## Beta

Text.
8 changes: 7 additions & 1 deletion crates/quarto-core/src/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ use crate::transforms::{
ReferenceLinkDiagnosticsTransform, ResourceCollectorTransform, SectionizeTransform,
ShortcodeResolveTransform, SidebarGenerateTransform, SidebarRenderTransform,
TableBootstrapClassTransform, TheoremSugarTransform, TitleBannerTransform, TitleBlockTransform,
TocGenerateTransform, TocRenderTransform, WebsiteBootstrapIconsTransform,
TocGenerateTransform, TocLocationTransform, TocRenderTransform, WebsiteBootstrapIconsTransform,
WebsiteCanonicalUrlTransform, WebsiteFaviconTransform, WebsiteTitlePrefixTransform,
};

Expand Down Expand Up @@ -1413,6 +1413,12 @@ pub fn build_transform_pipeline(
crate::project::listing::feed::ListingFeedLinkTransform::new(),
));
pipeline.push(Box::new(TocRenderTransform::new()));
// Placement decision for the rendered TOC (bd-e2kpwy7n): must run
// after TocRenderTransform (it gates on `rendered.navigation.toc`)
// and before SidebarRenderTransform (which consumes the
// `toc-in-sidebar` directive to merge the TOC into
// `nav#quarto-sidebar` for website pages).
pipeline.push(Box::new(TocLocationTransform::new()));
pipeline.push(Box::new(NavbarRenderTransform::new()));
pipeline.push(Box::new(SidebarRenderTransform::new()));
// Breadcrumbs derive from the resolved `navigation.sidebar`
Expand Down
Loading
Loading