Python interface

Mau can be used inside any Python project through the Mau class. This chapter describes the programmatic API and how to integrate Mau into your applications.

Basic usage

The simplest way to use Mau programmatically is through the process method, which runs the complete pipeline (lexing, parsing, visiting) in a single call.

from mau import Mau
from mau.message import LogMessageHandler
from mau.visitors.yaml_visitor import YamlVisitor

import logging

logger = logging.getLogger(__name__)

# Create a message handler.
message_handler = LogMessageHandler(logger)

# Create a Mau instance.
mau = Mau(message_handler)

# Process the input text.
text = "= Hello\n\nThis is a *test*."
result = mau.process(YamlVisitor, text, "input.mau")

print(result)

The three arguments to process are: the visitor class, the input text as a string, and a source filename used for error reporting.

Using a specific visitor

To render HTML output, install the HTML visitor plugin and use load_visitors to discover all available visitors.

from mau import Mau, load_visitors
from mau.message import LogMessageHandler

import logging

logger = logging.getLogger(__name__)
message_handler = LogMessageHandler(logger)

# Load all available visitors (discovers plugins via entry points).
visitors = load_visitors()

# Select the HTML visitor.
visitor_class = visitors["mau_html_visitor:HtmlVisitor"]

# Process the input.
mau = Mau(message_handler)
result = mau.process(visitor_class, "= Hello\n\nThis is a *test*.", "input.mau")

print(result)

The load_visitors function returns a dictionary mapping visitor names to their classes. Plugin visitors are discovered through the mau.visitors entry point group. The two core visitors core:YamlVisitor and core:JinjaVisitor are always available.

If you already have a reference to the visitor class (for example, because you imported it directly), you can pass it to process without using load_visitors.

Using the environment

The environment lets you pass configuration values and variables to the Mau processor. The Environment class holds a flat key-value store with dot-separated namespaces. Configuration values from a YAML file are loaded under the mau namespace.

import yaml

from mau import Mau, load_visitors, load_environment_files
from mau import load_environment_variables, BASE_NAMESPACE
from mau.environment.environment import Environment
from mau.message import LogMessageHandler

import logging

logger = logging.getLogger(__name__)
message_handler = LogMessageHandler(logger)

# Load configuration from a YAML file.
with open("config.yaml", "r") as f:
    config = yaml.safe_load(f)

# Build the environment from the configuration.
# BASE_NAMESPACE is "mau", so keys become "mau.parser.aliases", etc.
environment = Environment.from_dict(config, BASE_NAMESPACE)

# Optionally load additional YAML files.
# The content of data.yaml is stored under "mau.envfiles.data".
load_environment_files(environment, ["data=data.yaml"])

# Optionally set command-line-style variables.
# The variable is stored under "mau.envvars.answer".
load_environment_variables(environment, ["answer=42"])

# Create Mau with the environment.
visitors = load_visitors()
visitor_class = visitors["mau_html_visitor:HtmlVisitor"]

mau = Mau(message_handler, environment)
result = mau.process(
    visitor_class,
    "The answer is {mau.envvars.answer}.",
    "input.mau",
)

print(result)

The load_environment_files function accepts a list of strings in the format KEY=PATH or just PATH (in which case the filename stem is used as the key). The content is loaded with yaml.safe_load and stored under mau.envfiles.KEY by default. You can change the namespace by passing a namespace argument.

The load_environment_variables function accepts a list of strings in the format KEY=VALUE. Values are stored under mau.envvars.KEY by default, and the namespace can be changed in the same way.

Step-by-step processing

If you need more control over the pipeline, you can run each step individually. This is useful if you want to inspect intermediate results or perform custom processing between steps.

from mau import Mau
from mau.message import LogMessageHandler
from mau.visitors.yaml_visitor import YamlVisitor

import logging

logger = logging.getLogger(__name__)
message_handler = LogMessageHandler(logger)

mau = Mau(message_handler)

# Step 1: Create a text buffer from the input string.
text_buffer = mau.init_text_buffer("= Hello\n\nSome text.", "input.mau")

# Step 2: Run the lexer. It tokenises the input.
lexer = mau.run_lexer(text_buffer)
print(f"Tokens: {len(lexer.tokens)}")

# Step 3: Run the parser. It builds the AST from the tokens.
parser = mau.run_parser(lexer.tokens)

# The parser output contains:
# - parser.output.document: the root node of the AST
# - parser.output.toc: the table of contents node
# - parser.output.include_calls: the list of include calls
document = parser.output.document

# Step 4: Run the visitor. It renders the AST.
result = mau.run_visitor(YamlVisitor, document)

print(result)

Each step raises MauException if an error occurs. The Mau instance automatically sends the error message to the message handler before re-raising the exception.

Error handling

Mau raises MauException when an error occurs during lexing, parsing, or visiting. The exception wraps a MauMessage that contains details about the error.

from mau import Mau
from mau.message import LogMessageHandler, MauException
from mau.visitors.yaml_visitor import YamlVisitor

import logging

logger = logging.getLogger(__name__)
message_handler = LogMessageHandler(logger)

mau = Mau(message_handler)

try:
    result = mau.process(YamlVisitor, "Some text.", "input.mau")
