Automatic documentation from sources, for MkDocs.
—
HTML rendering components including syntax highlighting, heading management, and output formatting processors. The rendering system transforms collected documentation data into properly formatted HTML with consistent styling and structure.
Code syntax highlighter that matches Markdown configuration and provides consistent code formatting.
class Highlighter(Highlight):
"""Code highlighter that matches Markdown configuration."""
_highlight_config_keys: frozenset[str]
"""Configuration keys supported by the highlighter."""
def __init__(self, md: Markdown) -> None:
"""
Configure to match a Markdown instance.
Args:
md: The Markdown instance to read configs from
"""
def highlight(
self,
src: str,
language: str | None = None,
*,
inline: bool = False,
dedent: bool = True,
linenums: bool | None = None,
**kwargs: Any
) -> str:
"""
Highlight a code-snippet.
Args:
src: The code to highlight
language: Explicitly tell what language to use for highlighting
inline: Whether to highlight as inline
dedent: Whether to dedent the code before highlighting it or not
linenums: Whether to add line numbers in the result
**kwargs: Pass on to pymdownx.highlight.Highlight.highlight
Returns:
The highlighted code as HTML text, marked safe (not escaped for HTML)
"""Usage Examples:
Basic syntax highlighting:
from mkdocstrings import Highlighter
from markdown import Markdown
md = Markdown(extensions=['codehilite'])
highlighter = Highlighter(md)
# Highlight Python code
python_code = """
def hello_world():
print("Hello, World!")
return True
"""
highlighted = highlighter.highlight(python_code, language="python")
print(highlighted) # Returns HTML with syntax highlightingInline code highlighting:
# Highlight inline code
inline_code = "print('hello')"
highlighted_inline = highlighter.highlight(
inline_code,
language="python",
inline=True
)With line numbers:
# Highlight with line numbers
highlighted_with_lines = highlighter.highlight(
python_code,
language="python",
linenums=True
)Tree processor that shifts heading levels in rendered HTML content to maintain proper document hierarchy.
class HeadingShiftingTreeprocessor(Treeprocessor):
"""Shift levels of all Markdown headings according to the configured base level."""
name: str = "mkdocstrings_headings"
"""The name of the treeprocessor."""
regex: re.Pattern = re.compile(r"([Hh])([1-6])")
"""The regex to match heading tags."""
shift_by: int
"""The number of heading 'levels' to add to every heading. <h2> with shift_by = 3 becomes <h5>."""
def __init__(self, md: Markdown, shift_by: int) -> None:
"""
Initialize the object.
Args:
md: A markdown.Markdown instance
shift_by: The number of heading 'levels' to add to every heading
"""
def run(self, root: Element) -> None:
"""
Shift the levels of all headings in the document.
Args:
root: Root element of the tree to process
"""Usage Examples:
Shift headings in handler rendering:
from mkdocstrings import HeadingShiftingTreeprocessor
from markdown import Markdown
from xml.etree.ElementTree import Element, fromstring
# Create processor that shifts headings down by 2 levels
md = Markdown()
processor = HeadingShiftingTreeprocessor(md, shift_by=2)
# Process HTML content
html_content = """
<h1>Main Title</h1>
<h2>Subtitle</h2>
<h3>Section</h3>
"""
root = fromstring(f"<div>{html_content}</div>")
processor.run(root)
# Now h1 becomes h3, h2 becomes h4, h3 becomes h5Integration in handlers:
class MyHandler(BaseHandler):
def render(self, data: CollectorItem, options: HandlerOptions, *, locale: str | None = None) -> str:
# Get heading level from options
heading_level = options.get("heading_level", 1)
# Create inner Markdown with heading shifter
inner_md = Markdown()
if heading_level > 1:
shifter = HeadingShiftingTreeprocessor(inner_md, shift_by=heading_level - 1)
inner_md.treeprocessors.register(shifter, "heading_shift", 5)
# Process docstring with proper heading levels
docstring_html = inner_md.convert(data.docstring)
return template.render(data=data, docstring_html=docstring_html)Tree processor that prepends prefixes to HTML element IDs to avoid conflicts in the generated documentation.
class IdPrependingTreeprocessor(Treeprocessor):
"""Prepend the configured prefix to IDs of all HTML elements."""
name: str = "mkdocstrings_ids"
"""The name of the treeprocessor."""
id_prefix: str
"""The prefix to add to every ID. It is prepended without any separator; specify your own separator if needed."""
def __init__(self, md: Markdown, id_prefix: str) -> None:
"""
Initialize the object.
Args:
md: A markdown.Markdown instance
id_prefix: The prefix to add to every ID. It is prepended without any separator
"""
def run(self, root: Element) -> None:
"""
Prepend the configured prefix to all IDs in the document.
Args:
root: Root element of the tree to process
"""Usage Examples:
Prevent ID conflicts between multiple autodoc blocks:
from mkdocstrings import IdPrependingTreeprocessor
# Create processor with prefix
processor = IdPrependingTreeprocessor(md, id_prefix="module1-")
# Process HTML with IDs
html_with_ids = """
<h2 id="function">My Function</h2>
<div id="example">Example</div>
"""
root = fromstring(f"<div>{html_with_ids}</div>")
processor.run(root)
# IDs become: "module1-function", "module1-example"Handler integration:
class MyHandler(BaseHandler):
def render(self, data: CollectorItem, options: HandlerOptions, *, locale: str | None = None) -> str:
# Create unique ID prefix for this object
id_prefix = data.name.replace(".", "-") + "-"
# Process with ID prefixing
inner_md = Markdown()
id_processor = IdPrependingTreeprocessor(inner_md, id_prefix)
inner_md.treeprocessors.register(id_processor, "id_prefix", 10)
# Convert content with prefixed IDs
content_html = inner_md.convert(data.content)
return template.render(data=data, content_html=content_html)Tree processor that unwraps single paragraph elements from output for cleaner formatting.
class ParagraphStrippingTreeprocessor(Treeprocessor):
"""Unwraps the <p> element around the whole output."""
name: str = "mkdocstrings_strip_paragraph"
"""The name of the treeprocessor."""
strip: bool = False
"""Whether to strip <p> elements or not."""
def run(self, root: Element) -> Element | None:
"""
Unwrap the root element if it's a single <p> element.
Args:
root: Root element of the tree to process
Returns:
Modified root element or None
"""Usage Examples:
Strip single paragraphs from docstring content:
from mkdocstrings import ParagraphStrippingTreeprocessor
# Create processor
processor = ParagraphStrippingTreeprocessor()
processor.strip = True
# Process content that has unnecessary paragraph wrapper
html_content = "<p>Simple text content</p>"
root = fromstring(html_content)
result = processor.run(root)
# Result: "Simple text content" (paragraph wrapper removed)Handler usage for inline content:
def render_inline_docstring(self, docstring: str) -> str:
"""Render docstring as inline content without paragraph wrapper."""
inner_md = Markdown()
# Add paragraph stripper for inline rendering
stripper = ParagraphStrippingTreeprocessor()
stripper.strip = True
inner_md.treeprocessors.register(stripper, "strip_p", 15)
return inner_md.convert(docstring)Rendering components integrate with Jinja2 templates:
def render(self, data: CollectorItem, options: HandlerOptions, *, locale: str | None = None) -> str:
# Set up rendering environment
highlighter = Highlighter(self.md)
# Configure template globals
template_globals = {
"highlight": highlighter.highlight,
"shift_headings": self.shift_headings,
"config": options
}
# Render template with rendering utilities
template = self.env.get_template("handler.html")
return template.render(
data=data,
**template_globals
)Rendering components respect Markdown configuration:
# Handler initialization
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Create highlighter that matches main Markdown config
self.highlighter = Highlighter(self.md)
# Configure processors based on options
self.configure_processors()
def configure_processors(self):
"""Configure tree processors based on handler options."""
# Add heading shifter if needed
if self.config.get("heading_level", 1) > 1:
shift_by = self.config["heading_level"] - 1
shifter = HeadingShiftingTreeprocessor(self.inner_md, shift_by)
self.inner_md.treeprocessors.register(shifter, "shift_headings", 5)
# Add ID prefixer for unique IDs
if self.config.get("unique_ids", True):
id_prefix = self.get_id_prefix()
prefixer = IdPrependingTreeprocessor(self.inner_md, id_prefix)
self.inner_md.treeprocessors.register(prefixer, "prefix_ids", 10)Extend rendering for custom needs:
class CustomHandler(BaseHandler):
def setup_rendering(self):
"""Set up custom rendering pipeline."""
# Create custom highlighter with additional languages
self.highlighter = Highlighter(self.md)
# Add custom tree processors
self.add_custom_processors()
def add_custom_processors(self):
"""Add custom tree processors."""
# Custom processor for special formatting
custom_processor = CustomTreeProcessor(self.inner_md)
self.inner_md.treeprocessors.register(custom_processor, "custom", 20)
def render_with_custom_formatting(self, content: str, language: str) -> str:
"""Render content with custom formatting."""
# Highlight code
highlighted = self.highlighter.highlight(content, language=language)
# Apply custom processing
processed = self.apply_custom_processing(highlighted)
return processedExtension that should always be added to Markdown sub-documents that handlers request.
class MkdocstringsInnerExtension(Extension):
"""Extension that should always be added to Markdown sub-documents that handlers request (and only them)."""
headings: list[Element]
"""The list that will be populated with all HTML heading elements encountered in the document."""
def __init__(self, headings: list[Element]) -> None:
"""
Initialize the object.
Args:
headings: A list that will be populated with all HTML heading elements encountered in the document
"""
def extendMarkdown(self, md: Markdown) -> None:
"""
Register the extension.
Args:
md: A markdown.Markdown instance
"""Usage Examples:
Using the inner extension for handler sub-documents:
from mkdocstrings import MkdocstringsInnerExtension
from markdown import Markdown
from xml.etree.ElementTree import Element
# Create list to collect headings
headings: list[Element] = []
# Create inner extension
inner_extension = MkdocstringsInnerExtension(headings)
# Create Markdown instance for sub-document processing
inner_md = Markdown(extensions=[inner_extension])
# Process handler content
content = """
# Function Documentation
## Parameters
### param1
Description of parameter 1.
### param2
Description of parameter 2.
"""
result = inner_md.convert(content)
# headings list now contains all heading elements found
print(f"Found {len(headings)} headings")
for heading in headings:
print(f"- {heading.tag}: {heading.text}")Handler integration:
class MyHandler(BaseHandler):
def render(self, data: CollectorItem, options: HandlerOptions, *, locale: str | None = None) -> str:
# Create headings collector
headings: list[Element] = []
# Set up inner Markdown with extension
inner_extension = MkdocstringsInnerExtension(headings)
inner_md = Markdown(extensions=[
inner_extension,
'toc',
'codehilite'
])
# Process docstring content
docstring_html = inner_md.convert(data.docstring)
# Use collected headings for navigation or TOC
toc_data = [(h.tag, h.text, h.get('id')) for h in headings]
# Render template with processed content
template = self.env.get_template('function.html')
return template.render(
data=data,
docstring_html=docstring_html,
toc_data=toc_data
)This rendering system provides consistent, high-quality HTML output while maintaining flexibility for customization and integration with different themes and styling approaches.
Install with Tessl CLI
npx tessl i tessl/pypi-mkdocstrings