algorembrant commited on
Commit
9e8fb65
·
verified ·
1 Parent(s): 6913e93

Upload 12 files

Browse files
.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
+ [![Python](https://img.shields.io/badge/python-3.9%2B-blue.svg)](https://www.python.org/)
28
+ [![Node.js](https://img.shields.io/badge/node.js-18%2B-green.svg)](https://nodejs.org/)
29
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
30
+ [![Mermaid CLI](https://img.shields.io/badge/@mermaid--js%2Fmermaid--cli-11%2B-purple.svg)](https://github.com/mermaid-js/mermaid-cli)
31
+ [![Formats](https://img.shields.io/badge/output-PNG%20%7C%20SVG%20%7C%20PDF-orange.svg)](#supported-formats)
32
+ [![Themes](https://img.shields.io/badge/themes-15%20built--in-teal.svg)](#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]