Skip to content

fix(make-pdf): assign body heading ids so --toc anchors resolve (#1689)#1789

Open
0xDevNinja wants to merge 1 commit into
garrytan:mainfrom
0xDevNinja:fix/1689-toc-heading-ids
Open

fix(make-pdf): assign body heading ids so --toc anchors resolve (#1689)#1789
0xDevNinja wants to merge 1 commit into
garrytan:mainfrom
0xDevNinja:fix/1689-toc-heading-ids

Conversation

@0xDevNinja
Copy link
Copy Markdown
Contributor

Summary

  • make-pdf --toc built a table of contents whose <a href="#toc-N"> anchors and <span data-toc-target="toc-N"> page-number spans referenced element ids (toc-0, toc-1, ...) that no body heading ever carried.
  • buildTocBlock minted toc-${i} purely for its own markup; the body headings came straight from marked (<h1>One</h1>, no id) and wrapChaptersByH1 never assigned them. So every TOC link was a dead in-page fragment and Paged.js had no target element to resolve page numbers against.

Fixes #1689.

Fix

New annotateHeadingIds(html) pass:

  • Walks H1-H3 headings in document order.
  • A heading with no id gets id="toc-N" injected (N = document-order index).
  • A heading that already declares an id keeps it; the TOC points at the existing id.
  • Returns the rewritten body HTML plus the resolved {level, text, id} list, which buildTocBlock now consumes — so anchors and targets are guaranteed to agree with the body.

The id annotation only runs when --toc is requested (the ids exist solely for the TOC), so non-TOC renders are byte-for-byte unchanged.

Before / after (issue repro)

bun -e '
import { render } from "./make-pdf/src/render.ts";
const r = render({ markdown: "# One\n\n## Sub\n\nbody\n\n# Two\n", toc: true });
const hrefs = [...r.html.matchAll(/href="(#toc-\d+)"/g)].map(m => m[1]);
const ids = [...r.html.matchAll(/<h[1-3][^>]*\bid="([^"]+)"/g)].map(m => m[1]);
console.log("anchors:", hrefs, "heading ids:", ids);
'

Before: anchors: ["#toc-0","#toc-1","#toc-2"] heading ids: []
After: anchors: ["#toc-0","#toc-1","#toc-2"] heading ids: ["toc-0","toc-1","toc-2"]

Tests

3 new cases in make-pdf/test/render.test.ts:

  • TOC anchors + data-toc-target all resolve to a real body heading id.
  • A heading's pre-existing id is preserved and linked, id-less siblings still get a minted id.
  • No id="toc- injection when toc is off.
bun test make-pdf/test/render.test.ts
# 61 pass, 0 fail

- buildTocBlock minted `toc-N` ids for its `#toc-N` anchors and
  `data-toc-target` spans, but no body heading ever received those ids,
  so every TOC link was a dead fragment and Paged.js had no target to
  count page numbers against
- Add annotateHeadingIds: walk H1-H3 in document order, inject `id="toc-N"`
  on headings that lack one, preserve any heading that already declares an id
  and point the TOC at it
- Share the resolved heading/id list between body and TOC so anchors and
  targets always agree
- 3 regression tests: anchor-resolves-to-heading, existing-id preserved,
  no id injection when toc is off

Fixes garrytan#1689.
@jbetala7
Copy link
Copy Markdown
Contributor

Collision check: #1690 is already open for the same issue and same files. It was opened on 2026-05-25 by the issue author, also targets #1689, touches make-pdf/src/render.ts and make-pdf/test/render.test.ts, assigns stable TOC ids, preserves existing heading ids, and pins the no-TOC behavior.

This patch looks directionally correct, but functionally duplicate. I would treat #1690 as the canonical PR unless maintainers prefer this branch. If this one continues, it should explicitly explain why #1690 is being superseded.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

make-pdf: --toc generates dangling anchors and unresolvable page-number targets (no heading carries the toc-N id)

2 participants