| |
|
|
| import * as uiModule from './ui.js'; |
|
|
| |
| |
| |
|
|
| let pyodideInstance = null; |
| let pyodideLoading = false; |
| const pyodideQueue = []; |
|
|
| |
| |
| |
| function getOrCreatePanel(pre) { |
| let panel = pre.nextElementSibling; |
| if (panel && panel.classList.contains('code-runner-output')) { |
| panel.innerHTML = ''; |
| panel.style.display = 'block'; |
| return panel; |
| } |
| panel = document.createElement('div'); |
| panel.className = 'code-runner-output'; |
| pre.parentNode.insertBefore(panel, pre.nextSibling); |
| return panel; |
| } |
|
|
| |
| |
| |
| function showLoading(panel, msg) { |
| panel.innerHTML = `<div class="code-runner-loading">${msg}</div>`; |
| } |
|
|
| |
| |
| |
| function showOutput(panel, text, isError) { |
| const el = document.createElement('pre'); |
| el.className = isError ? 'code-runner-pre code-runner-error' : 'code-runner-pre'; |
| el.textContent = text; |
| panel.innerHTML = ''; |
| panel.appendChild(el); |
| |
| |
| if (text) { |
| const cbtn = document.createElement('button'); |
| cbtn.type = 'button'; |
| cbtn.className = 'code-runner-copy-inline'; |
| cbtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>Copy'; |
| cbtn.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| e.preventDefault(); |
| let ok = false; |
| try { |
| const ta = document.createElement('textarea'); |
| ta.value = text; |
| ta.style.cssText = 'position:fixed;left:0;top:0;width:1px;height:1px;opacity:0;'; |
| document.body.appendChild(ta); |
| ta.focus(); |
| ta.select(); |
| ta.setSelectionRange(0, text.length); |
| ok = document.execCommand && document.execCommand('copy'); |
| ta.remove(); |
| } catch (_) {} |
| if (!ok && navigator.clipboard && window.isSecureContext) { |
| navigator.clipboard.writeText(text).then(() => { |
| if (uiModule.showToast) uiModule.showToast('Copied'); |
| cbtn.textContent = 'Copied!'; |
| setTimeout(() => { cbtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>Copy'; }, 1500); |
| }).catch(() => { if (uiModule.showToast) uiModule.showToast('Copy failed'); }); |
| return; |
| } |
| if (uiModule.showToast) uiModule.showToast(ok ? 'Copied' : 'Copy failed'); |
| const orig = cbtn.innerHTML; |
| cbtn.textContent = ok ? 'Copied!' : 'Copy failed'; |
| setTimeout(() => { cbtn.innerHTML = orig; }, 1500); |
| }); |
| |
| |
| panel.appendChild(cbtn); |
| } |
| if (isError) { |
| setTimeout(() => { if (panel) panel.style.display = 'none'; }, 7000); |
| } |
| } |
|
|
| |
| |
| |
| |
| function addCopyBtn_unused(panel, text) { |
| if (!text) return; |
| const btn = document.createElement('button'); |
| btn.type = 'button'; |
| btn.className = 'code-runner-copy'; |
| btn.title = 'Copy output'; |
| btn.innerHTML = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>'; |
| btn.addEventListener('click', async (e) => { |
| e.stopPropagation(); |
| e.preventDefault(); |
| |
| |
| |
| |
| let ok = false; |
| try { |
| const ta = document.createElement('textarea'); |
| ta.value = text; |
| ta.style.cssText = 'position:fixed;left:0;top:0;width:1px;height:1px;opacity:0;'; |
| document.body.appendChild(ta); |
| ta.focus(); |
| ta.select(); |
| ta.setSelectionRange(0, text.length); |
| ok = document.execCommand && document.execCommand('copy'); |
| ta.remove(); |
| } catch (_) {} |
| |
| |
| if (!ok && navigator.clipboard && window.isSecureContext) { |
| try { await navigator.clipboard.writeText(text); ok = true; } catch (_) {} |
| } |
| if (uiModule && uiModule.showToast) { |
| uiModule.showToast(ok ? 'Copied' : 'Copy failed'); |
| } |
| const _orig = btn.innerHTML; |
| btn.innerHTML = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>'; |
| btn.classList.add('copied'); |
| setTimeout(() => { btn.innerHTML = _orig; btn.classList.remove('copied'); }, 1500); |
| }); |
| panel.prepend(btn); |
| } |
|
|
| |
| |
| |
| |
| |
| function addCloseBtn(_panel) { } |
|
|
| |
| |
| |
| function loadPyodide() { |
| if (pyodideInstance) return Promise.resolve(pyodideInstance); |
| if (pyodideLoading) { |
| return new Promise((resolve, reject) => { |
| pyodideQueue.push({ resolve, reject }); |
| }); |
| } |
| pyodideLoading = true; |
|
|
| return new Promise((resolve, reject) => { |
| const script = document.createElement('script'); |
| script.src = 'https://cdn.jsdelivr.net/pyodide/v0.27.5/full/pyodide.js'; |
| script.onload = () => { |
| window.loadPyodide({ indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.27.5/full/' }) |
| .then(py => { |
| pyodideInstance = py; |
| pyodideLoading = false; |
| pyodideQueue.forEach(q => q.resolve(py)); |
| pyodideQueue.length = 0; |
| resolve(py); |
| }) |
| .catch(err => { |
| pyodideLoading = false; |
| pyodideQueue.forEach(q => q.reject(err)); |
| pyodideQueue.length = 0; |
| reject(err); |
| }); |
| }; |
| script.onerror = () => { |
| pyodideLoading = false; |
| const err = new Error('Failed to load Pyodide'); |
| pyodideQueue.forEach(q => q.reject(err)); |
| pyodideQueue.length = 0; |
| reject(err); |
| }; |
| document.head.appendChild(script); |
| }); |
| } |
|
|
| |
| |
| |
| export async function runPython(code, panel) { |
| showLoading(panel, 'Loading Python runtime (first time ~10 MB)...'); |
|
|
| let py; |
| try { |
| py = await loadPyodide(); |
| } catch (e) { |
| showOutput(panel, 'Failed to load Python runtime: ' + e.message, true); |
| addCloseBtn(panel); |
| return; |
| } |
|
|
| showLoading(panel, 'Running...'); |
|
|
| const wrapper = ` |
| import sys, io |
| _stdout = io.StringIO() |
| _stderr = io.StringIO() |
| sys.stdout = _stdout |
| sys.stderr = _stderr |
| try: |
| exec(${JSON.stringify(code)}) |
| except Exception as _e: |
| _stderr.write(str(_e)) |
| finally: |
| sys.stdout = sys.__stdout__ |
| sys.stderr = sys.__stderr__ |
| (_stdout.getvalue(), _stderr.getvalue()) |
| `; |
|
|
| try { |
| const result = await Promise.race([ |
| py.runPythonAsync(wrapper), |
| new Promise((_, reject) => setTimeout(() => reject(new Error('Execution timed out (10 s)')), 10000)) |
| ]); |
|
|
| const stdout = result.toJs ? result.toJs()[0] : (result[0] || ''); |
| const stderr = result.toJs ? result.toJs()[1] : (result[1] || ''); |
| if (result.destroy) result.destroy(); |
|
|
| panel.innerHTML = ''; |
| if (stderr) { |
| showOutput(panel, stderr, true); |
| } else if (stdout) { |
| showOutput(panel, stdout, false); |
| } else { |
| showOutput(panel, '(no output)', false); |
| } |
| } catch (e) { |
| showOutput(panel, e.message, true); |
| } |
| addCloseBtn(panel); |
| } |
|
|
| |
| |
| |
| export function runJavaScript(code, panel) { |
| showLoading(panel, 'Running...'); |
|
|
| const iframe = document.createElement('iframe'); |
| iframe.style.display = 'none'; |
| iframe.sandbox = 'allow-scripts'; |
| document.body.appendChild(iframe); |
|
|
| let settled = false; |
| const cleanup = () => { |
| if (iframe.parentNode) iframe.remove(); |
| }; |
|
|
| const failsafe = setTimeout(() => { |
| if (!settled) { |
| settled = true; |
| showOutput(panel, 'Execution timed out (10 s)', true); |
| addCloseBtn(panel); |
| cleanup(); |
| } |
| }, 15000); |
|
|
| const onMessage = (e) => { |
| if (e.source !== iframe.contentWindow) return; |
| if (settled) return; |
| settled = true; |
| clearTimeout(failsafe); |
| window.removeEventListener('message', onMessage); |
|
|
| const data = e.data; |
| panel.innerHTML = ''; |
| if (data.error) { |
| showOutput(panel, data.error, true); |
| } else if (data.logs && data.logs.length > 0) { |
| showOutput(panel, data.logs.join('\n'), false); |
| } else { |
| showOutput(panel, '(no output)', false); |
| } |
| addCloseBtn(panel); |
| cleanup(); |
| }; |
|
|
| window.addEventListener('message', onMessage); |
|
|
| const wrappedCode = ` |
| <!DOCTYPE html><html><body><script> |
| var _logs = []; |
| var _origLog = console.log; |
| console.log = function() { _logs.push([].map.call(arguments, function(a) { try { return typeof a === 'object' ? JSON.stringify(a) : String(a); } catch(e) { return String(a); } }).join(' ')); }; |
| console.warn = function() { _logs.push('[warn] ' + [].map.call(arguments, String).join(' ')); }; |
| console.error = function() { _logs.push('[error] ' + [].map.call(arguments, String).join(' ')); }; |
| try { |
| var _timer = setTimeout(function() { parent.postMessage({error:'Execution timed out (10 s)'},'*'); }, 10000); |
| ${code.replace(/<\/script>/gi, '<\\/script>')} |
| clearTimeout(_timer); |
| parent.postMessage({logs: _logs}, '*'); |
| } catch(e) { |
| parent.postMessage({error: e.toString()}, '*'); |
| } |
| <\/script></body></html>`; |
|
|
| iframe.srcdoc = wrappedCode; |
| } |
|
|
| |
| |
| |
| export async function runServer(code, panel, lang) { |
| showLoading(panel, 'Running on server...'); |
| var command; |
| if (lang === 'python' || lang === 'py') { |
| command = 'python3 -c ' + JSON.stringify(code); |
| } else { |
| command = 'bash -c ' + JSON.stringify(code); |
| } |
| try { |
| var res = await fetch('/api/shell/exec', { |
| method: 'POST', |
| credentials: 'same-origin', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ command: command }), |
| }); |
| var data = await res.json(); |
| panel.innerHTML = ''; |
| if (data.stderr && data.stderr.trim()) { |
| showOutput(panel, data.stderr, true); |
| if (data.stdout && data.stdout.trim()) { |
| var stdoutEl = document.createElement('pre'); |
| stdoutEl.className = 'code-runner-pre'; |
| stdoutEl.textContent = data.stdout; |
| panel.appendChild(stdoutEl); |
| } |
| } else if (data.stdout && data.stdout.trim()) { |
| showOutput(panel, data.stdout, false); |
| } else { |
| showOutput(panel, '(no output)' + (data.exit_code ? ' — exit code ' + data.exit_code : ''), !data.exit_code ? false : true); |
| } |
| if (data.exit_code && data.exit_code !== 0) { |
| var exitEl = document.createElement('div'); |
| exitEl.style.cssText = 'font-size:0.75rem;opacity:0.5;padding:2px 8px;'; |
| exitEl.textContent = 'Exit code: ' + data.exit_code; |
| panel.appendChild(exitEl); |
| } |
| } catch (e) { |
| showOutput(panel, 'Execution failed: ' + e.message, true); |
| } |
| addCloseBtn(panel); |
| } |
|
|
| |
| |
| |
| export function runHTML(code, panel) { |
| panel.innerHTML = ''; |
|
|
| const win = window.open('', '_blank', 'width=800,height=600,menubar=no,toolbar=no,location=no,status=no'); |
| if (!win) { |
| showOutput(panel, 'Popup blocked — please allow popups for this site.', true); |
| addCloseBtn(panel); |
| return; |
| } |
| win.document.open(); |
| win.document.write(code); |
| win.document.close(); |
|
|
| showOutput(panel, 'Opened in new window', false); |
| addCloseBtn(panel); |
| } |
|
|
| |
| |
| |
| export function run(btn) { |
| const code = btn.getAttribute('data-code'); |
| const lang = (btn.getAttribute('data-lang') || '').toLowerCase(); |
| if (!code) return; |
|
|
| const pre = btn.closest('pre'); |
| if (!pre) return; |
|
|
| const panel = getOrCreatePanel(pre); |
|
|
| if (lang === 'bash' || lang === 'sh' || lang === 'shell' || lang === 'zsh') { |
| runServer(code, panel, 'bash'); |
| } else if (lang === 'python' || lang === 'py') { |
| runServer(code, panel, 'python'); |
| } else if (lang === 'javascript' || lang === 'js') { |
| runJavaScript(code, panel); |
| } else if (lang === 'html') { |
| runHTML(code, panel); |
| } |
| } |
|
|
| const codeRunnerModule = { run, runPython, runJavaScript, runHTML, runServer }; |
| export default codeRunnerModule; |
|
|