Example: a reStructuredText renderer#
Technique: tokens, handlers and join_item. reStructuredText (reST) is the markup of
Sphinx and docutils. Its math is LaTeX, so this renderer starts from LatexRenderer and
keeps all of its math. What changes is the text around the math: headings, paragraphs,
imports, how math groups are wrapped, and how block bodies are indented.
The code#
"""
A reStructuredText renderer: LaTeX math in ``.. math::`` directives, reST
headings, and indented block bodies. Built on LatexRenderer by changing its
tokens, a few handlers and the join step.
"""
import re
from handcalcs.renderers import LatexRenderer
INDENT = " "
UNDERLINES = "=-~^\""
# The inherited block headers ("Since $...$ is True:") use LaTeX inline math.
INLINE_MATH = re.compile(r"\$(.+?)\$")
class RstRenderer(LatexRenderer):
name = "rst"
# Inside a math group the rows are plain LaTeX; ``math_group`` below wraps
# them in the directive.
MATH_OPEN = "\\begin{aligned}"
MATH_CLOSE = "\\end{aligned}"
MATH_SKIP = ""
PARAGRAPH = ""
LINE_BREAK = "|" # a reST line block with one empty line
# A .rst file needs nothing around the output (not LaTeX's document).
preamble = ""
postamble = ""
def math_group(self, rows):
body = self.ROW_SEP.join(rows)
lines = [self.MATH_OPEN, *body.split("\n"), self.MATH_CLOSE]
return ".. math::\n\n" + "\n".join(INDENT + line for line in lines)
def paragraph(self, text):
return text
def join_item(self, item, depth, base_context):
"""A block body becomes an indented (block-quoted) paragraph group."""
if item and isinstance(item, list) and isinstance(item[-1], list):
header, body = item[0], item[-1]
is_math = bool(body) and body[0] == self.MATH_OPEN
if not is_math and isinstance(header, str) and header.strip():
inner = "".join(self.join_items(body, depth + 1, base_context))
indented = "\n".join(
INDENT + line if line else line for line in inner.strip("\n").split("\n")
)
header = INLINE_MATH.sub(r":math:`\1`", header)
return [f"{header}\n\n", f"{indented}\n\n"]
return super().join_item(item, depth, base_context)
def complete(self, tree, base_context):
return self.join(tree, base_context).strip() + "\n"
@RstRenderer.register("heading")
def rst_heading(renderer, node, base_context):
underline = UNDERLINES[min(node.heading_level, len(UNDERLINES)) - 1]
return f"{node.content}\n{underline * len(node.content)}"
@RstRenderer.register("comment_line")
def rst_comment_line(renderer, node, base_context):
return node.content
@RstRenderer.register("import")
def rst_import(renderer, node, base_context):
names = ", ".join(name.identifier for name in node.names)
if node.import_from:
return f"``from {node.import_from_module} import {names}``"
return f"``import {names}``"
The result#
## Web check
# The web must be at least class 2.
h_w = 310; t_w = 6; F_y = 350
x_1 = h_w / t_w # slenderness
if h_w / t_w <= 1100 / sqrt(F_y):
web_class = 1
Web check
The web must be at least class 2.
.. math::
\begin{aligned} h_{w} &= 310 ; &t_{w} &= 6 ; &F_{y} &= 350 ; \end{aligned}
.. math::
\begin{aligned} x_{1} &= \frac{h_{w}}{t_{w}} = \frac{310}{6} = 51.667 \ &\quad\text{(slenderness)} \end{aligned}
Since :math:\bigl(\frac{h_{w}}{t_{w}} \le \frac{1100}{\sqrt{F_{y}}}\bigr) \rightarrow \bigl(\frac{310}{6} \le \frac{1100}{\sqrt{350}}\bigr) is True:
.. math::
\begin{aligned} \mathrm{web}_{\mathrm{class}} &= 1 \end{aligned}
Web check
=========
The web must be at least class 2.
.. math::
\begin{aligned}
h_{w} &= 310 \; &t_{w} &= 6 \; &F_{y} &= 350 \;
\end{aligned}
.. math::
\begin{aligned}
x_{1} &= \frac{h_{w}}{t_{w}} = \frac{310}{6} = 51.667 \\
&\quad\text{(slenderness)}
\end{aligned}
Since :math:`\bigl(\frac{h_{w}}{t_{w}} \le \frac{1100}{\sqrt{F_{y}}}\bigr) \rightarrow \bigl(\frac{310}{6} \le \frac{1100}{\sqrt{350}}\bigr)` is True:
.. math::
\begin{aligned}
\mathrm{web}_{\mathrm{class}} &= 1
\end{aligned}
How it works#
The changes fall into the three levers, in order of size.
Tokens. MATH_SKIP, PARAGRAPH and LINE_BREAK replace LaTeX commands (\medskip,
\noindent) with reST equivalents. MATH_OPEN and MATH_CLOSE become the bare
aligned environment; they also act as markers that LatexRenderer.join_item uses to
recognise a math group, so they must stay distinct strings.
Handlers. heading, comment_line and import are replaced, because LaTeX’s
versions emit \section* and escape text for LaTeX. The math handlers (names,
operators, constants) are all inherited.
Methods.
math_groupwraps a group’s rows in a.. math::directive and indents them, which reST requires.paragraphreturns text unchanged (LaTeX adds\noindent).join_itemhandles the one shape that differs: a block with a header (aniforfor). reST indents a block body as a block quote, so the method joins the body one level deeper, indents every line, and converts the header’s inline$...$math (from the inheritedheader:if_blockhandler) to the:math:role. Every other item goes toLatexRenderer.join_item.completestrips the surrounding blank lines.preambleandpostambleare emptied, so a file written withdestination=holds only the reST, not the LaTeX document the parent class writes.
Checking the output#
reST is strict about indentation and blank lines, so check the output with docutils:
import docutils.core
docutils.core.publish_string(rst_text, writer_name="html",
settings_overrides={"halt_level": 2})
halt_level=2 turns warnings into exceptions, which makes this a one-line test. See
Testing your renderer.
Next: Example: structured data.