reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render Mermaid, Graphviz DOT, PlantUML, or SVG diagram source to an output file.
|
||||
|
||||
The script is intentionally dependency-tolerant. It uses common command-line
|
||||
renderers when available and prints actionable hints otherwise.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
MERMAID_EXTS = {".mmd", ".mermaid"}
|
||||
GRAPHVIZ_EXTS = {".dot", ".gv"}
|
||||
PLANTUML_EXTS = {".puml", ".plantuml"}
|
||||
SVG_EXTS = {".svg"}
|
||||
|
||||
|
||||
def run(cmd: list[str], cwd: Path | None = None) -> None:
|
||||
print("$ " + " ".join(cmd))
|
||||
completed = subprocess.run(cmd, cwd=str(cwd) if cwd else None, text=True, capture_output=True)
|
||||
if completed.stdout:
|
||||
print(completed.stdout, end="")
|
||||
if completed.stderr:
|
||||
print(completed.stderr, end="", file=sys.stderr)
|
||||
if completed.returncode != 0:
|
||||
raise RuntimeError(f"command failed with exit code {completed.returncode}")
|
||||
|
||||
|
||||
def infer_kind(path: Path) -> str:
|
||||
ext = path.suffix.lower()
|
||||
if ext in MERMAID_EXTS:
|
||||
return "mermaid"
|
||||
if ext in GRAPHVIZ_EXTS:
|
||||
return "graphviz"
|
||||
if ext in PLANTUML_EXTS:
|
||||
return "plantuml"
|
||||
if ext in SVG_EXTS:
|
||||
return "svg"
|
||||
text = path.read_text(encoding="utf-8", errors="replace").lstrip()
|
||||
if text.startswith("@startuml"):
|
||||
return "plantuml"
|
||||
if text.startswith("digraph") or text.startswith("graph"):
|
||||
return "graphviz"
|
||||
if text.startswith("<svg"):
|
||||
return "svg"
|
||||
return "mermaid"
|
||||
|
||||
|
||||
def render_mermaid(src: Path, out: Path, fmt: str) -> None:
|
||||
mmdc = shutil.which("mmdc")
|
||||
if not mmdc:
|
||||
raise RuntimeError(
|
||||
"Mermaid rendering requires the Mermaid CLI executable `mmdc`. "
|
||||
"Install it with `npm install -g @mermaid-js/mermaid-cli`, or return the Mermaid source only."
|
||||
)
|
||||
run([mmdc, "-i", str(src), "-o", str(out)])
|
||||
|
||||
|
||||
def render_graphviz(src: Path, out: Path, fmt: str) -> None:
|
||||
dot = shutil.which("dot")
|
||||
if not dot:
|
||||
raise RuntimeError(
|
||||
"Graphviz rendering requires the `dot` executable. Install Graphviz, or return the DOT source only."
|
||||
)
|
||||
run([dot, f"-T{fmt}", str(src), "-o", str(out)])
|
||||
|
||||
|
||||
def render_plantuml(src: Path, out: Path, fmt: str) -> None:
|
||||
plantuml = shutil.which("plantuml")
|
||||
java = shutil.which("java")
|
||||
if plantuml:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
temp_src = tmp_path / src.name
|
||||
temp_src.write_text(src.read_text(encoding="utf-8"), encoding="utf-8")
|
||||
run([plantuml, f"-t{fmt}", str(temp_src)], cwd=tmp_path)
|
||||
produced = temp_src.with_suffix("." + fmt)
|
||||
if not produced.exists():
|
||||
candidates = list(tmp_path.glob(f"*.{fmt}"))
|
||||
if not candidates:
|
||||
raise RuntimeError("PlantUML completed but no output file was found")
|
||||
produced = candidates[0]
|
||||
shutil.copyfile(produced, out)
|
||||
return
|
||||
jar = os.environ.get("PLANTUML_JAR")
|
||||
if java and jar and Path(jar).exists():
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
temp_src = tmp_path / src.name
|
||||
temp_src.write_text(src.read_text(encoding="utf-8"), encoding="utf-8")
|
||||
run([java, "-jar", jar, f"-t{fmt}", str(temp_src)], cwd=tmp_path)
|
||||
produced = temp_src.with_suffix("." + fmt)
|
||||
if not produced.exists():
|
||||
candidates = list(tmp_path.glob(f"*.{fmt}"))
|
||||
if not candidates:
|
||||
raise RuntimeError("PlantUML completed but no output file was found")
|
||||
produced = candidates[0]
|
||||
shutil.copyfile(produced, out)
|
||||
return
|
||||
raise RuntimeError(
|
||||
"PlantUML rendering requires `plantuml` on PATH, or Java plus PLANTUML_JAR pointing to plantuml.jar. "
|
||||
"Return the PlantUML source only if no renderer is available."
|
||||
)
|
||||
|
||||
|
||||
def render_svg_source(src: Path, out: Path, fmt: str) -> None:
|
||||
if fmt == "svg":
|
||||
shutil.copyfile(src, out)
|
||||
return
|
||||
rsvg = shutil.which("rsvg-convert")
|
||||
if rsvg:
|
||||
run([rsvg, "-f", fmt, "-o", str(out), str(src)])
|
||||
return
|
||||
inkscape = shutil.which("inkscape")
|
||||
if inkscape:
|
||||
run([inkscape, str(src), f"--export-type={fmt}", f"--export-filename={out}"])
|
||||
return
|
||||
raise RuntimeError(
|
||||
"SVG conversion requires `rsvg-convert` or `inkscape`. The SVG source itself is usable as an image."
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Render text diagram source to SVG, PNG, or PDF.")
|
||||
parser.add_argument("input", type=Path, help="Input .mmd, .dot/.gv, .puml, or .svg file")
|
||||
parser.add_argument("--format", choices=["svg", "png", "pdf"], default="svg", help="Output format")
|
||||
parser.add_argument("--out", type=Path, help="Output path. Defaults to input name with the selected extension")
|
||||
parser.add_argument("--kind", choices=["auto", "mermaid", "graphviz", "plantuml", "svg"], default="auto")
|
||||
args = parser.parse_args()
|
||||
|
||||
src = args.input.resolve()
|
||||
if not src.exists():
|
||||
print(f"input file not found: {src}", file=sys.stderr)
|
||||
return 2
|
||||
out = (args.out or src.with_suffix("." + args.format)).resolve()
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
kind = infer_kind(src) if args.kind == "auto" else args.kind
|
||||
|
||||
try:
|
||||
if kind == "mermaid":
|
||||
render_mermaid(src, out, args.format)
|
||||
elif kind == "graphviz":
|
||||
render_graphviz(src, out, args.format)
|
||||
elif kind == "plantuml":
|
||||
render_plantuml(src, out, args.format)
|
||||
elif kind == "svg":
|
||||
render_svg_source(src, out, args.format)
|
||||
else:
|
||||
raise RuntimeError(f"unknown diagram kind: {kind}")
|
||||
except Exception as exc:
|
||||
print(f"render failed: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if not out.exists() or out.stat().st_size == 0:
|
||||
print(f"render failed: output missing or empty: {out}", file=sys.stderr)
|
||||
return 1
|
||||
print(f"rendered {kind} diagram to {out}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user