Code View

#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""render.py — draw the message-file grammar's railroad diagrams.

Every NNNNN-<rule>.rrd in this folder is one grammar rule written in the
tabatkins railroad-diagrams notation (Diagram, Sequence, Choice, Optional,
ZeroOrMore, OneOrMore, Terminal, NonTerminal, Comment, Stack). This script
evaluates each one with the Python port of that library and writes

    NNNNN-<rule>.svg           next to it (committed; the source of record)
    <png-dir>/<rule>.png       with --png <png-dir> (for the web site)

both styled with railroad-style.css. The PNGs are what mstring.fedem.eu
serves: browsers size standalone railroad SVGs unreliably.

Requires:  pip install railroad-diagrams cairosvg   (cairosvg needs libcairo)
Usage:     python3 render.py [--png <dir>] [--scale 1.5]
"""
import argparse
import glob
import io
import os
import re
import sys

import railroad

HERE = os.path.dirname(os.path.abspath(__file__))


def load_css():
    with open(os.path.join(HERE, "railroad-style.css"), encoding="utf-8") as f:
        text = f.read()
    # Drop the leading comment block: only rules go into the SVG.
    return re.sub(r"/\*.*?\*/", "", text, flags=re.DOTALL).strip()


def diagram(path):
    names = {name: getattr(railroad, name) for name in dir(railroad) if not name.startswith("_")}
    with open(path, encoding="utf-8") as f:
        return eval(f.read(), {"__builtins__": {}}, names)


def svg_of(d, css):
    out = io.StringIO()
    d.writeStandalone(out.write, css=css)
    return out.getvalue()


def main():
    parser = argparse.ArgumentParser(description=__doc__.split("\n")[0])
    parser.add_argument("--png", metavar="DIR", help="also write <rule>.png into DIR")
    parser.add_argument("--scale", type=float, default=1.5, help="PNG scale factor (default 1.5)")
    args = parser.parse_args()

    css = load_css()
    sources = sorted(glob.glob(os.path.join(HERE, "*.rrd")))

    if args.png:
        import cairosvg
        os.makedirs(args.png, exist_ok=True)

    for source in sources:
        base = os.path.splitext(os.path.basename(source))[0]
        rule = base.split("-", 1)[1]
        svg = svg_of(diagram(source), css)

        with open(os.path.join(HERE, base + ".svg"), "w", encoding="utf-8") as f:
            f.write(svg)

        if args.png:
            cairosvg.svg2png(bytestring=svg.encode("utf-8"),
                             write_to=os.path.join(args.png, rule + ".png"),
                             scale=args.scale, background_color="white")

    print(f"{len(sources)} diagrams" + (f", PNGs in {args.png}" if args.png else ""))


if __name__ == "__main__":
    sys.exit(main())