// ========================================================================== // App State & DOM References // ========================================================================== let state = { chapters: [], currentChapterIndex: 0, themeMode: 'auto', // 'auto' | 'light' | 'dark' — what the user picked theme: 'dark', // 'dark' | 'light' — resolved theme actually applied layout: 'split', // 'split' | 'reader' | 'editor' flashEditor: null, zigEditor: null, editorLoaded: false, backend: true // false on static hosting (GitHub Pages) — no transpile API }; const dom = { chaptersList: document.getElementById('chapters-list'), chapterContent: document.getElementById('chapter-content'), prevBtn: document.getElementById('prev-btn'), nextBtn: document.getElementById('next-btn'), themeToggle: document.getElementById('theme-toggle'), layoutToggle: document.getElementById('layout-toggle'), transpileBtn: document.getElementById('transpile-btn'), clearTerminal: document.getElementById('clear-terminal'), terminalBody: document.getElementById('terminal-body'), terminalStatus: document.getElementById('terminal-status'), workspacePane: document.querySelector('.workspace-pane'), playgroundPanel: document.getElementById('playground-panel'), contentPanel: document.getElementById('content-panel'), tabButtons: document.querySelectorAll('.tab-btn'), tabContents: document.querySelectorAll('.tab-content'), flashEditorContainer: document.getElementById('editor-container'), zigEditorContainer: document.getElementById('output-container'), sidebar: document.getElementById('sidebar'), sidebarToggle: document.getElementById('sidebar-toggle'), sidebarBackdrop: document.getElementById('sidebar-backdrop'), terminalDrawer: document.getElementById('terminal-drawer'), terminalHeader: document.getElementById('terminal-header') }; // Narrow screens get drawer navigation + touch-friendly editor defaults. const mobileQuery = window.matchMedia('(max-width: 992px)'); const phoneQuery = window.matchMedia('(max-width: 600px)'); // ========================================================================== // Off-canvas Sidebar (narrow screens) // ========================================================================== function setSidebarOpen(open) { dom.sidebar.classList.toggle('open', open); dom.sidebarBackdrop.classList.toggle('visible', open); dom.sidebarToggle.setAttribute('aria-expanded', String(open)); } function initSidebarDrawer() { dom.sidebarToggle.addEventListener('click', () => { setSidebarOpen(!dom.sidebar.classList.contains('open')); }); dom.sidebarBackdrop.addEventListener('click', () => setSidebarOpen(false)); // Leaving the narrow layout: drop the drawer state so the static sidebar shows normally. mobileQuery.addEventListener('change', (e) => { if (!e.matches) setSidebarOpen(false); }); } // ========================================================================== // Collapsible Terminal Drawer // ========================================================================== function setTerminalCollapsed(collapsed) { dom.terminalDrawer.classList.toggle('collapsed', collapsed); // Monaco shares the column with the drawer — re-measure after the height transition. setTimeout(() => { if (state.flashEditor) state.flashEditor.layout(); if (state.zigEditor) state.zigEditor.layout(); }, 280); } function initTerminalDrawer() { dom.terminalHeader.addEventListener('click', (e) => { if (e.target.closest('#clear-terminal')) return; // clear button keeps its own action setTerminalCollapsed(!dom.terminalDrawer.classList.contains('collapsed')); }); // Phones: start collapsed so the editor gets the vertical space. if (phoneQuery.matches) setTerminalCollapsed(true); } // ========================================================================== // Theme and Layout Controls // ========================================================================== function initThemeAndLayout() { // Theme: 'auto' follows the OS; 'light'/'dark' are manual overrides. Default 'auto'. setThemeMode(localStorage.getItem('theme') || 'auto'); // Cycle auto -> light -> dark -> auto on each click. dom.themeToggle.addEventListener('click', () => { const cycle = ['auto', 'light', 'dark']; const next = cycle[(cycle.indexOf(state.themeMode) + 1) % cycle.length]; setThemeMode(next); }); // While in 'auto', live-follow the OS theme. if (window.matchMedia) { window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { if (state.themeMode === 'auto') applyResolvedTheme(getSystemTheme()); }); } // Layout const savedLayout = localStorage.getItem('layout') || 'split'; setLayout(savedLayout); dom.layoutToggle.addEventListener('click', () => { const layoutModes = ['split', 'reader', 'editor']; const nextIndex = (layoutModes.indexOf(state.layout) + 1) % layoutModes.length; setLayout(layoutModes[nextIndex]); }); // Tab buttons for Editor/Zig Output switcher dom.tabButtons.forEach(btn => { btn.addEventListener('click', () => { const targetTab = btn.getAttribute('data-tab'); switchTab(targetTab); }); }); } function getSystemTheme() { return window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; } // Set the user's chosen mode, persist it, update the button icon, and apply the result. function setThemeMode(mode) { state.themeMode = mode; localStorage.setItem('theme', mode); dom.themeToggle.dataset.mode = mode; dom.themeToggle.setAttribute('aria-label', `Theme: ${mode} (click to cycle auto/light/dark)`); dom.themeToggle.title = `Theme: ${mode}`; applyResolvedTheme(mode === 'auto' ? getSystemTheme() : mode); } // Apply a concrete 'dark'/'light' theme to the body and Monaco — no persistence. function applyResolvedTheme(theme) { state.theme = theme; if (theme === 'dark') { document.body.classList.remove('light-theme'); document.body.classList.add('dark-theme'); } else { document.body.classList.remove('dark-theme'); document.body.classList.add('light-theme'); } // Update Monaco editor themes if loaded if (state.editorLoaded && window.monaco) { const monacoTheme = theme === 'dark' ? 'atomo-one-dark' : 'atomo-one-light'; monaco.editor.setTheme(monacoTheme); // Re-colourise the reader's static blocks so they track the new theme. highlightFlashBlocks(dom.chapterContent); } } function setLayout(layout) { state.layout = layout; localStorage.setItem('layout', layout); // Reset layout classes dom.workspacePane.classList.remove('reader-only', 'editor-only'); if (layout === 'reader') { dom.workspacePane.classList.add('reader-only'); } else if (layout === 'editor') { dom.workspacePane.classList.add('editor-only'); } // Trigger monaco layout adjustment setTimeout(() => { if (state.flashEditor) state.flashEditor.layout(); if (state.zigEditor) state.zigEditor.layout(); }, 250); } function switchTab(tabName) { dom.tabButtons.forEach(btn => { if (btn.getAttribute('data-tab') === tabName) { btn.classList.add('active'); } else { btn.classList.remove('active'); } }); dom.tabContents.forEach(content => { if (content.id === `tab-${tabName}`) { content.classList.add('active'); } else { content.classList.remove('active'); } }); // Trigger editor layout adjustments setTimeout(() => { if (tabName === 'editor' && state.flashEditor) state.flashEditor.layout(); if (tabName === 'output' && state.zigEditor) state.zigEditor.layout(); }, 50); } // ========================================================================== // Monaco Editor Integration & Custom Highlight Definition // ========================================================================== function initMonaco() { require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.39.0/min/vs' } }); require(['vs/editor/editor.main'], function () { // 1. Register a new custom language for Flash monaco.languages.register({ id: 'flash' }); // 2. Define tokens for the Flash language monaco.languages.setMonarchTokensProvider('flash', { keywords: [ 'use', 'link', 'fn', 'const', 'var', 'export', 'noreturn', 'orelse', 'try', 'catch', 'defer', 'errdefer', 'struct', 'enum', 'union', 'if', 'else', 'while', 'for', 'in', 'pub', 'as', 'break', 'continue', 'undefined' ], typeKeywords: [ 'u8', 'u16', 'u32', 'u64', 'i8', 'i16', 'i32', 'i64', 'usize', 'isize', 'cstr', 'argv', 'bool', 'void', 'f32', 'f64' ], operators: [ '=', '+=', '-=', '*=', '/=', '%=', '==', '!=', '<', '<=', '>', '>=', '&&', '||', '!', '&', '|', '^', '<<', '>>', '->', ':=', ':', '::', '.' ], symbols: /[=> c.id === hash); if (activeChapterIndex !== -1) { await selectChapter(activeChapterIndex); } else { await selectChapter(0); } } catch (err) { console.error("Failed to fetch chapters metadata:", err); dom.chaptersList.innerHTML = ``; } } function renderChaptersSidebar() { dom.chaptersList.innerHTML = ''; state.chapters.forEach((chapter, index) => { const button = document.createElement('button'); button.className = `nav-item ${index === state.currentChapterIndex ? 'active' : ''}`; button.textContent = chapter.title; button.addEventListener('click', () => { selectChapter(index); }); dom.chaptersList.appendChild(button); }); } async function selectChapter(index) { if (index < 0 || index >= state.chapters.length) return; state.currentChapterIndex = index; const chapter = state.chapters[index]; // Update active class on sidebar items const navItems = dom.chaptersList.querySelectorAll('.nav-item'); navItems.forEach((item, i) => { if (i === index) { item.classList.add('active'); } else { item.classList.remove('active'); } }); // Update Prev/Next buttons dom.prevBtn.disabled = index === 0; dom.nextBtn.disabled = index === state.chapters.length - 1; // Narrow screens: picking a chapter closes the drawer so content is readable. if (mobileQuery.matches) setSidebarOpen(false); // Update URL hash window.location.hash = chapter.id; // Fetch and render markdown content dom.chapterContent.innerHTML = `

