Spaces:
Sleeping
Sleeping
initial working version
Browse files- README.md +213 -0
- __pycache__/mermaid_renderer.cpython-313.pyc +0 -0
- app.py +185 -0
- flowchart.pdf +0 -0
- flowchart.png +0 -0
- flowchart.svg +1 -0
- mermaid-rendering.py +165 -0
- mermaid_renderer.py +118 -0
- requirements.txt +1 -0
- templates/index.html +193 -0
README.md
CHANGED
|
@@ -9,3 +9,216 @@ license: mit
|
|
| 9 |
---
|
| 10 |
|
| 11 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
---
|
| 10 |
|
| 11 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
| 12 |
+
|
| 13 |
+
# Mermaid Live Renderer Web Application
|
| 14 |
+
|
| 15 |
+
This Flask application provides a web interface for rendering diagrams from [Mermaid](https://mermaid.js.org/) syntax code. It features a live preview that updates as you type and allows downloading the final diagram as PNG, SVG, or PDF.
|
| 16 |
+
|
| 17 |
+
## Features
|
| 18 |
+
|
| 19 |
+
* Web-based interface for entering Mermaid code.
|
| 20 |
+
* Live preview of the diagram (SVG) that updates automatically as you type or change the theme.
|
| 21 |
+
* Selectable themes (Default, Forest, Dark, Neutral).
|
| 22 |
+
* Download the rendered diagram as PNG, SVG, or PDF.
|
| 23 |
+
* Uses `@mermaid-js/mermaid-cli` (mmdc) behind the scenes.
|
| 24 |
+
|
| 25 |
+
## Project Structure
|
| 26 |
+
|
| 27 |
+
```
|
| 28 |
+
.
|
| 29 |
+
├── app.py # Main Flask application logic
|
| 30 |
+
├── mermaid_renderer.py # Core class for calling mmdc
|
| 31 |
+
├── requirements.txt # Python dependencies (Flask)
|
| 32 |
+
├── templates/
|
| 33 |
+
│ └── index.html # HTML template for the web interface
|
| 34 |
+
├── venv/ # Python virtual environment (created locally/on server)
|
| 35 |
+
└── README.md # This file
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
## Prerequisites
|
| 39 |
+
|
| 40 |
+
### Local Machine (for testing)
|
| 41 |
+
|
| 42 |
+
* Python 3 & pip
|
| 43 |
+
* Node.js & npm
|
| 44 |
+
* `@mermaid-js/mermaid-cli` installed globally (`npm install -g @mermaid-js/mermaid-cli`)
|
| 45 |
+
|
| 46 |
+
### Azure Ubuntu VM (for deployment)
|
| 47 |
+
|
| 48 |
+
* An Azure Virtual Machine running Ubuntu (e.g., Ubuntu 20.04 LTS or later).
|
| 49 |
+
* SSH access to the VM.
|
| 50 |
+
* Ability to configure Network Security Groups (NSGs) in Azure portal.
|
| 51 |
+
* `sudo` privileges on the VM.
|
| 52 |
+
|
| 53 |
+
## Local Testing
|
| 54 |
+
|
| 55 |
+
1. **Clone/Download:** Get the application files (`app.py`, `mermaid_renderer.py`, `requirements.txt`, `templates/`).
|
| 56 |
+
2. **Install Prerequisites:** Ensure Python 3, pip, Node.js, npm, and `@mermaid-js/mermaid-cli` are installed locally.
|
| 57 |
+
3. **Create Virtual Environment:**
|
| 58 |
+
```bash
|
| 59 |
+
cd /path/to/project/directory
|
| 60 |
+
python3 -m venv venv
|
| 61 |
+
source venv/bin/activate
|
| 62 |
+
```
|
| 63 |
+
4. **Install Dependencies:**
|
| 64 |
+
```bash
|
| 65 |
+
pip install -r requirements.txt
|
| 66 |
+
```
|
| 67 |
+
5. **Run Development Server:**
|
| 68 |
+
```bash
|
| 69 |
+
python app.py
|
| 70 |
+
```
|
| 71 |
+
6. **Access:** Open your browser to `http://127.0.0.1:5001` (or the port specified in `app.py`).
|
| 72 |
+
|
| 73 |
+
## Deployment to Azure Ubuntu VM
|
| 74 |
+
|
| 75 |
+
These steps guide you through deploying the application using Gunicorn and Nginx.
|
| 76 |
+
|
| 77 |
+
### 1. Connect to your VM
|
| 78 |
+
|
| 79 |
+
Connect to your Azure Ubuntu VM using SSH:
|
| 80 |
+
```bash
|
| 81 |
+
ssh your_username@your_vm_ip_address
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
### 2. Install System Dependencies
|
| 85 |
+
|
| 86 |
+
Update package lists and install necessary software:
|
| 87 |
+
```bash
|
| 88 |
+
sudo apt update
|
| 89 |
+
sudo apt install -y python3 python3-pip python3-venv nodejs npm nginx gunicorn
|
| 90 |
+
```
|
| 91 |
+
* `python3`, `python3-pip`, `python3-venv`: For running the Python application.
|
| 92 |
+
* `nodejs`, `npm`: Required by `@mermaid-js/mermaid-cli`.
|
| 93 |
+
* `nginx`: Web server to act as a reverse proxy.
|
| 94 |
+
* `gunicorn`: WSGI server to run the Flask application.
|
| 95 |
+
|
| 96 |
+
Verify Node.js and npm installation: `node -v`, `npm -v`.
|
| 97 |
+
|
| 98 |
+
### 3. (Optional but Recommended) Install Mermaid CLI Globally
|
| 99 |
+
|
| 100 |
+
While the `mermaid_renderer.py` script attempts to install `mmdc` if not found, it's often more reliable to install it manually on the server first:
|
| 101 |
+
```bash
|
| 102 |
+
# Use --unsafe-perm if needed, especially when running as root/sudo
|
| 103 |
+
sudo npm install -g @mermaid-js/mermaid-cli --unsafe-perm=true --allow-root
|
| 104 |
+
# Verify installation
|
| 105 |
+
mmdc --version
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
### 4. Transfer Application Files
|
| 109 |
+
|
| 110 |
+
* Create a directory for the application on the VM:
|
| 111 |
+
```bash
|
| 112 |
+
sudo mkdir -p /var/www/mermaid-app
|
| 113 |
+
# Set appropriate ownership (replace 'your_vm_user' with your actual user)
|
| 114 |
+
sudo chown your_vm_user:your_vm_user /var/www/mermaid-app
|
| 115 |
+
cd /var/www/mermaid-app
|
| 116 |
+
```
|
| 117 |
+
* From your **local machine**, copy the application files to the VM using `scp` or `rsync`. Replace placeholders:
|
| 118 |
+
```bash
|
| 119 |
+
scp -r /path/to/local/app.py /path/to/local/mermaid_renderer.py /path/to/local/requirements.txt /path/to/local/templates your_username@your_vm_ip_address:/var/www/mermaid-app/
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
### 5. Set Up Python Virtual Environment
|
| 123 |
+
|
| 124 |
+
On the VM, navigate to the application directory and set up the environment:
|
| 125 |
+
```bash
|
| 126 |
+
cd /var/www/mermaid-app
|
| 127 |
+
python3 -m venv venv
|
| 128 |
+
source venv/bin/activate
|
| 129 |
+
pip install -r requirements.txt
|
| 130 |
+
deactivate # Deactivate for now, systemd will handle activation
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
### 6. Configure systemd for Gunicorn
|
| 134 |
+
|
| 135 |
+
Create a systemd service file to manage the Gunicorn process.
|
| 136 |
+
|
| 137 |
+
* Create the file:
|
| 138 |
+
```bash
|
| 139 |
+
sudo nano /etc/systemd/system/mermaid-app.service
|
| 140 |
+
```
|
| 141 |
+
* Paste the following content. **Important:**
|
| 142 |
+
* Replace `your_vm_user` with the Linux user that should run the application (this user needs permissions for the app directory and potentially for npm global installs if `mmdc` wasn't pre-installed). Using your own user is fine for single-user setups. `www-data` is common if Nginx runs as `www-data`.
|
| 143 |
+
* **Set a strong, unique `FLASK_SECRET_KEY`!** Generate one using `python -c 'import os; print(os.urandom(24))'`.
|
| 144 |
+
|
| 145 |
+
```ini
|
| 146 |
+
[Unit]
|
| 147 |
+
Description=Gunicorn instance to serve Mermaid Live Renderer
|
| 148 |
+
After=network.target
|
| 149 |
+
|
| 150 |
+
[Service]
|
| 151 |
+
User=your_vm_user
|
| 152 |
+
Group=your_vm_user # Or www-data if User is www-data
|
| 153 |
+
WorkingDirectory=/var/www/mermaid-app
|
| 154 |
+
# Add venv's bin to the PATH and set the secret key
|
| 155 |
+
Environment="PATH=/var/www/mermaid-app/venv/bin"
|
| 156 |
+
Environment="FLASK_SECRET_KEY=replace_with_your_strong_random_secret_key"
|
| 157 |
+
# Command to start Gunicorn
|
| 158 |
+
ExecStart=/var/www/mermaid-app/venv/bin/gunicorn --workers 3 --bind unix:/var/www/mermaid-app/mermaid-app.sock -m 007 app:app
|
| 159 |
+
|
| 160 |
+
Restart=always
|
| 161 |
+
|
| 162 |
+
[Install]
|
| 163 |
+
WantedBy=multi-user.target
|
| 164 |
+
```
|
| 165 |
+
|
| 166 |
+
* Save and close the file (Ctrl+X, then Y, then Enter in `nano`).
|
| 167 |
+
* Start and enable the service:
|
| 168 |
+
```bash
|
| 169 |
+
sudo systemctl start mermaid-app
|
| 170 |
+
sudo systemctl enable mermaid-app
|
| 171 |
+
# Check status (look for 'active (running)')
|
| 172 |
+
sudo systemctl status mermaid-app
|
| 173 |
+
# Check for errors if it failed
|
| 174 |
+
# sudo journalctl -u mermaid-app
|
| 175 |
+
```
|
| 176 |
+
*Troubleshooting:* If the service fails, check permissions on `/var/www/mermaid-app` and the socket file (`mermaid-app.sock` which Gunicorn creates). Ensure the `User` specified can write the socket file. The `-m 007` in the `ExecStart` makes the socket group-writable, which helps if Nginx runs as a different group (like `www-data`).
|
| 177 |
+
|
| 178 |
+
### 7. Configure Nginx as Reverse Proxy
|
| 179 |
+
|
| 180 |
+
Configure Nginx to forward web requests to the Gunicorn socket.
|
| 181 |
+
|
| 182 |
+
* Create an Nginx configuration file:
|
| 183 |
+
```bash
|
| 184 |
+
sudo nano /etc/nginx/sites-available/mermaid-app
|
| 185 |
+
```
|
| 186 |
+
* Paste the following, replacing `your_domain_or_vm_ip` with your VM's public IP address or a domain name pointing to it:
|
| 187 |
+
```nginx
|
| 188 |
+
server {
|
| 189 |
+
listen 80;
|
| 190 |
+
server_name your_domain_or_vm_ip;
|
| 191 |
+
|
| 192 |
+
location / {
|
| 193 |
+
include proxy_params;
|
| 194 |
+
# Forward requests to the Gunicorn socket
|
| 195 |
+
proxy_pass http://unix:/var/www/mermaid-app/mermaid-app.sock;
|
| 196 |
+
}
|
| 197 |
+
}
|
| 198 |
+
```
|
| 199 |
+
* Save and close the file.
|
| 200 |
+
* Enable the site by creating a symbolic link:
|
| 201 |
+
```bash
|
| 202 |
+
# Remove default site if it exists and conflicts
|
| 203 |
+
# sudo rm /etc/nginx/sites-enabled/default
|
| 204 |
+
sudo ln -s /etc/nginx/sites-available/mermaid-app /etc/nginx/sites-enabled/
|
| 205 |
+
```
|
| 206 |
+
* Test Nginx configuration and restart:
|
| 207 |
+
```bash
|
| 208 |
+
sudo nginx -t
|
| 209 |
+
# If syntax is OK:
|
| 210 |
+
sudo systemctl restart nginx
|
| 211 |
+
```
|
| 212 |
+
|
| 213 |
+
### 8. Configure Firewall
|
| 214 |
+
|
| 215 |
+
* **Azure NSG:** In the Azure portal, go to your VM's Networking settings. Add an inbound security rule to allow traffic on port 80 (HTTP) from the internet (Source: `Any` or `Internet`).
|
| 216 |
+
* **VM Firewall (ufw):** If `ufw` is active on the VM, allow Nginx traffic:
|
| 217 |
+
```bash
|
| 218 |
+
sudo ufw allow 'Nginx Full'
|
| 219 |
+
# Check status if needed: sudo ufw status
|
| 220 |
+
```
|
| 221 |
+
|
| 222 |
+
### 9. Access the Application
|
| 223 |
+
|
| 224 |
+
Open your web browser and navigate to `http://your_domain_or_vm_ip`. You should see the Mermaid Live Renderer interface.
|
__pycache__/mermaid_renderer.cpython-313.pyc
ADDED
|
Binary file (6.45 kB). View file
|
|
|
app.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app.py
|
| 2 |
+
import os
|
| 3 |
+
import logging
|
| 4 |
+
import base64
|
| 5 |
+
import textwrap # Import the missing module
|
| 6 |
+
from flask import Flask, request, render_template, send_file, flash, redirect, url_for, jsonify
|
| 7 |
+
from mermaid_renderer import MermaidRenderer # Import the refactored class
|
| 8 |
+
|
| 9 |
+
# Configure logging (optional but recommended)
|
| 10 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 11 |
+
|
| 12 |
+
app = Flask(__name__)
|
| 13 |
+
# Required for flashing messages
|
| 14 |
+
# In a real deployment, use a persistent, environment-variable-based secret key
|
| 15 |
+
# Using a fixed key for simplicity here, replace in production
|
| 16 |
+
app.secret_key = os.environ.get('FLASK_SECRET_KEY', 'a_very_secret_key_change_in_prod')
|
| 17 |
+
|
| 18 |
+
# Initialize the renderer (could potentially be done once at startup)
|
| 19 |
+
# Handle potential init errors
|
| 20 |
+
renderer = None
|
| 21 |
+
try:
|
| 22 |
+
renderer = MermaidRenderer()
|
| 23 |
+
logging.info("MermaidRenderer initialized successfully.")
|
| 24 |
+
except RuntimeError as e:
|
| 25 |
+
logging.critical(f"Failed to initialize MermaidRenderer: {e}. The rendering endpoint will be unavailable.")
|
| 26 |
+
# Keep renderer as None to indicate failure
|
| 27 |
+
|
| 28 |
+
@app.route('/', methods=['GET'])
|
| 29 |
+
def index():
|
| 30 |
+
"""Display the main page with the form, including default example code."""
|
| 31 |
+
default_mermaid_code = textwrap.dedent("""
|
| 32 |
+
graph TD
|
| 33 |
+
A[Start] --> B{Is it?};
|
| 34 |
+
B -- Yes --> C[OK];
|
| 35 |
+
C --> D[End];
|
| 36 |
+
B -- No --> E[Really?];
|
| 37 |
+
E --> C;
|
| 38 |
+
""").strip()
|
| 39 |
+
|
| 40 |
+
if renderer is None:
|
| 41 |
+
flash("Error: Mermaid rendering service is unavailable due to initialization failure. Please check server logs.", "error")
|
| 42 |
+
return render_template('index.html', default_code=default_mermaid_code)
|
| 43 |
+
|
| 44 |
+
@app.route('/render', methods=['POST'])
|
| 45 |
+
def render_mermaid():
|
| 46 |
+
"""
|
| 47 |
+
Handle form submission.
|
| 48 |
+
Handle form submission for final download.
|
| 49 |
+
Re-renders the diagram based on form data and sends it as an attachment.
|
| 50 |
+
"""
|
| 51 |
+
if renderer is None:
|
| 52 |
+
flash("Mermaid rendering service is unavailable. Cannot process request.", "error")
|
| 53 |
+
return redirect(url_for('index'))
|
| 54 |
+
|
| 55 |
+
mermaid_code = request.form.get('mermaid_code', '')
|
| 56 |
+
output_format = request.form.get('output_format', 'png')
|
| 57 |
+
theme = request.form.get('theme', 'default')
|
| 58 |
+
|
| 59 |
+
if not mermaid_code.strip():
|
| 60 |
+
flash("Mermaid code cannot be empty.", "warning")
|
| 61 |
+
return redirect(url_for('index'))
|
| 62 |
+
|
| 63 |
+
output_path = None
|
| 64 |
+
input_path = None
|
| 65 |
+
try:
|
| 66 |
+
logging.info(f"Download request: format={output_format}, theme={theme}")
|
| 67 |
+
if not renderer:
|
| 68 |
+
raise RuntimeError("Renderer not initialized.")
|
| 69 |
+
|
| 70 |
+
output_path, input_path = renderer.render(mermaid_code, output_format, theme)
|
| 71 |
+
|
| 72 |
+
mime_types = {'png': 'image/png', 'svg': 'image/svg+xml', 'pdf': 'application/pdf'}
|
| 73 |
+
mime_type = mime_types.get(output_format, 'application/octet-stream')
|
| 74 |
+
|
| 75 |
+
return send_file(
|
| 76 |
+
output_path,
|
| 77 |
+
mimetype=mime_type,
|
| 78 |
+
as_attachment=True,
|
| 79 |
+
download_name=f'diagram.{output_format}'
|
| 80 |
+
)
|
| 81 |
+
except (ValueError, RuntimeError) as e:
|
| 82 |
+
logging.error(f"Download rendering failed: {e}")
|
| 83 |
+
flash(f"Error generating download: {e}", "error")
|
| 84 |
+
return redirect(url_for('index'))
|
| 85 |
+
except Exception as e:
|
| 86 |
+
logging.exception("An unexpected error occurred during rendering.") # Log full traceback
|
| 87 |
+
flash("An unexpected server error occurred. Please try again later.", "error")
|
| 88 |
+
return redirect(url_for('index'))
|
| 89 |
+
finally:
|
| 90 |
+
# IMPORTANT: Clean up temporary files after sending the response or on error
|
| 91 |
+
if output_path and os.path.exists(output_path):
|
| 92 |
+
try:
|
| 93 |
+
# If it was PDF, send_file might hold the handle, but unlinking should still work on Unix-like systems
|
| 94 |
+
# On Windows, this might cause issues if send_file hasn't finished.
|
| 95 |
+
# A more robust solution might involve background tasks or different cleanup strategies.
|
| 96 |
+
os.unlink(output_path)
|
| 97 |
+
logging.info(f"Cleaned up temporary output file: {output_path}")
|
| 98 |
+
except OSError as e:
|
| 99 |
+
logging.error(f"Error deleting temporary output file {output_path}: {e}")
|
| 100 |
+
if input_path and os.path.exists(input_path):
|
| 101 |
+
try:
|
| 102 |
+
os.unlink(input_path)
|
| 103 |
+
logging.info(f"Cleaned up temporary input file: {input_path}")
|
| 104 |
+
except OSError as e:
|
| 105 |
+
logging.error(f"Error deleting temporary input file {input_path}: {e}")
|
| 106 |
+
|
| 107 |
+
@app.route('/preview', methods=['POST'])
|
| 108 |
+
def preview_mermaid():
|
| 109 |
+
"""
|
| 110 |
+
Handles asynchronous preview requests.
|
| 111 |
+
Renders PNG/SVG and returns image data as JSON.
|
| 112 |
+
"""
|
| 113 |
+
if renderer is None:
|
| 114 |
+
return jsonify({"error": "Mermaid rendering service is unavailable."}), 503
|
| 115 |
+
|
| 116 |
+
data = request.get_json()
|
| 117 |
+
if not data:
|
| 118 |
+
return jsonify({"error": "Invalid request data."}), 400
|
| 119 |
+
|
| 120 |
+
mermaid_code = data.get('mermaid_code', '')
|
| 121 |
+
# Preview only supports PNG and SVG for embedding
|
| 122 |
+
output_format = data.get('output_format', 'svg') # Default to SVG for preview
|
| 123 |
+
if output_format not in ['png', 'svg']:
|
| 124 |
+
output_format = 'svg' # Force SVG if invalid format requested for preview
|
| 125 |
+
theme = data.get('theme', 'default')
|
| 126 |
+
|
| 127 |
+
if not mermaid_code.strip():
|
| 128 |
+
return jsonify({"error": "Mermaid code cannot be empty."}), 400
|
| 129 |
+
|
| 130 |
+
output_path = None
|
| 131 |
+
input_path = None
|
| 132 |
+
preview_data = None
|
| 133 |
+
try:
|
| 134 |
+
logging.info(f"Preview request: format={output_format}, theme={theme}")
|
| 135 |
+
if not renderer:
|
| 136 |
+
raise RuntimeError("Renderer not initialized.") # Should be caught above, but defensive
|
| 137 |
+
|
| 138 |
+
output_path, input_path = renderer.render(mermaid_code, output_format, theme)
|
| 139 |
+
|
| 140 |
+
with open(output_path, 'rb') as f:
|
| 141 |
+
file_content = f.read()
|
| 142 |
+
|
| 143 |
+
if output_format == 'png':
|
| 144 |
+
preview_data = base64.b64encode(file_content).decode('utf-8')
|
| 145 |
+
elif output_format == 'svg':
|
| 146 |
+
try:
|
| 147 |
+
preview_data = file_content.decode('utf-8')
|
| 148 |
+
except UnicodeDecodeError:
|
| 149 |
+
logging.error("SVG content is not valid UTF-8 for preview.")
|
| 150 |
+
raise RuntimeError("Generated SVG is not valid UTF-8.")
|
| 151 |
+
|
| 152 |
+
return jsonify({
|
| 153 |
+
"format": output_format,
|
| 154 |
+
"data": preview_data
|
| 155 |
+
})
|
| 156 |
+
|
| 157 |
+
except (ValueError, RuntimeError) as e:
|
| 158 |
+
logging.error(f"Preview rendering failed: {e}")
|
| 159 |
+
return jsonify({"error": f"Error rendering preview: {e}"}), 500
|
| 160 |
+
except Exception as e:
|
| 161 |
+
logging.exception("An unexpected error occurred during preview generation.")
|
| 162 |
+
return jsonify({"error": "An unexpected server error occurred during preview."}), 500
|
| 163 |
+
finally:
|
| 164 |
+
# Ensure cleanup after preview generation
|
| 165 |
+
if output_path and os.path.exists(output_path):
|
| 166 |
+
try:
|
| 167 |
+
os.unlink(output_path)
|
| 168 |
+
logging.info(f"Cleaned up temporary output file after download: {output_path}")
|
| 169 |
+
except OSError as e:
|
| 170 |
+
logging.error(f"Error deleting temporary output file {output_path} after download: {e}")
|
| 171 |
+
if input_path and os.path.exists(input_path):
|
| 172 |
+
try:
|
| 173 |
+
os.unlink(input_path)
|
| 174 |
+
logging.info(f"Cleaned up temporary input file after download: {input_path}")
|
| 175 |
+
except OSError as e:
|
| 176 |
+
logging.error(f"Error deleting temporary input file {input_path} after download: {e}")
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
if __name__ == '__main__':
|
| 180 |
+
# For local development only (use Gunicorn/Waitress in production)
|
| 181 |
+
# Use 0.0.0.0 to be accessible on the network
|
| 182 |
+
# Set debug=False for production-like testing, or True for development features
|
| 183 |
+
is_debug = os.environ.get('FLASK_DEBUG', 'false').lower() == 'true'
|
| 184 |
+
# Changed default port from 5000 to 5001 to avoid conflicts
|
| 185 |
+
app.run(debug=is_debug, host='0.0.0.0', port=int(os.environ.get('PORT', 5001)))
|
flowchart.pdf
ADDED
|
Binary file (31.6 kB). View file
|
|
|
flowchart.png
ADDED
|
flowchart.svg
ADDED
|
|
mermaid-rendering.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# Instructions to install dependencies and run the program:
|
| 3 |
+
#
|
| 4 |
+
# 1. Install Node.js:
|
| 5 |
+
# - Download and install Node.js from https://nodejs.org/
|
| 6 |
+
# - Verify installation by running `node --version` in your terminal.
|
| 7 |
+
#
|
| 8 |
+
# 2. Install the Mermaid CLI (@mermaid-js/mermaid-cli):
|
| 9 |
+
# - Run `npm install -g @mermaid-js/mermaid-cli` in your terminal.
|
| 10 |
+
# - If you encounter permissions issues, you might need to use `sudo` (for macOS/Linux)
|
| 11 |
+
# or run the command prompt as an administrator (for Windows).
|
| 12 |
+
# - Verify installation by running `mmdc --version`.
|
| 13 |
+
#
|
| 14 |
+
# 3. Run the script:
|
| 15 |
+
# - You can run the script directly with the embedded example by uncommenting the lines in the `if __name__ == "__main__":` block and running:
|
| 16 |
+
# `python3 mermaid-rendering.py`
|
| 17 |
+
# - Alternatively, you can use the command-line interface:
|
| 18 |
+
# - To render Mermaid code from a string:
|
| 19 |
+
# `python3 mermaid-rendering.py -c "your_mermaid_code"`
|
| 20 |
+
# - To render Mermaid code from a file:
|
| 21 |
+
# `python3 mermaid-rendering.py -f /path/to/your/file.mmd`
|
| 22 |
+
# - You can specify the output file with `-o /path/to/output.png`, output type with `-t [png, pdf, svg]`, and theme with `--theme [default, forest, dark, neutral]`.
|
| 23 |
+
# - For example:
|
| 24 |
+
# `python3 mermaid-rendering.py -f input.mmd -o output.png -t png --theme default`
|
| 25 |
+
|
| 26 |
+
import os
|
| 27 |
+
import sys
|
| 28 |
+
import subprocess
|
| 29 |
+
import argparse
|
| 30 |
+
import tempfile
|
| 31 |
+
import json
|
| 32 |
+
from pathlib import Path
|
| 33 |
+
import textwrap
|
| 34 |
+
|
| 35 |
+
class MermaidRenderer:
|
| 36 |
+
"""
|
| 37 |
+
A Python class to render Mermaid diagrams to various formats
|
| 38 |
+
using puppeteer-mermaid behind the scenes
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
def __init__(self):
|
| 42 |
+
"""Initialize the renderer and check if dependencies are installed"""
|
| 43 |
+
self._check_dependencies()
|
| 44 |
+
|
| 45 |
+
def _check_dependencies(self):
|
| 46 |
+
"""Check if Node.js and puppeteer-mermaid are installed"""
|
| 47 |
+
try:
|
| 48 |
+
# Check for Node.js
|
| 49 |
+
subprocess.run(["node", "--version"], capture_output=True, check=True)
|
| 50 |
+
except (subprocess.SubprocessError, FileNotFoundError):
|
| 51 |
+
sys.exit("Error: Node.js is not installed. Please install Node.js from https://nodejs.org/")
|
| 52 |
+
|
| 53 |
+
# Check if @mermaid-js/mermaid-cli is installed
|
| 54 |
+
result = subprocess.run(["npm", "list", "-g", "@mermaid-js/mermaid-cli"],
|
| 55 |
+
capture_output=True, text=True)
|
| 56 |
+
|
| 57 |
+
if "mermaid-cli" not in result.stdout:
|
| 58 |
+
print("Installing @mermaid-js/mermaid-cli globally...")
|
| 59 |
+
try:
|
| 60 |
+
subprocess.run(["npm", "install", "-g", "@mermaid-js/mermaid-cli"], check=True)
|
| 61 |
+
print("@mermaid-js/mermaid-cli installed successfully.")
|
| 62 |
+
except subprocess.SubprocessError:
|
| 63 |
+
sys.exit("Error: Failed to install @mermaid-js/mermaid-cli. Please install manually using: npm install -g @mermaid-js/mermaid-cli")
|
| 64 |
+
|
| 65 |
+
def render(self, mermaid_code, output_file=None, output_format="png", theme="default"):
|
| 66 |
+
"""
|
| 67 |
+
Render Mermaid code to the specified format
|
| 68 |
+
|
| 69 |
+
Args:
|
| 70 |
+
mermaid_code (str): The Mermaid diagram code
|
| 71 |
+
output_file (str, optional): Output file path. If None, generates a filename based on format.
|
| 72 |
+
output_format (str, optional): Output format. Options: png, pdf, svg. Default: png
|
| 73 |
+
theme (str, optional): Mermaid theme. Default: default
|
| 74 |
+
|
| 75 |
+
Returns:
|
| 76 |
+
str: Path to the generated file
|
| 77 |
+
"""
|
| 78 |
+
# Validate output format
|
| 79 |
+
valid_formats = ["png", "pdf", "svg"]
|
| 80 |
+
if output_format not in valid_formats:
|
| 81 |
+
sys.exit(f"Error: Invalid output format. Choose from: {', '.join(valid_formats)}")
|
| 82 |
+
|
| 83 |
+
# Create a temporary file for the Mermaid code
|
| 84 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.mmd', delete=False) as temp_file:
|
| 85 |
+
temp_file.write(mermaid_code)
|
| 86 |
+
input_path = temp_file.name
|
| 87 |
+
|
| 88 |
+
# Generate output file name if not provided
|
| 89 |
+
if not output_file:
|
| 90 |
+
output_file = f"diagram.{output_format}"
|
| 91 |
+
|
| 92 |
+
# Ensure output directory exists
|
| 93 |
+
output_dir = os.path.dirname(output_file)
|
| 94 |
+
if output_dir:
|
| 95 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 96 |
+
|
| 97 |
+
# Run puppeteer-mermaid
|
| 98 |
+
try:
|
| 99 |
+
cmd = [
|
| 100 |
+
"mmdc",
|
| 101 |
+
"-i", input_path,
|
| 102 |
+
"-o", output_file,
|
| 103 |
+
"-t", theme,
|
| 104 |
+
"-f", output_format
|
| 105 |
+
]
|
| 106 |
+
|
| 107 |
+
subprocess.run(cmd, check=True)
|
| 108 |
+
print(f"Diagram saved to: {output_file}")
|
| 109 |
+
|
| 110 |
+
# Clean up the temporary file
|
| 111 |
+
os.unlink(input_path)
|
| 112 |
+
|
| 113 |
+
return output_file
|
| 114 |
+
|
| 115 |
+
except subprocess.SubprocessError as e:
|
| 116 |
+
os.unlink(input_path)
|
| 117 |
+
sys.exit(f"Error rendering diagram: {str(e)}")
|
| 118 |
+
|
| 119 |
+
def main():
|
| 120 |
+
"""Command line interface for the Mermaid renderer"""
|
| 121 |
+
parser = argparse.ArgumentParser(description="Render Mermaid diagrams to PNG, PDF, or SVG.")
|
| 122 |
+
|
| 123 |
+
input_group = parser.add_mutually_exclusive_group(required=True)
|
| 124 |
+
input_group.add_argument("-c", "--code", help="Mermaid code as a string")
|
| 125 |
+
input_group.add_argument("-f", "--file", help="Path to a file containing Mermaid code")
|
| 126 |
+
|
| 127 |
+
parser.add_argument("-o", "--output", help="Output file path")
|
| 128 |
+
parser.add_argument("-t", "--type", default="png", choices=["png", "pdf", "svg"],
|
| 129 |
+
help="Output file type (default: png)")
|
| 130 |
+
parser.add_argument("--theme", default="default",
|
| 131 |
+
choices=["default", "forest", "dark", "neutral"],
|
| 132 |
+
help="Mermaid theme (default: default)")
|
| 133 |
+
|
| 134 |
+
args = parser.parse_args()
|
| 135 |
+
|
| 136 |
+
# Get Mermaid code from string or file
|
| 137 |
+
if args.code:
|
| 138 |
+
mermaid_code = args.code
|
| 139 |
+
else:
|
| 140 |
+
try:
|
| 141 |
+
with open(args.file, 'r') as f:
|
| 142 |
+
mermaid_code = f.read()
|
| 143 |
+
except (IOError, FileNotFoundError):
|
| 144 |
+
sys.exit(f"Error: Could not read file {args.file}")
|
| 145 |
+
|
| 146 |
+
# Create renderer and render the diagram
|
| 147 |
+
renderer = MermaidRenderer()
|
| 148 |
+
renderer.render(mermaid_code, args.output, args.type, args.theme)
|
| 149 |
+
|
| 150 |
+
# Example usage with multiline string for easy copy-paste
|
| 151 |
+
if __name__ == "__main__":
|
| 152 |
+
# You can replace this multiline string with your own Mermaid diagram code
|
| 153 |
+
MERMAID_CODE = """
|
| 154 |
+
pie title NETFLIX
|
| 155 |
+
"Time spent looking for movie" : 90
|
| 156 |
+
"Time spent watching it" : 10
|
| 157 |
+
"""
|
| 158 |
+
|
| 159 |
+
# Uncomment and modify these lines to run directly
|
| 160 |
+
renderer = MermaidRenderer()
|
| 161 |
+
renderer.render(MERMAID_CODE, "flowchart.svg", "svg", "default")
|
| 162 |
+
renderer.render(MERMAID_CODE, "flowchart.pdf", "pdf", "default")
|
| 163 |
+
|
| 164 |
+
# Or use the command-line interface
|
| 165 |
+
#main()
|
mermaid_renderer.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# mermaid_renderer.py
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
import subprocess
|
| 5 |
+
import tempfile
|
| 6 |
+
import logging
|
| 7 |
+
|
| 8 |
+
# Configure logging
|
| 9 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 10 |
+
|
| 11 |
+
class MermaidRenderer:
|
| 12 |
+
"""
|
| 13 |
+
A Python class to render Mermaid diagrams using @mermaid-js/mermaid-cli.
|
| 14 |
+
"""
|
| 15 |
+
def __init__(self):
|
| 16 |
+
"""Initialize the renderer and check if dependencies are installed"""
|
| 17 |
+
self._check_dependencies()
|
| 18 |
+
|
| 19 |
+
def _check_dependencies(self):
|
| 20 |
+
"""Check if Node.js and @mermaid-js/mermaid-cli are installed"""
|
| 21 |
+
try:
|
| 22 |
+
subprocess.run(["node", "--version"], capture_output=True, check=True, text=True)
|
| 23 |
+
logging.info("Node.js found.")
|
| 24 |
+
except (subprocess.SubprocessError, FileNotFoundError) as e:
|
| 25 |
+
logging.error(f"Node.js check failed: {e}")
|
| 26 |
+
# In a web context, exiting might not be ideal. Log error.
|
| 27 |
+
# Consider raising an exception or handling this state in the Flask app.
|
| 28 |
+
raise RuntimeError("Error: Node.js is not installed or not found in PATH.")
|
| 29 |
+
|
| 30 |
+
# Check if @mermaid-js/mermaid-cli is installed globally
|
| 31 |
+
# Note: Checking global npm packages can be slow and sometimes unreliable.
|
| 32 |
+
# A better approach in production might be to ensure it's installed during deployment.
|
| 33 |
+
try:
|
| 34 |
+
result = subprocess.run(["mmdc", "--version"], capture_output=True, check=True, text=True)
|
| 35 |
+
logging.info(f"@mermaid-js/mermaid-cli found: {result.stdout.strip()}")
|
| 36 |
+
except (subprocess.SubprocessError, FileNotFoundError):
|
| 37 |
+
logging.warning("@mermaid-js/mermaid-cli (mmdc) not found or failed to execute. Attempting installation...")
|
| 38 |
+
# Attempt installation if not found (consider security implications)
|
| 39 |
+
try:
|
| 40 |
+
# Using '--unsafe-perm' might be needed in some environments, but use with caution.
|
| 41 |
+
install_cmd = ["npm", "install", "-g", "@mermaid-js/mermaid-cli"]
|
| 42 |
+
subprocess.run(install_cmd, check=True, capture_output=True, text=True)
|
| 43 |
+
logging.info("@mermaid-js/mermaid-cli installed successfully via npm.")
|
| 44 |
+
except subprocess.SubprocessError as install_error:
|
| 45 |
+
logging.error(f"Failed to install @mermaid-js/mermaid-cli: {install_error.stderr}")
|
| 46 |
+
raise RuntimeError("Error: @mermaid-js/mermaid-cli (mmdc) is not installed and automatic installation failed. Please install manually: npm install -g @mermaid-js/mermaid-cli")
|
| 47 |
+
|
| 48 |
+
def render(self, mermaid_code, output_format="png", theme="default"):
|
| 49 |
+
"""
|
| 50 |
+
Render Mermaid code to the specified format into a temporary file.
|
| 51 |
+
|
| 52 |
+
Args:
|
| 53 |
+
mermaid_code (str): The Mermaid diagram code.
|
| 54 |
+
output_format (str, optional): Output format (png, pdf, svg). Default: png.
|
| 55 |
+
theme (str, optional): Mermaid theme. Default: default.
|
| 56 |
+
|
| 57 |
+
Returns:
|
| 58 |
+
tuple: (path_to_temp_output_file, temp_input_file_path) or raises Exception on error.
|
| 59 |
+
The caller is responsible for deleting these files.
|
| 60 |
+
"""
|
| 61 |
+
valid_formats = ["png", "pdf", "svg"]
|
| 62 |
+
if output_format not in valid_formats:
|
| 63 |
+
raise ValueError(f"Invalid output format '{output_format}'. Choose from: {', '.join(valid_formats)}")
|
| 64 |
+
|
| 65 |
+
valid_themes = ["default", "forest", "dark", "neutral"]
|
| 66 |
+
if theme not in valid_themes:
|
| 67 |
+
raise ValueError(f"Invalid theme '{theme}'. Choose from: {', '.join(valid_themes)}")
|
| 68 |
+
|
| 69 |
+
# Create temporary files (ensure they are deleted by the caller)
|
| 70 |
+
# Input file for mermaid code
|
| 71 |
+
temp_input_file = tempfile.NamedTemporaryFile(mode='w', suffix='.mmd', delete=False)
|
| 72 |
+
# Output file for the generated diagram
|
| 73 |
+
temp_output_file = tempfile.NamedTemporaryFile(suffix=f'.{output_format}', delete=False)
|
| 74 |
+
|
| 75 |
+
input_path = temp_input_file.name
|
| 76 |
+
output_path = temp_output_file.name
|
| 77 |
+
|
| 78 |
+
try:
|
| 79 |
+
temp_input_file.write(mermaid_code)
|
| 80 |
+
temp_input_file.close() # Close file before passing to subprocess
|
| 81 |
+
|
| 82 |
+
cmd = [
|
| 83 |
+
"mmdc",
|
| 84 |
+
"-i", input_path,
|
| 85 |
+
"-o", output_path,
|
| 86 |
+
"-t", theme,
|
| 87 |
+
# No -f flag needed for mmdc, format is determined by -o extension
|
| 88 |
+
# However, explicitly setting background color might be needed for transparency
|
| 89 |
+
# "-b", "transparent" # Example: if you want transparent background for PNG/SVG
|
| 90 |
+
]
|
| 91 |
+
logging.info(f"Running mmdc command: {' '.join(cmd)}")
|
| 92 |
+
|
| 93 |
+
result = subprocess.run(cmd, check=True, capture_output=True, text=True)
|
| 94 |
+
logging.info(f"mmdc execution successful. Output saved to: {output_path}")
|
| 95 |
+
if result.stderr:
|
| 96 |
+
logging.warning(f"mmdc stderr: {result.stderr}")
|
| 97 |
+
|
| 98 |
+
# Return paths for Flask to handle; caller must delete files
|
| 99 |
+
return output_path, input_path
|
| 100 |
+
|
| 101 |
+
except subprocess.CalledProcessError as e:
|
| 102 |
+
logging.error(f"Error rendering diagram with mmdc: {e}")
|
| 103 |
+
logging.error(f"mmdc stderr: {e.stderr}")
|
| 104 |
+
# Clean up files on error before raising
|
| 105 |
+
temp_output_file.close()
|
| 106 |
+
os.unlink(output_path)
|
| 107 |
+
if os.path.exists(input_path): # Input file might already be closed/deleted
|
| 108 |
+
os.unlink(input_path)
|
| 109 |
+
raise RuntimeError(f"Error rendering diagram: {e.stderr or e}")
|
| 110 |
+
except Exception as e:
|
| 111 |
+
# Catch any other unexpected errors
|
| 112 |
+
logging.error(f"Unexpected error during rendering: {e}")
|
| 113 |
+
# Ensure cleanup
|
| 114 |
+
temp_input_file.close()
|
| 115 |
+
temp_output_file.close()
|
| 116 |
+
if os.path.exists(input_path): os.unlink(input_path)
|
| 117 |
+
if os.path.exists(output_path): os.unlink(output_path)
|
| 118 |
+
raise # Re-raise the caught exception
|
requirements.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
Flask>=2.0
|
templates/index.html
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Mermaid Live Renderer</title>
|
| 7 |
+
<style>
|
| 8 |
+
body { font-family: sans-serif; margin: 2em; background-color: #f4f4f4; color: #333; }
|
| 9 |
+
.container { max-width: 1200px; margin: auto; background: #fff; padding: 2em; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); display: flex; gap: 2em;}
|
| 10 |
+
.input-area { flex: 1; }
|
| 11 |
+
.preview-area { flex: 1; border-left: 1px solid #eee; padding-left: 2em; }
|
| 12 |
+
h1 { color: #555; text-align: center; margin-bottom: 1em; width: 100%;}
|
| 13 |
+
label { display: block; margin-top: 1em; font-weight: bold; }
|
| 14 |
+
textarea { width: 95%; min-height: 300px; margin-top: 0.5em; padding: 10px; border: 1px solid #ccc; border-radius: 4px; font-family: monospace; font-size: 1rem; }
|
| 15 |
+
select, button { padding: 0.8em 1.2em; margin-top: 0.5em; border: 1px solid #ccc; border-radius: 4px; font-size: 1rem; cursor: pointer; margin-right: 0.5em; }
|
| 16 |
+
button.primary { background-color: #5cb85c; color: white; border-color: #4cae4c; font-weight: bold; }
|
| 17 |
+
button.primary:hover { background-color: #4cae4c; }
|
| 18 |
+
button.secondary { background-color: #5bc0de; color: white; border-color: #46b8da; }
|
| 19 |
+
button.secondary:hover { background-color: #31b0d5; }
|
| 20 |
+
.options { display: flex; gap: 1em; align-items: center; margin-top: 1em; flex-wrap: wrap;}
|
| 21 |
+
.flash { padding: 1em; margin-bottom: 1em; border-radius: 5px; border: 1px solid transparent; }
|
| 22 |
+
.flash.error { background-color: #f8d7da; color: #721c24; border-color: #f5c6cb; }
|
| 23 |
+
.flash.warning { background-color: #fff3cd; color: #856404; border-color: #ffeeba; }
|
| 24 |
+
.flash.info { background-color: #d1ecf1; color: #0c5460; border-color: #bee5eb; }
|
| 25 |
+
#preview-box { margin-top: 1em; padding: 1em; border: 1px solid #ccc; border-radius: 4px; min-height: 300px; text-align: center; background-color: #fdfdfd; overflow: auto; display: flex; justify-content: center; align-items: center;}
|
| 26 |
+
#preview-box img { max-width: 100%; height: auto; }
|
| 27 |
+
#preview-box svg { max-width: 100%; height: auto; }
|
| 28 |
+
#preview-status { margin-top: 0.5em; font-style: italic; color: #777; min-height: 1.2em;}
|
| 29 |
+
</style>
|
| 30 |
+
</head>
|
| 31 |
+
</head>
|
| 32 |
+
<body>
|
| 33 |
+
<div class="container">
|
| 34 |
+
<div class="input-area">
|
| 35 |
+
<h1>Mermaid Input</h1>
|
| 36 |
+
|
| 37 |
+
<!-- Flash messages -->
|
| 38 |
+
{% with messages = get_flashed_messages(with_categories=true) %}
|
| 39 |
+
{% if messages %}
|
| 40 |
+
{% for category, message in messages %}
|
| 41 |
+
<div class="flash {{ category }}">{{ message }}</div>
|
| 42 |
+
{% endfor %}
|
| 43 |
+
{% endif %}
|
| 44 |
+
{% endwith %}
|
| 45 |
+
|
| 46 |
+
<!-- Main form for final download -->
|
| 47 |
+
<form id="download-form" action="{{ url_for('render_mermaid') }}" method="post">
|
| 48 |
+
<label for="mermaid_code">Mermaid Code: (Edit the example below or paste your own)</label>
|
| 49 |
+
<textarea id="mermaid_code" name="mermaid_code" required>{{ default_code }}</textarea>
|
| 50 |
+
|
| 51 |
+
<div class="options">
|
| 52 |
+
<div>
|
| 53 |
+
<label for="output_format">Format:</label>
|
| 54 |
+
<select id="output_format" name="output_format">
|
| 55 |
+
<option value="png">PNG</option>
|
| 56 |
+
<option value="svg" selected>SVG</option> <!-- Default to SVG for better preview -->
|
| 57 |
+
<option value="pdf">PDF</option>
|
| 58 |
+
</select>
|
| 59 |
+
</div>
|
| 60 |
+
|
| 61 |
+
<div>
|
| 62 |
+
<label for="theme">Theme:</label>
|
| 63 |
+
<select id="theme" name="theme">
|
| 64 |
+
<option value="default" selected>Default</option>
|
| 65 |
+
<option value="forest">Forest</option>
|
| 66 |
+
<option value="dark">Dark</option>
|
| 67 |
+
<option value="neutral">Neutral</option>
|
| 68 |
+
</select>
|
| 69 |
+
</div>
|
| 70 |
+
</div>
|
| 71 |
+
|
| 72 |
+
<br><br>
|
| 73 |
+
<!-- Download Button (submits the form) -->
|
| 74 |
+
<button type="submit" class="primary">Download Diagram</button>
|
| 75 |
+
<!-- Preview button removed, preview updates automatically -->
|
| 76 |
+
</form>
|
| 77 |
+
</div>
|
| 78 |
+
|
| 79 |
+
<div class="preview-area">
|
| 80 |
+
<h1>Live Preview</h1>
|
| 81 |
+
<div id="preview-status">Enter code and click Preview.</div>
|
| 82 |
+
<div id="preview-box">
|
| 83 |
+
<!-- Preview will be loaded here by JavaScript -->
|
| 84 |
+
<p style="color: #aaa;">Preview Area</p>
|
| 85 |
+
</div>
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
|
| 89 |
+
<script>
|
| 90 |
+
// Debounce function
|
| 91 |
+
function debounce(func, wait) {
|
| 92 |
+
let timeout;
|
| 93 |
+
return function executedFunction(...args) {
|
| 94 |
+
const later = () => {
|
| 95 |
+
clearTimeout(timeout);
|
| 96 |
+
func(...args);
|
| 97 |
+
};
|
| 98 |
+
clearTimeout(timeout);
|
| 99 |
+
timeout = setTimeout(later, wait);
|
| 100 |
+
};
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
const previewBox = document.getElementById('preview-box');
|
| 104 |
+
const previewStatus = document.getElementById('preview-status');
|
| 105 |
+
const mermaidCodeInput = document.getElementById('mermaid_code');
|
| 106 |
+
const outputFormatSelect = document.getElementById('output_format'); // Still needed for download format
|
| 107 |
+
const themeSelect = document.getElementById('theme');
|
| 108 |
+
|
| 109 |
+
// Function to fetch and update preview
|
| 110 |
+
const updatePreview = async () => {
|
| 111 |
+
const mermaidCode = mermaidCodeInput.value;
|
| 112 |
+
// Preview will always use SVG for best results and simplicity, download format is separate
|
| 113 |
+
const previewFormat = 'svg';
|
| 114 |
+
const theme = themeSelect.value;
|
| 115 |
+
|
| 116 |
+
if (!mermaidCode.trim()) {
|
| 117 |
+
// Don't show error for empty input, just clear preview
|
| 118 |
+
previewStatus.textContent = 'Enter code to see preview.';
|
| 119 |
+
previewStatus.style.color = '#777';
|
| 120 |
+
previewBox.innerHTML = '<p style="color: #aaa;">Preview Area</p>';
|
| 121 |
+
return;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
previewStatus.textContent = 'Generating preview...';
|
| 125 |
+
previewStatus.style.color = '#777';
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
previewBox.innerHTML = '<p style="color: #aaa;">Loading...</p>'; // Show loading indicator
|
| 129 |
+
|
| 130 |
+
try {
|
| 131 |
+
const response = await fetch("{{ url_for('preview_mermaid') }}", {
|
| 132 |
+
method: 'POST',
|
| 133 |
+
headers: {
|
| 134 |
+
'Content-Type': 'application/json',
|
| 135 |
+
},
|
| 136 |
+
body: JSON.stringify({
|
| 137 |
+
mermaid_code: mermaidCode,
|
| 138 |
+
output_format: previewFormat, // Always request SVG for preview
|
| 139 |
+
theme: theme,
|
| 140 |
+
}),
|
| 141 |
+
});
|
| 142 |
+
|
| 143 |
+
if (!response.ok) {
|
| 144 |
+
const errorData = await response.json();
|
| 145 |
+
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
const result = await response.json();
|
| 149 |
+
|
| 150 |
+
previewBox.innerHTML = ''; // Clear previous preview/loading message
|
| 151 |
+
if (result.format === 'png') {
|
| 152 |
+
const img = document.createElement('img');
|
| 153 |
+
img.src = `data:image/png;base64,${result.data}`;
|
| 154 |
+
img.alt = 'Mermaid Diagram Preview';
|
| 155 |
+
previewBox.appendChild(img);
|
| 156 |
+
} else if (result.format === 'svg') {
|
| 157 |
+
// Directly insert SVG markup
|
| 158 |
+
previewBox.innerHTML = result.data;
|
| 159 |
+
// Optional: Re-run mermaid script if needed for interactivity (if using mermaid.js library client-side)
|
| 160 |
+
// if (typeof mermaid !== 'undefined') { mermaid.run({nodes: [previewBox]}); }
|
| 161 |
+
}
|
| 162 |
+
previewStatus.textContent = `Preview updated (SVG).`; // Preview is always SVG now
|
| 163 |
+
previewStatus.style.color = 'green';
|
| 164 |
+
|
| 165 |
+
} catch (error) {
|
| 166 |
+
console.error('Error fetching preview:', error);
|
| 167 |
+
previewStatus.textContent = `Error: ${error.message}`;
|
| 168 |
+
previewStatus.style.color = 'red';
|
| 169 |
+
previewBox.innerHTML = '<p style="color: red;">Failed to load preview.</p>';
|
| 170 |
+
}
|
| 171 |
+
};
|
| 172 |
+
|
| 173 |
+
// Debounced version of the updatePreview function
|
| 174 |
+
const debouncedUpdatePreview = debounce(updatePreview, 750); // 750ms delay
|
| 175 |
+
|
| 176 |
+
// Event listener for textarea input
|
| 177 |
+
mermaidCodeInput.addEventListener('input', debouncedUpdatePreview);
|
| 178 |
+
|
| 179 |
+
// Also update preview if theme changes
|
| 180 |
+
themeSelect.addEventListener('change', debouncedUpdatePreview);
|
| 181 |
+
|
| 182 |
+
// Initial preview load on page ready
|
| 183 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 184 |
+
if (mermaidCodeInput.value.trim()) {
|
| 185 |
+
previewStatus.textContent = 'Rendering initial example...';
|
| 186 |
+
updatePreview(); // Render the default code on load
|
| 187 |
+
} else {
|
| 188 |
+
previewStatus.textContent = 'Enter code to see preview.';
|
| 189 |
+
}
|
| 190 |
+
});
|
| 191 |
+
</script>
|
| 192 |
+
</body>
|
| 193 |
+
</html>
|