liaoch commited on
Commit
e40b114
·
1 Parent(s): 309d357

initial working version

Browse files
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>