Source code for scitex_todo._render

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Mermaid -> PNG renderer.

Primary path: ``mmdc`` (mermaid-cli) with a puppeteer- or playwright-cached
chromium (auto-discovered; launched with ``--no-sandbox``). Snap chromium is
intentionally excluded — snap confinement breaks puppeteer's launch protocol.

Fallback path: ``kroki.io`` POST when ``mmdc`` is unavailable or fails.
"""

from __future__ import annotations

import json
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path


[docs] class RenderError(RuntimeError): """Raised when every render path (mmdc, kroki) fails."""
[docs] def find_chromium() -> str | None: """Locate a puppeteer- or playwright-cached chromium for headless render. Resolution order: ``$PUPPETEER_EXECUTABLE_PATH``, then the puppeteer cache, then the playwright cache. Snap chromium is excluded. Returns ------- str or None Path to a chromium executable, or ``None`` if none is found. """ env_path = os.environ.get("PUPPETEER_EXECUTABLE_PATH") if env_path and Path(env_path).exists(): return env_path # puppeteer cache: ~/.cache/puppeteer/chrome/<ver>/chrome-linux64/chrome pup_root = Path.home() / ".cache" / "puppeteer" / "chrome" if pup_root.is_dir(): for cand in sorted(pup_root.glob("*/chrome-linux64/chrome"), reverse=True): if cand.exists(): return str(cand) # playwright cache: ~/.cache/ms-playwright/chromium-*/chrome-linux/chrome pw_root = Path.home() / ".cache" / "ms-playwright" if pw_root.is_dir(): for cand in sorted( pw_root.glob("chromium-*/chrome-linux/chrome"), reverse=True ): if cand.exists(): return str(cand) return None
[docs] def render_with_mmdc(mermaid_src: str, out_png: str | Path) -> bool: """Render via mermaid-cli. Returns ------- bool ``True`` on success, ``False`` to allow the caller to fall back. """ out_png = Path(out_png) if shutil.which("mmdc") is None: sys.stderr.write("mmdc not found on PATH; skipping mmdc render\n") return False chromium = find_chromium() if chromium is None: sys.stderr.write( "No puppeteer/playwright chromium found; skipping mmdc render\n" ) return False with tempfile.TemporaryDirectory() as tmp: tmp_dir = Path(tmp) mmd_path = tmp_dir / "graph.mmd" mmd_path.write_text(mermaid_src, encoding="utf-8") pptr_path = tmp_dir / "pptr.json" pptr_path.write_text( json.dumps({"args": ["--no-sandbox", "--disable-setuid-sandbox"]}), encoding="utf-8", ) env = dict(os.environ) env["PUPPETEER_EXECUTABLE_PATH"] = chromium cmd = [ "mmdc", "-i", str(mmd_path), "-o", str(out_png), "-b", "white", "-p", str(pptr_path), "-s", "2", ] try: result = subprocess.run( cmd, env=env, capture_output=True, text=True, timeout=120 ) except subprocess.TimeoutExpired: sys.stderr.write("mmdc render timed out\n") return False if result.returncode != 0: sys.stderr.write( f"mmdc render failed (rc={result.returncode}):\n{result.stderr}\n" ) return False return out_png.exists() and out_png.stat().st_size > 0
[docs] def render_with_kroki(mermaid_src: str, out_png: str | Path) -> bool: """Fallback: render via kroki.io. Returns ------- bool ``True`` on success, ``False`` on any network/HTTP failure. """ out_png = Path(out_png) try: import urllib.request req = urllib.request.Request( "https://kroki.io/mermaid/png", data=mermaid_src.encode("utf-8"), headers={"Content-Type": "text/plain"}, method="POST", ) with urllib.request.urlopen(req, timeout=60) as resp: if resp.status != 200: sys.stderr.write(f"kroki.io returned status {resp.status}\n") return False payload = resp.read() except Exception as exc: # noqa: BLE001 — fallback must report and bail sys.stderr.write(f"kroki.io render failed: {exc}\n") return False out_png.write_bytes(payload) return out_png.exists() and out_png.stat().st_size > 0
[docs] def render(mermaid_src: str, out_png: str | Path) -> str: """Render mermaid source to PNG, mmdc-first with kroki fallback. Parameters ---------- mermaid_src : str Mermaid source, typically from :func:`scitex_todo.build_mermaid`. out_png : str or pathlib.Path Destination PNG path. Parent directories are created if missing. Returns ------- str The engine that produced the output: ``"mmdc"`` or ``"kroki"``. Raises ------ RenderError If both the mmdc and kroki render paths fail. """ out_png = Path(out_png) out_png.parent.mkdir(parents=True, exist_ok=True) if render_with_mmdc(mermaid_src, out_png): return "mmdc" sys.stderr.write("Falling back to kroki.io\n") if render_with_kroki(mermaid_src, out_png): return "kroki" raise RenderError("Both mmdc and kroki.io render paths failed")
# EOF