Upload 12 files
Browse files- .gitattributes +0 -37
- .gitignore +60 -0
- LICENSE +21 -0
- README.md +354 -0
- render.py +802 -0
- requirements.txt +7 -0
- samples/classdiagram.mmd +25 -0
- samples/er.mmd +31 -0
- samples/flowchart.mmd +11 -0
- samples/sequence.mmd +16 -0
- samples/statediagram.mmd +11 -0
- samples/xychart.mmd +6 -0
.gitattributes
CHANGED
|
@@ -1,39 +1,3 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
-
|
| 37 |
# Auto-detect text files and normalize line endings to LF on checkout.
|
| 38 |
* text=auto eol=lf
|
| 39 |
|
|
@@ -84,4 +48,3 @@ build/** linguist-generated
|
|
| 84 |
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 85 |
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 86 |
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# Auto-detect text files and normalize line endings to LF on checkout.
|
| 2 |
* text=auto eol=lf
|
| 3 |
|
|
|
|
| 48 |
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 49 |
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 50 |
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
|
|
.gitignore
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
build/
|
| 8 |
+
dist/
|
| 9 |
+
*.egg-info/
|
| 10 |
+
.eggs/
|
| 11 |
+
.env
|
| 12 |
+
.venv
|
| 13 |
+
env/
|
| 14 |
+
venv/
|
| 15 |
+
ENV/
|
| 16 |
+
|
| 17 |
+
# Output files
|
| 18 |
+
*.png
|
| 19 |
+
*.svg
|
| 20 |
+
*.pdf
|
| 21 |
+
!examples/expected/*.png
|
| 22 |
+
!examples/expected/*.svg
|
| 23 |
+
|
| 24 |
+
# Temp
|
| 25 |
+
*.tmp
|
| 26 |
+
*.log
|
| 27 |
+
puppeteer_cfg_*.json
|
| 28 |
+
mermaid_cfg_*.json
|
| 29 |
+
|
| 30 |
+
# Node
|
| 31 |
+
node_modules/
|
| 32 |
+
npm-debug.log*
|
| 33 |
+
package-lock.json
|
| 34 |
+
yarn.lock
|
| 35 |
+
.pnpm-debug.log*
|
| 36 |
+
|
| 37 |
+
# OS
|
| 38 |
+
.DS_Store
|
| 39 |
+
.DS_Store?
|
| 40 |
+
._*
|
| 41 |
+
.Spotlight-V100
|
| 42 |
+
.Trashes
|
| 43 |
+
ehthumbs.db
|
| 44 |
+
Thumbs.db
|
| 45 |
+
desktop.ini
|
| 46 |
+
|
| 47 |
+
# IDE
|
| 48 |
+
.vscode/
|
| 49 |
+
.idea/
|
| 50 |
+
*.swp
|
| 51 |
+
*.swo
|
| 52 |
+
*~
|
| 53 |
+
|
| 54 |
+
# Puppeteer cache (large)
|
| 55 |
+
.cache/
|
| 56 |
+
|
| 57 |
+
# Coverage
|
| 58 |
+
.coverage
|
| 59 |
+
htmlcov/
|
| 60 |
+
.pytest_cache/
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025 algorembrant
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: mermaid-renderer
|
| 3 |
+
emoji: diagram
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: "4.44.0"
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
+
tags:
|
| 12 |
+
- mermaid
|
| 13 |
+
- diagram
|
| 14 |
+
- visualization
|
| 15 |
+
- code-generation
|
| 16 |
+
- developer-tools
|
| 17 |
+
- png
|
| 18 |
+
- svg
|
| 19 |
+
- pdf
|
| 20 |
+
- cli
|
| 21 |
+
authors:
|
| 22 |
+
- algorembrant
|
| 23 |
+
---
|
| 24 |
+
|
| 25 |
+
# mermaid-renderer
|
| 26 |
+
|
| 27 |
+
[](https://www.python.org/)
|
| 28 |
+
[](https://nodejs.org/)
|
| 29 |
+
[](LICENSE)
|
| 30 |
+
[](https://github.com/mermaid-js/mermaid-cli)
|
| 31 |
+
[](#supported-formats)
|
| 32 |
+
[](#built-in-themes)
|
| 33 |
+
|
| 34 |
+
Convert Mermaid diagrams to high-quality PNG, SVG, or PDF images from the command line. Supports all diagram types, 15 built-in themes from [beautiful-mermaid](https://github.com/lukilabs/beautiful-mermaid), batch processing, parallel rendering, and file-watch mode.
|
| 35 |
+
|
| 36 |
+
---
|
| 37 |
+
|
| 38 |
+
## Features
|
| 39 |
+
|
| 40 |
+
- All Mermaid diagram types: flowcharts, sequence, state, class, ER, XY charts
|
| 41 |
+
- 15 curated themes (tokyo-night, catppuccin, dracula, nord, github, etc.)
|
| 42 |
+
- PNG, SVG, and PDF output
|
| 43 |
+
- Batch rendering with parallel workers
|
| 44 |
+
- Watch mode for live re-render on file change
|
| 45 |
+
- Custom background and foreground color overrides
|
| 46 |
+
- Scale factor control for high-DPI output
|
| 47 |
+
- Zero Python runtime dependencies beyond stdlib (Pillow optional)
|
| 48 |
+
|
| 49 |
+
---
|
| 50 |
+
|
| 51 |
+
## Requirements
|
| 52 |
+
|
| 53 |
+
| Requirement | Version | Notes |
|
| 54 |
+
|-------------|---------|-------|
|
| 55 |
+
| Python | 3.9+ | Standard library only |
|
| 56 |
+
| Node.js | 18+ | Required by mmdc |
|
| 57 |
+
| npm | 9+ | For installing mmdc |
|
| 58 |
+
| @mermaid-js/mermaid-cli | 11+ | The render engine |
|
| 59 |
+
| Chrome or Chromium | any | Headless browser for rendering |
|
| 60 |
+
|
| 61 |
+
---
|
| 62 |
+
|
| 63 |
+
## Installation
|
| 64 |
+
|
| 65 |
+
### 1. Install Node.js and npm
|
| 66 |
+
|
| 67 |
+
Download from https://nodejs.org/ or use your package manager:
|
| 68 |
+
|
| 69 |
+
```bash
|
| 70 |
+
# Ubuntu / Debian
|
| 71 |
+
sudo apt install nodejs npm
|
| 72 |
+
|
| 73 |
+
# macOS (Homebrew)
|
| 74 |
+
brew install node
|
| 75 |
+
|
| 76 |
+
# Windows (Chocolatey)
|
| 77 |
+
choco install nodejs
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
### 2. Install @mermaid-js/mermaid-cli
|
| 81 |
+
|
| 82 |
+
```bash
|
| 83 |
+
npm install -g @mermaid-js/mermaid-cli
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
Verify:
|
| 87 |
+
```bash
|
| 88 |
+
mmdc --version
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
### 3. Install Chrome or Chromium
|
| 92 |
+
|
| 93 |
+
**Option A — Puppeteer-managed (recommended, automatic):**
|
| 94 |
+
|
| 95 |
+
```bash
|
| 96 |
+
# After installing mermaid-cli, install its bundled Chrome:
|
| 97 |
+
node -e "const p = require('puppeteer'); p.executablePath();"
|
| 98 |
+
# Or use npx to install it:
|
| 99 |
+
npx puppeteer browsers install chrome
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
**Option B — System Chromium:**
|
| 103 |
+
|
| 104 |
+
```bash
|
| 105 |
+
# Ubuntu / Debian
|
| 106 |
+
sudo apt install chromium-browser
|
| 107 |
+
|
| 108 |
+
# macOS
|
| 109 |
+
brew install --cask google-chrome
|
| 110 |
+
|
| 111 |
+
# Windows: download from https://www.google.com/chrome/
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
### 4. Clone this repo and install Python deps
|
| 115 |
+
|
| 116 |
+
```bash
|
| 117 |
+
git clone https://github.com/algorembrant/mermaid-renderer.git
|
| 118 |
+
cd mermaid-renderer
|
| 119 |
+
pip install -r requirements.txt
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
### 5. Verify everything works
|
| 123 |
+
|
| 124 |
+
```bash
|
| 125 |
+
python render.py --check
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
Expected output:
|
| 129 |
+
```
|
| 130 |
+
mermaid-renderer — environment check
|
| 131 |
+
----------------------------------------
|
| 132 |
+
Python : 3.12.3
|
| 133 |
+
mmdc : /usr/local/bin/mmdc
|
| 134 |
+
Chrome/Chrom: /home/user/.cache/puppeteer/chrome/.../chrome
|
| 135 |
+
mmdc version: 11.12.0
|
| 136 |
+
|
| 137 |
+
All dependencies OK. Rendering should work.
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
---
|
| 141 |
+
|
| 142 |
+
## Quick Start
|
| 143 |
+
|
| 144 |
+
### Render a file
|
| 145 |
+
|
| 146 |
+
```bash
|
| 147 |
+
python render.py diagram.mmd
|
| 148 |
+
# Output: diagram.png
|
| 149 |
+
```
|
| 150 |
+
|
| 151 |
+
### Inline diagram
|
| 152 |
+
|
| 153 |
+
```bash
|
| 154 |
+
python render.py --text "graph TD; A[Start] --> B{Decision}; B -->|Yes| C[Done]; B -->|No| D[End]"
|
| 155 |
+
# Output: diagram.png
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
### SVG output
|
| 159 |
+
|
| 160 |
+
```bash
|
| 161 |
+
python render.py diagram.mmd -o output.svg
|
| 162 |
+
```
|
| 163 |
+
|
| 164 |
+
### Apply a theme
|
| 165 |
+
|
| 166 |
+
```bash
|
| 167 |
+
python render.py diagram.mmd --theme tokyo-night
|
| 168 |
+
python render.py diagram.mmd --theme catppuccin-mocha -o dark.png
|
| 169 |
+
```
|
| 170 |
+
|
| 171 |
+
---
|
| 172 |
+
|
| 173 |
+
## Usage Reference
|
| 174 |
+
|
| 175 |
+
### Single File
|
| 176 |
+
|
| 177 |
+
| Command | Description |
|
| 178 |
+
|---------|-------------|
|
| 179 |
+
| `python render.py diagram.mmd` | Render to PNG (same name) |
|
| 180 |
+
| `python render.py diagram.mmd -o out.png` | Specify output path |
|
| 181 |
+
| `python render.py diagram.mmd -o out.svg` | SVG output |
|
| 182 |
+
| `python render.py diagram.mmd -o out.pdf` | PDF output |
|
| 183 |
+
| `python render.py --text "graph LR; A-->B"` | Inline diagram text |
|
| 184 |
+
|
| 185 |
+
### Themes
|
| 186 |
+
|
| 187 |
+
| Command | Description |
|
| 188 |
+
|---------|-------------|
|
| 189 |
+
| `python render.py d.mmd --theme tokyo-night` | Apply dark theme |
|
| 190 |
+
| `python render.py d.mmd --theme github-light` | Apply light theme |
|
| 191 |
+
| `python render.py d.mmd --bg "#1a1b26" --fg "#a9b1d6"` | Custom colors |
|
| 192 |
+
| `python render.py d.mmd --bg transparent` | Transparent background |
|
| 193 |
+
| `python render.py --list-themes` | Print all built-in themes |
|
| 194 |
+
|
| 195 |
+
### Quality / Size
|
| 196 |
+
|
| 197 |
+
| Command | Description |
|
| 198 |
+
|---------|-------------|
|
| 199 |
+
| `python render.py d.mmd --scale 2` | 2x resolution (default) |
|
| 200 |
+
| `python render.py d.mmd --scale 3` | 3x for print quality |
|
| 201 |
+
| `python render.py d.mmd --width 1920` | Set canvas width |
|
| 202 |
+
|
| 203 |
+
### Batch Mode
|
| 204 |
+
|
| 205 |
+
| Command | Description |
|
| 206 |
+
|---------|-------------|
|
| 207 |
+
| `python render.py --batch ./diagrams/` | Render all .mmd in directory |
|
| 208 |
+
| `python render.py --batch ./diagrams/ -o ./out/` | Output to separate directory |
|
| 209 |
+
| `python render.py --batch "./diagrams/*.mmd"` | Glob pattern |
|
| 210 |
+
| `python render.py --batch ./diagrams/ --workers 4` | Parallel (4 processes) |
|
| 211 |
+
| `python render.py --batch ./diagrams/ --format svg` | SVG batch output |
|
| 212 |
+
|
| 213 |
+
### Watch Mode
|
| 214 |
+
|
| 215 |
+
| Command | Description |
|
| 216 |
+
|---------|-------------|
|
| 217 |
+
| `python render.py diagram.mmd --watch` | Re-render on file change |
|
| 218 |
+
| `python render.py --batch ./diagrams/ --watch` | Watch entire directory |
|
| 219 |
+
|
| 220 |
+
### Utility
|
| 221 |
+
|
| 222 |
+
| Command | Description |
|
| 223 |
+
|---------|-------------|
|
| 224 |
+
| `python render.py --check` | Verify environment and deps |
|
| 225 |
+
| `python render.py --list-themes` | List all 15 built-in themes |
|
| 226 |
+
| `python render.py -v diagram.mmd` | Verbose (show mmdc output) |
|
| 227 |
+
| `python render.py --help` | Show help |
|
| 228 |
+
|
| 229 |
+
---
|
| 230 |
+
|
| 231 |
+
## Built-in Themes
|
| 232 |
+
|
| 233 |
+
| Theme | Type | Background | Accent |
|
| 234 |
+
|-------|------|------------|--------|
|
| 235 |
+
| `default` | light | `#FFFFFF` | (derived) |
|
| 236 |
+
| `zinc-light` | light | `#FFFFFF` | (derived) |
|
| 237 |
+
| `zinc-dark` | dark | `#18181B` | (derived) |
|
| 238 |
+
| `tokyo-night` | dark | `#1a1b26` | `#7aa2f7` |
|
| 239 |
+
| `tokyo-night-storm` | dark | `#24283b` | `#7aa2f7` |
|
| 240 |
+
| `tokyo-night-light` | light | `#d5d6db` | `#34548a` |
|
| 241 |
+
| `catppuccin-mocha` | dark | `#1e1e2e` | `#cba6f7` |
|
| 242 |
+
| `catppuccin-latte` | light | `#eff1f5` | `#8839ef` |
|
| 243 |
+
| `nord` | dark | `#2e3440` | `#88c0d0` |
|
| 244 |
+
| `nord-light` | light | `#eceff4` | `#5e81ac` |
|
| 245 |
+
| `dracula` | dark | `#282a36` | `#bd93f9` |
|
| 246 |
+
| `github-light` | light | `#ffffff` | `#0969da` |
|
| 247 |
+
| `github-dark` | dark | `#0d1117` | `#4493f8` |
|
| 248 |
+
| `solarized-light` | light | `#fdf6e3` | `#268bd2` |
|
| 249 |
+
| `solarized-dark` | dark | `#002b36` | `#268bd2` |
|
| 250 |
+
| `one-dark` | dark | `#282c34` | `#c678dd` |
|
| 251 |
+
|
| 252 |
+
---
|
| 253 |
+
|
| 254 |
+
## Supported Diagram Types
|
| 255 |
+
|
| 256 |
+
All Mermaid diagram types are supported:
|
| 257 |
+
|
| 258 |
+
- Flowcharts (`graph TD`, `graph LR`, etc.)
|
| 259 |
+
- Sequence diagrams (`sequenceDiagram`)
|
| 260 |
+
- State diagrams (`stateDiagram-v2`)
|
| 261 |
+
- Class diagrams (`classDiagram`)
|
| 262 |
+
- Entity-relationship diagrams (`erDiagram`)
|
| 263 |
+
- XY charts / bar / line (`xychart-beta`)
|
| 264 |
+
- Gantt charts (`gantt`)
|
| 265 |
+
- Pie charts (`pie`)
|
| 266 |
+
- Mindmaps (`mindmap`)
|
| 267 |
+
- Journey diagrams (`journey`)
|
| 268 |
+
- Git graphs (`gitGraph`)
|
| 269 |
+
|
| 270 |
+
---
|
| 271 |
+
|
| 272 |
+
## Supported Formats
|
| 273 |
+
|
| 274 |
+
| Format | Extension | Notes |
|
| 275 |
+
|--------|-----------|-------|
|
| 276 |
+
| PNG | `.png` | Raster, recommended for most uses. Use `--scale 2` or `--scale 3` for sharpness. |
|
| 277 |
+
| SVG | `.svg` | Vector, lossless, ideal for web and print. |
|
| 278 |
+
| PDF | `.pdf` | Vector PDF, fitted to diagram bounds. |
|
| 279 |
+
|
| 280 |
+
---
|
| 281 |
+
|
| 282 |
+
## Troubleshooting
|
| 283 |
+
|
| 284 |
+
### mmdc not found
|
| 285 |
+
|
| 286 |
+
```bash
|
| 287 |
+
npm install -g @mermaid-js/mermaid-cli
|
| 288 |
+
# Then ensure npm bin is on PATH:
|
| 289 |
+
export PATH="$(npm bin -g):$PATH"
|
| 290 |
+
```
|
| 291 |
+
|
| 292 |
+
### Chrome not found
|
| 293 |
+
|
| 294 |
+
```bash
|
| 295 |
+
# Install puppeteer-managed Chrome:
|
| 296 |
+
npx puppeteer browsers install chrome
|
| 297 |
+
# Or install system Chrome:
|
| 298 |
+
sudo apt install chromium-browser # Linux
|
| 299 |
+
brew install --cask google-chrome # macOS
|
| 300 |
+
```
|
| 301 |
+
|
| 302 |
+
### Sandbox errors on Linux
|
| 303 |
+
|
| 304 |
+
Add to your puppeteer config or run with:
|
| 305 |
+
```bash
|
| 306 |
+
# render.py auto-adds --no-sandbox when using puppeteer config
|
| 307 |
+
```
|
| 308 |
+
|
| 309 |
+
### Diagram renders blank / cut off
|
| 310 |
+
|
| 311 |
+
Increase canvas width:
|
| 312 |
+
```bash
|
| 313 |
+
python render.py diagram.mmd --width 2000
|
| 314 |
+
```
|
| 315 |
+
|
| 316 |
+
---
|
| 317 |
+
|
| 318 |
+
## Project Structure
|
| 319 |
+
|
| 320 |
+
```
|
| 321 |
+
mermaid-renderer/
|
| 322 |
+
render.py Main CLI script
|
| 323 |
+
requirements.txt Python dependencies (minimal)
|
| 324 |
+
README.md This file
|
| 325 |
+
.gitignore Git ignore rules
|
| 326 |
+
.gitattributes Git attributes
|
| 327 |
+
examples/ Sample .mmd files
|
| 328 |
+
flowchart.mmd
|
| 329 |
+
sequence.mmd
|
| 330 |
+
statediagram.mmd
|
| 331 |
+
classdagram.mmd
|
| 332 |
+
er.mmd
|
| 333 |
+
xychart.mmd
|
| 334 |
+
```
|
| 335 |
+
|
| 336 |
+
---
|
| 337 |
+
|
| 338 |
+
## License
|
| 339 |
+
|
| 340 |
+
MIT License. See [LICENSE](LICENSE).
|
| 341 |
+
|
| 342 |
+
---
|
| 343 |
+
|
| 344 |
+
## Author
|
| 345 |
+
|
| 346 |
+
algorembrant
|
| 347 |
+
|
| 348 |
+
---
|
| 349 |
+
|
| 350 |
+
## Acknowledgments
|
| 351 |
+
|
| 352 |
+
- [mermaid-js/mermaid-cli](https://github.com/mermaid-js/mermaid-cli) — the render engine
|
| 353 |
+
- [lukilabs/beautiful-mermaid](https://github.com/lukilabs/beautiful-mermaid) — theme system reference
|
| 354 |
+
- [mermaid.js.org](https://mermaid.js.org/) — Mermaid diagram specification
|
render.py
ADDED
|
@@ -0,0 +1,802 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
mermaid-renderer — Convert Mermaid diagrams to images using @mermaid-js/mermaid-cli
|
| 3 |
+
Author: algorembrant
|
| 4 |
+
License: MIT
|
| 5 |
+
|
| 6 |
+
USAGE COMMANDS
|
| 7 |
+
==============
|
| 8 |
+
|
| 9 |
+
Single file:
|
| 10 |
+
python render.py diagram.mmd
|
| 11 |
+
python render.py diagram.mmd -o output.png
|
| 12 |
+
python render.py diagram.mmd -o output.svg
|
| 13 |
+
python render.py diagram.mmd -o output.pdf
|
| 14 |
+
|
| 15 |
+
Inline diagram:
|
| 16 |
+
python render.py --text "graph TD; A --> B --> C"
|
| 17 |
+
python render.py --text "sequenceDiagram; Alice->>Bob: Hello"
|
| 18 |
+
|
| 19 |
+
Themes (15 built-in from beautiful-mermaid):
|
| 20 |
+
python render.py diagram.mmd --theme tokyo-night
|
| 21 |
+
python render.py diagram.mmd --theme catppuccin-mocha
|
| 22 |
+
python render.py diagram.mmd --theme github-light
|
| 23 |
+
python render.py diagram.mmd --theme dracula
|
| 24 |
+
python render.py diagram.mmd --theme nord
|
| 25 |
+
python render.py diagram.mmd --theme one-dark
|
| 26 |
+
|
| 27 |
+
Custom colors:
|
| 28 |
+
python render.py diagram.mmd --bg "#1a1b26" --fg "#a9b1d6"
|
| 29 |
+
python render.py diagram.mmd --bg "#ffffff" --fg "#333333" --accent "#0969da"
|
| 30 |
+
|
| 31 |
+
Background:
|
| 32 |
+
python render.py diagram.mmd --background white
|
| 33 |
+
python render.py diagram.mmd --background transparent
|
| 34 |
+
python render.py diagram.mmd --background "#1e1e2e"
|
| 35 |
+
|
| 36 |
+
Scale / quality:
|
| 37 |
+
python render.py diagram.mmd --scale 2
|
| 38 |
+
python render.py diagram.mmd --scale 3
|
| 39 |
+
python render.py diagram.mmd --width 1920
|
| 40 |
+
|
| 41 |
+
Batch (directory or glob):
|
| 42 |
+
python render.py --batch ./diagrams/
|
| 43 |
+
python render.py --batch ./diagrams/ -o ./output/ --format png
|
| 44 |
+
python render.py --batch "./diagrams/*.mmd" --theme tokyo-night --scale 2
|
| 45 |
+
|
| 46 |
+
Batch parallel workers:
|
| 47 |
+
python render.py --batch ./diagrams/ --workers 4
|
| 48 |
+
|
| 49 |
+
Watch mode (re-render on change):
|
| 50 |
+
python render.py diagram.mmd --watch
|
| 51 |
+
python render.py --batch ./diagrams/ --watch
|
| 52 |
+
|
| 53 |
+
List built-in themes:
|
| 54 |
+
python render.py --list-themes
|
| 55 |
+
|
| 56 |
+
Check environment / dependencies:
|
| 57 |
+
python render.py --check
|
| 58 |
+
|
| 59 |
+
Verbose logging:
|
| 60 |
+
python render.py diagram.mmd -v
|
| 61 |
+
python render.py --batch ./diagrams/ -v --workers 2
|
| 62 |
+
|
| 63 |
+
Help:
|
| 64 |
+
python render.py --help
|
| 65 |
+
"""
|
| 66 |
+
|
| 67 |
+
from __future__ import annotations
|
| 68 |
+
|
| 69 |
+
import argparse
|
| 70 |
+
import glob
|
| 71 |
+
import json
|
| 72 |
+
import logging
|
| 73 |
+
import os
|
| 74 |
+
import shutil
|
| 75 |
+
import subprocess
|
| 76 |
+
import sys
|
| 77 |
+
import tempfile
|
| 78 |
+
import time
|
| 79 |
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
| 80 |
+
from pathlib import Path
|
| 81 |
+
from typing import Optional
|
| 82 |
+
|
| 83 |
+
# ---------------------------------------------------------------------------
|
| 84 |
+
# Logging
|
| 85 |
+
# ---------------------------------------------------------------------------
|
| 86 |
+
logging.basicConfig(
|
| 87 |
+
level=logging.WARNING,
|
| 88 |
+
format="[%(levelname)s] %(message)s",
|
| 89 |
+
)
|
| 90 |
+
log = logging.getLogger("mermaid-renderer")
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
# ---------------------------------------------------------------------------
|
| 94 |
+
# Theme definitions (mirrors beautiful-mermaid THEMES)
|
| 95 |
+
# ---------------------------------------------------------------------------
|
| 96 |
+
THEMES: dict[str, dict] = {
|
| 97 |
+
"default": {
|
| 98 |
+
"bg": "#FFFFFF",
|
| 99 |
+
"fg": "#27272A",
|
| 100 |
+
"mermaid_theme": "default",
|
| 101 |
+
},
|
| 102 |
+
"zinc-light": {
|
| 103 |
+
"bg": "#FFFFFF",
|
| 104 |
+
"fg": "#18181B",
|
| 105 |
+
"mermaid_theme": "default",
|
| 106 |
+
},
|
| 107 |
+
"zinc-dark": {
|
| 108 |
+
"bg": "#18181B",
|
| 109 |
+
"fg": "#E4E4E7",
|
| 110 |
+
"mermaid_theme": "dark",
|
| 111 |
+
},
|
| 112 |
+
"tokyo-night": {
|
| 113 |
+
"bg": "#1a1b26",
|
| 114 |
+
"fg": "#a9b1d6",
|
| 115 |
+
"accent": "#7aa2f7",
|
| 116 |
+
"mermaid_theme": "dark",
|
| 117 |
+
},
|
| 118 |
+
"tokyo-night-storm": {
|
| 119 |
+
"bg": "#24283b",
|
| 120 |
+
"fg": "#c0caf5",
|
| 121 |
+
"accent": "#7aa2f7",
|
| 122 |
+
"mermaid_theme": "dark",
|
| 123 |
+
},
|
| 124 |
+
"tokyo-night-light": {
|
| 125 |
+
"bg": "#d5d6db",
|
| 126 |
+
"fg": "#343b58",
|
| 127 |
+
"accent": "#34548a",
|
| 128 |
+
"mermaid_theme": "default",
|
| 129 |
+
},
|
| 130 |
+
"catppuccin-mocha": {
|
| 131 |
+
"bg": "#1e1e2e",
|
| 132 |
+
"fg": "#cdd6f4",
|
| 133 |
+
"accent": "#cba6f7",
|
| 134 |
+
"mermaid_theme": "dark",
|
| 135 |
+
},
|
| 136 |
+
"catppuccin-latte": {
|
| 137 |
+
"bg": "#eff1f5",
|
| 138 |
+
"fg": "#4c4f69",
|
| 139 |
+
"accent": "#8839ef",
|
| 140 |
+
"mermaid_theme": "default",
|
| 141 |
+
},
|
| 142 |
+
"nord": {
|
| 143 |
+
"bg": "#2e3440",
|
| 144 |
+
"fg": "#d8dee9",
|
| 145 |
+
"accent": "#88c0d0",
|
| 146 |
+
"mermaid_theme": "dark",
|
| 147 |
+
},
|
| 148 |
+
"nord-light": {
|
| 149 |
+
"bg": "#eceff4",
|
| 150 |
+
"fg": "#2e3440",
|
| 151 |
+
"accent": "#5e81ac",
|
| 152 |
+
"mermaid_theme": "default",
|
| 153 |
+
},
|
| 154 |
+
"dracula": {
|
| 155 |
+
"bg": "#282a36",
|
| 156 |
+
"fg": "#f8f8f2",
|
| 157 |
+
"accent": "#bd93f9",
|
| 158 |
+
"mermaid_theme": "dark",
|
| 159 |
+
},
|
| 160 |
+
"github-light": {
|
| 161 |
+
"bg": "#ffffff",
|
| 162 |
+
"fg": "#1F2328",
|
| 163 |
+
"accent": "#0969da",
|
| 164 |
+
"mermaid_theme": "default",
|
| 165 |
+
},
|
| 166 |
+
"github-dark": {
|
| 167 |
+
"bg": "#0d1117",
|
| 168 |
+
"fg": "#e6edf3",
|
| 169 |
+
"accent": "#4493f8",
|
| 170 |
+
"mermaid_theme": "dark",
|
| 171 |
+
},
|
| 172 |
+
"solarized-light": {
|
| 173 |
+
"bg": "#fdf6e3",
|
| 174 |
+
"fg": "#657b83",
|
| 175 |
+
"accent": "#268bd2",
|
| 176 |
+
"mermaid_theme": "default",
|
| 177 |
+
},
|
| 178 |
+
"solarized-dark": {
|
| 179 |
+
"bg": "#002b36",
|
| 180 |
+
"fg": "#839496",
|
| 181 |
+
"accent": "#268bd2",
|
| 182 |
+
"mermaid_theme": "dark",
|
| 183 |
+
},
|
| 184 |
+
"one-dark": {
|
| 185 |
+
"bg": "#282c34",
|
| 186 |
+
"fg": "#abb2bf",
|
| 187 |
+
"accent": "#c678dd",
|
| 188 |
+
"mermaid_theme": "dark",
|
| 189 |
+
},
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
# ---------------------------------------------------------------------------
|
| 194 |
+
# Environment detection
|
| 195 |
+
# ---------------------------------------------------------------------------
|
| 196 |
+
|
| 197 |
+
def find_mmdc() -> Optional[str]:
|
| 198 |
+
"""Return path to mmdc binary or None."""
|
| 199 |
+
# Prefer the npm-global install used by this environment
|
| 200 |
+
candidates = [
|
| 201 |
+
shutil.which("mmdc"),
|
| 202 |
+
shutil.which("mmdc.cmd"),
|
| 203 |
+
os.path.expanduser("~/.npm-global/bin/mmdc"),
|
| 204 |
+
os.path.join(os.environ.get("APPDATA", ""), "npm", "mmdc.cmd"),
|
| 205 |
+
"/usr/local/bin/mmdc",
|
| 206 |
+
"/usr/bin/mmdc",
|
| 207 |
+
]
|
| 208 |
+
for c in candidates:
|
| 209 |
+
if c and os.path.isfile(c):
|
| 210 |
+
return c
|
| 211 |
+
return None
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def find_chrome() -> Optional[str]:
|
| 215 |
+
"""Return path to a Chromium/Chrome binary or None."""
|
| 216 |
+
candidates = [
|
| 217 |
+
# Windows paths
|
| 218 |
+
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
| 219 |
+
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
|
| 220 |
+
os.path.join(os.environ.get("LOCALAPPDATA", ""), r"Google\Chrome\Application\chrome.exe"),
|
| 221 |
+
# Puppeteer-managed chrome (most reliable in this env)
|
| 222 |
+
os.path.expanduser(
|
| 223 |
+
"~/.cache/puppeteer/chrome/linux-131.0.6778.204/chrome-linux64/chrome"
|
| 224 |
+
),
|
| 225 |
+
"/opt/google/chrome/chrome",
|
| 226 |
+
"/opt/pw-browsers/chromium-1194/chrome-linux/chrome",
|
| 227 |
+
shutil.which("google-chrome"),
|
| 228 |
+
shutil.which("chromium"),
|
| 229 |
+
shutil.which("chromium-browser"),
|
| 230 |
+
shutil.which("chrome"),
|
| 231 |
+
]
|
| 232 |
+
for c in candidates:
|
| 233 |
+
if c and os.path.isfile(c):
|
| 234 |
+
return c
|
| 235 |
+
return None
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
def build_puppeteer_config(chrome_path: str) -> str:
|
| 239 |
+
"""Write a puppeteer config JSON to a temp file, return its path."""
|
| 240 |
+
cfg = {
|
| 241 |
+
"executablePath": chrome_path,
|
| 242 |
+
"args": [
|
| 243 |
+
"--no-sandbox",
|
| 244 |
+
"--disable-setuid-sandbox",
|
| 245 |
+
"--disable-dev-shm-usage",
|
| 246 |
+
"--disable-gpu",
|
| 247 |
+
],
|
| 248 |
+
}
|
| 249 |
+
fd, path = tempfile.mkstemp(suffix=".json", prefix="puppeteer_cfg_")
|
| 250 |
+
with os.fdopen(fd, "w") as f:
|
| 251 |
+
json.dump(cfg, f)
|
| 252 |
+
return path
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
def build_mermaid_config(theme_cfg: dict, bg: Optional[str]) -> str:
|
| 256 |
+
"""Write a mermaid config JSON, return its path."""
|
| 257 |
+
mermaid_theme = theme_cfg.get("mermaid_theme", "default")
|
| 258 |
+
cfg: dict = {
|
| 259 |
+
"theme": mermaid_theme,
|
| 260 |
+
}
|
| 261 |
+
if mermaid_theme == "dark":
|
| 262 |
+
cfg["themeVariables"] = {
|
| 263 |
+
"darkMode": True,
|
| 264 |
+
"background": theme_cfg.get("bg", "#1e1e2e"),
|
| 265 |
+
"primaryColor": theme_cfg.get("accent", theme_cfg.get("fg", "#88c0d0")),
|
| 266 |
+
"primaryTextColor": theme_cfg.get("fg", "#d8dee9"),
|
| 267 |
+
"lineColor": theme_cfg.get("accent", "#88c0d0"),
|
| 268 |
+
}
|
| 269 |
+
else:
|
| 270 |
+
cfg["themeVariables"] = {
|
| 271 |
+
"darkMode": False,
|
| 272 |
+
"background": theme_cfg.get("bg", "#ffffff"),
|
| 273 |
+
"primaryColor": theme_cfg.get("accent", "#ececff"),
|
| 274 |
+
"primaryTextColor": theme_cfg.get("fg", "#333333"),
|
| 275 |
+
"lineColor": theme_cfg.get("accent", "#333333"),
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
if bg:
|
| 279 |
+
cfg["themeVariables"]["background"] = bg
|
| 280 |
+
|
| 281 |
+
fd, path = tempfile.mkstemp(suffix=".json", prefix="mermaid_cfg_")
|
| 282 |
+
with os.fdopen(fd, "w") as f:
|
| 283 |
+
json.dump(cfg, f)
|
| 284 |
+
return path
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
# ---------------------------------------------------------------------------
|
| 288 |
+
# Core render function
|
| 289 |
+
# ---------------------------------------------------------------------------
|
| 290 |
+
|
| 291 |
+
def render_diagram(
|
| 292 |
+
source: str,
|
| 293 |
+
output_path: str,
|
| 294 |
+
*,
|
| 295 |
+
mmdc: str,
|
| 296 |
+
puppeteer_cfg: str,
|
| 297 |
+
theme_name: str = "default",
|
| 298 |
+
bg: Optional[str] = None,
|
| 299 |
+
scale: float = 2.0,
|
| 300 |
+
width: Optional[int] = None,
|
| 301 |
+
fmt: str = "png",
|
| 302 |
+
verbose: bool = False,
|
| 303 |
+
) -> dict:
|
| 304 |
+
"""
|
| 305 |
+
Render a Mermaid diagram string to an image file.
|
| 306 |
+
|
| 307 |
+
Parameters
|
| 308 |
+
----------
|
| 309 |
+
source : Mermaid diagram text
|
| 310 |
+
output_path : Destination file path
|
| 311 |
+
mmdc : Path to mmdc binary
|
| 312 |
+
puppeteer_cfg: Path to puppeteer config JSON
|
| 313 |
+
theme_name : One of THEMES keys or 'default'
|
| 314 |
+
bg : Override background color (hex or 'transparent')
|
| 315 |
+
scale : Device pixel ratio (default 2 = 2x)
|
| 316 |
+
width : Optional canvas width in pixels
|
| 317 |
+
fmt : Output format: png | svg | pdf
|
| 318 |
+
verbose : Log mmdc stdout/stderr
|
| 319 |
+
|
| 320 |
+
Returns
|
| 321 |
+
-------
|
| 322 |
+
dict with keys: success, output, error, duration_ms
|
| 323 |
+
"""
|
| 324 |
+
theme_cfg = THEMES.get(theme_name, THEMES["default"])
|
| 325 |
+
t0 = time.perf_counter()
|
| 326 |
+
|
| 327 |
+
# Write source to temp .mmd file
|
| 328 |
+
fd, src_path = tempfile.mkstemp(suffix=".mmd", prefix="mermaid_src_")
|
| 329 |
+
mermaid_cfg_path = None
|
| 330 |
+
try:
|
| 331 |
+
with os.fdopen(fd, "w") as f:
|
| 332 |
+
f.write(source)
|
| 333 |
+
|
| 334 |
+
mermaid_cfg_path = build_mermaid_config(theme_cfg, bg)
|
| 335 |
+
|
| 336 |
+
cmd = [
|
| 337 |
+
mmdc,
|
| 338 |
+
"-i", src_path,
|
| 339 |
+
"-o", output_path,
|
| 340 |
+
"--puppeteerConfigFile", puppeteer_cfg,
|
| 341 |
+
"--configFile", mermaid_cfg_path,
|
| 342 |
+
"--scale", str(scale),
|
| 343 |
+
]
|
| 344 |
+
|
| 345 |
+
if bg == "transparent":
|
| 346 |
+
cmd += ["-b", "transparent"]
|
| 347 |
+
elif bg:
|
| 348 |
+
cmd += ["-b", bg]
|
| 349 |
+
elif "bg" in theme_cfg:
|
| 350 |
+
cmd += ["-b", theme_cfg["bg"]]
|
| 351 |
+
|
| 352 |
+
if width:
|
| 353 |
+
cmd += ["-w", str(width)]
|
| 354 |
+
|
| 355 |
+
if fmt == "pdf":
|
| 356 |
+
cmd += ["--pdfFit"]
|
| 357 |
+
|
| 358 |
+
log.debug("CMD: %s", " ".join(cmd))
|
| 359 |
+
|
| 360 |
+
result = subprocess.run(
|
| 361 |
+
cmd,
|
| 362 |
+
capture_output=True,
|
| 363 |
+
text=True,
|
| 364 |
+
timeout=60,
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
+
duration_ms = int((time.perf_counter() - t0) * 1000)
|
| 368 |
+
|
| 369 |
+
if verbose:
|
| 370 |
+
if result.stdout.strip():
|
| 371 |
+
print(f" [mmdc stdout] {result.stdout.strip()}")
|
| 372 |
+
if result.stderr.strip():
|
| 373 |
+
print(f" [mmdc stderr] {result.stderr.strip()}")
|
| 374 |
+
|
| 375 |
+
success = result.returncode == 0 and os.path.isfile(output_path)
|
| 376 |
+
error = None
|
| 377 |
+
if not success:
|
| 378 |
+
error = (result.stderr or result.stdout or "mmdc exited with code "
|
| 379 |
+
+ str(result.returncode)).strip()
|
| 380 |
+
|
| 381 |
+
return {
|
| 382 |
+
"success": success,
|
| 383 |
+
"output": output_path if success else None,
|
| 384 |
+
"error": error,
|
| 385 |
+
"duration_ms": duration_ms,
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
except subprocess.TimeoutExpired:
|
| 389 |
+
return {
|
| 390 |
+
"success": False,
|
| 391 |
+
"output": None,
|
| 392 |
+
"error": "Render timed out (60s)",
|
| 393 |
+
"duration_ms": int((time.perf_counter() - t0) * 1000),
|
| 394 |
+
}
|
| 395 |
+
except Exception as exc:
|
| 396 |
+
return {
|
| 397 |
+
"success": False,
|
| 398 |
+
"output": None,
|
| 399 |
+
"error": str(exc),
|
| 400 |
+
"duration_ms": int((time.perf_counter() - t0) * 1000),
|
| 401 |
+
}
|
| 402 |
+
finally:
|
| 403 |
+
try:
|
| 404 |
+
os.unlink(src_path)
|
| 405 |
+
except OSError:
|
| 406 |
+
pass
|
| 407 |
+
if mermaid_cfg_path:
|
| 408 |
+
try:
|
| 409 |
+
os.unlink(mermaid_cfg_path)
|
| 410 |
+
except OSError:
|
| 411 |
+
pass
|
| 412 |
+
|
| 413 |
+
|
| 414 |
+
# ---------------------------------------------------------------------------
|
| 415 |
+
# Batch helpers
|
| 416 |
+
# ---------------------------------------------------------------------------
|
| 417 |
+
|
| 418 |
+
def _batch_worker(args: tuple) -> dict:
|
| 419 |
+
"""Top-level function for ProcessPoolExecutor (must be picklable)."""
|
| 420 |
+
(
|
| 421 |
+
src_file, out_file, mmdc, puppeteer_cfg,
|
| 422 |
+
theme, bg, scale, width, fmt, verbose,
|
| 423 |
+
) = args
|
| 424 |
+
source = Path(src_file).read_text(encoding="utf-8")
|
| 425 |
+
return {
|
| 426 |
+
"input": src_file,
|
| 427 |
+
**render_diagram(
|
| 428 |
+
source, out_file,
|
| 429 |
+
mmdc=mmdc, puppeteer_cfg=puppeteer_cfg,
|
| 430 |
+
theme_name=theme, bg=bg, scale=scale,
|
| 431 |
+
width=width, fmt=fmt, verbose=verbose,
|
| 432 |
+
),
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
def resolve_batch_inputs(batch_path: str, fmt: str, out_dir: Optional[str]) -> list[tuple[str, str]]:
|
| 437 |
+
"""Return list of (input_path, output_path) tuples for a batch run."""
|
| 438 |
+
p = Path(batch_path)
|
| 439 |
+
pairs: list[tuple[str, str]] = []
|
| 440 |
+
|
| 441 |
+
if "*" in batch_path or "?" in batch_path:
|
| 442 |
+
inputs = sorted(glob.glob(batch_path))
|
| 443 |
+
elif p.is_dir():
|
| 444 |
+
inputs = sorted(
|
| 445 |
+
str(f) for f in p.rglob("*")
|
| 446 |
+
if f.suffix.lower() in {".mmd", ".mermaid", ".txt"}
|
| 447 |
+
and f.is_file()
|
| 448 |
+
)
|
| 449 |
+
elif p.is_file():
|
| 450 |
+
inputs = [str(p)]
|
| 451 |
+
else:
|
| 452 |
+
print(f"[ERROR] Batch path not found: {batch_path}", file=sys.stderr)
|
| 453 |
+
return []
|
| 454 |
+
|
| 455 |
+
for inp in inputs:
|
| 456 |
+
in_path = Path(inp)
|
| 457 |
+
if out_dir:
|
| 458 |
+
out_base = Path(out_dir)
|
| 459 |
+
out_base.mkdir(parents=True, exist_ok=True)
|
| 460 |
+
out_file = str(out_base / (in_path.stem + f".{fmt}"))
|
| 461 |
+
else:
|
| 462 |
+
out_file = str(in_path.with_suffix(f".{fmt}"))
|
| 463 |
+
pairs.append((inp, out_file))
|
| 464 |
+
|
| 465 |
+
return pairs
|
| 466 |
+
|
| 467 |
+
|
| 468 |
+
# ---------------------------------------------------------------------------
|
| 469 |
+
# Watch mode
|
| 470 |
+
# ---------------------------------------------------------------------------
|
| 471 |
+
|
| 472 |
+
def watch_file(path: str) -> float:
|
| 473 |
+
try:
|
| 474 |
+
return os.path.getmtime(path)
|
| 475 |
+
except OSError:
|
| 476 |
+
return 0.0
|
| 477 |
+
|
| 478 |
+
|
| 479 |
+
def run_watch(
|
| 480 |
+
input_files: list[str],
|
| 481 |
+
output_map: dict[str, str],
|
| 482 |
+
render_kwargs: dict,
|
| 483 |
+
poll_interval: float = 0.8,
|
| 484 |
+
) -> None:
|
| 485 |
+
"""Poll input files and re-render on modification."""
|
| 486 |
+
mtimes = {f: watch_file(f) for f in input_files}
|
| 487 |
+
print(f"Watching {len(input_files)} file(s). Press Ctrl+C to stop.")
|
| 488 |
+
try:
|
| 489 |
+
while True:
|
| 490 |
+
time.sleep(poll_interval)
|
| 491 |
+
for f in input_files:
|
| 492 |
+
mtime = watch_file(f)
|
| 493 |
+
if mtime != mtimes[f]:
|
| 494 |
+
mtimes[f] = mtime
|
| 495 |
+
print(f" Changed: {f} — re-rendering...")
|
| 496 |
+
source = Path(f).read_text(encoding="utf-8")
|
| 497 |
+
result = render_diagram(source, output_map[f], **render_kwargs)
|
| 498 |
+
if result["success"]:
|
| 499 |
+
print(f" OK: {result['output']} ({result['duration_ms']}ms)")
|
| 500 |
+
else:
|
| 501 |
+
print(f" FAIL: {result['error']}", file=sys.stderr)
|
| 502 |
+
except KeyboardInterrupt:
|
| 503 |
+
print("\nWatch mode stopped.")
|
| 504 |
+
|
| 505 |
+
|
| 506 |
+
# ---------------------------------------------------------------------------
|
| 507 |
+
# CLI
|
| 508 |
+
# ---------------------------------------------------------------------------
|
| 509 |
+
|
| 510 |
+
def build_parser() -> argparse.ArgumentParser:
|
| 511 |
+
p = argparse.ArgumentParser(
|
| 512 |
+
prog="mermaid-renderer",
|
| 513 |
+
description="Convert Mermaid diagrams to PNG, SVG, or PDF images.",
|
| 514 |
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 515 |
+
epilog=__doc__,
|
| 516 |
+
)
|
| 517 |
+
|
| 518 |
+
p.add_argument(
|
| 519 |
+
"input",
|
| 520 |
+
nargs="?",
|
| 521 |
+
help="Path to .mmd / .mermaid file (omit when using --text or --batch)",
|
| 522 |
+
)
|
| 523 |
+
p.add_argument(
|
| 524 |
+
"-o", "--output",
|
| 525 |
+
default=None,
|
| 526 |
+
help="Output file path (default: same name as input, auto-extension)",
|
| 527 |
+
)
|
| 528 |
+
p.add_argument(
|
| 529 |
+
"--text",
|
| 530 |
+
default=None,
|
| 531 |
+
help="Inline Mermaid diagram text (alternative to file input)",
|
| 532 |
+
)
|
| 533 |
+
p.add_argument(
|
| 534 |
+
"--batch",
|
| 535 |
+
default=None,
|
| 536 |
+
metavar="DIR_OR_GLOB",
|
| 537 |
+
help="Render all .mmd/.mermaid files in a directory or matching a glob",
|
| 538 |
+
)
|
| 539 |
+
p.add_argument(
|
| 540 |
+
"--format", "-f",
|
| 541 |
+
default="png",
|
| 542 |
+
choices=["png", "svg", "pdf"],
|
| 543 |
+
help="Output format (default: png)",
|
| 544 |
+
)
|
| 545 |
+
p.add_argument(
|
| 546 |
+
"--theme", "-t",
|
| 547 |
+
default="default",
|
| 548 |
+
choices=list(THEMES.keys()),
|
| 549 |
+
help="Named theme (default: default)",
|
| 550 |
+
)
|
| 551 |
+
p.add_argument(
|
| 552 |
+
"--bg", "--background",
|
| 553 |
+
default=None,
|
| 554 |
+
dest="bg",
|
| 555 |
+
help='Background color hex or "transparent"',
|
| 556 |
+
)
|
| 557 |
+
p.add_argument(
|
| 558 |
+
"--fg", "--foreground",
|
| 559 |
+
default=None,
|
| 560 |
+
dest="fg",
|
| 561 |
+
help="Foreground color hex (overrides theme fg — informational only)",
|
| 562 |
+
)
|
| 563 |
+
p.add_argument(
|
| 564 |
+
"--accent",
|
| 565 |
+
default=None,
|
| 566 |
+
help="Accent color hex (overrides theme accent — informational only)",
|
| 567 |
+
)
|
| 568 |
+
p.add_argument(
|
| 569 |
+
"--scale",
|
| 570 |
+
type=float,
|
| 571 |
+
default=2.0,
|
| 572 |
+
help="Device pixel ratio / scale factor (default: 2.0)",
|
| 573 |
+
)
|
| 574 |
+
p.add_argument(
|
| 575 |
+
"--width", "-W",
|
| 576 |
+
type=int,
|
| 577 |
+
default=None,
|
| 578 |
+
help="Canvas width in pixels",
|
| 579 |
+
)
|
| 580 |
+
p.add_argument(
|
| 581 |
+
"--workers", "-j",
|
| 582 |
+
type=int,
|
| 583 |
+
default=1,
|
| 584 |
+
help="Parallel workers for batch mode (default: 1)",
|
| 585 |
+
)
|
| 586 |
+
p.add_argument(
|
| 587 |
+
"--watch", "-w",
|
| 588 |
+
action="store_true",
|
| 589 |
+
help="Watch input files and re-render on change",
|
| 590 |
+
)
|
| 591 |
+
p.add_argument(
|
| 592 |
+
"--list-themes",
|
| 593 |
+
action="store_true",
|
| 594 |
+
help="Print all available themes and exit",
|
| 595 |
+
)
|
| 596 |
+
p.add_argument(
|
| 597 |
+
"--check",
|
| 598 |
+
action="store_true",
|
| 599 |
+
help="Check environment dependencies and exit",
|
| 600 |
+
)
|
| 601 |
+
p.add_argument(
|
| 602 |
+
"-v", "--verbose",
|
| 603 |
+
action="store_true",
|
| 604 |
+
help="Show mmdc stdout/stderr",
|
| 605 |
+
)
|
| 606 |
+
|
| 607 |
+
return p
|
| 608 |
+
|
| 609 |
+
|
| 610 |
+
def cmd_check() -> None:
|
| 611 |
+
"""Print environment diagnostic info."""
|
| 612 |
+
mmdc = find_mmdc()
|
| 613 |
+
chrome = find_chrome()
|
| 614 |
+
|
| 615 |
+
print("mermaid-renderer — environment check")
|
| 616 |
+
print("-" * 40)
|
| 617 |
+
print(f" Python : {sys.version.split()[0]}")
|
| 618 |
+
print(f" mmdc : {mmdc or 'NOT FOUND'}")
|
| 619 |
+
print(f" Chrome/Chrom: {chrome or 'NOT FOUND'}")
|
| 620 |
+
|
| 621 |
+
if mmdc:
|
| 622 |
+
try:
|
| 623 |
+
r = subprocess.run([mmdc, "--version"], capture_output=True, text=True, timeout=5)
|
| 624 |
+
print(f" mmdc version: {r.stdout.strip()}")
|
| 625 |
+
except Exception:
|
| 626 |
+
print(" mmdc version: (could not query)")
|
| 627 |
+
|
| 628 |
+
if mmdc and chrome:
|
| 629 |
+
print("\n All dependencies OK. Rendering should work.")
|
| 630 |
+
else:
|
| 631 |
+
missing = []
|
| 632 |
+
if not mmdc:
|
| 633 |
+
missing.append("mmdc (install: npm install -g @mermaid-js/mermaid-cli)")
|
| 634 |
+
if not chrome:
|
| 635 |
+
missing.append("Chrome/Chromium")
|
| 636 |
+
print("\n MISSING:", ", ".join(missing))
|
| 637 |
+
sys.exit(1)
|
| 638 |
+
|
| 639 |
+
|
| 640 |
+
def cmd_list_themes() -> None:
|
| 641 |
+
col_w = 24
|
| 642 |
+
print(f"\n{'Theme':<{col_w}} {'Type':<8} {'Background':<12} {'Accent'}")
|
| 643 |
+
print("-" * 64)
|
| 644 |
+
for name, cfg in THEMES.items():
|
| 645 |
+
theme_type = "dark" if cfg.get("mermaid_theme") == "dark" else "light"
|
| 646 |
+
accent = cfg.get("accent", "(derived)")
|
| 647 |
+
print(f" {name:<{col_w-2}} {theme_type:<8} {cfg.get('bg',''):<12} {accent}")
|
| 648 |
+
print()
|
| 649 |
+
|
| 650 |
+
|
| 651 |
+
def main() -> None:
|
| 652 |
+
parser = build_parser()
|
| 653 |
+
args = parser.parse_args()
|
| 654 |
+
|
| 655 |
+
if args.verbose:
|
| 656 |
+
log.setLevel(logging.DEBUG)
|
| 657 |
+
|
| 658 |
+
if args.check:
|
| 659 |
+
cmd_check()
|
| 660 |
+
return
|
| 661 |
+
|
| 662 |
+
if args.list_themes:
|
| 663 |
+
cmd_list_themes()
|
| 664 |
+
return
|
| 665 |
+
|
| 666 |
+
# Resolve dependencies
|
| 667 |
+
mmdc = find_mmdc()
|
| 668 |
+
chrome = find_chrome()
|
| 669 |
+
|
| 670 |
+
if not mmdc:
|
| 671 |
+
print(
|
| 672 |
+
"[ERROR] mmdc not found. Install with:\n"
|
| 673 |
+
" npm install -g @mermaid-js/mermaid-cli\n"
|
| 674 |
+
"Then run: python render.py --check",
|
| 675 |
+
file=sys.stderr,
|
| 676 |
+
)
|
| 677 |
+
sys.exit(1)
|
| 678 |
+
|
| 679 |
+
if not chrome:
|
| 680 |
+
print(
|
| 681 |
+
"[ERROR] No Chrome/Chromium found. See README for install instructions.",
|
| 682 |
+
file=sys.stderr,
|
| 683 |
+
)
|
| 684 |
+
sys.exit(1)
|
| 685 |
+
|
| 686 |
+
puppeteer_cfg = build_puppeteer_config(chrome)
|
| 687 |
+
|
| 688 |
+
# Apply per-run theme overrides
|
| 689 |
+
theme_cfg = dict(THEMES.get(args.theme, THEMES["default"]))
|
| 690 |
+
if args.fg:
|
| 691 |
+
theme_cfg["fg"] = args.fg
|
| 692 |
+
if args.accent:
|
| 693 |
+
theme_cfg["accent"] = args.accent
|
| 694 |
+
|
| 695 |
+
render_kwargs = dict(
|
| 696 |
+
mmdc=mmdc,
|
| 697 |
+
puppeteer_cfg=puppeteer_cfg,
|
| 698 |
+
theme_name=args.theme,
|
| 699 |
+
bg=args.bg,
|
| 700 |
+
scale=args.scale,
|
| 701 |
+
width=args.width,
|
| 702 |
+
fmt=args.format,
|
| 703 |
+
verbose=args.verbose,
|
| 704 |
+
)
|
| 705 |
+
|
| 706 |
+
try:
|
| 707 |
+
# -----------------------------------------------------------------------
|
| 708 |
+
# BATCH MODE
|
| 709 |
+
# -----------------------------------------------------------------------
|
| 710 |
+
if args.batch:
|
| 711 |
+
pairs = resolve_batch_inputs(args.batch, args.format, args.output)
|
| 712 |
+
if not pairs:
|
| 713 |
+
print("[ERROR] No input files found.", file=sys.stderr)
|
| 714 |
+
sys.exit(1)
|
| 715 |
+
|
| 716 |
+
print(f"Batch: {len(pairs)} file(s) | theme={args.theme} | "
|
| 717 |
+
f"format={args.format} | workers={args.workers}")
|
| 718 |
+
|
| 719 |
+
if args.watch:
|
| 720 |
+
input_files = [p[0] for p in pairs]
|
| 721 |
+
output_map = {p[0]: p[1] for p in pairs}
|
| 722 |
+
run_watch(input_files, output_map, render_kwargs)
|
| 723 |
+
return
|
| 724 |
+
|
| 725 |
+
if args.workers > 1:
|
| 726 |
+
worker_args = [
|
| 727 |
+
(inp, out, mmdc, puppeteer_cfg,
|
| 728 |
+
args.theme, args.bg, args.scale, args.width,
|
| 729 |
+
args.format, args.verbose)
|
| 730 |
+
for inp, out in pairs
|
| 731 |
+
]
|
| 732 |
+
results = []
|
| 733 |
+
with ProcessPoolExecutor(max_workers=args.workers) as ex:
|
| 734 |
+
futures = {ex.submit(_batch_worker, a): a[0] for a in worker_args}
|
| 735 |
+
for fut in as_completed(futures):
|
| 736 |
+
results.append(fut.result())
|
| 737 |
+
else:
|
| 738 |
+
results = []
|
| 739 |
+
for inp, out in pairs:
|
| 740 |
+
source = Path(inp).read_text(encoding="utf-8")
|
| 741 |
+
r = render_diagram(source, out, **render_kwargs)
|
| 742 |
+
results.append({"input": inp, **r})
|
| 743 |
+
status = "OK" if r["success"] else "FAIL"
|
| 744 |
+
print(f" [{status}] {inp} -> {out} ({r['duration_ms']}ms)")
|
| 745 |
+
if not r["success"]:
|
| 746 |
+
print(f" Error: {r['error']}", file=sys.stderr)
|
| 747 |
+
|
| 748 |
+
ok = sum(1 for r in results if r["success"])
|
| 749 |
+
fail = len(results) - ok
|
| 750 |
+
print(f"\nDone: {ok} succeeded, {fail} failed.")
|
| 751 |
+
if fail > 0:
|
| 752 |
+
sys.exit(1)
|
| 753 |
+
return
|
| 754 |
+
|
| 755 |
+
# -----------------------------------------------------------------------
|
| 756 |
+
# SINGLE FILE OR INLINE TEXT
|
| 757 |
+
# -----------------------------------------------------------------------
|
| 758 |
+
if args.text:
|
| 759 |
+
source = args.text.replace("; ", "\n").replace(";", "\n")
|
| 760 |
+
default_stem = "diagram"
|
| 761 |
+
elif args.input:
|
| 762 |
+
in_path = Path(args.input)
|
| 763 |
+
if not in_path.is_file():
|
| 764 |
+
print(f"[ERROR] Input file not found: {args.input}", file=sys.stderr)
|
| 765 |
+
sys.exit(1)
|
| 766 |
+
source = in_path.read_text(encoding="utf-8")
|
| 767 |
+
default_stem = in_path.stem
|
| 768 |
+
else:
|
| 769 |
+
parser.print_help()
|
| 770 |
+
sys.exit(0)
|
| 771 |
+
|
| 772 |
+
# Determine output path
|
| 773 |
+
if args.output:
|
| 774 |
+
out_path = args.output
|
| 775 |
+
else:
|
| 776 |
+
suffix = f".{args.format}"
|
| 777 |
+
out_path = str(Path(default_stem).with_suffix(suffix))
|
| 778 |
+
|
| 779 |
+
print(f"Rendering: {out_path} | theme={args.theme} | format={args.format} | scale={args.scale}")
|
| 780 |
+
|
| 781 |
+
result = render_diagram(source, out_path, **render_kwargs)
|
| 782 |
+
|
| 783 |
+
if result["success"]:
|
| 784 |
+
size = os.path.getsize(result["output"])
|
| 785 |
+
print(f" OK: {result['output']} ({size:,} bytes, {result['duration_ms']}ms)")
|
| 786 |
+
else:
|
| 787 |
+
print(f" FAIL: {result['error']}", file=sys.stderr)
|
| 788 |
+
sys.exit(1)
|
| 789 |
+
|
| 790 |
+
# Watch mode for single file
|
| 791 |
+
if args.watch and args.input:
|
| 792 |
+
run_watch([args.input], {args.input: out_path}, render_kwargs)
|
| 793 |
+
|
| 794 |
+
finally:
|
| 795 |
+
try:
|
| 796 |
+
os.unlink(puppeteer_cfg)
|
| 797 |
+
except OSError:
|
| 798 |
+
pass
|
| 799 |
+
|
| 800 |
+
|
| 801 |
+
if __name__ == "__main__":
|
| 802 |
+
main()
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# mermaid-renderer requirements
|
| 2 |
+
# Core — no external Python packages strictly required beyond stdlib.
|
| 3 |
+
# The heavy lifting is done by @mermaid-js/mermaid-cli (Node.js).
|
| 4 |
+
#
|
| 5 |
+
# Optional: install these for extended image post-processing and CLI polish.
|
| 6 |
+
Pillow>=10.0.0
|
| 7 |
+
click>=8.1.0
|
samples/classdiagram.mmd
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
classDiagram
|
| 2 |
+
class Animal {
|
| 3 |
+
+String name
|
| 4 |
+
+int age
|
| 5 |
+
+makeSound() void
|
| 6 |
+
+move() void
|
| 7 |
+
}
|
| 8 |
+
class Dog {
|
| 9 |
+
+String breed
|
| 10 |
+
+fetch() void
|
| 11 |
+
+bark() void
|
| 12 |
+
}
|
| 13 |
+
class Cat {
|
| 14 |
+
+bool indoor
|
| 15 |
+
+purr() void
|
| 16 |
+
+scratch() void
|
| 17 |
+
}
|
| 18 |
+
class Parrot {
|
| 19 |
+
+String[] vocabulary
|
| 20 |
+
+speak() String
|
| 21 |
+
+fly() void
|
| 22 |
+
}
|
| 23 |
+
Animal <|-- Dog
|
| 24 |
+
Animal <|-- Cat
|
| 25 |
+
Animal <|-- Parrot
|
samples/er.mmd
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
erDiagram
|
| 2 |
+
USER {
|
| 3 |
+
int id PK
|
| 4 |
+
string email UK
|
| 5 |
+
string name
|
| 6 |
+
datetime created_at
|
| 7 |
+
}
|
| 8 |
+
ORDER {
|
| 9 |
+
int id PK
|
| 10 |
+
int user_id FK
|
| 11 |
+
decimal total
|
| 12 |
+
string status
|
| 13 |
+
datetime placed_at
|
| 14 |
+
}
|
| 15 |
+
ORDER_ITEM {
|
| 16 |
+
int id PK
|
| 17 |
+
int order_id FK
|
| 18 |
+
int product_id FK
|
| 19 |
+
int quantity
|
| 20 |
+
decimal unit_price
|
| 21 |
+
}
|
| 22 |
+
PRODUCT {
|
| 23 |
+
int id PK
|
| 24 |
+
string name
|
| 25 |
+
decimal price
|
| 26 |
+
int stock
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
USER ||--o{ ORDER : places
|
| 30 |
+
ORDER ||--|{ ORDER_ITEM : contains
|
| 31 |
+
PRODUCT ||--o{ ORDER_ITEM : "is in"
|
samples/flowchart.mmd
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
graph TD
|
| 2 |
+
A[Start] --> B{Is authenticated?}
|
| 3 |
+
B -->|Yes| C[Load Dashboard]
|
| 4 |
+
B -->|No| D[Show Login]
|
| 5 |
+
D --> E[Submit Credentials]
|
| 6 |
+
E --> F{Valid?}
|
| 7 |
+
F -->|Yes| G[Issue JWT]
|
| 8 |
+
G --> C
|
| 9 |
+
F -->|No| H[Show Error]
|
| 10 |
+
H --> D
|
| 11 |
+
C --> I[End]
|
samples/sequence.mmd
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
sequenceDiagram
|
| 2 |
+
participant User
|
| 3 |
+
participant API
|
| 4 |
+
participant DB
|
| 5 |
+
participant Cache
|
| 6 |
+
|
| 7 |
+
User->>API: GET /profile
|
| 8 |
+
API->>Cache: Check cache
|
| 9 |
+
alt Cache hit
|
| 10 |
+
Cache-->>API: Return cached profile
|
| 11 |
+
else Cache miss
|
| 12 |
+
API->>DB: SELECT * FROM users WHERE id=?
|
| 13 |
+
DB-->>API: User row
|
| 14 |
+
API->>Cache: SET profile (TTL 300s)
|
| 15 |
+
end
|
| 16 |
+
API-->>User: 200 OK { profile }
|
samples/statediagram.mmd
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
stateDiagram-v2
|
| 2 |
+
[*] --> Idle
|
| 3 |
+
Idle --> Connecting: connect()
|
| 4 |
+
Connecting --> Connected: success
|
| 5 |
+
Connecting --> Error: timeout
|
| 6 |
+
Connected --> Idle: disconnect()
|
| 7 |
+
Connected --> Reconnecting: connection lost
|
| 8 |
+
Reconnecting --> Connected: reconnected
|
| 9 |
+
Reconnecting --> Error: max retries
|
| 10 |
+
Error --> Idle: reset()
|
| 11 |
+
Error --> [*]: fatal
|
samples/xychart.mmd
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
xychart-beta
|
| 2 |
+
title "Monthly Revenue vs Target"
|
| 3 |
+
x-axis [Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
|
| 4 |
+
y-axis "Revenue ($K)" 0 --> 600
|
| 5 |
+
bar [180, 220, 310, 280, 350, 420, 390, 450, 480, 510, 540, 580]
|
| 6 |
+
line [200, 240, 260, 300, 320, 360, 380, 400, 430, 460, 500, 560]
|