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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Splitting on fence markers and keeping even-indexed parts cleanly excludes code blocks from parsing.
- 2Tracking seen slugs in a dict lets you disambiguate duplicate headings deterministically.
- 3Ceiling division via -(-n // d) computes page counts without importing math.ceil.
Related explainers
python
from fastapi import FastAPI, WebSocket, WebSocketDisconnect app = FastAPI()
Building a WebSocket chat with FastAPI
websockets
broadcast
connection-management
Intermediate
9 steps
ruby
class UserAgentParser BROWSERS = [ [/Edg\/([\d.]+)/, "Edge"], [/OPR\/([\d.]+)/, "Opera"],
Parsing user-agent strings in Ruby
regex
pattern-matching
lookup-tables
Intermediate
8 steps
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
Intermediate
7 steps
javascript
function evaluate(expression) { const tokens = tokenize(expression); let pos = 0;
Building a recursive descent calculator
parsing
recursion
operator-precedence
Intermediate
8 steps
php
<?php namespace App\Services;
How a password strength validator works in PHP
validation
regular-expressions
data-driven
Intermediate
8 steps
python
import random from typing import Iterator, List
How reservoir sampling picks k items
reservoir-sampling
streaming
randomness
Intermediate
5 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-a-table-of-contents-from-markdown-explained-python-7dad/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.