Markdown Recipes
Contents
- Parse document structure (headings, sections)
- Replace one section in place
- Generate a table of contents
- Check links and images
- Extract tables and code blocks
- Pandoc conversion options
- Gotchas (flavors, fences, whitespace)
Parse document structure (headings, sections)
Heading detection must ignore ``` fenced regions — # inside code is not a heading:
python
def headings(path):
"""Yield (line_no, level, title) for real headings only."""
in_fence = False
for i, line in enumerate(open(path, encoding="utf-8")):
if line.lstrip().startswith("```"):
in_fence = not in_fence
elif not in_fence and line.startswith("#"):
level = len(line) - len(line.lstrip("#"))
if level <= 6 and line[level:level + 1] == " ":
yield i, level, line[level:].strip()Replace one section in place
A section spans from its heading to the next heading of the same or higher level:
python
def replace_section(path, title, new_body):
lines = open(path, encoding="utf-8").read().splitlines(keepends=True)
hs = list(headings(path))
match = [h for h in hs if h[2] == title]
if len(match) != 1:
raise ValueError(f"'{title}': found {len(match)} occurrences, need exactly 1")
start_line, level, _ = match[0]
later = [h for h in hs if h[0] > start_line and h[1] <= level]
end_line = later[0][0] if later else len(lines)
new = lines[:start_line + 1] + [new_body.rstrip() + "\n\n"] + lines[end_line:]
open(path, "w", encoding="utf-8").writelines(new)Generate a table of contents
GitHub anchor rule: lowercase, spaces → -, strip everything except word chars and hyphens:
python
import re
def toc(path, max_level=3):
out = []
for _, level, title in headings(path):
if 2 <= level <= max_level: # skip the H1 itself
anchor = re.sub(r"[^\w\- ]", "", title).strip().lower().replace(" ", "-")
out.append(f"{' ' * (level - 2)}- [{title}](#{anchor})")
return "\n".join(out)Duplicate titles get -1, -2 suffixes on GitHub — deduplicate with a counter if titles repeat.
Check links and images
Run before any pandoc conversion; missing images abort PDF builds:
python
import os, re
def broken_refs(path):
text = open(path, encoding="utf-8").read()
base = os.path.dirname(os.path.abspath(path))
broken = []
for target in re.findall(r"!?\[[^\]]*\]\(([^)#\s]+)[^)]*\)", text):
if not target.startswith(("http://", "https://", "mailto:")):
if not os.path.exists(os.path.join(base, target)):
broken.append(target)
return broken(External URLs: check only if the user asks — needs network calls.)
Extract tables and code blocks
python
import re
text = open("doc.md", encoding="utf-8").read()
# Fenced code blocks with language → [(lang, code), ...]
blocks = re.findall(r"```(\w*)\n(.*?)```", text, flags=re.S)
# Pipe tables → rows of cells (skip the |---| separator line)
rows = [[c.strip() for c in line.strip().strip("|").split("|")]
for line in text.splitlines()
if line.lstrip().startswith("|") and not re.match(r"^\s*\|[\s:|-]+\|\s*$", line)]Pandoc conversion options
bash
# Standalone HTML with title metadata (otherwise pandoc emits a fragment)
pandoc README.md -s --metadata title="README" -o README.html
# GFM input explicitly (tables, task lists, strikethrough)
pandoc -f gfm README.md -o out.docx
# PDF with margins and a TOC
pandoc report.md --toc -V geometry:margin=1in -o report.pdf
# Word → Markdown, keep images
pandoc report.docx --extract-media=./media -t gfm -o report.md
# Custom Word styling: reuse an existing doc's styles
pandoc report.md --reference-doc=template.docx -o styled.docxGotchas (flavors, fences, whitespace)
- Flavor differences. Tables, task lists (
- [ ]), footnotes, and strikethrough are GFM/extensions — original Markdown renderers ignore them. Pandoc's default input is pandoc markdown, not GFM: pass-f gfmwhen the source came from GitHub. - Fences must balance. An unclosed ``` swallows the rest of the document. Count fence lines before structural edits (the
headings()recipe already guards this). - Indented code blocks. 4-space-indented lines are code in classic Markdown — a list continuation indented 4+ spaces can silently become a code block. Prefer fenced blocks everywhere.
- Two trailing spaces = line break. Invisible but meaningful; don't strip trailing whitespace blindly in an existing file.
- Bare URLs don't auto-link in all renderers — wrap in
<...>or[text](url). - HTML inside Markdown passes through most renderers but is stripped by some (and by pandoc to some formats) — flag it when converting.
- Setext headings (
Title\n=====) are equivalent to#/##; the structure parser above misses them — normalize them first if a file mixes both styles, but only with the user's agreement (it rewrites lines). - Anchor links differ across renderers. The GitHub rule in the TOC recipe does not match GitLab/MkDocs exactly for punctuation-heavy titles; verify on the target platform.