[project.entry-points."mau.visitors"]
visitor = "my_package:MyVisitor"Create a custom visitor
This chapter shows how to create custom visitors for Mau. A visitor is a Python class that processes the AST and produces output. We will build two examples: a simple JSON visitor based on BaseVisitor, and a Markdown visitor based on JinjaVisitor.
Visitor registration
Mau discovers visitors through Python entry points. To make a visitor available to the command line tool, you need to:
- Create a Python package with the visitor class.
- Register it in
pyproject.tomlunder themau.visitorsentry point group.
The entry point value is in the format module:ClassName. Once installed, your visitor becomes available as -t my_package:MyVisitor on the command line. Mau will also list it among the available visitors in --help.
When using Mau programmatically, you can skip the entry point and pass the visitor class directly.
Example 1: A JSON visitor
The simplest way to create a custom visitor is to subclass BaseVisitor. The base class already visits every node and returns a Python dictionary. All you need to do is convert that dictionary to your desired output format.
The YamlVisitor does exactly this for YAML. Here is the equivalent for JSON:
import json
from mau.visitors.base_visitor import BaseVisitor
class JsonVisitor(BaseVisitor):
format_code = "json"
extension = "json"
def _postprocess(self, result, *args, **kwargs):
result = super()._postprocess(result, *args, **kwargs)
return json.dumps(result, indent=2, default=str)The class attributes format_code and extension are used by the command line tool to identify the visitor and to determine the output file extension.
The _postprocess method is called after the entire AST has been visited. Here, we convert the Python dictionary returned by BaseVisitor.visit into a JSON string. The default=str argument handles any non-serialisable values by converting them to strings.
To register this visitor, add the following to pyproject.toml:
[project.entry-points."mau.visitors"]
visitor = "mau_json_visitor:JsonVisitor"You can now run:
mau -i input.mau -t mau_json_visitor:JsonVisitor -o output.jsonLike the YAML visitor, the JSON visitor is useful for debugging and for building tooling that needs to inspect the AST programmatically.
Example 2: A Markdown visitor
A more interesting example is a visitor that transforms Mau into Markdown. Since Markdown is a text format, we can use JinjaVisitor and write Jinja templates for each node type.
The visitor class
from importlib.resources import files
from mau.environment.environment import Environment
from mau.visitors.jinja_visitor import JinjaVisitor, _load_templates_from_path
TEMPLATES_EXTENSION = ".md"
templates = _load_templates_from_path(
str(files(__package__).joinpath("templates")),
TEMPLATES_EXTENSION,
)
class MarkdownVisitor(JinjaVisitor):
format_code = "md"
extension = TEMPLATES_EXTENSION
default_templates = Environment.from_dict(templates)The class is minimal. The JinjaVisitor base class handles template discovery, specificity matching, and rendering. All we need to do is:
- Set the template extension to
.md. - Load our default templates using
_load_templates_from_path. - Pass them to the
default_templatesclass attribute.
Unlike the HTML visitor, there is no need for text escaping or special preprocessing in most cases. If you needed to escape characters that are special in Markdown (like * or _), you could override _visit_text.
Templates
Templates are placed in a templates/ directory inside the package, with the .md extension. Here are some examples.
Text and verbatim
{{ value }}`{{ value }}`Styles
Markdown uses **double stars** for bold and *single stars* for italic. Since Mau's style system is template-driven, we just map each style to the correct Markdown syntax.
**{{ content }}***{{ content }}*~~{{ content }}~~Markdown has no native syntax for superscript and subscript, so those can fall back to HTML:
<sup>{{ content }}</sup>Paragraphs
{{ content }}
{{ content }}The blank line after the content is essential — it is how Markdown separates paragraphs.
Headers
{% set hashes = '#' * level %}{{ hashes }} {{ content }}
Lists
{{ ' ' * (level - 1) }}{% if ordered %}{{ number }}. {% else %}- {% endif %}{{ content }}{{ content }}This requires the template to receive level, ordered, and number variables from the visitor. If the base visitor does not provide all of them, you can override _visit_list_item to add them.
Links and images
[{{ content }}]({{ target }})Source blocks
```{{ language }}
{{ content }}
```
{{ line_content }}Horizontal rule
---
Document
{{ content }}Limitations
This Markdown visitor is a starting point, not a complete implementation. Some Mau features have no direct Markdown equivalent:
- Labels — Markdown has no concept of labels. They can be ignored or rendered as comments.
- Block groups — there is no equivalent. Custom templates would need to flatten them.
- Footnotes — some Markdown flavours support
[^note]syntax, but it is not universal. - Classes — Markdown has no equivalent. Some flavours support
{.class}attributes. - Custom macros — would need a fallback template.
Despite these limitations, the example demonstrates how little code is needed to create a functional visitor. The JinjaVisitor base class handles all the complexity of template discovery and rendering, and the visitor itself is just a few lines of Python plus a set of templates.