#!/usr/bin/env python3 """ Rendert alle @startuml ... @enduml Bloecke aus arch/**/*.md als SVG. Verwendet einen erreichbaren PlantUML-HTTP-Server (Default: www.plantuml.com). In CI kann die Server-URL ueber PLANTUML_SERVER ueberschrieben werden. Output: docs/diagrams/-.svg Run: python3 tools/render_plantuml.py PLANTUML_SERVER=http://plantuml-server:8080 python3 tools/render_plantuml.py """ from __future__ import annotations import os import re import sys import urllib.request import zlib from pathlib import Path REPO = Path(__file__).resolve().parent.parent SERVER = os.environ.get("PLANTUML_SERVER", "https://www.plantuml.com/plantuml") # Map standard base64 to PlantUML's base64 alphabet _B64 = ( "0123456789" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" "-_" ) def _encode_3bytes(b1: int, b2: int, b3: int) -> str: c1 = b1 >> 2 c2 = ((b1 & 0x3) << 4) | (b2 >> 4) c3 = ((b2 & 0xF) << 2) | (b3 >> 6) c4 = b3 & 0x3F return _B64[c1] + _B64[c2] + _B64[c3] + _B64[c4] def plantuml_encode(text: str) -> str: """Encode PlantUML source to its URL-safe representation.""" compressed = zlib.compress(text.encode("utf-8"), 9) # Strip zlib header (2 bytes) and Adler32 trailer (4 bytes) raw = compressed[2:-4] out = "" i = 0 while i < len(raw): b1 = raw[i] b2 = raw[i + 1] if i + 1 < len(raw) else 0 b3 = raw[i + 2] if i + 2 < len(raw) else 0 out += _encode_3bytes(b1, b2, b3) i += 3 return out BLOCK_RE = re.compile(r"```plantuml\s*\n(@startuml.*?@enduml)\s*\n```", re.DOTALL) def extract_blocks(md: Path) -> list[str]: return BLOCK_RE.findall(md.read_text()) def render_one(src: str, out_path: Path) -> None: encoded = plantuml_encode(src) url = f"{SERVER.rstrip('/')}/svg/{encoded}" req = urllib.request.Request(url, headers={"User-Agent": "demo-epb/1.0"}) with urllib.request.urlopen(req, timeout=30) as resp: data = resp.read() out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_bytes(data) def main() -> int: out_dir = REPO / "docs" / "diagrams" out_dir.mkdir(parents=True, exist_ok=True) md_files = sorted((REPO / "arch").rglob("*.md")) if not md_files: print("Keine arch/**/*.md gefunden.") return 0 total = 0 for md in md_files: rel = md.relative_to(REPO) blocks = extract_blocks(md) if not blocks: continue for i, block in enumerate(blocks, start=1): stem = md.stem target = out_dir / f"{stem}-{i}.svg" try: render_one(block, target) print(f" {rel} block {i} -> {target.relative_to(REPO)}") total += 1 except Exception as e: print(f" WARN: {rel} block {i} render failed: {e}") print(f"\nDone: {total} Diagrams rendered to {out_dir.relative_to(REPO)}/") return 0 if __name__ == "__main__": sys.exit(main())