File size: 4,213 Bytes
40de84e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#!/usr/bin/env python3
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""
Render the mermaid block from docs/architecture.md to docs/architecture.png.

Uses `npx mermaid-cli` (mmdc) if available. Falls back gracefully if Node /
mermaid-cli is not installed — prints instructions and leaves the markdown
intact so README embedding still works via inline mermaid or ASCII.

Usage:
    python scripts/render_diagram.py
    python scripts/render_diagram.py --background transparent --width 900

Requires (optional):
    Node.js + `npm install -g @mermaid-js/mermaid-cli`
    or it will fall back to one-shot `npx -p @mermaid-js/mermaid-cli mmdc`.
"""

from __future__ import annotations

import argparse
import re
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path


REPO_ROOT = Path(__file__).resolve().parent.parent
DOC_PATH = REPO_ROOT / "docs" / "architecture.md"
OUT_PATH = REPO_ROOT / "docs" / "architecture.png"


def extract_mermaid(md_text: str) -> str | None:
    """Pull the FIRST ```mermaid ... ``` fenced block from md_text."""
    match = re.search(r"```mermaid\s*\n(.*?)```", md_text, re.DOTALL)
    if not match:
        return None
    return match.group(1).strip()


def find_mmdc() -> list[str] | None:
    """Return the command vector for invoking mermaid-cli, or None."""
    # 1) Direct binary on PATH
    if shutil.which("mmdc"):
        return ["mmdc"]
    # 2) npx (one-shot)
    if shutil.which("npx"):
        return ["npx", "-y", "-p", "@mermaid-js/mermaid-cli", "mmdc"]
    return None


def render(width: int, background: str) -> int:
    if not DOC_PATH.exists():
        print(f"[render_diagram] missing source file: {DOC_PATH}", file=sys.stderr)
        return 1

    md_text = DOC_PATH.read_text(encoding="utf-8")
    mermaid_src = extract_mermaid(md_text)
    if not mermaid_src:
        print(
            "[render_diagram] no ```mermaid``` block found in "
            f"{DOC_PATH}. Nothing to render.",
            file=sys.stderr,
        )
        return 1

    mmdc = find_mmdc()
    if mmdc is None:
        print(
            "[render_diagram] mermaid-cli not found.\n"
            "  Install one of:\n"
            "    npm install -g @mermaid-js/mermaid-cli   # global mmdc\n"
            "    npm install -D @mermaid-js/mermaid-cli   # local devDep\n"
            "  Or rely on inline mermaid in the README — GitHub renders it natively.\n"
            "  Skipping PNG render. Markdown source at "
            f"{DOC_PATH.relative_to(REPO_ROOT)} is unchanged.",
            file=sys.stderr,
        )
        return 0  # not an error — graceful fallback

    with tempfile.TemporaryDirectory() as tmp:
        src = Path(tmp) / "diagram.mmd"
        src.write_text(mermaid_src, encoding="utf-8")
        cmd = [
            *mmdc,
            "-i", str(src),
            "-o", str(OUT_PATH),
            "-w", str(width),
            "-b", background,
        ]
        print(f"[render_diagram] running: {' '.join(cmd)}")
        try:
            result = subprocess.run(cmd, check=False)
        except FileNotFoundError as exc:
            print(f"[render_diagram] failed to launch mermaid-cli: {exc}", file=sys.stderr)
            return 1
        if result.returncode != 0:
            print(
                f"[render_diagram] mermaid-cli exited with {result.returncode}. "
                "PNG was not produced.",
                file=sys.stderr,
            )
            return result.returncode

    print(f"[render_diagram] wrote {OUT_PATH.relative_to(REPO_ROOT)}")
    return 0


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--width", type=int, default=900, help="Output width in px (default: 900)")
    parser.add_argument(
        "--background",
        default="white",
        help="Background colour (e.g. white, transparent, '#0f172a'). Default: white",
    )
    args = parser.parse_args()
    return render(args.width, args.background)


if __name__ == "__main__":
    raise SystemExit(main())