except MauException as exc:
    # The message has already been sent to the handler,
    # which logged it. You can also inspect it directly.
    print(f"Error: {exc.message.text}")

Message handlers

Mau uses a message handler to process errors and debug messages. The message handler is an instance of a class that inherits from BaseMessageHandler and implements four methods:

  • process_lexer_error(message) — handles lexer errors.
  • process_parser_error(message) — handles parser errors.
  • process_visitor_error(message) — handles visitor errors.
  • process_visitor_debug(message) — handles debug messages (e.g. from the #mau:debug tag).

The BaseMessageHandler.process method dispatches incoming messages to the correct method based on their type.

Mau provides LogMessageHandler, which logs messages through a standard Python Logger. It accepts an optional prefix (default "[MAU] ") and a debug_logging_level (default INFO).

from mau.message import LogMessageHandler

import logging

logger = logging.getLogger("mau")
logger.setLevel(logging.DEBUG)

# Messages are logged at INFO level for debug, ERROR for errors.
handler = LogMessageHandler(logger, prefix="[MAU] ", debug_logging_level=logging.DEBUG)

To create a custom message handler, subclass BaseMessageHandler and implement the four abstract methods. Each method receives a specific message type:

  • MauLexerErrorMessage — contains text, source, and position.
  • MauParserErrorMessage — contains text, context, and help_text.
  • MauVisitorErrorMessage — contains text, context, node_type, data, environment, and additional_info.
  • MauVisitorDebugMessage — contains the same fields as the visitor error message.
from mau.message import BaseMessageHandler

class PrintMessageHandler(BaseMessageHandler):
    def process_lexer_error(self, message):
        print(f"Lexer error: {message.text} at {message.position}")

    def process_parser_error(self, message):
        print(f"Parser error: {message.text}")
        if message.help_text:
            print(f"  Help: {message.help_text}")

    def process_visitor_error(self, message):
        print(f"Visitor error: {message.text} (node: {message.node_type})")

    def process_visitor_debug(self, message):
        print(f"Debug [{message.node_type}]: {message.text}")
        if message.data:
            for k, v in message.data.items():
                print(f"  {k}: {v}")

Using Mau in Pelican

You can use Mau to write posts and pages in Pelican. First install the plugin

pip install pelican-mau-reader

See the project page for detailed documentation.

Every file in your content directory that ends with .mau will be processed by the plugin. Metadata is specified using Mau variables under the pelican namespace

:pelican.title:This is a post written with Mau
:pelican.date:2021-02-17 13:00:00
:pelican.category:tests
:pelican.tags:foo, bar, foobar
:pelican.summary:I have a lot to write

Pelican settings

The plugin reads two dictionaries from Pelican's configuration file (pelicanconf.py):

  • MAU — a dictionary of Mau configuration. It is loaded under the mau namespace, so MAU = {"visitor": {"name": "mau_html_visitor:HtmlVisitor"}} becomes mau.visitor.name.
  • MAU_VARIABLES — a dictionary of additional variables merged into the environment. Use this to pass values that your documents can reference with {namespace.key}.
# pelicanconf.py

MAU = {
    "visitor": {
        "name": "mau_html_visitor:HtmlVisitor",
        "templates": {
            "paths": ["mau/templates"],
        },
    },
}

MAU_VARIABLES = {
    "website": "true",
}

The visitor is selected through mau.visitor.name. The value must match a visitor registered via the mau.visitors entry point group. If the visitor is not installed, the plugin raises an error. The default is mau_html_visitor:HtmlVisitor.

Pelican prefixes

The plugin automatically creates template prefixes from two Pelican metadata values: pelican.series and pelican.template. If a document declares

:pelican.series:My Book
:pelican.template:doc

the plugin normalises the series name (lowercase, spaces replaced with dashes, only letters, numbers, and dashes kept) and creates the prefixes ["my-book", "doc"]. These are stored in mau.visitor.templates.prefixes before the visitor runs.

This means you can create templates that apply only to pages of a specific series or Pelican template. For example, a template file named header.pf_my-book.j2 will be used for headers in pages that belong to the "My Book" series, while header.j2 remains the fallback for all other pages. See the chapter on templates for details on how prefixes affect template matching.

Table of contents as metadata

After rendering the document, the plugin renders the table of contents separately and stores it in the Pelican metadata under metadata["mau"]["toc"]. This makes the ToC available in your Pelican templates (for example, to display it in a sidebar).

When rendering the ToC, the plugin changes the template prefixes by prepending page- to each prefix and adding a bare page prefix. So if the document prefixes are ["my-book", "doc"], the ToC is rendered with ["page-my-book", "page-doc", "page"]. This allows you to define ToC templates that render differently when used as page-level metadata (e.g. a compact sidebar ToC) versus when included inside the document itself.

A concrete example can be found in the source code of this documentation, where the template toc.pf_page.html renders the ToC of each single page on the right side of the screen.

Formatted fields

Pelican defines a list of metadata fields that support rich text formatting (the FORMATTED_FIELDS setting, which by default includes summary). When the Mau reader encounters one of these fields, it processes the value through the full Mau pipeline, so you can use Mau syntax in your summary

:pelican.summary:This post is about *important* things

The summary will be rendered as HTML (or whatever format the selected visitor produces) before being passed to Pelican.