Loading chapter: ${chapter.title}...

`; try { const response = await fetch(chapter.file); if (!response.ok) throw new Error(`Status: ${response.status}`); const markdown = await response.text(); // Configure marked options marked.setOptions({ gfm: true, breaks: true }); // Set custom marked renderer to detect code blocks and output example blocks const renderer = new marked.Renderer(); let codeBlockCounter = 0; renderer.code = function(codeOrToken, language) { let codeText = ""; let lang = ""; // Handle token parameter as object (marked v11+) or string (marked /g, '>') .replace(/"/g, '"') .replace(/'/g, '''); return `
Example ${codeBlockCounter}
${escapedCode}
`; } return `
${codeText}
`; }; renderer.blockquote = function(token) { let text = ""; let htmlContent = ""; // Handle token parameter as object (marked v11+) or string (marked \s*(?:)?\s*/i, '

'); return `

${titleText}
${innerHtml}
`; } return `
${htmlContent}
`; }; dom.chapterContent.innerHTML = marked.parse(markdown, { renderer }); // Re-run lucide icons rendering lucide.createIcons(); // Syntax-highlight the static Flash example blocks (no-op until Monaco // is ready; initMonaco re-runs this for the first-loaded chapter). highlightFlashBlocks(dom.chapterContent); // Bind load events to the dynamically generated "Try in Editor" buttons dom.chapterContent.querySelectorAll('.load-example-btn').forEach(btn => { btn.addEventListener('click', (e) => { const code = btn.getAttribute('data-code') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, "'"); loadCodeIntoEditor(code); }); }); // Scroll reader panel back to top dom.contentPanel.querySelector('.content-body').scrollTop = 0; } catch (err) { console.error("Failed to load chapter file:", err); dom.chapterContent.innerHTML = `
Error Loading Content

