Skip to content

fix(ai-toc): include setext headings in the generated table of contents - #393

Merged
petertzy merged 3 commits into
petertzy:mainfrom
harsh-thakkar7:fix/ai-toc-setext-headings
Oct 8, 2026
Merged

petertzy merged 3 commits into
petertzy:mainfrom
harsh-thakkar7:fix/ai-toc-setext-headings

Conversation

@harsh-thakkar7

Copy link
Copy Markdown
Contributor

Problem

The AI table-of-contents generator (_generate_markdown_toc in backend/ai_logic.py, used by /toc) only recognizes ATX (# Heading) headings. For a document written with CommonMark setext headings it returns an empty string, so /toc produces no table of contents at all — yet the preview renderer anchors those same headings (<h1 id="title">, <h2 id="sub">). backend/heading_anchor.py exists precisely so the outline and the TOC “never diverge again”; this path was still diverging.

Title
=====

Intro.

Sub
---

Body.

Before: _generate_markdown_toc(...) returned "".
After: it returns - [Title](#title) and - [Sub](#sub), matching the renderer’s anchors.

Change

  • Add _iter_toc_headings(), a fence-aware scanner that yields (level, title) for both ATX and setext headings — a text line over = is level 1, over two or more - is level 2 (a single - is a list item, not a heading). This mirrors the renderer, so every emitted anchor resolves.
  • _generate_markdown_toc() now consumes it, keeping source order and the existing GitHub-style duplicate-anchor suffixes (-1, -2, …).

Tests

Added to tests/test_markdown_toc.py: setext inclusion and levels, source-order mixing with ATX, duplicate setext anchors, the two-dash boundary, the blank-line-is-a-thematic-break case, and setext-looking text inside a code fence. Mutation-tested the new suite (2-dash bound, level swap, fence skip, dedup). Full backend suite: 478 passed; ruff check and ruff format --check clean.

harsh-thakkar7 and others added 3 commits October 8, 2026 17:37
_generate_markdown_toc only recognised ATX ("# Heading") lines, so a
document written with setext headings produced an empty
"## Table of Contents" even though the preview renderer anchors those
headings as h1/h2. The outline endpoint (backend/routers/markdown.py)
already treats setext headings as real headings, and heading_anchor.py
exists precisely so the two callers cannot diverge.

Teach the TOC generator to recognise a text line followed by a run of
"=" (level 1) or two or more "-" (level 2), matching the renderer:
backtick/tilde fences are skipped, a blank line before the underline
makes it a thematic break, and ATX/underline lines never double as
setext text. Duplicate slugs are still disambiguated GitHub-style.
@petertzy

petertzy commented Oct 8, 2026

Copy link
Copy Markdown
Owner

Approved — this is a necessary correctness fix. The AI-generated table of contents now includes Setext headings with the correct levels, source order, and duplicate-anchor handling, so generated links match the document heading structure.
I also hardened fenced-code handling: a backtick line with an info string can no longer close an existing fence and expose code content as a TOC entry, while preserving the existing tilde-fence behavior. Backend tests (494 passed), frontend tests (94/94), Ruff, ESLint, and TypeScript checks all pass.

@petertzy
petertzy merged commit a344cbd into petertzy:main Oct 8, 2026
2 checks passed
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.

2 participants