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
class Parser: def __init__(self, text): self.tokens = self._tokenize(text) self.pos = 0
A recursive descent arithmetic parser
recursive-descent
tokenizer
operator-precedence
Intermediate
9 steps
javascript
const IBAN_LENGTHS = { DE: 22, FR: 27, GB: 22, ES: 24, IT: 27, NL: 18, BE: 16, CH: 21, AT: 20, PT: 25, };
How IBAN validation works in JavaScript
validation
checksum
modular-arithmetic
Intermediate
8 steps
python
from flask import Blueprint, render_template, redirect, url_for, session, request from wtforms import Form, StringField, SelectField, IntegerField from wtforms.validators import DataRequired, Email, NumberRange
A multi-step signup wizard in Flask
blueprints
session-state
form-validation
Intermediate
10 steps
python
from itertools import cycle from collections import defaultdict
Round-robin task distribution in Python
round-robin
iterators
load-balancing
Intermediate
6 steps
python
import base64 import json from typing import Annotated, Optional
Cursor pagination in a FastAPI endpoint
pagination
cursor
async
Intermediate
9 steps
java
@RestController @RequestMapping("/api/products") @Validated public class ProductSearchController {
Validating query params in a Spring controller
validation
pagination
rest-api
Intermediate
8 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.