Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }} & PDM
uses: pdm-project/setup-pdm@v3
uses: pdm-project/setup-pdm@v4
with:
python-version: ${{ matrix.python-version }}
cache: true
Expand All @@ -39,15 +39,15 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Validate links in Markdown files
uses: JustinBeckwith/linkinator-action@v1
with:
retry: true

- name: Set up Python & PDM
uses: pdm-project/setup-pdm@v3
uses: pdm-project/setup-pdm@v4
with:
python-version: "3.10"
cache: true
Expand All @@ -71,12 +71,12 @@ jobs:
id-token: write

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
token: ${{ secrets.GH_TOKEN }}

- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: "3.10"

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ Mau Reader is a Pelican plugin that converts the [Mau](https://github.com/Projec

This plugin requires:

* Python 3.8+
* Python 3.10+
* Pelican 4.5+
* Mau 2.0+
* Mau 5.0+

## Installation

Expand Down
3 changes: 3 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Release type: major

Update the reader to the Mau 5.0.0 interface. Requires Python 3.10+.
252 changes: 193 additions & 59 deletions pelican/plugins/mau_reader/mau_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,34 @@

"""

from logging import WARNING, getLogger
import re

from pelican import signals
from pelican.readers import BaseReader
from pelican.utils import pelican_open

logger = getLogger(__name__)


try:
from mau import Mau, load_visitors
from mau import BASE_NAMESPACE, Mau, load_visitors
from mau.environment.environment import Environment
from mau.errors import MauErrorException, print_error
from mau.message import LogMessageHandler, MauException

visitor_classes = load_visitors()
# Load a dictionary of all visitors,
# indexed by the output format.
visitors = load_visitors()
mau_enabled = True
except ImportError:
visitor_classes = []
visitors = {}
mau_enabled = False


visitors = {i.format_code: i for i in visitor_classes}
# This is the additional namespace
# used by elements that are rendered for
# the entire page and not just for the
# document, like the metadata ToC.
DEFAULT_PAGE_NAMESPACE = "page"


class OutputFormatNotSupported(Exception):
Expand All @@ -40,6 +51,21 @@ def __init__(self, filename):
super().__init__(f"The file {filename} cannot be parsed")


def _text_to_name(text: str | None) -> str | None:
if not text:
return text

# The input text can contain spaces an mixed-case
# characters. Convert it into lowercase.
text = text.lower()

# Replace spaces and underscores with dashes.
text = re.sub(r"[\ _]+", "-", text)

# Get only letters, numbers, dashes.
return "".join(re.findall("[a-z0-9-\\. ]+", text))


class MauReader(BaseReader):
"""Mau Reader class method."""

Expand All @@ -49,88 +75,196 @@ class MauReader(BaseReader):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

# The message handler uses the Pelican
# logger to send messages.
self.message_handler = LogMessageHandler(logger, debug_logging_level=WARNING)

def read(self, source_path):
# Import Mau settings from Pelican settings
config = self.settings.get("MAU", {})
self.environment = Environment()
self.environment.update(config, "mau")
mau_config_dict = self.settings.get("MAU", {})

output_format = config.get("output_format", "html")
# Load the Mau configuration under
# the base namespace.
environment = Environment.from_dict(mau_config_dict, BASE_NAMESPACE)

if output_format not in visitors:
raise OutputFormatNotSupported(output_format)
# Get the Mau configuration as an environment.
config = environment.get(BASE_NAMESPACE)

visitor_class = visitors[output_format]
self.environment.setvar("mau.visitor.class", visitor_class)
self.environment.setvar("mau.visitor.format", output_format)
# Load Mau variables from Pelican settings
mau_variables = Environment.from_dict(self.settings.get("MAU_VARIABLES", {}))

self._source_path = source_path
# Update the environment adding the variables.
environment.update(mau_variables, overwrite=False)

mau = Mau(
source_path,
self.environment,
# Extract the selected visitor or use the
# HTML visitor from the standard plugin.
selected_visitor = config.get(
"mau.visitor.name", "mau_html_visitor:HtmlVisitor"
)

try:
with pelican_open(source_path) as text:
mau.run_lexer(text)
# If the selected visitor is not available
# we need to stop and sound the alarm.
if selected_visitor not in visitors:
raise OutputFormatNotSupported(selected_visitor)

# Run the Mau parser
mau.run_parser(mau.lexer.tokens)
# Let's store the selected visitor class.
# We will need it when we parse the
# metadata later.
self.visitor_class = visitors[selected_visitor]

# These are the templates prefixes
prefixes = [
self.environment.getvar("pelican.series"),
self.environment.getvar("pelican.template"),
]
prefixes = [i for i in prefixes if i is not None]
self.environment.setvar("mau.visitor.prefixes", prefixes)
# This is stored in a variable to
# be available inside documents.
environment["mau.visitor.format"] = self.visitor_class.format_code

# This is another value we need later.
self._source_path = source_path

# Run the visitor on the main content
content = mau.run_visitor(mau.parser.output["content"])
if visitor_class.transform:
content = visitor_class.transform(content)
# Instantiate the Mau object.
mau = Mau(self.message_handler, environment=environment)

metadata = self._parse_metadata()
# The following code processes the
# Pelican input document.
# It is a giant try/except because
# so many things can go wrong and in
# all those cases Pelican needs to
# know what and to move on.
try:
# Read the source file and instantiate
# a text buffer on it.
with pelican_open(source_path) as text:
# Initialise the Text Buffer.
text_buffer = mau.init_text_buffer(text, source_path)

# Lex and parse the input document.
lexer = mau.run_lexer(text_buffer)
parser = mau.run_parser(lexer.tokens)

# Pelican allows to store metadata
# in the document file itself, so
# we need to save the environment
# AFTER the parsing, which contains
# the configuration we created above
# plus any variable created in the
# document.
self.environment = mau.environment

# Mau allows us to define custom
# prefixes for templates. This reader
# uses the Pelican template and the
# Pelican series as prefixes. This
# means that we can customise Mau
# templates according to the Pelican
# series or the Pelican template.

# The list of custom Mau prefixes.
prefixes = []

# This is the name of the Pelican series.
# The name of a series can contain spaces
# and other characters that do not sit well
# with file naming for Mau templates.
# This is why we process it to transform it
# into a usable name.
prefixes.append(_text_to_name(self.environment.get("pelican.series")))

# This is the name of the Pelican template.
prefixes.append(self.environment.get("pelican.template"))

# Filter out any invalid value.
prefixes = [i for i in prefixes if i is not None]

# Store the prefixes into the environment.
self.environment["mau.visitor.templates.prefixes"] = prefixes

prefixes = [f"{i}.page" for i in prefixes] + ["page"]
self.environment.setvar("mau.visitor.prefixes", prefixes)
# Get the main output node from the parser.
document = parser.output.document

metadata["mau"] = {}
metadata["mau"]["toc"] = mau.run_visitor(mau.parser.output["toc"])
# Process the node with the
# selected visitor.
content = mau.run_visitor(self.visitor_class, document)

except MauErrorException as exception:
print_error(exception.error)
# Process the Pelican metadata.
# This returns a dictionary
# {metadata: processed value}
metadata = self._parse_metadata()

# Add the ToC as metadata, so that
# it is available to Pelican and can
# be addedd to the page outside
# the rendered document itself
# (e.g. in a sidebar).

# From this point, all custom prefixes
# created before (Pelican template and series)
# are given the additional namespace `page`
# to signal that the rendered text belongs
# to the page and not the document.
prefixes = [f"{DEFAULT_PAGE_NAMESPACE}-{i}" for i in prefixes]
prefixes.append(DEFAULT_PAGE_NAMESPACE)

# Store the prefixes into the environment.
self.environment["mau.visitor.templates.prefixes"] = prefixes

# Create Mau metadata inside the Pelican metadata.
metadata["mau"] = {
# Add the rendered nested ToC to the metadata.
"toc": mau.run_visitor(self.visitor_class, parser.output.toc)
}

except MauException as exception:
self.message_handler.process(exception.message)
raise ErrorInSourceFile(source_path) from exception

return content, metadata

def _parse_metadata(self):
"""Return the dict containing document metadata."""
meta = self.environment.getvar("pelican").asdict()

# Finds metadata defined inside the document,
# process it, collect it into a dictionary,
# and return it.

# All Pelican metadata must be
# defined under the namespace
# `pelican`.
pelican_env = self.environment.get("pelican")
meta = pelican_env.asdict() if pelican_env is not None else {}

# Get from Pelican the list of fields
# that support formatting (i.e. rich text).
formatted_fields = self.settings["FORMATTED_FIELDS"]

mau = Mau(
self._source_path,
self.environment,
)
# Instantiate the Mau object.
mau = Mau(self.message_handler, environment=self.environment)

# This is the final dictionary
# of processed metadata.
output = {}

# For each metadata variable defined
# in the document, extract name and value,
# process the value if needed, and store
# it in the final dictionary.
for name, value in meta.items():
# Lowercase all metadata.
name = name.lower()

if name in formatted_fields:
mau.run_lexer(value)
mau.run_parser(mau.lexer.tokens)
formatted = mau.run_visitor(mau.parser.output["content"])
output[name] = self.process_metadata(name, formatted)
elif len(value) > 1:
# handle list metadata as list of string
output[name] = self.process_metadata(name, value)
else:
# otherwise, handle metadata as single string
output[name] = self.process_metadata(name, value[0])
# For debugging purposes, change the
# source path adding the metadata field
# we are processing.
source_path = f"{self._source_path}::{name}"

# Process the value of the metadata field.
content = mau.process(self.visitor_class, value, source_path)

# Add it to the final output.
output[name] = self.process_metadata(name, content)

continue

# The value is a plain string from
# Mau's Environment. Pass it directly
# to Pelican's metadata processing.
output[name] = self.process_metadata(name, value)

return output

Expand Down
Loading
Loading