Python’s markdown library converts Markdown text to HTML. pip install markdown. Basic: import markdown; html = markdown.markdown(text). Extensions: markdown.markdown(text, extensions=["tables","fenced_code","codehilite","toc","attr_list","meta","nl2br"]). Tables: | col | col | syntax. Fenced code: ```python blocks. Codehilite: syntax highlighting with Pygments. TOC: [TOC] in text, md.toc after conversion. Attr list: {.class #id} on elements. Meta: YAML-style headers — md.Meta["title"] after convert. nl2br: newline → <br>. Stateful: from markdown import Markdown; md = Markdown(extensions=[...]); md.convert(text); md.reset() — reset between documents. Output: markdown.markdown(text, output_format="html5"). output_format="xhtml". Extensions config: extensions=["codehilite"], extension_configs={"codehilite":{"linenums":True}}. Toc config: extension_configs={"toc":{"permalink":True,"title":"Contents"}}. Custom extension: subclass Extension, register Preprocessor, Treeprocessor, Postprocessor. Preprocessor: modify lines before parsing. Postprocessor: modify HTML after parsing. Treeprocessor: walk ElementTree. Convert+meta: call md.convert(src) then md.Meta dict. Strip HTML: md.convert(text) then re.sub(r"<[^>]+>","",html). Batch: [md.convert(src); md.reset() for src in sources]. Sanitize: pipe through bleach after conversion. Claude Code generates markdown-to-HTML pipelines, extension stacks, and template renderers.
CLAUDE.md for markdown
## markdown Stack
- Version: markdown >= 3.5 | pip install markdown
- Basic: markdown.markdown(text, extensions=["tables","fenced_code","toc"])
- Stateful: md = Markdown(extensions=[...]); md.convert(text); md.reset()
- Meta: md.Meta["title"][0] after md.convert(src) — YAML front matter dict
- TOC: md.toc string after convert — inject into template as sidebar/nav
- Highlight: extensions=["codehilite"], extension_configs={"codehilite":{"guess_lang":False}}
- Sanitize: always pipe html through bleach.clean() for untrusted Markdown input
markdown Conversion Pipeline
# app/md_render.py — Python markdown conversion, extensions, meta, and Jinja2 integration
from __future__ import annotations
import re
import textwrap
from pathlib import Path
from typing import Any
import markdown
from markdown import Extension
from markdown.preprocessors import Preprocessor
from markdown.postprocessors import Postprocessor
from markdown.treeprocessors import Treeprocessor
from markdown import Markdown
# ─────────────────────────────────────────────────────────────────────────────
# 1. Standard conversion with extension stack
# ─────────────────────────────────────────────────────────────────────────────
EXTENSIONS = [
"tables",
"fenced_code",
"codehilite",
"toc",
"attr_list",
"def_list",
"footnotes",
"meta",
"nl2br",
"sane_lists",
"smarty",
]
EXTENSION_CONFIGS: dict[str, dict] = {
"codehilite": {
"linenums": False,
"guess_lang": False,
"css_class": "highlight",
},
"toc": {
"permalink": True,
"title": "Contents",
"toc_depth": "2-3",
},
"smarty": {
"smart_dashes": True,
"smart_quotes": True,
"smart_ellipses": True,
},
}
def convert(text: str) -> str:
"""
One-shot conversion — creates a fresh Markdown instance each call.
Use make_md() + md.convert() + md.reset() when converting many documents.
"""
return markdown.markdown(
text,
extensions=EXTENSIONS,
extension_configs=EXTENSION_CONFIGS,
output_format="html5",
)
def make_md() -> Markdown:
"""
Reusable Markdown instance — call md.reset() between documents to clear
state (TOC, meta, footnote counter). Faster than constructing per-call.
"""
return Markdown(
extensions=EXTENSIONS,
extension_configs=EXTENSION_CONFIGS,
output_format="html5",
)
# ─────────────────────────────────────────────────────────────────────────────
# 2. Document with front matter (meta extension)
# ─────────────────────────────────────────────────────────────────────────────
def parse_document(source: str) -> dict[str, Any]:
"""
The meta extension reads YAML-style key: value headers at the top of the
document. md.Meta["title"] → list of strings (one per line value).
Example front matter:
Title: My Post
Tags: python, markdown
Date: 2024-01-05
(blank line)
# Heading...
"""
md = make_md()
html = md.convert(source)
meta = {k: v[0] if len(v) == 1 else v for k, v in md.Meta.items()}
return {
"html": html,
"toc": md.toc, # the generated <ul> TOC string
"meta": meta,
}
def parse_documents(sources: list[str]) -> list[dict[str, Any]]:
"""Batch conversion — reuses md instance, resets between documents."""
md = make_md()
results = []
for src in sources:
html = md.convert(src)
meta = {k: v[0] if len(v) == 1 else v for k, v in md.Meta.items()}
results.append({"html": html, "toc": md.toc, "meta": meta})
md.reset()
return results
# ─────────────────────────────────────────────────────────────────────────────
# 3. Custom preprocessor — strip YAML front-matter block
# ─────────────────────────────────────────────────────────────────────────────
class FrontMatterPreprocessor(Preprocessor):
"""
Remove Hugo/Jekyll triple-dash YAML front matter before markdown parses it.
The meta extension reads key: value pairs, not --- delimiters.
"""
def run(self, lines: list[str]) -> list[str]:
if not lines or lines[0].strip() != "---":
return lines
end = None
for i, line in enumerate(lines[1:], start=1):
if line.strip() == "---":
end = i
break
return lines[end + 1 :] if end else lines
class FrontMatterExtension(Extension):
def extendMarkdown(self, md: Markdown) -> None:
md.preprocessors.register(
FrontMatterPreprocessor(md), "front_matter", 175
)
# ─────────────────────────────────────────────────────────────────────────────
# 4. Custom treeprocessor — add target="_blank" to external links
# ─────────────────────────────────────────────────────────────────────────────
import xml.etree.ElementTree as ET
class ExternalLinkProcessor(Treeprocessor):
"""Walk the parsed element tree and annotate external links."""
def run(self, root: ET.Element) -> None:
for element in root.iter("a"):
href = element.get("href", "")
if href.startswith("http://") or href.startswith("https://"):
element.set("target", "_blank")
element.set("rel", "noopener noreferrer")
class ExternalLinkExtension(Extension):
def extendMarkdown(self, md: Markdown) -> None:
md.treeprocessors.register(
ExternalLinkProcessor(md), "external_links", 5
)
# ─────────────────────────────────────────────────────────────────────────────
# 5. Custom postprocessor — wrap <table> in a scrollable div
# ─────────────────────────────────────────────────────────────────────────────
class TableWrapPostprocessor(Postprocessor):
"""Wrap every <table> in a responsive scroll container."""
_TABLE_RE = re.compile(r"<table>", re.IGNORECASE)
_END_RE = re.compile(r"</table>", re.IGNORECASE)
def run(self, text: str) -> str:
text = self._TABLE_RE.sub('<div class="table-scroll"><table>', text)
text = self._END_RE.sub("</table></div>", text)
return text
class TableWrapExtension(Extension):
def extendMarkdown(self, md: Markdown) -> None:
md.postprocessors.register(
TableWrapPostprocessor(md), "table_wrap", 5
)
# ─────────────────────────────────────────────────────────────────────────────
# 6. Full-featured renderer (all custom extensions + standard stack)
# ─────────────────────────────────────────────────────────────────────────────
def make_full_md() -> Markdown:
"""
Markdown instance with standard extensions plus:
- FrontMatterExtension — strip --- ... --- blocks
- ExternalLinkExtension — add target="_blank" to external links
- TableWrapExtension — wrap <table> in responsive div
"""
return Markdown(
extensions=[
"tables", "fenced_code", "codehilite", "toc",
"attr_list", "def_list", "footnotes", "meta",
"nl2br", "sane_lists", "smarty",
FrontMatterExtension(),
ExternalLinkExtension(),
TableWrapExtension(),
],
extension_configs=EXTENSION_CONFIGS,
output_format="html5",
)
def render_page(source: str) -> dict[str, Any]:
"""Render one page with full extension stack."""
md = make_full_md()
html = md.convert(source)
meta = {k: (v[0] if len(v) == 1 else v) for k, v in md.Meta.items()}
return {"html": html, "toc": md.toc, "meta": meta}
# ─────────────────────────────────────────────────────────────────────────────
# 7. File-based blog renderer
# ─────────────────────────────────────────────────────────────────────────────
def render_directory(
content_dir: Path,
pattern: str = "*.md",
) -> list[dict[str, Any]]:
"""
Walk a directory, parse every Markdown file, return sorted by date meta.
"""
md = make_full_md()
posts = []
for path in sorted(content_dir.glob(pattern)):
source = path.read_text(encoding="utf-8")
html = md.convert(source)
meta = {k: (v[0] if len(v) == 1 else v) for k, v in md.Meta.items()}
posts.append({
"slug": path.stem,
"html": html,
"toc": md.toc,
"meta": meta,
"title": meta.get("title", path.stem),
"date": meta.get("date", ""),
})
md.reset()
return sorted(posts, key=lambda p: p["date"], reverse=True)
# ─────────────────────────────────────────────────────────────────────────────
# 8. Jinja2 integration
# ─────────────────────────────────────────────────────────────────────────────
def register_markdown_filter(env) -> None:
"""
Register markdown conversion as a Jinja2 filter.
Usage in templates: {{ post.body | mdrender | safe }}
"""
_md = make_full_md()
def _filter(text: str) -> str:
html = _md.convert(text)
_md.reset()
return html
env.filters["mdrender"] = _filter
# ─────────────────────────────────────────────────────────────────────────────
# 9. Plain text extraction (strip all HTML tags)
# ─────────────────────────────────────────────────────────────────────────────
_TAG_RE = re.compile(r"<[^>]+>")
def to_plain_text(markdown_source: str) -> str:
"""Convert Markdown → HTML → strip tags → plain text (for search indexing)."""
html = convert(markdown_source)
return _TAG_RE.sub("", html).strip()
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
SAMPLE_MD = textwrap.dedent("""\
Title: Python Markdown Demo
Date: 2024-01-15
Tags: python, markdown
# Heading 1
[TOC]
## Tables
| Library | Stars | Type |
|-----------|------:|----------|
| markdown | 3.5k | renderer |
| mistune | 2.8k | renderer |
| commonmark| 1.2k | spec |
## Fenced code
```python
def hello(name: str) -> str:
return f"Hello, {name}!"
```
## External link
Visit [Python docs](https://docs.python.org).
## Footnote
This sentence has a footnote.[^1]
[^1]: The footnote content here.
""")
if __name__ == "__main__":
print("=== Front matter + TOC + Meta ===")
doc = parse_document(SAMPLE_MD)
print(f" meta: {doc['meta']}")
print(f" toc: {doc['toc'][:120]}…")
print(f" html snippet: {doc['html'][:200]}…")
print("\n=== Plain text extraction ===")
plain = to_plain_text(SAMPLE_MD)
print(plain[:300])
print("\n=== Full renderer ===")
page = render_page(SAMPLE_MD)
print(f" title from meta: {page['meta'].get('title')}")
print(f" html length: {len(page['html'])} chars")
print("\n=== Batch conversion (5 docs) ===")
docs = parse_documents([SAMPLE_MD] * 5)
print(f" Parsed {len(docs)} documents")
For the mistune alternative — mistune 3.x uses a plugin system (mistune.create_markdown(plugins=["table","url","strikethrough"])) and is faster for raw throughput, while Python’s markdown package has a richer extension ecosystem including codehilite for syntax highlighting via Pygments, a meta extension for front-matter key extraction, toc with permalink generation, and an Extension API with Preprocessor, Treeprocessor, and Postprocessor hooks — three distinct phases that let you modify the source lines before parsing, walk the element tree after parsing, and rewrite the HTML string after serialization, none of which mistune exposes as cleanly. For the commonmark / cmark alternative — the commonmark Python package is a CommonMark-spec-compliant parser that produces predictable output across implementations, while Python’s markdown library is more widely deployed, has more available third-party extensions, and supports md.Meta front-matter parsing and md.toc sidebar generation out of the box without extra packages — though if strict CommonMark compliance is required (for content exchanged across platforms), commonmark or mistune with CommonMark plugin is the better choice. The Claude Skills 360 bundle includes markdown skill sets covering markdown.markdown with extension list, Markdown class with reset for batch conversion, meta extension and md.Meta front-matter parsing, toc extension with permalink and toc_depth, codehilite with Pygments and linenums, custom Preprocessor for front-matter stripping, Treeprocessor for external link annotation, Postprocessor for table wrapping, FrontMatterExtension/ExternalLinkExtension/TableWrapExtension composition, batch render_directory for static site generation, and Jinja2 mdrender filter registration. Start with the free tier to try Markdown rendering pipeline code generation.