Spaces:
Running
Running
add a sample screenshot
Browse files- README.md +4 -0
- flowchart.png +0 -0
- templates/index.html +150 -16
README.md
CHANGED
@@ -14,10 +14,14 @@ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-
|
|
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.
|
|
|
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 |
+

|
18 |
+
*Screenshot of the Mermaid Live Renderer interface.*
|
19 |
+
|
20 |
## Features
|
21 |
|
22 |
* Web-based interface for entering Mermaid code.
|
23 |
* Live preview of the diagram (SVG) that updates automatically as you type or change the theme.
|
24 |
+
* Zoom and pan controls (buttons and drag-to-pan) for the live preview area.
|
25 |
* Selectable themes (Default, Forest, Dark, Neutral).
|
26 |
* Download the rendered diagram as PNG, SVG, or PDF.
|
27 |
* Uses `@mermaid-js/mermaid-cli` (mmdc) behind the scenes.
|
flowchart.png
DELETED
Binary file (35.2 kB)
|
|
templates/index.html
CHANGED
@@ -23,9 +23,20 @@
|
|
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
|
27 |
-
#preview-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
#preview-status { margin-top: 0.5em; font-style: italic; color: #777; min-height: 1.2em;}
|
|
|
|
|
29 |
</style>
|
30 |
</head>
|
31 |
</head>
|
@@ -33,6 +44,7 @@
|
|
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) %}
|
@@ -45,8 +57,8 @@
|
|
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
|
49 |
-
<textarea id="mermaid_code" name="mermaid_code" required>{{ default_code }}</textarea>
|
50 |
|
51 |
<div class="options">
|
52 |
<div>
|
@@ -78,10 +90,22 @@
|
|
78 |
|
79 |
<div class="preview-area">
|
80 |
<h1>Live Preview</h1>
|
81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
82 |
<div id="preview-box">
|
83 |
-
|
84 |
-
|
|
|
|
|
85 |
</div>
|
86 |
</div>
|
87 |
</div>
|
@@ -101,6 +125,7 @@
|
|
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
|
@@ -117,7 +142,8 @@
|
|
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 |
-
|
|
|
121 |
return;
|
122 |
}
|
123 |
|
@@ -125,7 +151,7 @@
|
|
125 |
previewStatus.style.color = '#777';
|
126 |
|
127 |
|
128 |
-
|
129 |
|
130 |
try {
|
131 |
const response = await fetch("{{ url_for('preview_mermaid') }}", {
|
@@ -146,18 +172,27 @@
|
|
146 |
}
|
147 |
|
148 |
const result = await response.json();
|
|
|
149 |
|
150 |
-
|
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 |
-
|
|
|
|
|
156 |
} else if (result.format === 'svg') {
|
157 |
-
// Directly insert SVG markup
|
158 |
-
|
159 |
-
|
160 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
161 |
}
|
162 |
previewStatus.textContent = `Preview updated (SVG).`; // Preview is always SVG now
|
163 |
previewStatus.style.color = 'green';
|
@@ -166,10 +201,109 @@
|
|
166 |
console.error('Error fetching preview:', error);
|
167 |
previewStatus.textContent = `Error: ${error.message}`;
|
168 |
previewStatus.style.color = 'red';
|
169 |
-
|
170 |
}
|
171 |
};
|
172 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
173 |
// Debounced version of the updatePreview function
|
174 |
const debouncedUpdatePreview = debounce(updatePreview, 750); // 750ms delay
|
175 |
|
|
|
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, #preview-box svg { display: block; /* Prevent extra space below */ }
|
27 |
+
#preview-content-wrapper { /* Inner wrapper for transformations */
|
28 |
+
width: 100%;
|
29 |
+
height: 100%;
|
30 |
+
transition: transform 0.2s ease-out; /* Smooth transitions */
|
31 |
+
transform-origin: center center; /* Zoom from center */
|
32 |
+
cursor: grab; /* Indicate pannable */
|
33 |
+
}
|
34 |
+
#preview-content-wrapper:active {
|
35 |
+
cursor: grabbing;
|
36 |
+
}
|
37 |
#preview-status { margin-top: 0.5em; font-style: italic; color: #777; min-height: 1.2em;}
|
38 |
+
.preview-controls { text-align: center; margin-bottom: 0.5em; }
|
39 |
+
.preview-controls button { padding: 0.3em 0.6em; font-size: 0.9rem; min-width: 30px; }
|
40 |
</style>
|
41 |
</head>
|
42 |
</head>
|
|
|
44 |
<div class="container">
|
45 |
<div class="input-area">
|
46 |
<h1>Mermaid Input</h1>
|
47 |
+
<p style="font-size: 0.9em; color: #666; margin-top: -0.5em; margin-bottom: 1.5em;">Enter Mermaid code below. The preview on the right updates automatically.</p>
|
48 |
|
49 |
<!-- Flash messages -->
|
50 |
{% with messages = get_flashed_messages(with_categories=true) %}
|
|
|
57 |
|
58 |
<!-- Main form for final download -->
|
59 |
<form id="download-form" action="{{ url_for('render_mermaid') }}" method="post">
|
60 |
+
<label for="mermaid_code">Mermaid Code:</label>
|
61 |
+
<textarea id="mermaid_code" name="mermaid_code" required placeholder="e.g., graph TD; A-->B;">{{ default_code }}</textarea>
|
62 |
|
63 |
<div class="options">
|
64 |
<div>
|
|
|
90 |
|
91 |
<div class="preview-area">
|
92 |
<h1>Live Preview</h1>
|
93 |
+
<p style="font-size: 0.9em; color: #666; margin-top: -0.5em; margin-bottom: 0.5em;">Use controls to zoom/pan, or click & drag the preview.</p>
|
94 |
+
<div id="preview-status">Enter code to see preview.</div>
|
95 |
+
<div class="preview-controls">
|
96 |
+
<button id="zoom-in">+</button>
|
97 |
+
<button id="zoom-out">-</button>
|
98 |
+
<button id="pan-up">↑</button>
|
99 |
+
<button id="pan-down">↓</button>
|
100 |
+
<button id="pan-left">←</button>
|
101 |
+
<button id="pan-right">→</button>
|
102 |
+
<button id="reset-view">Reset</button>
|
103 |
+
</div>
|
104 |
<div id="preview-box">
|
105 |
+
<div id="preview-content-wrapper">
|
106 |
+
<!-- Preview will be loaded here by JavaScript -->
|
107 |
+
<p style="color: #aaa;">Preview Area</p>
|
108 |
+
</div>
|
109 |
</div>
|
110 |
</div>
|
111 |
</div>
|
|
|
125 |
}
|
126 |
|
127 |
const previewBox = document.getElementById('preview-box');
|
128 |
+
const previewContentWrapper = document.getElementById('preview-content-wrapper');
|
129 |
const previewStatus = document.getElementById('preview-status');
|
130 |
const mermaidCodeInput = document.getElementById('mermaid_code');
|
131 |
const outputFormatSelect = document.getElementById('output_format'); // Still needed for download format
|
|
|
142 |
// Don't show error for empty input, just clear preview
|
143 |
previewStatus.textContent = 'Enter code to see preview.';
|
144 |
previewStatus.style.color = '#777';
|
145 |
+
previewContentWrapper.innerHTML = '<p style="color: #aaa;">Preview Area</p>';
|
146 |
+
resetTransform(); // Reset view when input is empty
|
147 |
return;
|
148 |
}
|
149 |
|
|
|
151 |
previewStatus.style.color = '#777';
|
152 |
|
153 |
|
154 |
+
previewContentWrapper.innerHTML = '<p style="color: #aaa;">Loading...</p>'; // Show loading indicator in the wrapper
|
155 |
|
156 |
try {
|
157 |
const response = await fetch("{{ url_for('preview_mermaid') }}", {
|
|
|
172 |
}
|
173 |
|
174 |
const result = await response.json();
|
175 |
+
resetTransform(); // Reset view before loading new content
|
176 |
|
177 |
+
previewContentWrapper.innerHTML = ''; // Clear previous preview/loading message
|
178 |
if (result.format === 'png') {
|
179 |
const img = document.createElement('img');
|
180 |
img.src = `data:image/png;base64,${result.data}`;
|
181 |
img.alt = 'Mermaid Diagram Preview';
|
182 |
+
// Prevent dragging the image itself, we'll handle panning
|
183 |
+
img.style.pointerEvents = 'none';
|
184 |
+
previewContentWrapper.appendChild(img);
|
185 |
} else if (result.format === 'svg') {
|
186 |
+
// Directly insert SVG markup into the wrapper
|
187 |
+
previewContentWrapper.innerHTML = result.data;
|
188 |
+
const svgElement = previewContentWrapper.querySelector('svg');
|
189 |
+
if (svgElement) {
|
190 |
+
// Make SVG take up space correctly and prevent internal pointer events interfering
|
191 |
+
svgElement.style.maxWidth = '100%';
|
192 |
+
svgElement.style.height = 'auto';
|
193 |
+
svgElement.style.display = 'block';
|
194 |
+
svgElement.style.pointerEvents = 'none';
|
195 |
+
}
|
196 |
}
|
197 |
previewStatus.textContent = `Preview updated (SVG).`; // Preview is always SVG now
|
198 |
previewStatus.style.color = 'green';
|
|
|
201 |
console.error('Error fetching preview:', error);
|
202 |
previewStatus.textContent = `Error: ${error.message}`;
|
203 |
previewStatus.style.color = 'red';
|
204 |
+
previewContentWrapper.innerHTML = '<p style="color: red;">Failed to load preview.</p>';
|
205 |
}
|
206 |
};
|
207 |
|
208 |
+
// --- Zoom and Pan Logic ---
|
209 |
+
let scale = 1;
|
210 |
+
let translateX = 0;
|
211 |
+
let translateY = 0;
|
212 |
+
const zoomStep = 0.1;
|
213 |
+
const panStep = 30; // pixels
|
214 |
+
|
215 |
+
function applyTransform() {
|
216 |
+
previewContentWrapper.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
|
217 |
+
}
|
218 |
+
|
219 |
+
function resetTransform() {
|
220 |
+
scale = 1;
|
221 |
+
translateX = 0;
|
222 |
+
translateY = 0;
|
223 |
+
applyTransform();
|
224 |
+
}
|
225 |
+
|
226 |
+
// Button Event Listeners
|
227 |
+
document.getElementById('zoom-in').addEventListener('click', () => {
|
228 |
+
scale += zoomStep;
|
229 |
+
applyTransform();
|
230 |
+
});
|
231 |
+
|
232 |
+
document.getElementById('zoom-out').addEventListener('click', () => {
|
233 |
+
scale = Math.max(0.1, scale - zoomStep); // Prevent zooming out too much
|
234 |
+
applyTransform();
|
235 |
+
});
|
236 |
+
|
237 |
+
document.getElementById('pan-up').addEventListener('click', () => {
|
238 |
+
translateY -= panStep;
|
239 |
+
applyTransform();
|
240 |
+
});
|
241 |
+
|
242 |
+
document.getElementById('pan-down').addEventListener('click', () => {
|
243 |
+
translateY += panStep;
|
244 |
+
applyTransform();
|
245 |
+
});
|
246 |
+
|
247 |
+
document.getElementById('pan-left').addEventListener('click', () => {
|
248 |
+
translateX -= panStep;
|
249 |
+
applyTransform();
|
250 |
+
});
|
251 |
+
|
252 |
+
document.getElementById('pan-right').addEventListener('click', () => {
|
253 |
+
translateX += panStep;
|
254 |
+
applyTransform();
|
255 |
+
});
|
256 |
+
|
257 |
+
document.getElementById('reset-view').addEventListener('click', resetTransform);
|
258 |
+
|
259 |
+
// --- Drag to Pan Logic ---
|
260 |
+
let isDragging = false;
|
261 |
+
let startX, startY;
|
262 |
+
let initialTranslateX, initialTranslateY;
|
263 |
+
|
264 |
+
previewBox.addEventListener('mousedown', (e) => {
|
265 |
+
// Only start drag if clicking directly on the preview box (not buttons etc.)
|
266 |
+
if (e.target === previewBox || e.target === previewContentWrapper) {
|
267 |
+
isDragging = true;
|
268 |
+
startX = e.clientX;
|
269 |
+
startY = e.clientY;
|
270 |
+
initialTranslateX = translateX;
|
271 |
+
initialTranslateY = translateY;
|
272 |
+
previewContentWrapper.style.transition = 'none'; // Disable transition during drag
|
273 |
+
previewContentWrapper.style.cursor = 'grabbing'; // Change cursor
|
274 |
+
previewBox.style.userSelect = 'none'; // Prevent text selection during drag
|
275 |
+
}
|
276 |
+
});
|
277 |
+
|
278 |
+
document.addEventListener('mousemove', (e) => {
|
279 |
+
if (!isDragging) return;
|
280 |
+
const currentX = e.clientX;
|
281 |
+
const currentY = e.clientY;
|
282 |
+
translateX = initialTranslateX + (currentX - startX);
|
283 |
+
translateY = initialTranslateY + (currentY - startY);
|
284 |
+
applyTransform();
|
285 |
+
});
|
286 |
+
|
287 |
+
document.addEventListener('mouseup', () => {
|
288 |
+
if (isDragging) {
|
289 |
+
isDragging = false;
|
290 |
+
previewContentWrapper.style.transition = 'transform 0.2s ease-out'; // Re-enable transition
|
291 |
+
previewContentWrapper.style.cursor = 'grab'; // Restore cursor
|
292 |
+
previewBox.style.userSelect = ''; // Re-enable text selection
|
293 |
+
}
|
294 |
+
});
|
295 |
+
|
296 |
+
// Prevent dragging state from sticking if mouse leaves the window
|
297 |
+
document.addEventListener('mouseleave', () => {
|
298 |
+
if (isDragging) {
|
299 |
+
isDragging = false;
|
300 |
+
previewContentWrapper.style.transition = 'transform 0.2s ease-out';
|
301 |
+
previewContentWrapper.style.cursor = 'grab';
|
302 |
+
previewBox.style.userSelect = '';
|
303 |
+
}
|
304 |
+
});
|
305 |
+
|
306 |
+
|
307 |
// Debounced version of the updatePreview function
|
308 |
const debouncedUpdatePreview = debounce(updatePreview, 750); // 750ms delay
|
309 |
|