python 61 lines · 9 steps

Building a table of contents from Markdown

Parse Markdown headers into a paginated table of contents with unique anchors, skipping fenced code blocks.

Explained by highlit
1import re
2from dataclasses import dataclass, field
3 
4 
5@dataclass
6class TocEntry:
7 level: int
8 title: str
9 anchor: str
10 page: int
11 
12 
13_HEADER = re.compile(r"^(#{1,6})\s+(.+?)\s*#*$", re.MULTILINE)
14_FENCE = re.compile(r"^```", re.MULTILINE)
15 
16 
17def _slugify(text: str) -> str:
18 slug = re.sub(r"[^\w\s-]", "", text.lower())
19 return re.sub(r"[\s_]+", "-", slug).strip("-")
20 
21 
22def _strip_code_blocks(markdown: str) -> str:
23 parts = _FENCE.split(markdown)
24 return "".join(parts[::2])
25 
26 
27def build_toc(markdown: str, per_page: int = 20, max_level: int = 3):
28 seen: dict[str, int] = {}
29 entries: list[TocEntry] = []
30 
31 for match in _HEADER.finditer(_strip_code_blocks(markdown)):
32 level = len(match.group(1))
33 if level > max_level:
34 continue
35 
36 title = match.group(2).strip()
37 base = _slugify(title)
38 count = seen.get(base, 0)
39 seen[base] = count + 1
40 anchor = base if count == 0 else f"{base}-{count}"
41 
42 entries.append(TocEntry(level, title, anchor, page=0))
43 
44 for index, entry in enumerate(entries):
45 entry.page = index // per_page + 1
46 
47 return entries
48 
49 
50def paginate_toc(markdown: str, page: int, per_page: int = 20):
51 entries = build_toc(markdown, per_page)
52 total_pages = max(1, -(-len(entries) // per_page))
53 page = max(1, min(page, total_pages))
54 start = (page - 1) * per_page
55 
56 return {
57 "page": page,
58 "total_pages": total_pages,
59 "total_entries": len(entries),
60 "entries": entries[start:start + per_page],
61 }
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Splitting on fence markers and keeping even-indexed parts cleanly excludes code blocks from parsing.
  2. 2Tracking seen slugs in a dict lets you disambiguate duplicate headings deterministically.
  3. 3Ceiling division via -(-n // d) computes page counts without importing math.ceil.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Building a table of contents from Markdown — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code