Source code
Revision control
Copy as Markdown
Other Tools
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
"""Fail the build on a mermaid block that opens with a wrapped option value.
A mermaid diagram is drawn by the reader's browser, not by the build, so a
block mermaid cannot parse publishes its own source and nothing in the build
log says so. A block ends up that way when a directive option's value wraps:
MyST ends the option block at the first line without a leading colon, so the
continuation becomes the first line of the body and mermaid finds prose ahead
of the diagram type.
MyST keeps the continuation's indentation, and a diagram's opening line is not
indented deeper than the lines below it, so a first line indented past a later
one is the wrap. The diagram type itself is not checked: mermaid's keyword list
lives in the JavaScript the reader's browser loads, and several keywords are
ordinary words a caption can wrap onto ("graph", "info", "pie").
"""
import re
import textwrap
from sphinx.transforms.post_transforms import SphinxPostTransform
from sphinx.util import logging
from sphinxcontrib.mermaid import mermaid
logger = logging.getLogger(__name__)
# What mermaid discards before it reads the diagram type: a YAML front matter
# block, a %%{...}%% init directive, and a %% comment line. Any of them may sit
# indented ahead of the diagram, and the directive prepends a front matter
# block of its own for :title:, :config: and mermaid_config, ahead of whatever
# a wrapped option left at the top of the body.
FRONT_MATTER = re.compile(r"^-{3}\s*[\n\r].*?[\n\r]-{3}\s*[\n\r]+", re.DOTALL)
INIT_DIRECTIVE = re.compile(r"%%\{.*?\}%%\s*", re.DOTALL)
COMMENT = re.compile(r"^\s*%%(?!\{)[^\n]+\n?", re.MULTILINE)
WRAPPED_OPTION = (
"mermaid block opens with %s, indented past the lines below it; that is a "
"directive option's value wrapped onto a second line, and mermaid will not "
"draw the block. Keep the value on one line or use the YAML options block "
"(see tools/moztreedocs/docs/mermaid-integration.md)."
)
def indentation(line):
return len(line) - len(line.lstrip(" "))
def opens_with_wrapped_option(code):
"""Whether the body's first line is indented past a line below it."""
lines = [line for line in code.split("\n") if line.strip()]
if not lines:
return False
first = indentation(lines[0])
return any(first > indentation(line) for line in lines[1:])
class CheckWrappedOption(SphinxPostTransform):
"""Warn about a mermaid block whose body opens with a wrapped option value."""
default_priority = 5
def run(self, **kwargs):
for node in self.document.findall(mermaid):
# mermaid dedents the block before it reads it.
code = FRONT_MATTER.sub("", textwrap.dedent(node["code"]))
code = COMMENT.sub("", INIT_DIRECTIVE.sub("", code))
if opens_with_wrapped_option(code):
wrapped = next(line for line in code.split("\n") if line.strip())
logger.warning(WRAPPED_OPTION, repr(wrapped.strip()), location=node)
def setup(app):
app.add_post_transform(CheckWrappedOption)
return {
"version": "1.0",
"parallel_read_safe": True,
"parallel_write_safe": True,
}