Could not load the markdown documentation for this chapter. Make sure the file exists under public/${chapter.file}.

Details: ${err.message}

`; } } function loadCodeIntoEditor(code) { if (state.flashEditor) { state.flashEditor.setValue(code.trim()); logTerminal("Loaded example code into Flash Editor.", "info"); if (phoneQuery.matches) { // Phones: a stacked split leaves the editor a sliver — go editor-only. setLayout('editor'); } else if (state.layout === 'reader') { // If the layout is 'reader-only', expand to 'split' so the user can see the editor setLayout('split'); } // Focus on the editor tab switchTab('editor'); } } // ========================================================================== // Static Code-Block Highlighting (reader pane) // -------------------------------------------------------------------------- // Reuse the Flash grammar already registered with Monaco (Monarch tokenizer + // the One Dark / One Light themes) to colourise the example
 blocks in
// the reader, so reader and editor highlight identically — no extra grammar
// or wasm in the browser. Re-run on theme switch so colours track the theme.
async function highlightFlashBlocks(root) {
    if (!(state.editorLoaded && window.monaco)) return;
    const scope = root || document;
    const blocks = scope.querySelectorAll('pre code.language-flash, pre code.language-go');
    for (const code of blocks) {
        // Stash the original source the first time so re-highlights start from
        // plain text, not already-colourised markup.
        if (code.dataset.raw === undefined) code.dataset.raw = code.textContent;
        try {
            code.innerHTML = await monaco.editor.colorize(code.dataset.raw, 'flash', { tabSize: 4 });
        } catch (err) {
            console.warn('Flash highlight failed:', err);
        }
    }
}

// ==========================================================================
// Transpiler API Pipeline Integration
// ==========================================================================
// One-line notice shown whenever the transpile backend is absent (static build).
function logStaticNotice() {
    logTerminal(
        "Static build — reading chapters and loading examples into the editor work, " +
        "but live transpilation needs the local dev server.\n" +
        "Clone https://github.com/ajhahnde/Flash and run `npm start` in tutorial/.",
        "warning"
    );
}

async function runTranspilation() {
    if (!state.flashEditor) return;

    if (!state.backend) {
        updateTerminalStatus('idle', 'Static');
        logStaticNotice();
        return;
    }

    const code = state.flashEditor.getValue();
    
    updateTerminalStatus('transpiling', 'Transpiling...');
    logTerminal("Invoking compiler backend...", "info");

    try {
        const response = await fetch('/api/transpile', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ code })
        });

        const result = await response.json();

        if (result.success) {
            // Update output editor
            if (state.zigEditor) {
                state.zigEditor.setValue(result.output || '// Empty output.');
            }
            
            // Switch view to output
            switchTab('output');
            
            // Log status
            updateTerminalStatus('success', 'Success');
            
            let logMsg = "Transpilation succeeded.\n";
            if (result.error) {
                logMsg += `\nCompiler Warnings:\n${result.error}`;
                logTerminal(logMsg, "warning");
            } else {
                logTerminal(logMsg + "No compiler errors or warnings.", "success");
            }
            
        } else {
            // Transpilation failed (compiler returned non-zero code)
            updateTerminalStatus('error', 'Compiler Error');
            logTerminal(`Compiler Execution Failed:\n\n${result.error}`, "error");
            
            // Focus on terminal drawer if hidden/small
            console.warn("Compilation failed. Visual logs added to terminal drawer.");
        }

    } catch (err) {
        console.error("Transpilation API call failed:", err);
        updateTerminalStatus('error', 'Network Error');
        logTerminal(`HTTP network error while communicating with backend: ${err.message}\nEnsure the local server is running on http://localhost:3000.`, "error");
    }
}

