liaoch commited on
Commit
786ba07
·
1 Parent(s): e40b114

add a sample screenshot

Browse files
Files changed (3) hide show
  1. README.md +4 -0
  2. flowchart.png +0 -0
  3. 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
+ ![Application Screenshot](sample.png)
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 { 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>
@@ -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: (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>
@@ -78,10 +90,22 @@
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>
@@ -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
- previewBox.innerHTML = '<p style="color: #aaa;">Preview Area</p>';
 
121
  return;
122
  }
123
 
@@ -125,7 +151,7 @@
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') }}", {
@@ -146,18 +172,27 @@
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';
@@ -166,10 +201,109 @@
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
 
 
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