Extension API
This page explains how to make rich-rst render your own docutils node types.
If you have a custom directive/transform that creates custom nodes, this is the API you use to tell rich-rst how those nodes should appear.
The entry point is rich_rst.RSTVisitor.
When should I use this?
Use this API when all of the following are true:
You already have (or will create) a custom docutils node class.
You want rich-rst output to show that node in a custom way.
Built-in rendering is not enough.
If you only use standard reStructuredText/Sphinx features, you usually do not need this page.
Overview
During rendering, rich-rst visits nodes in the parsed document tree.
For each node:
For each node,
rich_rst.RSTVisitorchecks its custom registry.If a handler is registered for that exact node class, the handler runs.
Otherwise, normal
visit_*/depart_*method lookup is used.
Important: registration is by exact class match (not by subclass hierarchy).
The custom registry is class-level and stores:
{node_class: (visit_fn, depart_fn)}
Quick start (5 steps)
Define (or import) your custom node class.
Write a visit handler with signature
handler(visitor, node).Register it with
rich_rst.RSTVisitor.register_visitor().Render your document normally.
Unregister when the handler should no longer be active.
Minimal pattern:
from rich_rst import RSTVisitor
def visit_my_node(visitor, node):
# Add any Rich renderable you want.
visitor.renderables.append(...)
RSTVisitor.register_visitor(MyNode, visit_fn=visit_my_node)
try:
... # render documents
finally:
RSTVisitor.unregister_visitor(MyNode)
Public methods
RSTVisitor.register_visitor(node_class, visit_fn=None, depart_fn=None)Register custom handlers for a node class.
node_classis the docutils node class to handle.visit_fnruns when entering the node.depart_fnis optional and runs after children are visited.Direct form:
RSTVisitor.register_visitor(MyNode, visit_fn=my_visit, depart_fn=my_depart)
Decorator form (registers the function as
visit_fn):@RSTVisitor.register_visitor(MyNode) def my_visit(visitor, node): ...
RSTVisitor.unregister_visitor(node_class)Remove a registration. Calling this for a class that is not registered is safe and does nothing.
RSTVisitor.list_registered_visitors()Return a snapshot of the registry as a dict. Mutating the returned dict does not change the live registry.
Handler contract
Visit/depart handlers are called as handler(visitor, node).
Inside handlers:
visitoris the activerich_rst.RSTVisitorinstance.nodeis the current docutils node instance.
Common patterns:
Add Rich renderables through
visitor.renderables.append(...).Raise
docutils.nodes.SkipChildrenfrom a visit handler when you fully handle a node and do not want child traversal.Use a depart handler when you need closing behavior after children are visited.
End-to-end example (node + directive + RST)
This example shows the full flow most users need:
Define a custom node class.
Define a directive that emits that node.
Register a visitor handler for that node.
Render an RST document that uses the directive.
from rich import print
from rich.panel import Panel
from rich_rst import RestructuredText, RSTVisitor
from rich_rst._vendor import docutils
class MyNode(docutils.nodes.General, docutils.nodes.Body, docutils.nodes.Element):
pass
class MyCalloutDirective(docutils.parsers.rst.Directive):
has_content = True
def run(self):
node = MyNode()
self.state.nested_parse(self.content, self.content_offset, node)
return [node]
def visit_my_node(visitor, node):
# Render the node as a Rich panel and skip normal child traversal.
visitor.renderables.append(Panel(node.astext(), title="my-callout", border_style="cyan"))
raise docutils.nodes.SkipChildren()
def enable_extensions():
docutils.parsers.rst.directives.register_directive("my-callout", MyCalloutDirective)
RSTVisitor.register_visitor(MyNode, visit_fn=visit_my_node)
def disable_extensions():
RSTVisitor.unregister_visitor(MyNode)
enable_extensions()
try:
rst_source = (
"Before.\n\n"
".. my-callout::\n\n"
" This text is parsed into MyNode and rendered by visit_my_node.\n\n"
"After.\n"
)
print(RestructuredText(rst_source))
finally:
disable_extensions()
Notes:
register_directiveandregister_visitorare both global registrations.In tests, prefer fixture-based setup/teardown to avoid cross-test leakage.
The
try/finallyabove ensures the visitor is always unregistered.
Example: decorator registration
Use this form when you like declaration-style setup:
from rich.text import Text
from rich_rst import RSTVisitor
from rich_rst._vendor import docutils
class DecoratedNode(docutils.nodes.General, docutils.nodes.Inline, docutils.nodes.Element):
pass
@RSTVisitor.register_visitor(DecoratedNode)
def visit_decorated(visitor, node):
visitor.renderables.append(Text("from decorator"))
raise docutils.nodes.SkipChildren()
# later, when no longer needed:
RSTVisitor.unregister_visitor(DecoratedNode)
Subclass behavior
Registrations on rich_rst.RSTVisitor are global to that class and are
visible to all instances.
If you call register_visitor on a subclass of RSTVisitor, that subclass
gets its own registry dictionary. This isolates subclass-specific extensions
from the base class and from sibling subclasses.
Troubleshooting
Handler did not run: Confirm you registered the exact node class being produced.
Custom output appears twice: If your visit handler fully handles content, raise
docutils.nodes.SkipChildren.Tests affect each other: Unregister handlers in test teardown/fixtures.
Registry changes do not stick:
list_registered_visitors()returns a snapshot copy, not a live dict.
Recommendations
Register handlers at application startup, not per document.
Unregister temporary handlers in tests or short-lived plugin scopes.
Keep handlers narrow and side-effect free except for appending renderables.