// ==========================================================================
// Terminal Drawer Logger Utilities
// ==========================================================================
function logTerminal(message, type = 'info') {
    dom.terminalBody.classList.remove('log-error', 'log-success', 'log-warning');
    
    const timestamp = new Date().toLocaleTimeString();
    const prefix = `[${timestamp}] `;
    
    dom.terminalBody.innerText = prefix + message;

    if (type === 'error') {
        dom.terminalBody.classList.add('log-error');
    } else if (type === 'success') {
        dom.terminalBody.classList.add('log-success');
    } else if (type === 'warning') {
        dom.terminalBody.classList.add('log-warning');
    }

    // Errors and warnings must be readable — pop the drawer open if collapsed.
    if ((type === 'error' || type === 'warning') && dom.terminalDrawer.classList.contains('collapsed')) {
        setTerminalCollapsed(false);
    }
}

function updateTerminalStatus(statusClass, label) {
    dom.terminalStatus.className = `status-indicator ${statusClass}`;
    dom.terminalStatus.textContent = label;
}

// ==========================================================================
// Initialization
// ==========================================================================
document.addEventListener('DOMContentLoaded', () => {
    // 0. Probe the transpile backend once; static hosting (GitHub Pages) has none.
    fetch('/api/chapters')
        .then(r => { state.backend = r.ok; })
        .catch(() => { state.backend = false; })
        .finally(() => {
            if (!state.backend) {
                updateTerminalStatus('idle', 'Static');
                logStaticNotice();
            }
        });

    // 1. Initialize icons
    lucide.createIcons();

    // 2. Initialize UI configuration (Themes & layout)
    initThemeAndLayout();
    initSidebarDrawer();
    initTerminalDrawer();

    // 3. Load Monaco Code Editors
    initMonaco();

    // 4. Fetch chapters navigation list
    loadChapters();

    // 5. Bind action buttons
    dom.transpileBtn.addEventListener('click', runTranspilation);
    
    dom.clearTerminal.addEventListener('click', () => {
        dom.terminalBody.innerHTML = 'Terminal cleared.';
        updateTerminalStatus('idle', 'Idle');
    });

    dom.prevBtn.addEventListener('click', () => {
        if (state.currentChapterIndex > 0) {
            selectChapter(state.currentChapterIndex - 1);
        }
    });

    dom.nextBtn.addEventListener('click', () => {
        if (state.currentChapterIndex < state.chapters.length - 1) {
            selectChapter(state.currentChapterIndex + 1);
        }
    });

    // Handle back/forward history navigation via hash change
    window.addEventListener('hashchange', () => {
        const hash = window.location.hash.substring(1);
        const activeChapterIndex = state.chapters.findIndex(c => c.id === hash);
        if (activeChapterIndex !== -1 && activeChapterIndex !== state.currentChapterIndex) {
            selectChapter(activeChapterIndex);
        }
    });
});