KilloWatts commited on
Commit
fc8b2f7
·
verified ·
1 Parent(s): eaf0cb2

Create script.js

Browse files
Files changed (1) hide show
  1. script.js +1303 -0
script.js ADDED
@@ -0,0 +1,1303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Start with minimal template and variables
2
+ const defaultTemplate = `Hello {{ name }}!`;
3
+
4
+ const defaultVars = {
5
+ name: "World"
6
+ };
7
+
8
+ // --- EDITOR SETUP ---
9
+ const commonEditorOptions = {
10
+ lineNumbers: true,
11
+ theme: 'material-darker',
12
+ lineWrapping: true,
13
+ scrollbarStyle: 'native'
14
+ };
15
+
16
+ const jinjaEditor = CodeMirror.fromTextArea(document.getElementById('jinja-template'), {
17
+ ...commonEditorOptions,
18
+ mode: 'jinja2',
19
+ });
20
+
21
+ const varsEditor = CodeMirror.fromTextArea(document.getElementById('variables'), {
22
+ ...commonEditorOptions,
23
+ mode: { name: 'javascript', json: true },
24
+ });
25
+
26
+ jinjaEditor.setValue(defaultTemplate);
27
+ varsEditor.setValue(JSON.stringify(defaultVars, null, 2));
28
+
29
+ const outputElement = document.getElementById('output');
30
+ const markdownOutputElement = document.getElementById('markdown-output');
31
+ const loader = document.getElementById('loader');
32
+ const loadingOverlay = document.getElementById('loading-overlay');
33
+
34
+ // Pyodide setup
35
+ let pyodide = null;
36
+ let isInitialized = false;
37
+
38
+ // --- CONTROL ELEMENTS ---
39
+ const textWrapToggle = document.getElementById('text-wrap-toggle');
40
+ const autoRerenderToggle = document.getElementById('auto-rerender-toggle');
41
+ const manualRerenderBtn = document.getElementById('manual-rerender');
42
+ const extractVariablesBtn = document.getElementById('extract-variables-header');
43
+ const toggleModeBtn = document.getElementById('toggle-mode');
44
+ const variablesForm = document.getElementById('variables-form');
45
+ const variablesHeader = document.getElementById('variables-header');
46
+ const copyTemplateBtn = document.getElementById('copy-template-btn');
47
+ const copyOutputBtn = document.getElementById('copy-output-btn');
48
+ const showWhitespaceToggle = document.getElementById('show-whitespace-toggle');
49
+ const themeToggle = document.getElementById('theme-toggle');
50
+ const markdownToggle = document.getElementById('markdown-toggle');
51
+ const mermaidToggle = document.getElementById('mermaid-toggle');
52
+
53
+ // --- STATE MANAGEMENT ---
54
+ let isFormMode = false;
55
+ let extractedVariables = new Set();
56
+ let currentVariableValues = {};
57
+ let isMarkdownMode = false;
58
+ let isMermaidMode = false;
59
+ let lastRenderedOutput = '';
60
+
61
+ // Store debounced function references for proper event listener removal
62
+ let debouncedUpdateFromJinja = null;
63
+ let debouncedUpdateFromVars = null;
64
+
65
+ // --- RESIZE STATE ---
66
+ let isResizing = false;
67
+ let resizeType = null;
68
+ let startX = 0;
69
+ let startY = 0;
70
+ let startWidth = 0;
71
+ let startHeight = 0;
72
+
73
+ // --- MERMAID SETUP ---
74
+
75
+ // Initialize Mermaid with configuration
76
+ mermaid.initialize({
77
+ startOnLoad: false,
78
+ theme: document.body.classList.contains('dark-mode') ? 'dark' : 'default',
79
+ securityLevel: 'loose',
80
+ flowchart: {
81
+ useMaxWidth: true,
82
+ htmlLabels: true,
83
+ curve: 'basis',
84
+ wrap: true
85
+ },
86
+ themeVariables: {
87
+ fontSize: '14px'
88
+ }
89
+ });
90
+
91
+ // --- PYODIDE SETUP ---
92
+
93
+ async function setupPyodide() {
94
+ try {
95
+ loader.style.display = 'block';
96
+ loadingOverlay.style.display = 'block';
97
+
98
+ pyodide = await loadPyodide();
99
+ await pyodide.loadPackage("jinja2");
100
+
101
+ isInitialized = true;
102
+ loader.style.display = 'none';
103
+ loadingOverlay.style.display = 'none';
104
+
105
+ // Initial render after setup
106
+ update();
107
+ } catch (error) {
108
+ loader.textContent = `Failed to load Python environment: ${error.message}`;
109
+ loader.style.color = '#d32f2f';
110
+ }
111
+ }
112
+
113
+ // --- CORE LOGIC ---
114
+
115
+ /**
116
+ * Provides visual feedback for button clicks
117
+ */
118
+ function showButtonFeedback(button, message = 'Done!', duration = 1500) {
119
+ const originalText = button.textContent;
120
+ const originalBackground = button.style.background || getComputedStyle(button).backgroundColor;
121
+
122
+ const successColor = getComputedStyle(document.documentElement).getPropertyValue('--success-color').trim();
123
+
124
+ button.textContent = message;
125
+ button.style.background = successColor;
126
+ button.disabled = true;
127
+
128
+ setTimeout(() => {
129
+ button.textContent = originalText;
130
+ button.style.background = originalBackground;
131
+ button.disabled = false;
132
+ }, duration);
133
+ }
134
+
135
+ /**
136
+ * Provides visual feedback for toggle switches
137
+ */
138
+ function showToggleFeedback(toggleElement, message) {
139
+ const successColor = getComputedStyle(document.documentElement).getPropertyValue('--success-color').trim();
140
+
141
+ // Create a temporary tooltip-like element
142
+ const feedback = document.createElement('div');
143
+ feedback.textContent = message;
144
+ feedback.style.cssText = `
145
+ position: absolute;
146
+ background: ${successColor};
147
+ color: white;
148
+ padding: 6px 10px;
149
+ border-radius: 6px;
150
+ font-size: 11px;
151
+ font-weight: 500;
152
+ z-index: 1000;
153
+ pointer-events: none;
154
+ transform: translateX(-50%);
155
+ white-space: nowrap;
156
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
157
+ `;
158
+
159
+ // Position relative to the toggle
160
+ const rect = toggleElement.getBoundingClientRect();
161
+ feedback.style.left = `${rect.left + rect.width / 2}px`;
162
+ feedback.style.top = `${rect.top - 35}px`;
163
+
164
+ document.body.appendChild(feedback);
165
+
166
+ setTimeout(() => {
167
+ if (feedback.parentNode) {
168
+ feedback.parentNode.removeChild(feedback);
169
+ }
170
+ }, 1000);
171
+ }
172
+
173
+ /**
174
+ * UPDATED: Renders text with visible whitespace characters without affecting layout.
175
+ */
176
+ function renderWhitespace(text) {
177
+ // First, escape any potential HTML in the text to prevent XSS
178
+ const escapedText = text.replace(/&/g, '&')
179
+ .replace(/</g, '&lt;')
180
+ .replace(/>/g, '&gt;')
181
+ .replace(/"/g, '&quot;')
182
+ .replace(/'/g, '&#039;');
183
+
184
+ // Wrap whitespace characters in spans. The original characters are
185
+ // preserved for layout, and CSS pseudo-elements add the visual symbols.
186
+ return escapedText
187
+ .replace(/ /g, '<span class="whitespace-char space"> </span>')
188
+ .replace(/\t/g, '<span class="whitespace-char tab">\t</span>')
189
+ .replace(/\n/g, '<span class="whitespace-char newline"></span>\n');
190
+ }
191
+
192
+ /**
193
+ * Renders markdown with Mermaid diagram support
194
+ */
195
+ async function renderMarkdown(text) {
196
+ // Store the text for later use
197
+ lastRenderedOutput = text;
198
+
199
+ // Extract mermaid code blocks before markdown parsing
200
+ const mermaidBlocks = [];
201
+ const mermaidPlaceholder = text.replace(/```mermaid\n([\s\S]*?)```/g, (match, code) => {
202
+ mermaidBlocks.push(code.trim());
203
+ return `<div class="mermaid-placeholder" data-index="${mermaidBlocks.length - 1}"></div>`;
204
+ });
205
+
206
+ // Parse markdown
207
+ const html = marked.parse(mermaidPlaceholder);
208
+
209
+ // Insert HTML into the output element
210
+ markdownOutputElement.innerHTML = html;
211
+
212
+ // Replace placeholders with actual mermaid diagrams
213
+ const placeholders = markdownOutputElement.querySelectorAll('.mermaid-placeholder');
214
+ for (let i = 0; i < placeholders.length; i++) {
215
+ const placeholder = placeholders[i];
216
+ const index = parseInt(placeholder.getAttribute('data-index'));
217
+ const code = mermaidBlocks[index];
218
+
219
+ // Create a container for the mermaid diagram
220
+ const mermaidDiv = document.createElement('div');
221
+ mermaidDiv.className = 'mermaid';
222
+ mermaidDiv.textContent = code;
223
+
224
+ // Replace the placeholder
225
+ placeholder.parentNode.replaceChild(mermaidDiv, placeholder);
226
+ }
227
+
228
+ // Render all mermaid diagrams
229
+ try {
230
+ await mermaid.run({
231
+ querySelector: '.markdown-content .mermaid'
232
+ });
233
+ } catch (error) {
234
+ console.error('Mermaid rendering error:', error);
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Renders pure Mermaid diagram (assumes entire output is mermaid syntax)
240
+ */
241
+ async function renderPureMermaid(text) {
242
+ // Store the text for later use
243
+ lastRenderedOutput = text;
244
+
245
+ // Clear the markdown output and add a single mermaid diagram
246
+ markdownOutputElement.innerHTML = '';
247
+
248
+ // Create a container for the mermaid diagram
249
+ const mermaidDiv = document.createElement('div');
250
+ mermaidDiv.className = 'mermaid';
251
+ mermaidDiv.textContent = text.trim();
252
+
253
+ markdownOutputElement.appendChild(mermaidDiv);
254
+
255
+ // Render the mermaid diagram
256
+ try {
257
+ await mermaid.run({
258
+ querySelector: '.markdown-content .mermaid'
259
+ });
260
+ } catch (error) {
261
+ console.error('Mermaid rendering error:', error);
262
+ // Show error in a user-friendly way
263
+ markdownOutputElement.innerHTML = `<div style="color: #d32f2f; padding: 20px; border: 2px solid #d32f2f; border-radius: 8px; margin: 20px;">
264
+ <strong>⚠️ Mermaid Rendering Error</strong><br><br>
265
+ ${error.message || 'Failed to render diagram'}<br><br>
266
+ <small>Please check your Mermaid syntax.</small>
267
+ </div>`;
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Extracts variable names and structures from a Jinja template
273
+ */
274
+ function extractVariablesFromTemplate(template) {
275
+ const variableStructures = {};
276
+ const referencedVariables = new Set(); // Track variables that are referenced (not just assigned)
277
+
278
+ // Helper function to set nested property
279
+ function setNestedProperty(obj, path, value) {
280
+ const keys = path.split('.');
281
+ let current = obj;
282
+
283
+ for (let i = 0; i < keys.length - 1; i++) {
284
+ const key = keys[i];
285
+ if (!(key in current)) {
286
+ // Determine if next key is numeric (array index) or not
287
+ const nextKey = keys[i + 1];
288
+ current[key] = /^\d+$/.test(nextKey) ? [] : {};
289
+ }
290
+ current = current[key];
291
+ }
292
+
293
+ const lastKey = keys[keys.length - 1];
294
+ if (Array.isArray(current) && /^\d+$/.test(lastKey)) {
295
+ const index = parseInt(lastKey);
296
+ while (current.length <= index) {
297
+ current.push('');
298
+ }
299
+ current[index] = value;
300
+ } else {
301
+ current[lastKey] = value;
302
+ }
303
+ }
304
+
305
+ // Helper function to safely set variable without overriding existing complex types
306
+ function safeSetVariable(varName, newValue, allowOverride = false) {
307
+ if (!(varName in variableStructures)) {
308
+ variableStructures[varName] = newValue;
309
+ } else if (allowOverride) {
310
+ // Only override if the existing value is a simple type and new value is complex
311
+ const existing = variableStructures[varName];
312
+ const isExistingSimple = typeof existing === 'string' || typeof existing === 'boolean' || typeof existing === 'number';
313
+ const isNewComplex = typeof newValue === 'object' && newValue !== null;
314
+
315
+ if (isExistingSimple && isNewComplex) {
316
+ variableStructures[varName] = newValue;
317
+ }
318
+ }
319
+ }
320
+
321
+ // 0. First pass: Extract {% set %} patterns to identify assignments vs references
322
+ const setPattern = /\{\%\s*set\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\s*\%\}/g;
323
+ let setMatch;
324
+
325
+ while ((setMatch = setPattern.exec(template)) !== null) {
326
+ const assignedVar = setMatch[1]; // This is a reference, not extracted
327
+ const sourceVar = setMatch[2]; // This should be extracted
328
+
329
+ // Mark the source variable for extraction
330
+ const rootSourceVar = sourceVar.split('.')[0];
331
+ referencedVariables.add(rootSourceVar);
332
+
333
+ if (sourceVar.includes('.')) {
334
+ // Source is an object property access
335
+ safeSetVariable(rootSourceVar, {});
336
+ setNestedProperty(variableStructures, sourceVar, '');
337
+ } else {
338
+ // Simple source variable
339
+ safeSetVariable(rootSourceVar, '');
340
+ }
341
+ }
342
+
343
+ // 1. Match {{ variable.property }} and {{ variable.property.nested }} patterns
344
+ const variablePattern = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)(?:\s*\|\s*[^}]+)?\s*\}\}/g;
345
+ let match;
346
+
347
+ while ((match = variablePattern.exec(template)) !== null) {
348
+ const fullPath = match[1];
349
+ const rootVar = fullPath.split('.')[0];
350
+ referencedVariables.add(rootVar);
351
+
352
+ if (fullPath.includes('.')) {
353
+ // This is an object property access
354
+ safeSetVariable(rootVar, {}, true);
355
+ setNestedProperty(variableStructures, fullPath, '');
356
+ } else {
357
+ // Simple variable
358
+ safeSetVariable(rootVar, '');
359
+ }
360
+ }
361
+
362
+ // 2. Match {% for item in variable %} patterns - indicates variable is a list/array
363
+ const forPattern = /\{\%\s*for\s+\w+\s+in\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\%\}/g;
364
+ while ((match = forPattern.exec(template)) !== null) {
365
+ const varName = match[1];
366
+ referencedVariables.add(varName);
367
+
368
+ if (!(varName in variableStructures)) {
369
+ variableStructures[varName] = ['']; // Single empty string for lists
370
+ } else if (!Array.isArray(variableStructures[varName]) && typeof variableStructures[varName] !== 'object') {
371
+ // Convert to array if it was a simple string
372
+ variableStructures[varName] = [''];
373
+ }
374
+ }
375
+
376
+ // 3. Match {% for key, value in variable.items() %} patterns - indicates variable is a dict
377
+ const dictForPattern = /\{\%\s*for\s+\w+,\s*\w+\s+in\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\.\s*items\s*\(\s*\)\s*\%\}/g;
378
+ while ((match = dictForPattern.exec(template)) !== null) {
379
+ const varName = match[1];
380
+ referencedVariables.add(varName);
381
+
382
+ safeSetVariable(varName, { key1: 'value1', key2: 'value2' }, true);
383
+ }
384
+
385
+ // 4. Match {% if variable %} and {% if variable.property %} patterns
386
+ const ifPattern = /\{\%\s*if\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)/g;
387
+ while ((match = ifPattern.exec(template)) !== null) {
388
+ const fullPath = match[1];
389
+ const rootVar = fullPath.split('.')[0];
390
+ referencedVariables.add(rootVar);
391
+
392
+ if (fullPath.includes('.')) {
393
+ // Property access - ensure root is an object
394
+ safeSetVariable(rootVar, {}, true);
395
+ setNestedProperty(variableStructures, fullPath, true); // Boolean for if conditions
396
+ } else {
397
+ // Simple variable in if condition - only set as boolean if not already a complex type
398
+ if (!(rootVar in variableStructures)) {
399
+ variableStructures[rootVar] = true; // Default boolean for if conditions
400
+ }
401
+ // Don't override existing objects/arrays with boolean when used in truthiness check
402
+ }
403
+ }
404
+
405
+ // 5. Match array access patterns like {{ variable[0] }} or {{ variable.items[0] }}
406
+ const arrayAccessPattern = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\s*\[\s*(\d+)\s*\](?:\s*\|\s*[^}]+)?\s*\}\}/g;
407
+ while ((match = arrayAccessPattern.exec(template)) !== null) {
408
+ const basePath = match[1];
409
+ const index = parseInt(match[2]);
410
+ const rootVar = basePath.split('.')[0];
411
+ referencedVariables.add(rootVar);
412
+
413
+ safeSetVariable(rootVar, basePath.includes('.') ? {} : [], true);
414
+
415
+ // Create array structure
416
+ const arrayPath = basePath + '.' + index;
417
+ setNestedProperty(variableStructures, arrayPath, '');
418
+ }
419
+
420
+ // 6. Look for loop variables that access properties: {% for item in items %}{{ item.name }}{% endfor %}
421
+ const loopWithPropertyPattern = /\{\%\s*for\s+(\w+)\s+in\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\%\}(.*?)\{\%\s*endfor\s*\%\}/gs;
422
+ while ((match = loopWithPropertyPattern.exec(template)) !== null) {
423
+ const loopVar = match[1];
424
+ const arrayVar = match[2];
425
+ const loopContent = match[3];
426
+ referencedVariables.add(arrayVar);
427
+
428
+ // Find properties accessed on the loop variable
429
+ const loopVarPattern = new RegExp(`\\{\\{\\s*${loopVar}\\.([a-zA-Z_][a-zA-Z0-9_]*)`, 'g');
430
+ let propMatch;
431
+ const itemStructure = {};
432
+
433
+ while ((propMatch = loopVarPattern.exec(loopContent)) !== null) {
434
+ itemStructure[propMatch[1]] = '';
435
+ }
436
+
437
+ if (Object.keys(itemStructure).length > 0) {
438
+ // Create array of objects
439
+ safeSetVariable(arrayVar, [itemStructure, itemStructure], true);
440
+ }
441
+ }
442
+
443
+ // Final step: Only return variables that were actually referenced in the template
444
+ const finalVariableStructures = {};
445
+ for (const [varName, structure] of Object.entries(variableStructures)) {
446
+ if (referencedVariables.has(varName)) {
447
+ finalVariableStructures[varName] = structure;
448
+ }
449
+ }
450
+
451
+ return finalVariableStructures;
452
+ }
453
+
454
+ /**
455
+ * Creates form inputs for extracted variables
456
+ */
457
+ function createVariableForm(variableStructures) {
458
+ variablesForm.innerHTML = '';
459
+
460
+ if (Object.keys(variableStructures).length === 0) {
461
+ variablesForm.innerHTML = '<p style="color: #666; font-style: italic;">No variables found in template. Use {{ variable_name }} syntax.</p>';
462
+ return;
463
+ }
464
+
465
+ // Helper function to create form inputs recursively
466
+ function createInputsForStructure(structure, baseName = '', level = 0) {
467
+ const container = document.createElement('div');
468
+ container.style.marginLeft = `${level * 15}px`;
469
+
470
+ if (Array.isArray(structure)) {
471
+ // Handle arrays
472
+ const label = document.createElement('label');
473
+ label.textContent = `${baseName} (Array)`;
474
+ label.style.fontWeight = 'bold';
475
+ label.style.color = '#2196F3';
476
+ label.style.display = 'block';
477
+ label.style.marginBottom = '5px';
478
+ container.appendChild(label);
479
+
480
+ const textarea = document.createElement('textarea');
481
+ textarea.id = `var-${baseName}`;
482
+ textarea.name = baseName;
483
+ textarea.value = JSON.stringify(structure, null, 2);
484
+ textarea.placeholder = `JSON array for ${baseName}`;
485
+ textarea.style.width = '100%';
486
+ textarea.style.minHeight = '80px';
487
+ textarea.style.padding = '6px 8px';
488
+ textarea.style.border = '1px solid #e0e0e0';
489
+ textarea.style.borderRadius = '4px';
490
+ textarea.style.fontSize = '12px';
491
+ textarea.style.fontFamily = '"Menlo", "Consolas", monospace';
492
+ textarea.style.marginBottom = '15px';
493
+ textarea.style.resize = 'vertical';
494
+
495
+ textarea.addEventListener('input', function() {
496
+ try {
497
+ const parsed = JSON.parse(this.value);
498
+ currentVariableValues[baseName] = parsed;
499
+ this.style.borderColor = '#e0e0e0';
500
+ } catch (e) {
501
+ this.style.borderColor = '#d32f2f';
502
+ currentVariableValues[baseName] = this.value;
503
+ }
504
+ if (autoRerenderToggle.checked) {
505
+ debounce(update, 300)();
506
+ }
507
+ });
508
+
509
+ container.appendChild(textarea);
510
+
511
+ } else if (typeof structure === 'object' && structure !== null) {
512
+ // Handle objects
513
+ if (baseName) {
514
+ const label = document.createElement('label');
515
+ label.textContent = `${baseName} (Object)`;
516
+ label.style.fontWeight = 'bold';
517
+ label.style.color = '#4CAF50';
518
+ label.style.display = 'block';
519
+ label.style.marginBottom = '5px';
520
+ container.appendChild(label);
521
+ }
522
+
523
+ // Check if it's a simple object (all values are primitives)
524
+ const isSimpleObject = Object.values(structure).every(val =>
525
+ typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean'
526
+ );
527
+
528
+ if (isSimpleObject && Object.keys(structure).length <= 5) {
529
+ // Create individual inputs for simple objects
530
+ Object.entries(structure).forEach(([key, value]) => {
531
+ const inputDiv = document.createElement('div');
532
+ inputDiv.className = 'variable-input';
533
+ inputDiv.style.marginLeft = `${(level + 1) * 15}px`;
534
+
535
+ const label = document.createElement('label');
536
+ label.textContent = `${baseName ? baseName + '.' : ''}${key}`;
537
+ label.style.fontSize = '11px';
538
+ label.style.color = '#666';
539
+
540
+ const input = document.createElement('input');
541
+ input.type = typeof value === 'boolean' ? 'checkbox' : 'text';
542
+ input.id = `var-${baseName ? baseName + '.' : ''}${key}`;
543
+ input.name = `${baseName ? baseName + '.' : ''}${key}`;
544
+
545
+ if (typeof value === 'boolean') {
546
+ input.checked = value;
547
+ input.addEventListener('change', function() {
548
+ const path = this.name.split('.');
549
+ let current = currentVariableValues;
550
+ for (let i = 0; i < path.length - 1; i++) {
551
+ if (!(path[i] in current)) current[path[i]] = {};
552
+ current = current[path[i]];
553
+ }
554
+ current[path[path.length - 1]] = this.checked;
555
+ if (autoRerenderToggle.checked) {
556
+ debounce(update, 300)();
557
+ }
558
+ });
559
+ } else {
560
+ input.value = value;
561
+ input.addEventListener('input', function() {
562
+ const path = this.name.split('.');
563
+ let current = currentVariableValues;
564
+ for (let i = 0; i < path.length - 1; i++) {
565
+ if (!(path[i] in current)) current[path[i]] = {};
566
+ current = current[path[i]];
567
+ }
568
+ current[path[path.length - 1]] = this.value;
569
+ if (autoRerenderToggle.checked) {
570
+ debounce(update, 300)();
571
+ }
572
+ });
573
+ }
574
+
575
+ inputDiv.appendChild(label);
576
+ inputDiv.appendChild(input);
577
+ container.appendChild(inputDiv);
578
+ });
579
+ } else {
580
+ // Complex object - use JSON textarea
581
+ const textarea = document.createElement('textarea');
582
+ textarea.id = `var-${baseName}`;
583
+ textarea.name = baseName;
584
+ textarea.value = JSON.stringify(structure, null, 2);
585
+ textarea.placeholder = `JSON object for ${baseName}`;
586
+ textarea.style.width = '100%';
587
+ textarea.style.minHeight = '100px';
588
+ textarea.style.padding = '6px 8px';
589
+ textarea.style.border = '1px solid #e0e0e0';
590
+ textarea.style.borderRadius = '4px';
591
+ textarea.style.fontSize = '12px';
592
+ textarea.style.fontFamily = '"Menlo", "Consolas", monospace';
593
+ textarea.style.marginBottom = '15px';
594
+ textarea.style.resize = 'vertical';
595
+
596
+ textarea.addEventListener('input', function() {
597
+ try {
598
+ const parsed = JSON.parse(this.value);
599
+ currentVariableValues[baseName] = parsed;
600
+ this.style.borderColor = '#e0e0e0';
601
+ } catch (e) {
602
+ this.style.borderColor = '#d32f2f';
603
+ currentVariableValues[baseName] = this.value;
604
+ }
605
+ if (autoRerenderToggle.checked) {
606
+ debounce(update, 300)();
607
+ }
608
+ });
609
+
610
+ container.appendChild(textarea);
611
+ }
612
+
613
+ } else {
614
+ // Handle primitive values
615
+ const inputDiv = document.createElement('div');
616
+ inputDiv.className = 'variable-input';
617
+
618
+ const label = document.createElement('label');
619
+ label.textContent = baseName;
620
+ label.setAttribute('for', `var-${baseName}`);
621
+
622
+ const input = document.createElement(typeof structure === 'boolean' ? 'input' :
623
+ (typeof structure === 'string' && structure.length > 50) ? 'textarea' : 'input');
624
+
625
+ input.id = `var-${baseName}`;
626
+ input.name = baseName;
627
+
628
+ if (typeof structure === 'boolean') {
629
+ input.type = 'checkbox';
630
+ input.checked = structure;
631
+ input.addEventListener('change', function() {
632
+ currentVariableValues[baseName] = this.checked;
633
+ if (autoRerenderToggle.checked) {
634
+ debounce(update, 300)();
635
+ }
636
+ });
637
+ } else {
638
+ if (input.tagName === 'TEXTAREA') {
639
+ input.value = structure;
640
+ input.style.minHeight = '60px';
641
+ input.style.resize = 'vertical';
642
+ } else {
643
+ input.type = 'text';
644
+ input.value = structure;
645
+ input.placeholder = `Enter value for ${baseName}`;
646
+ }
647
+
648
+ input.addEventListener('input', function() {
649
+ currentVariableValues[baseName] = this.value;
650
+ if (autoRerenderToggle.checked) {
651
+ debounce(update, 300)();
652
+ }
653
+ });
654
+ }
655
+
656
+ inputDiv.appendChild(label);
657
+ inputDiv.appendChild(input);
658
+ container.appendChild(inputDiv);
659
+ }
660
+
661
+ return container;
662
+ }
663
+
664
+ // Create inputs for each top-level variable
665
+ Object.entries(variableStructures).forEach(([varName, structure]) => {
666
+ const container = createInputsForStructure(structure, varName);
667
+ variablesForm.appendChild(container);
668
+ });
669
+ }
670
+
671
+ /**
672
+ * Gets current variable values from form or JSON
673
+ */
674
+ function getCurrentVariables() {
675
+ if (isFormMode) {
676
+ const formData = {};
677
+ variablesForm.querySelectorAll('input, textarea').forEach(input => {
678
+ const varName = input.name;
679
+ let value = input.value;
680
+
681
+ // Try to parse as JSON if it looks like JSON
682
+ if (value.trim().startsWith('{') || value.trim().startsWith('[') ||
683
+ value === 'true' || value === 'false' ||
684
+ (value.trim() && !isNaN(value.trim()))) {
685
+ try {
686
+ value = JSON.parse(value);
687
+ } catch (e) {
688
+ // Keep as string if not valid JSON
689
+ }
690
+ }
691
+
692
+ formData[varName] = value;
693
+ });
694
+ return formData;
695
+ } else {
696
+ try {
697
+ return JSON.parse(varsEditor.getValue() || '{}');
698
+ } catch (e) {
699
+ return {};
700
+ }
701
+ }
702
+ }
703
+
704
+ /**
705
+ * The main function to update the rendering. It gets triggered on any change.
706
+ */
707
+ async function update() {
708
+ if (!pyodide || !isInitialized) {
709
+ outputElement.textContent = 'Python environment is still loading...';
710
+ outputElement.className = '';
711
+ return;
712
+ }
713
+
714
+ const template = jinjaEditor.getValue();
715
+ let context;
716
+
717
+ // 1. Get variables from current mode (form or JSON)
718
+ try {
719
+ context = getCurrentVariables();
720
+ } catch (e) {
721
+ // If there's an error getting variables, show it
722
+ outputElement.textContent = `Error in variables:\n${e.message}`;
723
+ outputElement.className = 'error';
724
+ return;
725
+ }
726
+
727
+ // 2. Render the template with the context using Python Jinja2
728
+ try {
729
+ const contextJson = JSON.stringify(context);
730
+
731
+ // Escape template and context strings for Python
732
+ const escapedTemplate = template.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
733
+ const escapedContext = contextJson.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
734
+
735
+ const result = pyodide.runPython(`
736
+ import jinja2
737
+ import json
738
+
739
+ try:
740
+ template_str = """${escapedTemplate}"""
741
+ context_str = """${escapedContext}"""
742
+
743
+ template = jinja2.Template(template_str)
744
+ context = json.loads(context_str)
745
+ result = template.render(context)
746
+ except jinja2.exceptions.TemplateError as e:
747
+ result = f"Jinja2 Template Error: {e}"
748
+ except json.JSONDecodeError as e:
749
+ result = f"JSON Error: {e}"
750
+ except Exception as e:
751
+ result = f"Error: {e}"
752
+
753
+ result
754
+ `);
755
+
756
+ // Store the result
757
+ lastRenderedOutput = result;
758
+
759
+ // Set the main content based on mode
760
+ if (isMermaidMode) {
761
+ // Render as pure mermaid diagram
762
+ outputElement.style.display = 'none';
763
+ markdownOutputElement.style.display = 'block';
764
+ await renderPureMermaid(result);
765
+ } else if (isMarkdownMode) {
766
+ // Render as markdown
767
+ outputElement.style.display = 'none';
768
+ markdownOutputElement.style.display = 'block';
769
+ await renderMarkdown(result);
770
+ } else {
771
+ // Render as plain text
772
+ outputElement.style.display = 'block';
773
+ markdownOutputElement.style.display = 'none';
774
+
775
+ if (showWhitespaceToggle.checked) {
776
+ outputElement.innerHTML = renderWhitespace(result);
777
+ } else {
778
+ outputElement.textContent = result;
779
+ }
780
+ outputElement.className = result.includes('Error:') ? 'error' : '';
781
+ }
782
+ } catch (e) {
783
+ outputElement.textContent = `Python execution error: ${e.message}`;
784
+ outputElement.className = 'error';
785
+ }
786
+ }
787
+
788
+ // --- CONTROL HANDLERS ---
789
+
790
+ // Extract variables button
791
+ extractVariablesBtn.addEventListener('click', function() {
792
+ const template = jinjaEditor.getValue();
793
+ const newVariableStructures = extractVariablesFromTemplate(template);
794
+
795
+ // Get current values from the active mode (form or JSON)
796
+ const currentValues = getCurrentVariables();
797
+
798
+ // Merge existing values with new structure, preserving user data where possible
799
+ function mergeStructures(newStruct, existingValues) {
800
+ if (Array.isArray(newStruct)) {
801
+ return existingValues && Array.isArray(existingValues) ? existingValues : newStruct;
802
+ } else if (typeof newStruct === 'object' && newStruct !== null) {
803
+ const merged = {};
804
+ Object.keys(newStruct).forEach(key => {
805
+ if (existingValues && typeof existingValues === 'object' && key in existingValues) {
806
+ merged[key] = mergeStructures(newStruct[key], existingValues[key]);
807
+ } else {
808
+ merged[key] = newStruct[key];
809
+ }
810
+ });
811
+ return merged;
812
+ } else {
813
+ return existingValues !== undefined ? existingValues : newStruct;
814
+ }
815
+ }
816
+
817
+ const mergedVariables = {};
818
+ Object.keys(newVariableStructures).forEach(varName => {
819
+ mergedVariables[varName] = mergeStructures(
820
+ newVariableStructures[varName],
821
+ currentValues[varName]
822
+ );
823
+ });
824
+
825
+ // Update state
826
+ extractedVariables = new Set(Object.keys(newVariableStructures));
827
+ currentVariableValues = mergedVariables;
828
+
829
+ // If in form mode, recreate the form
830
+ if (isFormMode) {
831
+ createVariableForm(newVariableStructures);
832
+ }
833
+
834
+ // Update JSON editor to reflect current values
835
+ varsEditor.setValue(JSON.stringify(mergedVariables, null, 2));
836
+
837
+ // Re-render
838
+ update();
839
+
840
+ // Show feedback
841
+ const variableCount = Object.keys(newVariableStructures).length;
842
+ const message = variableCount > 0 ? `Found ${variableCount} variable${variableCount !== 1 ? 's' : ''}!` : 'No variables found!';
843
+ showButtonFeedback(this, message, 2000);
844
+ });
845
+
846
+ // Mode toggle button
847
+ toggleModeBtn.addEventListener('click', function() {
848
+ const wasFormMode = isFormMode;
849
+ isFormMode = !isFormMode;
850
+
851
+ if (isFormMode) {
852
+ // Switch to form mode
853
+ varsEditor.getWrapperElement().style.display = 'none';
854
+ variablesForm.style.display = 'block';
855
+ toggleModeBtn.textContent = 'Switch to JSON Mode';
856
+ variablesHeader.textContent = 'Variables (Form)';
857
+
858
+ // Get current variables from JSON and update our state
859
+ try {
860
+ const currentVars = JSON.parse(varsEditor.getValue() || '{}');
861
+ currentVariableValues = currentVars;
862
+
863
+ // Convert to the structure format expected by createVariableForm
864
+ const variableStructures = {};
865
+ Object.keys(currentVars).forEach(key => {
866
+ variableStructures[key] = currentVars[key];
867
+ });
868
+
869
+ extractedVariables = new Set(Object.keys(currentVars));
870
+
871
+ // Create form with current variables
872
+ createVariableForm(variableStructures);
873
+ } catch (e) {
874
+ // If JSON is invalid, keep existing state or create empty form
875
+ createVariableForm({});
876
+ }
877
+ } else {
878
+ // Switch to JSON mode
879
+ varsEditor.getWrapperElement().style.display = 'block';
880
+ variablesForm.style.display = 'none';
881
+ toggleModeBtn.textContent = 'Switch to Form Mode';
882
+ variablesHeader.textContent = 'Variables (JSON)';
883
+
884
+ // Update JSON editor with current form values
885
+ const currentVars = getCurrentVariables();
886
+ varsEditor.setValue(JSON.stringify(currentVars, null, 2));
887
+ }
888
+
889
+ // Show feedback
890
+ const mode = isFormMode ? 'Form' : 'JSON';
891
+ showButtonFeedback(this, `Switched to ${mode}!`, 1500);
892
+ });
893
+
894
+ // Text wrap toggle
895
+ textWrapToggle.addEventListener('change', function() {
896
+ const wrapMode = this.checked;
897
+ jinjaEditor.setOption('lineWrapping', wrapMode);
898
+ varsEditor.setOption('lineWrapping', wrapMode);
899
+
900
+ // Show feedback
901
+ const message = wrapMode ? 'Text wrap enabled!' : 'Text wrap disabled!';
902
+ showToggleFeedback(this.parentElement, message);
903
+ });
904
+
905
+ // Whitespace toggle
906
+ showWhitespaceToggle.addEventListener('change', function() {
907
+ update(); // Re-render the output with the new setting
908
+ const message = this.checked ? 'Whitespace visible' : 'Whitespace hidden';
909
+ showToggleFeedback(this.parentElement, message);
910
+ });
911
+
912
+ // Markdown toggle
913
+ markdownToggle.addEventListener('change', async function() {
914
+ if (this.checked) {
915
+ // Disable mermaid mode if it's on
916
+ if (isMermaidMode) {
917
+ mermaidToggle.checked = false;
918
+ isMermaidMode = false;
919
+ }
920
+
921
+ isMarkdownMode = true;
922
+
923
+ // Switch to markdown mode
924
+ outputElement.style.display = 'none';
925
+ markdownOutputElement.style.display = 'block';
926
+
927
+ // If we have output, render it as markdown
928
+ if (lastRenderedOutput) {
929
+ await renderMarkdown(lastRenderedOutput);
930
+ }
931
+
932
+ // Disable whitespace toggle in markdown mode
933
+ showWhitespaceToggle.disabled = true;
934
+ showWhitespaceToggle.parentElement.style.opacity = '0.5';
935
+
936
+ // Show feedback
937
+ showToggleFeedback(this.parentElement, 'Markdown mode enabled!');
938
+ } else {
939
+ isMarkdownMode = false;
940
+
941
+ // Switch to plain text mode
942
+ outputElement.style.display = 'block';
943
+ markdownOutputElement.style.display = 'none';
944
+
945
+ // Re-render as plain text
946
+ if (lastRenderedOutput) {
947
+ if (showWhitespaceToggle.checked) {
948
+ outputElement.innerHTML = renderWhitespace(lastRenderedOutput);
949
+ } else {
950
+ outputElement.textContent = lastRenderedOutput;
951
+ }
952
+ outputElement.className = lastRenderedOutput.includes('Error:') ? 'error' : '';
953
+ }
954
+
955
+ // Re-enable whitespace toggle
956
+ showWhitespaceToggle.disabled = false;
957
+ showWhitespaceToggle.parentElement.style.opacity = '1';
958
+
959
+ // Show feedback
960
+ showToggleFeedback(this.parentElement, 'Plain text mode enabled!');
961
+ }
962
+ });
963
+
964
+ // Mermaid toggle
965
+ mermaidToggle.addEventListener('change', async function() {
966
+ if (this.checked) {
967
+ // Disable markdown mode if it's on
968
+ if (isMarkdownMode) {
969
+ markdownToggle.checked = false;
970
+ isMarkdownMode = false;
971
+ }
972
+
973
+ isMermaidMode = true;
974
+
975
+ // Switch to mermaid mode
976
+ outputElement.style.display = 'none';
977
+ markdownOutputElement.style.display = 'block';
978
+
979
+ // If we have output, render it as mermaid
980
+ if (lastRenderedOutput) {
981
+ await renderPureMermaid(lastRenderedOutput);
982
+ }
983
+
984
+ // Disable whitespace toggle in mermaid mode
985
+ showWhitespaceToggle.disabled = true;
986
+ showWhitespaceToggle.parentElement.style.opacity = '0.5';
987
+
988
+ // Show feedback
989
+ showToggleFeedback(this.parentElement, 'Mermaid mode enabled!');
990
+ } else {
991
+ isMermaidMode = false;
992
+
993
+ // Switch to plain text mode
994
+ outputElement.style.display = 'block';
995
+ markdownOutputElement.style.display = 'none';
996
+
997
+ // Re-render as plain text
998
+ if (lastRenderedOutput) {
999
+ if (showWhitespaceToggle.checked) {
1000
+ outputElement.innerHTML = renderWhitespace(lastRenderedOutput);
1001
+ } else {
1002
+ outputElement.textContent = lastRenderedOutput;
1003
+ }
1004
+ outputElement.className = lastRenderedOutput.includes('Error:') ? 'error' : '';
1005
+ }
1006
+
1007
+ // Re-enable whitespace toggle
1008
+ showWhitespaceToggle.disabled = false;
1009
+ showWhitespaceToggle.parentElement.style.opacity = '1';
1010
+
1011
+ // Show feedback
1012
+ showToggleFeedback(this.parentElement, 'Plain text mode enabled!');
1013
+ }
1014
+ });
1015
+
1016
+ // Auto rerender toggle
1017
+ autoRerenderToggle.addEventListener('change', function() {
1018
+ manualRerenderBtn.disabled = this.checked;
1019
+ setupEventListeners();
1020
+
1021
+ // Show feedback
1022
+ const message = this.checked ? 'Auto rerender enabled!' : 'Auto rerender disabled!';
1023
+ showToggleFeedback(this.parentElement, message);
1024
+ });
1025
+
1026
+ // Manual rerender button
1027
+ manualRerenderBtn.addEventListener('click', function() {
1028
+ update();
1029
+ showButtonFeedback(this, 'Rerendered!', 1000);
1030
+ });
1031
+
1032
+ // Copy template button
1033
+ copyTemplateBtn.addEventListener('click', async function() {
1034
+ try {
1035
+ const templateContent = jinjaEditor.getValue();
1036
+ await navigator.clipboard.writeText(templateContent);
1037
+ showButtonFeedback(this, 'Copied!', 1500);
1038
+ } catch (err) {
1039
+ // Fallback for older browsers
1040
+ const textArea = document.createElement('textarea');
1041
+ textArea.value = jinjaEditor.getValue();
1042
+ document.body.appendChild(textArea);
1043
+ textArea.select();
1044
+ document.execCommand('copy');
1045
+ document.body.removeChild(textArea);
1046
+ showButtonFeedback(this, 'Copied!', 1500);
1047
+ }
1048
+ });
1049
+
1050
+ // Copy output button
1051
+ copyOutputBtn.addEventListener('click', async function() {
1052
+ try {
1053
+ const outputContent = outputElement.textContent;
1054
+ await navigator.clipboard.writeText(outputContent);
1055
+ showButtonFeedback(this, 'Copied!', 1500);
1056
+ } catch (err) {
1057
+ // Fallback for older browsers
1058
+ const textArea = document.createElement('textarea');
1059
+ textArea.value = outputElement.textContent;
1060
+ document.body.appendChild(textArea);
1061
+ textArea.select();
1062
+ document.execCommand('copy');
1063
+ document.body.removeChild(textArea);
1064
+ showButtonFeedback(this, 'Copied!', 1500);
1065
+ }
1066
+ });
1067
+
1068
+ // Theme toggle
1069
+ themeToggle.addEventListener('change', function() {
1070
+ const isLightMode = this.checked;
1071
+
1072
+ if (isLightMode) {
1073
+ // Switch to light mode
1074
+ document.body.classList.remove('dark-mode');
1075
+ localStorage.setItem('theme', 'light');
1076
+ jinjaEditor.setOption('theme', 'default');
1077
+ varsEditor.setOption('theme', 'default');
1078
+
1079
+ // Update Mermaid theme
1080
+ mermaid.initialize({
1081
+ startOnLoad: false,
1082
+ theme: 'default',
1083
+ securityLevel: 'loose',
1084
+ flowchart: {
1085
+ useMaxWidth: true,
1086
+ htmlLabels: true,
1087
+ curve: 'basis',
1088
+ wrap: true
1089
+ },
1090
+ themeVariables: {
1091
+ fontSize: '14px'
1092
+ }
1093
+ });
1094
+ } else {
1095
+ // Switch to dark mode
1096
+ document.body.classList.add('dark-mode');
1097
+ localStorage.setItem('theme', 'dark');
1098
+ jinjaEditor.setOption('theme', 'material-darker');
1099
+ varsEditor.setOption('theme', 'material-darker');
1100
+
1101
+ // Update Mermaid theme
1102
+ mermaid.initialize({
1103
+ startOnLoad: false,
1104
+ theme: 'dark',
1105
+ securityLevel: 'loose',
1106
+ flowchart: {
1107
+ useMaxWidth: true,
1108
+ htmlLabels: true,
1109
+ curve: 'basis',
1110
+ wrap: true
1111
+ },
1112
+ themeVariables: {
1113
+ fontSize: '14px'
1114
+ }
1115
+ });
1116
+ }
1117
+
1118
+ // If in markdown or mermaid mode, re-render to apply new Mermaid theme
1119
+ if (isMarkdownMode && lastRenderedOutput) {
1120
+ renderMarkdown(lastRenderedOutput);
1121
+ } else if (isMermaidMode && lastRenderedOutput) {
1122
+ renderPureMermaid(lastRenderedOutput);
1123
+ }
1124
+
1125
+ // Refresh CodeMirror editors to apply theme
1126
+ setTimeout(() => {
1127
+ jinjaEditor.refresh();
1128
+ varsEditor.refresh();
1129
+ }, 10);
1130
+ });
1131
+
1132
+ // --- EVENT LISTENERS ---
1133
+ // Conditional event listeners based on auto-rerender setting
1134
+ function setupEventListeners() {
1135
+ // Remove any existing listeners first
1136
+ if (debouncedUpdateFromJinja) {
1137
+ jinjaEditor.off('change', debouncedUpdateFromJinja);
1138
+ }
1139
+ if (debouncedUpdateFromVars) {
1140
+ varsEditor.off('change', debouncedUpdateFromVars);
1141
+ }
1142
+
1143
+ if (autoRerenderToggle.checked) {
1144
+ // Create new debounced functions and store references
1145
+ debouncedUpdateFromJinja = debounce(update, 300);
1146
+ debouncedUpdateFromVars = debounce(update, 300);
1147
+
1148
+ // Add the event listeners
1149
+ jinjaEditor.on('change', debouncedUpdateFromJinja);
1150
+ varsEditor.on('change', debouncedUpdateFromVars);
1151
+ } else {
1152
+ // Clear the references when disabled
1153
+ debouncedUpdateFromJinja = null;
1154
+ debouncedUpdateFromVars = null;
1155
+ }
1156
+ }
1157
+
1158
+ // Debounce function to prevent too frequent updates
1159
+ function debounce(func, wait) {
1160
+ let timeout;
1161
+ return function executedFunction(...args) {
1162
+ const later = () => {
1163
+ clearTimeout(timeout);
1164
+ func(...args);
1165
+ };
1166
+ clearTimeout(timeout);
1167
+ timeout = setTimeout(later, wait);
1168
+ };
1169
+ }
1170
+
1171
+ // --- RESIZE FUNCTIONALITY ---
1172
+
1173
+ // Get resize elements
1174
+ const horizontalResize = document.getElementById('horizontal-resize');
1175
+ const verticalResize = document.getElementById('vertical-resize');
1176
+ const leftPanel = document.getElementById('left-panel');
1177
+ const rightPanel = document.getElementById('right-panel');
1178
+ const templatePane = document.getElementById('template-pane');
1179
+ const variablesPane = document.getElementById('variables-pane');
1180
+ const mainContainer = document.getElementById('main-container');
1181
+
1182
+ // Initialize default sizes
1183
+ let leftPanelWidth = 50; // percentage
1184
+ let templatePaneHeight = 60; // percentage
1185
+
1186
+ function setInitialSizes() {
1187
+ const containerRect = mainContainer.getBoundingClientRect();
1188
+ leftPanel.style.width = `${leftPanelWidth}%`;
1189
+ rightPanel.style.width = `${100 - leftPanelWidth}%`;
1190
+
1191
+ const leftPanelRect = leftPanel.getBoundingClientRect();
1192
+ templatePane.style.height = `${templatePaneHeight}%`;
1193
+ variablesPane.style.height = `${100 - templatePaneHeight}%`;
1194
+ }
1195
+
1196
+ // Horizontal resize (between template and variables)
1197
+ horizontalResize.addEventListener('mousedown', function(e) {
1198
+ isResizing = true;
1199
+ resizeType = 'horizontal';
1200
+ startY = e.clientY;
1201
+
1202
+ const leftPanelRect = leftPanel.getBoundingClientRect();
1203
+ const templateRect = templatePane.getBoundingClientRect();
1204
+ startHeight = templateRect.height;
1205
+
1206
+ document.addEventListener('mousemove', handleResize);
1207
+ document.addEventListener('mouseup', stopResize);
1208
+ e.preventDefault();
1209
+ });
1210
+
1211
+ // Vertical resize (between left and right panels)
1212
+ verticalResize.addEventListener('mousedown', function(e) {
1213
+ isResizing = true;
1214
+ resizeType = 'vertical';
1215
+ startX = e.clientX;
1216
+
1217
+ const containerRect = mainContainer.getBoundingClientRect();
1218
+ const leftRect = leftPanel.getBoundingClientRect();
1219
+ startWidth = leftRect.width;
1220
+
1221
+ document.addEventListener('mousemove', handleResize);
1222
+ document.addEventListener('mouseup', stopResize);
1223
+ e.preventDefault();
1224
+ });
1225
+
1226
+ function handleResize(e) {
1227
+ if (!isResizing) return;
1228
+
1229
+ if (resizeType === 'horizontal') {
1230
+ const deltaY = e.clientY - startY;
1231
+ const leftPanelRect = leftPanel.getBoundingClientRect();
1232
+ const newTemplateHeight = startHeight + deltaY;
1233
+ const minHeight = 100;
1234
+ const maxHeight = leftPanelRect.height - minHeight - 4; // 4px for resize handle
1235
+
1236
+ if (newTemplateHeight >= minHeight && newTemplateHeight <= maxHeight) {
1237
+ const templatePercentage = (newTemplateHeight / leftPanelRect.height) * 100;
1238
+ const variablesPercentage = 100 - templatePercentage;
1239
+
1240
+ templatePane.style.height = `${templatePercentage}%`;
1241
+ variablesPane.style.height = `${variablesPercentage}%`;
1242
+ templatePaneHeight = templatePercentage;
1243
+ }
1244
+ } else if (resizeType === 'vertical') {
1245
+ const deltaX = e.clientX - startX;
1246
+ const containerRect = mainContainer.getBoundingClientRect();
1247
+ const newLeftWidth = startWidth + deltaX;
1248
+ const minWidth = 200;
1249
+ const maxWidth = containerRect.width - minWidth - 4; // 4px for resize handle
1250
+
1251
+ if (newLeftWidth >= minWidth && newLeftWidth <= maxWidth) {
1252
+ const leftPercentage = (newLeftWidth / containerRect.width) * 100;
1253
+ const rightPercentage = 100 - leftPercentage;
1254
+
1255
+ leftPanel.style.width = `${leftPercentage}%`;
1256
+ rightPanel.style.width = `${rightPercentage}%`;
1257
+ leftPanelWidth = leftPercentage;
1258
+ }
1259
+ }
1260
+
1261
+ // Refresh CodeMirror editors after resize
1262
+ setTimeout(() => {
1263
+ jinjaEditor.refresh();
1264
+ varsEditor.refresh();
1265
+ }, 10);
1266
+ }
1267
+
1268
+ function stopResize() {
1269
+ isResizing = false;
1270
+ resizeType = null;
1271
+ document.removeEventListener('mousemove', handleResize);
1272
+ document.removeEventListener('mouseup', stopResize);
1273
+ }
1274
+
1275
+ // Handle window resize
1276
+ window.addEventListener('resize', function() {
1277
+ setTimeout(() => {
1278
+ jinjaEditor.refresh();
1279
+ varsEditor.refresh();
1280
+ }, 100);
1281
+ });
1282
+
1283
+ // Initial setup
1284
+ setInitialSizes();
1285
+ setupEventListeners();
1286
+
1287
+ // Load saved theme preference
1288
+ const savedTheme = localStorage.getItem('theme');
1289
+ if (savedTheme === 'light') {
1290
+ document.body.classList.remove('dark-mode');
1291
+ themeToggle.checked = true;
1292
+ jinjaEditor.setOption('theme', 'default');
1293
+ varsEditor.setOption('theme', 'default');
1294
+ } else {
1295
+ // Default to dark mode
1296
+ document.body.classList.add('dark-mode');
1297
+ themeToggle.checked = false;
1298
+ jinjaEditor.setOption('theme', 'material-darker');
1299
+ varsEditor.setOption('theme', 'material-darker');
1300
+ }
1301
+
1302
+ // Start Pyodide and initial render
1303
+ setupPyodide();