// Start with minimal template and variables const defaultTemplate = `Hello {{ name }}!`; const defaultVars = { name: "World" }; // --- EDITOR SETUP --- const commonEditorOptions = { lineNumbers: true, theme: 'material-darker', lineWrapping: true, scrollbarStyle: 'native' }; const jinjaEditor = CodeMirror.fromTextArea(document.getElementById('jinja-template'), { ...commonEditorOptions, mode: 'jinja2', }); const varsEditor = CodeMirror.fromTextArea(document.getElementById('variables'), { ...commonEditorOptions, mode: { name: 'javascript', json: true }, }); jinjaEditor.setValue(defaultTemplate); varsEditor.setValue(JSON.stringify(defaultVars, null, 2)); const outputElement = document.getElementById('output'); const markdownOutputElement = document.getElementById('markdown-output'); const loader = document.getElementById('loader'); const loadingOverlay = document.getElementById('loading-overlay'); // Pyodide setup let pyodide = null; let isInitialized = false; // --- CONTROL ELEMENTS --- const textWrapToggle = document.getElementById('text-wrap-toggle'); const autoRerenderToggle = document.getElementById('auto-rerender-toggle'); const manualRerenderBtn = document.getElementById('manual-rerender'); const extractVariablesBtn = document.getElementById('extract-variables-header'); const toggleModeBtn = document.getElementById('toggle-mode'); const variablesForm = document.getElementById('variables-form'); const variablesHeader = document.getElementById('variables-header'); const copyTemplateBtn = document.getElementById('copy-template-btn'); const copyOutputBtn = document.getElementById('copy-output-btn'); const showWhitespaceToggle = document.getElementById('show-whitespace-toggle'); const themeToggle = document.getElementById('theme-toggle'); const markdownToggle = document.getElementById('markdown-toggle'); const mermaidToggle = document.getElementById('mermaid-toggle'); // --- STATE MANAGEMENT --- let isFormMode = false; let extractedVariables = new Set(); let currentVariableValues = {}; let isMarkdownMode = false; let isMermaidMode = false; let lastRenderedOutput = ''; // Store debounced function references for proper event listener removal let debouncedUpdateFromJinja = null; let debouncedUpdateFromVars = null; // --- RESIZE STATE --- let isResizing = false; let resizeType = null; let startX = 0; let startY = 0; let startWidth = 0; let startHeight = 0; // --- MERMAID SETUP --- // Initialize Mermaid with configuration mermaid.initialize({ startOnLoad: false, theme: document.body.classList.contains('dark-mode') ? 'dark' : 'default', securityLevel: 'loose', flowchart: { useMaxWidth: true, htmlLabels: true, curve: 'basis', wrap: true }, themeVariables: { fontSize: '14px' } }); // --- PYODIDE SETUP --- async function setupPyodide() { try { loader.style.display = 'block'; loadingOverlay.style.display = 'block'; pyodide = await loadPyodide(); await pyodide.loadPackage("jinja2"); isInitialized = true; loader.style.display = 'none'; loadingOverlay.style.display = 'none'; // Initial render after setup update(); } catch (error) { loader.textContent = `Failed to load Python environment: ${error.message}`; loader.style.color = '#d32f2f'; } } // --- CORE LOGIC --- /** * Provides visual feedback for button clicks */ function showButtonFeedback(button, message = 'Done!', duration = 1500) { const originalText = button.textContent; const originalBackground = button.style.background || getComputedStyle(button).backgroundColor; const successColor = getComputedStyle(document.documentElement).getPropertyValue('--success-color').trim(); button.textContent = message; button.style.background = successColor; button.disabled = true; setTimeout(() => { button.textContent = originalText; button.style.background = originalBackground; button.disabled = false; }, duration); } /** * Provides visual feedback for toggle switches */ function showToggleFeedback(toggleElement, message) { const successColor = getComputedStyle(document.documentElement).getPropertyValue('--success-color').trim(); // Create a temporary tooltip-like element const feedback = document.createElement('div'); feedback.textContent = message; feedback.style.cssText = ` position: absolute; background: ${successColor}; color: white; padding: 6px 10px; border-radius: 6px; font-size: 11px; font-weight: 500; z-index: 1000; pointer-events: none; transform: translateX(-50%); white-space: nowrap; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); `; // Position relative to the toggle const rect = toggleElement.getBoundingClientRect(); feedback.style.left = `${rect.left + rect.width / 2}px`; feedback.style.top = `${rect.top - 35}px`; document.body.appendChild(feedback); setTimeout(() => { if (feedback.parentNode) { feedback.parentNode.removeChild(feedback); } }, 1000); } /** * UPDATED: Renders text with visible whitespace characters without affecting layout. */ function renderWhitespace(text) { // First, escape any potential HTML in the text to prevent XSS const escapedText = text.replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); // Wrap whitespace characters in spans. The original characters are // preserved for layout, and CSS pseudo-elements add the visual symbols. return escapedText .replace(/ /g, ' ') .replace(/\t/g, '\t') .replace(/\n/g, '\n'); } /** * Renders markdown with Mermaid diagram support */ async function renderMarkdown(text) { // Store the text for later use lastRenderedOutput = text; // Extract mermaid code blocks before markdown parsing const mermaidBlocks = []; const mermaidPlaceholder = text.replace(/```mermaid\n([\s\S]*?)```/g, (match, code) => { mermaidBlocks.push(code.trim()); return `
`; }); // Parse markdown const html = marked.parse(mermaidPlaceholder); // Insert HTML into the output element markdownOutputElement.innerHTML = html; // Replace placeholders with actual mermaid diagrams const placeholders = markdownOutputElement.querySelectorAll('.mermaid-placeholder'); for (let i = 0; i < placeholders.length; i++) { const placeholder = placeholders[i]; const index = parseInt(placeholder.getAttribute('data-index')); const code = mermaidBlocks[index]; // Create a container for the mermaid diagram const mermaidDiv = document.createElement('div'); mermaidDiv.className = 'mermaid'; mermaidDiv.textContent = code; // Replace the placeholder placeholder.parentNode.replaceChild(mermaidDiv, placeholder); } // Render all mermaid diagrams try { await mermaid.run({ querySelector: '.markdown-content .mermaid' }); } catch (error) { console.error('Mermaid rendering error:', error); } } /** * Renders pure Mermaid diagram (assumes entire output is mermaid syntax) */ async function renderPureMermaid(text) { // Store the text for later use lastRenderedOutput = text; // Clear the markdown output and add a single mermaid diagram markdownOutputElement.innerHTML = ''; // Create a container for the mermaid diagram const mermaidDiv = document.createElement('div'); mermaidDiv.className = 'mermaid'; mermaidDiv.textContent = text.trim(); markdownOutputElement.appendChild(mermaidDiv); // Render the mermaid diagram try { await mermaid.run({ querySelector: '.markdown-content .mermaid' }); } catch (error) { console.error('Mermaid rendering error:', error); // Show error in a user-friendly way markdownOutputElement.innerHTML = `No variables found in template. Use {{ variable_name }} syntax.
'; return; } // Helper function to create form inputs recursively function createInputsForStructure(structure, baseName = '', level = 0) { const container = document.createElement('div'); container.style.marginLeft = `${level * 15}px`; if (Array.isArray(structure)) { // Handle arrays const label = document.createElement('label'); label.textContent = `${baseName} (Array)`; label.style.fontWeight = 'bold'; label.style.color = '#2196F3'; label.style.display = 'block'; label.style.marginBottom = '5px'; container.appendChild(label); const textarea = document.createElement('textarea'); textarea.id = `var-${baseName}`; textarea.name = baseName; textarea.value = JSON.stringify(structure, null, 2); textarea.placeholder = `JSON array for ${baseName}`; textarea.style.width = '100%'; textarea.style.minHeight = '80px'; textarea.style.padding = '6px 8px'; textarea.style.border = '1px solid #e0e0e0'; textarea.style.borderRadius = '4px'; textarea.style.fontSize = '12px'; textarea.style.fontFamily = '"Menlo", "Consolas", monospace'; textarea.style.marginBottom = '15px'; textarea.style.resize = 'vertical'; textarea.addEventListener('input', function() { try { const parsed = JSON.parse(this.value); currentVariableValues[baseName] = parsed; this.style.borderColor = '#e0e0e0'; } catch (e) { this.style.borderColor = '#d32f2f'; currentVariableValues[baseName] = this.value; } if (autoRerenderToggle.checked) { debounce(update, 300)(); } }); container.appendChild(textarea); } else if (typeof structure === 'object' && structure !== null) { // Handle objects if (baseName) { const label = document.createElement('label'); label.textContent = `${baseName} (Object)`; label.style.fontWeight = 'bold'; label.style.color = '#4CAF50'; label.style.display = 'block'; label.style.marginBottom = '5px'; container.appendChild(label); } // Check if it's a simple object (all values are primitives) const isSimpleObject = Object.values(structure).every(val => typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean' ); if (isSimpleObject && Object.keys(structure).length <= 5) { // Create individual inputs for simple objects Object.entries(structure).forEach(([key, value]) => { const inputDiv = document.createElement('div'); inputDiv.className = 'variable-input'; inputDiv.style.marginLeft = `${(level + 1) * 15}px`; const label = document.createElement('label'); label.textContent = `${baseName ? baseName + '.' : ''}${key}`; label.style.fontSize = '11px'; label.style.color = '#666'; const input = document.createElement('input'); input.type = typeof value === 'boolean' ? 'checkbox' : 'text'; input.id = `var-${baseName ? baseName + '.' : ''}${key}`; input.name = `${baseName ? baseName + '.' : ''}${key}`; if (typeof value === 'boolean') { input.checked = value; input.addEventListener('change', function() { const path = this.name.split('.'); let current = currentVariableValues; for (let i = 0; i < path.length - 1; i++) { if (!(path[i] in current)) current[path[i]] = {}; current = current[path[i]]; } current[path[path.length - 1]] = this.checked; if (autoRerenderToggle.checked) { debounce(update, 300)(); } }); } else { input.value = value; input.addEventListener('input', function() { const path = this.name.split('.'); let current = currentVariableValues; for (let i = 0; i < path.length - 1; i++) { if (!(path[i] in current)) current[path[i]] = {}; current = current[path[i]]; } current[path[path.length - 1]] = this.value; if (autoRerenderToggle.checked) { debounce(update, 300)(); } }); } inputDiv.appendChild(label); inputDiv.appendChild(input); container.appendChild(inputDiv); }); } else { // Complex object - use JSON textarea const textarea = document.createElement('textarea'); textarea.id = `var-${baseName}`; textarea.name = baseName; textarea.value = JSON.stringify(structure, null, 2); textarea.placeholder = `JSON object for ${baseName}`; textarea.style.width = '100%'; textarea.style.minHeight = '100px'; textarea.style.padding = '6px 8px'; textarea.style.border = '1px solid #e0e0e0'; textarea.style.borderRadius = '4px'; textarea.style.fontSize = '12px'; textarea.style.fontFamily = '"Menlo", "Consolas", monospace'; textarea.style.marginBottom = '15px'; textarea.style.resize = 'vertical'; textarea.addEventListener('input', function() { try { const parsed = JSON.parse(this.value); currentVariableValues[baseName] = parsed; this.style.borderColor = '#e0e0e0'; } catch (e) { this.style.borderColor = '#d32f2f'; currentVariableValues[baseName] = this.value; } if (autoRerenderToggle.checked) { debounce(update, 300)(); } }); container.appendChild(textarea); } } else { // Handle primitive values const inputDiv = document.createElement('div'); inputDiv.className = 'variable-input'; const label = document.createElement('label'); label.textContent = baseName; label.setAttribute('for', `var-${baseName}`); const input = document.createElement(typeof structure === 'boolean' ? 'input' : (typeof structure === 'string' && structure.length > 50) ? 'textarea' : 'input'); input.id = `var-${baseName}`; input.name = baseName; if (typeof structure === 'boolean') { input.type = 'checkbox'; input.checked = structure; input.addEventListener('change', function() { currentVariableValues[baseName] = this.checked; if (autoRerenderToggle.checked) { debounce(update, 300)(); } }); } else { if (input.tagName === 'TEXTAREA') { input.value = structure; input.style.minHeight = '60px'; input.style.resize = 'vertical'; } else { input.type = 'text'; input.value = structure; input.placeholder = `Enter value for ${baseName}`; } input.addEventListener('input', function() { currentVariableValues[baseName] = this.value; if (autoRerenderToggle.checked) { debounce(update, 300)(); } }); } inputDiv.appendChild(label); inputDiv.appendChild(input); container.appendChild(inputDiv); } return container; } // Create inputs for each top-level variable Object.entries(variableStructures).forEach(([varName, structure]) => { const container = createInputsForStructure(structure, varName); variablesForm.appendChild(container); }); } /** * Gets current variable values from form or JSON */ function getCurrentVariables() { if (isFormMode) { const formData = {}; variablesForm.querySelectorAll('input, textarea').forEach(input => { const varName = input.name; let value = input.value; // Try to parse as JSON if it looks like JSON if (value.trim().startsWith('{') || value.trim().startsWith('[') || value === 'true' || value === 'false' || (value.trim() && !isNaN(value.trim()))) { try { value = JSON.parse(value); } catch (e) { // Keep as string if not valid JSON } } formData[varName] = value; }); return formData; } else { try { return JSON.parse(varsEditor.getValue() || '{}'); } catch (e) { return {}; } } } /** * The main function to update the rendering. It gets triggered on any change. */ async function update() { if (!pyodide || !isInitialized) { outputElement.textContent = 'Python environment is still loading...'; outputElement.className = ''; return; } const template = jinjaEditor.getValue(); let context; // 1. Get variables from current mode (form or JSON) try { context = getCurrentVariables(); } catch (e) { // If there's an error getting variables, show it outputElement.textContent = `Error in variables:\n${e.message}`; outputElement.className = 'error'; return; } // 2. Render the template with the context using Python Jinja2 try { const contextJson = JSON.stringify(context); // Escape template and context strings for Python const escapedTemplate = template.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n'); const escapedContext = contextJson.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); const result = pyodide.runPython(` import jinja2 import json try: template_str = """${escapedTemplate}""" context_str = """${escapedContext}""" template = jinja2.Template(template_str) context = json.loads(context_str) result = template.render(context) except jinja2.exceptions.TemplateError as e: result = f"Jinja2 Template Error: {e}" except json.JSONDecodeError as e: result = f"JSON Error: {e}" except Exception as e: result = f"Error: {e}" result `); // Store the result lastRenderedOutput = result; // Set the main content based on mode if (isMermaidMode) { // Render as pure mermaid diagram outputElement.style.display = 'none'; markdownOutputElement.style.display = 'block'; await renderPureMermaid(result); } else if (isMarkdownMode) { // Render as markdown outputElement.style.display = 'none'; markdownOutputElement.style.display = 'block'; await renderMarkdown(result); } else { // Render as plain text outputElement.style.display = 'block'; markdownOutputElement.style.display = 'none'; if (showWhitespaceToggle.checked) { outputElement.innerHTML = renderWhitespace(result); } else { outputElement.textContent = result; } outputElement.className = result.includes('Error:') ? 'error' : ''; } } catch (e) { outputElement.textContent = `Python execution error: ${e.message}`; outputElement.className = 'error'; } } // --- CONTROL HANDLERS --- // Extract variables button extractVariablesBtn.addEventListener('click', function() { const template = jinjaEditor.getValue(); const newVariableStructures = extractVariablesFromTemplate(template); // Get current values from the active mode (form or JSON) const currentValues = getCurrentVariables(); // Merge existing values with new structure, preserving user data where possible function mergeStructures(newStruct, existingValues) { if (Array.isArray(newStruct)) { return existingValues && Array.isArray(existingValues) ? existingValues : newStruct; } else if (typeof newStruct === 'object' && newStruct !== null) { const merged = {}; Object.keys(newStruct).forEach(key => { if (existingValues && typeof existingValues === 'object' && key in existingValues) { merged[key] = mergeStructures(newStruct[key], existingValues[key]); } else { merged[key] = newStruct[key]; } }); return merged; } else { return existingValues !== undefined ? existingValues : newStruct; } } const mergedVariables = {}; Object.keys(newVariableStructures).forEach(varName => { mergedVariables[varName] = mergeStructures( newVariableStructures[varName], currentValues[varName] ); }); // Update state extractedVariables = new Set(Object.keys(newVariableStructures)); currentVariableValues = mergedVariables; // If in form mode, recreate the form if (isFormMode) { createVariableForm(newVariableStructures); } // Update JSON editor to reflect current values varsEditor.setValue(JSON.stringify(mergedVariables, null, 2)); // Re-render update(); // Show feedback const variableCount = Object.keys(newVariableStructures).length; const message = variableCount > 0 ? `Found ${variableCount} variable${variableCount !== 1 ? 's' : ''}!` : 'No variables found!'; showButtonFeedback(this, message, 2000); }); // Mode toggle button toggleModeBtn.addEventListener('click', function() { const wasFormMode = isFormMode; isFormMode = !isFormMode; if (isFormMode) { // Switch to form mode varsEditor.getWrapperElement().style.display = 'none'; variablesForm.style.display = 'block'; toggleModeBtn.textContent = 'Switch to JSON Mode'; variablesHeader.textContent = 'Variables (Form)'; // Get current variables from JSON and update our state try { const currentVars = JSON.parse(varsEditor.getValue() || '{}'); currentVariableValues = currentVars; // Convert to the structure format expected by createVariableForm const variableStructures = {}; Object.keys(currentVars).forEach(key => { variableStructures[key] = currentVars[key]; }); extractedVariables = new Set(Object.keys(currentVars)); // Create form with current variables createVariableForm(variableStructures); } catch (e) { // If JSON is invalid, keep existing state or create empty form createVariableForm({}); } } else { // Switch to JSON mode varsEditor.getWrapperElement().style.display = 'block'; variablesForm.style.display = 'none'; toggleModeBtn.textContent = 'Switch to Form Mode'; variablesHeader.textContent = 'Variables (JSON)'; // Update JSON editor with current form values const currentVars = getCurrentVariables(); varsEditor.setValue(JSON.stringify(currentVars, null, 2)); } // Show feedback const mode = isFormMode ? 'Form' : 'JSON'; showButtonFeedback(this, `Switched to ${mode}!`, 1500); }); // Text wrap toggle textWrapToggle.addEventListener('change', function() { const wrapMode = this.checked; jinjaEditor.setOption('lineWrapping', wrapMode); varsEditor.setOption('lineWrapping', wrapMode); // Show feedback const message = wrapMode ? 'Text wrap enabled!' : 'Text wrap disabled!'; showToggleFeedback(this.parentElement, message); }); // Whitespace toggle showWhitespaceToggle.addEventListener('change', function() { update(); // Re-render the output with the new setting const message = this.checked ? 'Whitespace visible' : 'Whitespace hidden'; showToggleFeedback(this.parentElement, message); }); // Markdown toggle markdownToggle.addEventListener('change', async function() { if (this.checked) { // Disable mermaid mode if it's on if (isMermaidMode) { mermaidToggle.checked = false; isMermaidMode = false; } isMarkdownMode = true; // Switch to markdown mode outputElement.style.display = 'none'; markdownOutputElement.style.display = 'block'; // If we have output, render it as markdown if (lastRenderedOutput) { await renderMarkdown(lastRenderedOutput); } // Disable whitespace toggle in markdown mode showWhitespaceToggle.disabled = true; showWhitespaceToggle.parentElement.style.opacity = '0.5'; // Show feedback showToggleFeedback(this.parentElement, 'Markdown mode enabled!'); } else { isMarkdownMode = false; // Switch to plain text mode outputElement.style.display = 'block'; markdownOutputElement.style.display = 'none'; // Re-render as plain text if (lastRenderedOutput) { if (showWhitespaceToggle.checked) { outputElement.innerHTML = renderWhitespace(lastRenderedOutput); } else { outputElement.textContent = lastRenderedOutput; } outputElement.className = lastRenderedOutput.includes('Error:') ? 'error' : ''; } // Re-enable whitespace toggle showWhitespaceToggle.disabled = false; showWhitespaceToggle.parentElement.style.opacity = '1'; // Show feedback showToggleFeedback(this.parentElement, 'Plain text mode enabled!'); } }); // Mermaid toggle mermaidToggle.addEventListener('change', async function() { if (this.checked) { // Disable markdown mode if it's on if (isMarkdownMode) { markdownToggle.checked = false; isMarkdownMode = false; } isMermaidMode = true; // Switch to mermaid mode outputElement.style.display = 'none'; markdownOutputElement.style.display = 'block'; // If we have output, render it as mermaid if (lastRenderedOutput) { await renderPureMermaid(lastRenderedOutput); } // Disable whitespace toggle in mermaid mode showWhitespaceToggle.disabled = true; showWhitespaceToggle.parentElement.style.opacity = '0.5'; // Show feedback showToggleFeedback(this.parentElement, 'Mermaid mode enabled!'); } else { isMermaidMode = false; // Switch to plain text mode outputElement.style.display = 'block'; markdownOutputElement.style.display = 'none'; // Re-render as plain text if (lastRenderedOutput) { if (showWhitespaceToggle.checked) { outputElement.innerHTML = renderWhitespace(lastRenderedOutput); } else { outputElement.textContent = lastRenderedOutput; } outputElement.className = lastRenderedOutput.includes('Error:') ? 'error' : ''; } // Re-enable whitespace toggle showWhitespaceToggle.disabled = false; showWhitespaceToggle.parentElement.style.opacity = '1'; // Show feedback showToggleFeedback(this.parentElement, 'Plain text mode enabled!'); } }); // Auto rerender toggle autoRerenderToggle.addEventListener('change', function() { manualRerenderBtn.disabled = this.checked; setupEventListeners(); // Show feedback const message = this.checked ? 'Auto rerender enabled!' : 'Auto rerender disabled!'; showToggleFeedback(this.parentElement, message); }); // Manual rerender button manualRerenderBtn.addEventListener('click', function() { update(); showButtonFeedback(this, 'Rerendered!', 1000); }); // Copy template button copyTemplateBtn.addEventListener('click', async function() { try { const templateContent = jinjaEditor.getValue(); await navigator.clipboard.writeText(templateContent); showButtonFeedback(this, 'Copied!', 1500); } catch (err) { // Fallback for older browsers const textArea = document.createElement('textarea'); textArea.value = jinjaEditor.getValue(); document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); showButtonFeedback(this, 'Copied!', 1500); } }); // Copy output button copyOutputBtn.addEventListener('click', async function() { try { const outputContent = outputElement.textContent; await navigator.clipboard.writeText(outputContent); showButtonFeedback(this, 'Copied!', 1500); } catch (err) { // Fallback for older browsers const textArea = document.createElement('textarea'); textArea.value = outputElement.textContent; document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); showButtonFeedback(this, 'Copied!', 1500); } }); // Theme toggle themeToggle.addEventListener('change', function() { const isLightMode = this.checked; if (isLightMode) { // Switch to light mode document.body.classList.remove('dark-mode'); localStorage.setItem('theme', 'light'); jinjaEditor.setOption('theme', 'default'); varsEditor.setOption('theme', 'default'); // Update Mermaid theme mermaid.initialize({ startOnLoad: false, theme: 'default', securityLevel: 'loose', flowchart: { useMaxWidth: true, htmlLabels: true, curve: 'basis', wrap: true }, themeVariables: { fontSize: '14px' } }); } else { // Switch to dark mode document.body.classList.add('dark-mode'); localStorage.setItem('theme', 'dark'); jinjaEditor.setOption('theme', 'material-darker'); varsEditor.setOption('theme', 'material-darker'); // Update Mermaid theme mermaid.initialize({ startOnLoad: false, theme: 'dark', securityLevel: 'loose', flowchart: { useMaxWidth: true, htmlLabels: true, curve: 'basis', wrap: true }, themeVariables: { fontSize: '14px' } }); } // If in markdown or mermaid mode, re-render to apply new Mermaid theme if (isMarkdownMode && lastRenderedOutput) { renderMarkdown(lastRenderedOutput); } else if (isMermaidMode && lastRenderedOutput) { renderPureMermaid(lastRenderedOutput); } // Refresh CodeMirror editors to apply theme setTimeout(() => { jinjaEditor.refresh(); varsEditor.refresh(); }, 10); }); // --- EVENT LISTENERS --- // Conditional event listeners based on auto-rerender setting function setupEventListeners() { // Remove any existing listeners first if (debouncedUpdateFromJinja) { jinjaEditor.off('change', debouncedUpdateFromJinja); } if (debouncedUpdateFromVars) { varsEditor.off('change', debouncedUpdateFromVars); } if (autoRerenderToggle.checked) { // Create new debounced functions and store references debouncedUpdateFromJinja = debounce(update, 300); debouncedUpdateFromVars = debounce(update, 300); // Add the event listeners jinjaEditor.on('change', debouncedUpdateFromJinja); varsEditor.on('change', debouncedUpdateFromVars); } else { // Clear the references when disabled debouncedUpdateFromJinja = null; debouncedUpdateFromVars = null; } } // Debounce function to prevent too frequent updates function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // --- RESIZE FUNCTIONALITY --- // Get resize elements const horizontalResize = document.getElementById('horizontal-resize'); const verticalResize = document.getElementById('vertical-resize'); const leftPanel = document.getElementById('left-panel'); const rightPanel = document.getElementById('right-panel'); const templatePane = document.getElementById('template-pane'); const variablesPane = document.getElementById('variables-pane'); const mainContainer = document.getElementById('main-container'); // Initialize default sizes let leftPanelWidth = 50; // percentage let templatePaneHeight = 60; // percentage function setInitialSizes() { const containerRect = mainContainer.getBoundingClientRect(); leftPanel.style.width = `${leftPanelWidth}%`; rightPanel.style.width = `${100 - leftPanelWidth}%`; const leftPanelRect = leftPanel.getBoundingClientRect(); templatePane.style.height = `${templatePaneHeight}%`; variablesPane.style.height = `${100 - templatePaneHeight}%`; } // Horizontal resize (between template and variables) horizontalResize.addEventListener('mousedown', function(e) { isResizing = true; resizeType = 'horizontal'; startY = e.clientY; const leftPanelRect = leftPanel.getBoundingClientRect(); const templateRect = templatePane.getBoundingClientRect(); startHeight = templateRect.height; document.addEventListener('mousemove', handleResize); document.addEventListener('mouseup', stopResize); e.preventDefault(); }); // Vertical resize (between left and right panels) verticalResize.addEventListener('mousedown', function(e) { isResizing = true; resizeType = 'vertical'; startX = e.clientX; const containerRect = mainContainer.getBoundingClientRect(); const leftRect = leftPanel.getBoundingClientRect(); startWidth = leftRect.width; document.addEventListener('mousemove', handleResize); document.addEventListener('mouseup', stopResize); e.preventDefault(); }); function handleResize(e) { if (!isResizing) return; if (resizeType === 'horizontal') { const deltaY = e.clientY - startY; const leftPanelRect = leftPanel.getBoundingClientRect(); const newTemplateHeight = startHeight + deltaY; const minHeight = 100; const maxHeight = leftPanelRect.height - minHeight - 4; // 4px for resize handle if (newTemplateHeight >= minHeight && newTemplateHeight <= maxHeight) { const templatePercentage = (newTemplateHeight / leftPanelRect.height) * 100; const variablesPercentage = 100 - templatePercentage; templatePane.style.height = `${templatePercentage}%`; variablesPane.style.height = `${variablesPercentage}%`; templatePaneHeight = templatePercentage; } } else if (resizeType === 'vertical') { const deltaX = e.clientX - startX; const containerRect = mainContainer.getBoundingClientRect(); const newLeftWidth = startWidth + deltaX; const minWidth = 200; const maxWidth = containerRect.width - minWidth - 4; // 4px for resize handle if (newLeftWidth >= minWidth && newLeftWidth <= maxWidth) { const leftPercentage = (newLeftWidth / containerRect.width) * 100; const rightPercentage = 100 - leftPercentage; leftPanel.style.width = `${leftPercentage}%`; rightPanel.style.width = `${rightPercentage}%`; leftPanelWidth = leftPercentage; } } // Refresh CodeMirror editors after resize setTimeout(() => { jinjaEditor.refresh(); varsEditor.refresh(); }, 10); } function stopResize() { isResizing = false; resizeType = null; document.removeEventListener('mousemove', handleResize); document.removeEventListener('mouseup', stopResize); } // Handle window resize window.addEventListener('resize', function() { setTimeout(() => { jinjaEditor.refresh(); varsEditor.refresh(); }, 100); }); // Initial setup setInitialSizes(); setupEventListeners(); // Load saved theme preference const savedTheme = localStorage.getItem('theme'); if (savedTheme === 'light') { document.body.classList.remove('dark-mode'); themeToggle.checked = true; jinjaEditor.setOption('theme', 'default'); varsEditor.setOption('theme', 'default'); } else { // Default to dark mode document.body.classList.add('dark-mode'); themeToggle.checked = false; jinjaEditor.setOption('theme', 'material-darker'); varsEditor.setOption('theme', 'material-darker'); } // Start Pyodide and initial render setupPyodide();