Add Python REPL, instant deploy, Gist import, presentation mode, and CSS visual tools
- Python mode via Pyodide WASM runtime with stdout/stderr console integration - Publish fiddles to clean /p/:slug URLs as standalone HTML pages - Import code from GitHub Gist URLs with auto-detection of language/mode - Presentation mode with slide management, fullscreen viewer, and keyboard nav - Enable Monaco color decorators for inline CSS color pickers - Extract reusable generateStandaloneHtml from export module
This commit is contained in:
@@ -53,6 +53,41 @@ export function revertVersion(id, ver) {
|
||||
return request(`${BASE}/${id}/revert/${ver}`, { method: 'POST' });
|
||||
}
|
||||
|
||||
// Slides (presentation mode)
|
||||
export function listSlides(fiddleId) {
|
||||
return request(`${BASE}/${fiddleId}/slides`);
|
||||
}
|
||||
|
||||
export function createSlide(fiddleId, data) {
|
||||
return request(`${BASE}/${fiddleId}/slides`, { method: 'POST', body: JSON.stringify(data) });
|
||||
}
|
||||
|
||||
export function updateSlide(slideId, data) {
|
||||
return request(`/api/slides/${slideId}`, { method: 'PUT', body: JSON.stringify(data) });
|
||||
}
|
||||
|
||||
export function deleteSlide(slideId) {
|
||||
return request(`/api/slides/${slideId}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
// Gist import
|
||||
export function importGist(url) {
|
||||
return request('/api/import/gist', { method: 'POST', body: JSON.stringify({ url }) });
|
||||
}
|
||||
|
||||
// Publishing
|
||||
export function publishFiddle(id, html) {
|
||||
return request(`${BASE}/${id}/publish`, { method: 'POST', body: JSON.stringify({ html }) });
|
||||
}
|
||||
|
||||
export function unpublishFiddle(id) {
|
||||
return request(`${BASE}/${id}/publish`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
export function getPublishStatus(id) {
|
||||
return request(`${BASE}/${id}/publish`);
|
||||
}
|
||||
|
||||
// Collections
|
||||
export function createCollection(data) {
|
||||
return request('/api/collections', { method: 'POST', body: JSON.stringify(data) });
|
||||
|
||||
@@ -13,12 +13,14 @@ import {
|
||||
createFiddle, loadFiddle, updateFiddle, listTags,
|
||||
listVersions, getVersion, revertVersion,
|
||||
listCollections, createCollection, addToCollection,
|
||||
publishFiddle, unpublishFiddle, getPublishStatus,
|
||||
importGist,
|
||||
} from './api.js';
|
||||
import { getPref, setPref } from './preferences.js';
|
||||
import { initEmmet } from './emmet.js';
|
||||
import { initKeybindings } from './keybindings.js';
|
||||
import { initResizer, clearInlineSizes } from './resizer.js';
|
||||
import { exportHtml } from './export.js';
|
||||
import { exportHtml, generateStandaloneHtml } from './export.js';
|
||||
import { showQrModal } from './qr.js';
|
||||
import { initDevtools } from './devtools.js';
|
||||
import { initNetwork, clearNetwork } from './network-panel.js';
|
||||
@@ -31,6 +33,7 @@ import { registerCustomThemes, THEMES } from './editor-themes.js';
|
||||
import { GALLERY_TEMPLATES } from './templates.js';
|
||||
import { configureTypeDefaults, registerSnippetProviders } from './autocomplete.js';
|
||||
import { initNpmSearch } from './npm-search.js';
|
||||
import { openSlideManager, addCurrentSlide, startPresentation, stopPresentation } from './presentation.js';
|
||||
|
||||
let currentId = null;
|
||||
let debounceTimer = null;
|
||||
@@ -68,6 +71,10 @@ const STARTER_TEMPLATES = {
|
||||
js: `# Hello Markdown\n\nThis is a **Markdown** fiddle. Write your content here and see it rendered in the preview.\n\n## Features\n\n- Headers, **bold**, *italic*\n- Lists (ordered and unordered)\n- Code blocks with syntax highlighting\n- Links, images, and more\n\n### Code Example\n\n\`\`\`javascript\nconst greeting = "Hello, World!";\nconsole.log(greeting);\n\`\`\`\n\n> Blockquotes work too!\n\n| Column 1 | Column 2 |\n|----------|----------|\n| Cell A | Cell B |`,
|
||||
css: `body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;\n max-width: 720px;\n margin: 0 auto;\n padding: 24px;\n line-height: 1.6;\n color: #1a1a1a;\n}\n\nh1, h2, h3 { margin-top: 1.5em; margin-bottom: 0.5em; }\nh1 { border-bottom: 2px solid #eee; padding-bottom: 0.3em; }\n\ncode {\n background: #f4f4f4;\n padding: 2px 6px;\n border-radius: 3px;\n font-size: 0.9em;\n}\n\npre {\n background: #f4f4f4;\n padding: 16px;\n border-radius: 6px;\n overflow-x: auto;\n}\n\npre code { background: none; padding: 0; }\n\nblockquote {\n border-left: 4px solid #ddd;\n margin: 1em 0;\n padding: 0.5em 1em;\n color: #555;\n}\n\ntable {\n border-collapse: collapse;\n width: 100%;\n margin: 1em 0;\n}\n\nth, td {\n border: 1px solid #ddd;\n padding: 8px 12px;\n text-align: left;\n}\n\nth { background: #f4f4f4; font-weight: 600; }`,
|
||||
},
|
||||
'python': {
|
||||
js: `print("Hello from Python!")\n\nimport math\nprint(f"Pi is {math.pi:.4f}")\n\n# Use the sys module\nimport sys\nprint(f"Python {sys.version}")\n\n# List comprehension\nsquares = [x**2 for x in range(10)]\nprint(f"Squares: {squares}")`,
|
||||
html: '',
|
||||
},
|
||||
'wasm': {
|
||||
html: '<h1>WebAssembly Demo</h1>\n<div id="output"></div>',
|
||||
css: `body {\n font-family: monospace;\n padding: 24px;\n background: #1a1a2e;\n color: #0f0;\n}\n\nh1 { color: #00d4ff; margin-bottom: 16px; }\n#output { white-space: pre; font-size: 14px; line-height: 1.8; }`,
|
||||
@@ -494,6 +501,63 @@ async function init() {
|
||||
}
|
||||
});
|
||||
|
||||
// Publish button
|
||||
$('#btn-publish').addEventListener('click', async () => {
|
||||
if (!currentId) {
|
||||
showToast('Save the fiddle first before publishing');
|
||||
return;
|
||||
}
|
||||
const mode = getCurrentMode();
|
||||
const { html, css, js } = getEditorValues();
|
||||
const cssType = getCssType();
|
||||
const title = $('#title-input').value || 'Untitled';
|
||||
try {
|
||||
const compiledCss = await compileCss(css, cssType);
|
||||
const result = await compileJs(js, mode);
|
||||
const standaloneHtml = generateStandaloneHtml({
|
||||
title, html, css: compiledCss, js: result.js, mode,
|
||||
extraCss: result.extraCss,
|
||||
tailwind: getTailwindChecked(),
|
||||
isModule: result.isModule || false,
|
||||
renderedHtml: result.renderedHtml || null,
|
||||
previewTheme: getPref('previewTheme'),
|
||||
resources: currentResources,
|
||||
});
|
||||
const pubResult = await publishFiddle(currentId, standaloneHtml);
|
||||
const fullUrl = `${location.origin}${pubResult.url}`;
|
||||
try { await navigator.clipboard.writeText(fullUrl); } catch (_) {}
|
||||
showToast(`Published! ${fullUrl} (copied)`);
|
||||
} catch (e) {
|
||||
showToast(`Publish failed: ${e.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Import Gist button
|
||||
$('#btn-import-gist').addEventListener('click', async () => {
|
||||
const gistUrl = prompt('Paste a GitHub Gist URL:');
|
||||
if (!gistUrl || !gistUrl.trim()) return;
|
||||
try {
|
||||
const data = await importGist(gistUrl.trim());
|
||||
// Switch mode
|
||||
if (data.mode) {
|
||||
$('#framework-mode').value = data.mode;
|
||||
handleModeChange(data.mode);
|
||||
}
|
||||
// Set CSS type
|
||||
if (data.css_type && data.css_type !== 'css') {
|
||||
setCssType(data.css_type);
|
||||
}
|
||||
// Set editor values
|
||||
setEditorValues({ html: data.html || '', css: data.css || '', js: data.js || '' });
|
||||
// Set title
|
||||
if (data.title) $('#title-input').value = data.title;
|
||||
run();
|
||||
showToast('Gist imported successfully');
|
||||
} catch (e) {
|
||||
showToast(`Import failed: ${e.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
$('#btn-qr').addEventListener('click', () => {
|
||||
const url = currentId ? `${location.origin}/f/${currentId}` : location.href;
|
||||
showQrModal(url);
|
||||
@@ -531,6 +595,21 @@ async function init() {
|
||||
openCollectionModal(); // refresh list
|
||||
});
|
||||
|
||||
// Presentation mode
|
||||
const presModal = $('#presentation-modal');
|
||||
$('#btn-presentation').addEventListener('click', () => openSlideManager(currentId));
|
||||
$('#presentation-modal-close').addEventListener('click', () => presModal.classList.add('hidden'));
|
||||
presModal.addEventListener('click', (e) => { if (e.target === presModal) presModal.classList.add('hidden'); });
|
||||
$('#btn-add-slide').addEventListener('click', () => addCurrentSlide(currentId));
|
||||
$('#btn-start-presentation').addEventListener('click', () => startPresentation(currentId));
|
||||
$('#pres-exit').addEventListener('click', () => stopPresentation());
|
||||
$('#pres-prev').addEventListener('click', () => {
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft' }));
|
||||
});
|
||||
$('#pres-next').addEventListener('click', () => {
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight' }));
|
||||
});
|
||||
|
||||
// npm search in resources modal
|
||||
initNpmSearch((pkg) => {
|
||||
currentResources.push({ type: pkg.type, url: pkg.url });
|
||||
|
||||
@@ -33,6 +33,7 @@ const editorOpts = {
|
||||
autoClosingQuotes: 'always',
|
||||
autoSurround: 'languageDefined',
|
||||
bracketPairColorization: { enabled: true },
|
||||
colorDecorators: true,
|
||||
};
|
||||
|
||||
export const MODE_TABS = {
|
||||
@@ -73,6 +74,10 @@ export const MODE_TABS = {
|
||||
{ id: 'css', label: 'CSS', lang: 'css', ext: 'css' },
|
||||
{ id: 'js', label: 'JavaScript', lang: 'javascript', ext: 'js' },
|
||||
],
|
||||
'python': [
|
||||
{ id: 'js', label: 'Python', lang: 'python', ext: 'py' },
|
||||
{ id: 'html', label: 'HTML', lang: 'html', ext: 'html' },
|
||||
],
|
||||
};
|
||||
|
||||
// Map mode names to js_type values stored in DB
|
||||
@@ -85,6 +90,7 @@ export const MODE_TO_JS_TYPE = {
|
||||
'svelte': 'svelte',
|
||||
'markdown': 'markdown',
|
||||
'wasm': 'wasm',
|
||||
'python': 'python',
|
||||
};
|
||||
|
||||
export const JS_TYPE_TO_MODE = Object.fromEntries(
|
||||
|
||||
@@ -2,9 +2,9 @@ import { getFrameworkRuntime } from './js-preprocessors.js';
|
||||
import { extractBareImports, buildImportMapTag } from './import-map.js';
|
||||
|
||||
/**
|
||||
* Export a fiddle as a standalone HTML file and trigger download.
|
||||
* Generate standalone HTML string from fiddle data.
|
||||
*/
|
||||
export function exportHtml({ title, html, css, js, mode, extraCss = '', isModule = false, tailwind = false, renderedHtml = null, previewTheme = 'light', resources = [] }) {
|
||||
export function generateStandaloneHtml({ title, html, css, js, mode, extraCss = '', isModule = false, tailwind = false, renderedHtml = null, previewTheme = 'light', resources = [] }) {
|
||||
const runtime = getFrameworkRuntime(mode);
|
||||
const allCss = extraCss ? `${css}\n${extraCss}` : css;
|
||||
|
||||
@@ -22,7 +22,6 @@ export function exportHtml({ title, html, css, js, mode, extraCss = '', isModule
|
||||
|
||||
const effectiveIsModule = isModule || mode === 'svelte';
|
||||
|
||||
// Build importmap for module scripts with bare imports
|
||||
let importMapTag = '';
|
||||
if (effectiveIsModule && finalJs) {
|
||||
const bareImports = extractBareImports(finalJs);
|
||||
@@ -59,7 +58,7 @@ export function exportHtml({ title, html, css, js, mode, extraCss = '', isModule
|
||||
}
|
||||
}
|
||||
|
||||
const doc = `<!DOCTYPE html>
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
@@ -75,12 +74,18 @@ ${bodyContent}
|
||||
${scripts}
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a fiddle as a standalone HTML file and trigger download.
|
||||
*/
|
||||
export function exportHtml(opts) {
|
||||
const doc = generateStandaloneHtml(opts);
|
||||
const blob = new Blob([doc], { type: 'text/html' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${title.replace(/[^a-zA-Z0-9_-]/g, '_')}.html`;
|
||||
a.download = `${(opts.title || 'fiddle').replace(/[^a-zA-Z0-9_-]/g, '_')}.html`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
@@ -89,6 +89,9 @@ export async function compileJs(code, mode) {
|
||||
return { js: code, isModule: hasModuleSyntax || hasTopLevelAwait };
|
||||
}
|
||||
|
||||
case 'python':
|
||||
return compilePython(code);
|
||||
|
||||
default:
|
||||
return { js: code };
|
||||
}
|
||||
@@ -188,6 +191,34 @@ async function compileSvelte(code) {
|
||||
return { js, warnings, isModule: true };
|
||||
}
|
||||
|
||||
function compilePython(code) {
|
||||
// Wrap Python code in a JS loader that runs it via Pyodide in the iframe
|
||||
const escapedCode = JSON.stringify(code);
|
||||
const js = `
|
||||
(async function() {
|
||||
const status = document.getElementById('pyodide-status');
|
||||
try {
|
||||
if (status) status.textContent = 'Loading Python runtime...';
|
||||
const pyodide = await loadPyodide();
|
||||
pyodide.setStdout({ batched: (text) => {
|
||||
if (window.__fiddle_console) window.__fiddle_console('log', text);
|
||||
else console.log(text);
|
||||
}});
|
||||
pyodide.setStderr({ batched: (text) => {
|
||||
if (window.__fiddle_console) window.__fiddle_console('error', text);
|
||||
else console.error(text);
|
||||
}});
|
||||
if (status) status.textContent = '';
|
||||
await pyodide.runPythonAsync(${escapedCode});
|
||||
} catch(e) {
|
||||
if (status) status.textContent = '';
|
||||
console.error('Python error: ' + e.message);
|
||||
}
|
||||
})();
|
||||
`;
|
||||
return { js, isModule: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get runtime scripts and body HTML to inject into the preview iframe.
|
||||
*/
|
||||
@@ -223,6 +254,12 @@ export function getFrameworkRuntime(mode) {
|
||||
case 'wasm':
|
||||
return { scripts: [], bodyHtml: '' };
|
||||
|
||||
case 'python':
|
||||
return {
|
||||
scripts: ['https://cdn.jsdelivr.net/pyodide/v0.27.2/full/pyodide.js'],
|
||||
bodyHtml: '<div id="pyodide-status" style="font-family:monospace;color:#888;padding:12px">Loading Python...</div>',
|
||||
};
|
||||
|
||||
default:
|
||||
return { scripts: [], bodyHtml: '' };
|
||||
}
|
||||
|
||||
217
public/js/presentation.js
Normal file
217
public/js/presentation.js
Normal file
@@ -0,0 +1,217 @@
|
||||
import { listSlides, createSlide, deleteSlide } from './api.js';
|
||||
import { getEditorValues } from './editors.js';
|
||||
import { renderPreview } from './preview.js';
|
||||
import { compileCss } from './preprocessors.js';
|
||||
import { compileJs } from './js-preprocessors.js';
|
||||
import { getCurrentMode, getCssType } from './editors.js';
|
||||
import { getPref } from './preferences.js';
|
||||
|
||||
let slides = [];
|
||||
let currentSlideIndex = 0;
|
||||
let isPresenting = false;
|
||||
|
||||
const $ = (sel) => document.querySelector(sel);
|
||||
|
||||
function showToast(msg) {
|
||||
const toast = $('#share-toast');
|
||||
if (!toast) return;
|
||||
toast.textContent = msg;
|
||||
toast.classList.remove('hidden');
|
||||
setTimeout(() => toast.classList.add('hidden'), 3000);
|
||||
}
|
||||
|
||||
export async function openSlideManager(fiddleId) {
|
||||
if (!fiddleId) {
|
||||
showToast('Save the fiddle first to use presentation mode');
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = $('#presentation-modal');
|
||||
modal.classList.remove('hidden');
|
||||
|
||||
try {
|
||||
const result = await listSlides(fiddleId);
|
||||
slides = result.slides || [];
|
||||
renderSlideList(fiddleId);
|
||||
} catch (e) {
|
||||
showToast('Failed to load slides: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function renderSlideList(fiddleId) {
|
||||
const list = $('#slide-list');
|
||||
list.innerHTML = '';
|
||||
|
||||
if (!slides.length) {
|
||||
list.innerHTML = '<div style="padding:16px;color:var(--text-dim);text-align:center">No slides yet. Add the current editor state as a slide.</div>';
|
||||
} else {
|
||||
slides.forEach((slide, i) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'slide-item';
|
||||
item.innerHTML = `
|
||||
<span class="slide-number">${i + 1}</span>
|
||||
<span class="slide-preview-text">${escHtml(slide.notes || slide.js.slice(0, 60) || slide.html.slice(0, 60) || '(empty)')}</span>
|
||||
<button class="slide-delete-btn btn-small" data-id="${slide.id}">×</button>
|
||||
`;
|
||||
list.appendChild(item);
|
||||
});
|
||||
|
||||
list.querySelectorAll('.slide-delete-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
await deleteSlide(btn.dataset.id);
|
||||
slides = slides.filter(s => s.id !== btn.dataset.id);
|
||||
renderSlideList(fiddleId);
|
||||
} catch (err) {
|
||||
showToast('Delete failed: ' + err.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Update slide count
|
||||
const countEl = $('#slide-count');
|
||||
if (countEl) countEl.textContent = `${slides.length} slide${slides.length !== 1 ? 's' : ''}`;
|
||||
}
|
||||
|
||||
export async function addCurrentSlide(fiddleId) {
|
||||
if (!fiddleId) return;
|
||||
const { html, css, js } = getEditorValues();
|
||||
const notes = prompt('Slide notes (optional):') || '';
|
||||
try {
|
||||
const slide = await createSlide(fiddleId, { html, css, js, notes });
|
||||
slides.push(slide);
|
||||
renderSlideList(fiddleId);
|
||||
showToast(`Slide ${slides.length} added`);
|
||||
} catch (e) {
|
||||
showToast('Failed to add slide: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
export async function startPresentation(fiddleId) {
|
||||
if (!fiddleId) {
|
||||
showToast('Save the fiddle first');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await listSlides(fiddleId);
|
||||
slides = result.slides || [];
|
||||
} catch (e) {
|
||||
showToast('Failed to load slides: ' + e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!slides.length) {
|
||||
showToast('Add at least one slide first');
|
||||
return;
|
||||
}
|
||||
|
||||
isPresenting = true;
|
||||
currentSlideIndex = 0;
|
||||
|
||||
const overlay = $('#presentation-overlay');
|
||||
overlay.classList.remove('hidden');
|
||||
|
||||
// Close the manager modal if open
|
||||
$('#presentation-modal').classList.add('hidden');
|
||||
|
||||
renderCurrentSlide();
|
||||
document.addEventListener('keydown', presentationKeyHandler);
|
||||
}
|
||||
|
||||
export function stopPresentation() {
|
||||
isPresenting = false;
|
||||
const overlay = $('#presentation-overlay');
|
||||
overlay.classList.add('hidden');
|
||||
document.removeEventListener('keydown', presentationKeyHandler);
|
||||
}
|
||||
|
||||
function presentationKeyHandler(e) {
|
||||
if (!isPresenting) return;
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
stopPresentation();
|
||||
} else if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
if (currentSlideIndex < slides.length - 1) {
|
||||
currentSlideIndex++;
|
||||
renderCurrentSlide();
|
||||
}
|
||||
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
if (currentSlideIndex > 0) {
|
||||
currentSlideIndex--;
|
||||
renderCurrentSlide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function renderCurrentSlide() {
|
||||
const slide = slides[currentSlideIndex];
|
||||
if (!slide) return;
|
||||
|
||||
const counter = $('#pres-counter');
|
||||
counter.textContent = `${currentSlideIndex + 1} / ${slides.length}`;
|
||||
|
||||
const notes = $('#pres-notes');
|
||||
notes.textContent = slide.notes || '';
|
||||
|
||||
// Render the slide's code into the presentation iframe
|
||||
const frame = $('#pres-preview-frame');
|
||||
const mode = getCurrentMode();
|
||||
const cssType = getCssType();
|
||||
|
||||
try {
|
||||
const compiledCss = await compileCss(slide.css, cssType);
|
||||
const result = await compileJs(slide.js, mode);
|
||||
|
||||
const { getFrameworkRuntime } = await import('./js-preprocessors.js');
|
||||
const runtime = getFrameworkRuntime(mode);
|
||||
const allCss = result.extraCss ? `${compiledCss}\n${result.extraCss}` : compiledCss;
|
||||
|
||||
const finalHtml = result.renderedHtml || slide.html;
|
||||
const finalJs = result.renderedHtml ? '' : result.js;
|
||||
|
||||
let bodyContent;
|
||||
if (mode === 'vue' || mode === 'svelte') {
|
||||
bodyContent = finalHtml ? `${finalHtml}\n${runtime.bodyHtml}` : runtime.bodyHtml;
|
||||
} else if (runtime.bodyHtml) {
|
||||
bodyContent = `${finalHtml}\n${runtime.bodyHtml}`;
|
||||
} else {
|
||||
bodyContent = finalHtml;
|
||||
}
|
||||
|
||||
const isModule = result.isModule || mode === 'svelte';
|
||||
|
||||
let scripts = '';
|
||||
if (finalJs) {
|
||||
if (isModule) {
|
||||
scripts = `<script type="module">\n${finalJs}\n<\/script>`;
|
||||
} else {
|
||||
for (const url of runtime.scripts) {
|
||||
scripts += `<script src="${url}"><\/script>\n`;
|
||||
}
|
||||
scripts += `<script>\n${finalJs}\n<\/script>`;
|
||||
}
|
||||
}
|
||||
|
||||
const previewTheme = getPref('previewTheme');
|
||||
const darkCss = previewTheme === 'dark'
|
||||
? `<style>body { background: #1e1e1e; color: #ccc; }</style>\n`
|
||||
: '';
|
||||
|
||||
const doc = `<!DOCTYPE html>
|
||||
<html><head><meta charset="UTF-8">${darkCss}<style>${allCss}</style></head>
|
||||
<body>${bodyContent}${scripts}</body></html>`;
|
||||
|
||||
frame.srcdoc = doc;
|
||||
} catch (e) {
|
||||
frame.srcdoc = `<pre style="color:red;padding:20px">Error: ${escHtml(e.message)}</pre>`;
|
||||
}
|
||||
}
|
||||
|
||||
function escHtml(str) {
|
||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
@@ -76,6 +76,11 @@ const consoleInterceptor = `
|
||||
if (e.data && e.data.type === 'devtools-request' && e.data.tab === 'elements') sendElements();
|
||||
});
|
||||
|
||||
// Global helper for Pyodide stdout/stderr bridging
|
||||
window.__fiddle_console = function(method, text) {
|
||||
parent.postMessage({ type: 'console', method: method, args: [String(text)] }, '*');
|
||||
};
|
||||
|
||||
// --- Performance: timing metrics ---
|
||||
window.__fiddle_scriptStart = performance.now();
|
||||
window.addEventListener('load', function() {
|
||||
|
||||
Reference in New Issue
Block a user