JS
hello_world.js
●
javascript/hello_world.js
⟲ Reset
✕ Clear
▶ Run
⌘↵
hello_world.js
JavaScript
Idle
Copy
Clear
Press ▶ Run (or ⌘/Ctrl+Enter) to execute the editor's code.
Loading…
×
Embedded from
source ↗
{ b.classList.toggle('active', b.dataset.file === name); }); fileNameEl.firstChild.textContent = name; if(!state.loaded[name]){ editor.setValue('// Loading ' + name + '…'); await loadFile(name); } // (Re)compute steps from the current "All" buffer state.steps[name] = parseSteps(state.buffers[name] ?? state.originals[name] ?? ''); // First time this file is opened: default to the LAST step when there are // multiple steps; otherwise fall back to All (0). if(state.activeStep[name] == null){ const ss = state.steps[name]; state.activeStep[name] = (ss && ss.length >= 2) ? ss[ss.length - 1].n : 0; } // If a previously-active step no longer exists (e.g. headings removed), fall back to the last available step or All if(state.activeStep[name] !== 0 && !(state.steps[name] || []).some(s => s.n === state.activeStep[name])){ const ss = state.steps[name]; state.activeStep[name] = (ss && ss.length) ? ss[ss.length - 1].n : 0; } loadEditorForCurrentStep(); renderStepTabs(); renderFileLinks(); refreshDirty(name); editor.focus(); } function refreshDirty(name){ // "Dirty" = the file's canonical (All) buffer differs from the original. // The editor view may be a step extraction — that derivation is not // a real edit, so don't read editor.getValue() here. const buf = state.buffers[name]; const dirty = state.loaded[name] && buf != null && buf !== state.originals[name]; const btn = document.querySelector(`.file-btn[data-file="${name}"]`); if(btn) btn.classList.toggle('dirty', dirty); if(name === state.active){ fileNameEl.classList.toggle('dirty', dirty); } } editor.on('change', ()=>{ if(!state.active) return; const file = state.active; const step = state.activeStep[file] || 0; const v = editor.getValue(); if(step === 0){ state.buffers[file] = v; if(state.stepEdits[file]) delete state.stepEdits[file]; const newSteps = parseSteps(v); state.steps[file] = newSteps; renderStepTabs(); }else{ if(!state.stepEdits[file]) state.stepEdits[file] = {}; state.stepEdits[file][step] = v; } refreshDirty(file); }); /* ========================================================== Reset / Clear / Run ========================================================== */ document.getElementById('resetBtn').addEventListener('click', ()=>{ const file = state.active; if(!file || state.originals[file] == null) return; const step = state.activeStep[file] || 0; if(step === 0){ state.buffers[file] = state.originals[file]; if(state.stepEdits[file]) delete state.stepEdits[file]; state.steps[file] = parseSteps(state.originals[file]); loadEditorForCurrentStep(); renderStepTabs(); toast('Reset ' + file); }else{ // Forget any in-flight edits for this step, then regenerate from current All buffer if(state.stepEdits[file]) delete state.stepEdits[file][step]; loadEditorForCurrentStep(); toast(`Reset step ${step}`); } refreshDirty(file); }); document.getElementById('clearBtn').addEventListener('click', clearConsole); document.getElementById('clearOutBtn').addEventListener('click', clearConsole); document.getElementById('runBtn').addEventListener('click', () => { if(isRunning) stopRun('user'); else runCode(); }); document.getElementById('copyOutBtn').addEventListener('click', ()=>{ const out = document.getElementById('consoleOut'); const text = Array.from(out.querySelectorAll('.line .c')).map(n=>n.textContent).join('\n'); if(!text){ toast('Nothing to copy'); return; } navigator.clipboard?.writeText(text).then(()=> toast('Output copied')); }); window.addEventListener('keydown', (e)=>{ if((e.metaKey || e.ctrlKey) && e.key === 'Enter'){ e.preventDefault(); runCode(); } }); /* ========================================================== Console output ========================================================== */ const consoleOut = document.getElementById('consoleOut'); const consoleStatus = document.getElementById('consoleStatus'); const consoleStats = document.getElementById('consoleStats'); let runStartTime = 0; let logCount = 0; let warnCount = 0; let errorCount = 0; function clearConsole(){ consoleOut.innerHTML = '
Console cleared.
'; logCount = warnCount = errorCount = 0; consoleStats.textContent = ''; consoleStatus.classList.remove('error','running'); consoleStatus.textContent = 'Idle'; } function ensureConsoleEmptyCleared(){ const empty = consoleOut.querySelector('.empty'); if(empty) empty.remove(); } function ts(){ const now = (performance.now() - runStartTime) / 1000; return (now >= 0 && runStartTime > 0) ? '+' + now.toFixed(3) + 's' : ''; } function appendLine(kind, parts){ ensureConsoleEmptyCleared(); const line = document.createElement('div'); line.className = 'line ' + kind; const t = document.createElement('span'); t.className = 't'; t.textContent = ts(); const c = document.createElement('span'); c.className = 'c'; c.textContent = parts.map(formatVal).join(' '); line.append(t, c); consoleOut.appendChild(line); consoleOut.scrollTop = consoleOut.scrollHeight; } function appendTable(parts){ ensureConsoleEmptyCleared(); const data = parts[0]; if(data == null || typeof data !== 'object'){ appendLine('log', parts); return; } const rows = Array.isArray(data) ? data : Object.entries(data).map(([k,v])=>({ '(key)':k, value:v })); const cols = new Set(); rows.forEach(r => { if(r && typeof r === 'object') Object.keys(r).forEach(k=>cols.add(k)); else cols.add('value'); }); const colArr = ['(index)', ...cols]; const tbl = document.createElement('table'); const thead = document.createElement('thead'); const trH = document.createElement('tr'); colArr.forEach(c=>{ const th = document.createElement('th'); th.textContent = c; trH.appendChild(th); }); thead.appendChild(trH); tbl.appendChild(thead); const tbody = document.createElement('tbody'); rows.forEach((r, i)=>{ const tr = document.createElement('tr'); colArr.forEach(col=>{ const td = document.createElement('td'); let val; if(col === '(index)') val = i; else if(r && typeof r === 'object') val = r[col]; else if(col === 'value') val = r; td.textContent = val === undefined ? '' : formatVal(val); tr.appendChild(td); }); tbody.appendChild(tr); }); tbl.appendChild(tbody); const wrap = document.createElement('div'); wrap.className = 'line log'; const t = document.createElement('span'); t.className = 't'; t.textContent = ts(); const c = document.createElement('span'); c.className = 'c'; c.appendChild(tbl); wrap.append(t, c); consoleOut.appendChild(wrap); consoleOut.scrollTop = consoleOut.scrollHeight; } function formatVal(v){ if(v === null) return 'null'; if(v === undefined) return 'undefined'; if(typeof v === 'string') return v; if(typeof v === 'number' || typeof v === 'boolean') return String(v); if(typeof v === 'bigint') return v.toString() + 'n'; if(typeof v === 'function') return v.toString(); if(typeof v === 'symbol') return v.toString(); try { return JSON.stringify(v, replacer, 2); } catch { return String(v); } } function replacer(_, value){ if(typeof value === 'bigint') return value.toString() + 'n'; if(typeof value === 'function') return '[Function ' + (value.name || 'anonymous') + ']'; if(typeof value === 'symbol') return value.toString(); return value; } function bumpStats(kind){ if(kind === 'log' || kind === 'info') logCount++; else if(kind === 'warn') warnCount++; else if(kind === 'error') errorCount++; consoleStats.textContent = `${logCount} log · ${warnCount} warn · ${errorCount} err`; } /* ========================================================== Sandbox runner — uses srcdoc iframe so timers + async work, and so the runner cannot pollute or break this page. ========================================================== */ let sandbox = null; let sandboxId = 0; function killSandbox(){ if(sandbox){ sandbox.remove(); sandbox = null; } } /* Run/Stop button state — keeps Stop visible while the iframe is alive (so async setTimeout/Promise output can still arrive). Auto-reverts to Run after a brief idle window with no incoming messages, or immediately if the user clicks Stop. */ let isRunning = false; let idleTimer = null; const IDLE_WINDOW_MS = 800; function setRunButton(running){ const btn = document.getElementById('runBtn'); if(!btn) return; if(running){ btn.classList.remove('run-btn'); btn.classList.add('stop-btn'); btn.title = 'Stop running code'; btn.innerHTML = '■ Stop
⌘↵
'; }else{ btn.classList.remove('stop-btn'); btn.classList.add('run-btn'); btn.title = 'Run code'; btn.innerHTML = '▶ Run
⌘↵
'; } } function scheduleIdle(){ if(idleTimer) clearTimeout(idleTimer); idleTimer = setTimeout(()=>{ idleTimer = null; if(!isRunning) return; killSandbox(); isRunning = false; setRunButton(false); }, IDLE_WINDOW_MS); } function stopRun(reason){ if(idleTimer){ clearTimeout(idleTimer); idleTimer = null; } const wasRunning = isRunning; killSandbox(); isRunning = false; setRunButton(false); if(wasRunning){ consoleStatus.classList.remove('running'); if(errorCount > 0){ consoleStatus.classList.add('error'); consoleStatus.textContent = 'Failed'; } else { consoleStatus.textContent = reason === 'user' ? 'Stopped' : 'Done'; } if(reason === 'user') appendLine('system', ['■ Stopped by user']); } } function buildSandboxSrc(code, runId){ // serialize the user code as a JSON string so quotes/backticks survive intact const codeJson = JSON.stringify(code); return `