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:
root
2026-02-27 15:50:55 -06:00
parent 26e232fd41
commit ae8dbafb20
11 changed files with 666 additions and 6 deletions

144
server.js
View File

@@ -22,6 +22,13 @@ app.get('/f/:id', (_req, res) => {
res.sendFile('index.html', { root: 'public' });
});
// Published fiddle (clean URL)
app.get('/p/:slug', (req, res) => {
const row = stmts.getPublishedFiddle.get(req.params.slug);
if (!row || !row.published_html) return res.status(404).send('Not found');
res.type('html').send(row.published_html);
});
app.use(express.static('public', { index: false }));
// API: Create fiddle
@@ -124,6 +131,109 @@ app.get('/api/tags', (_req, res) => {
res.json({ tags: stmts.listTags.all() });
});
// ===================== Gist Import API =====================
app.post('/api/import/gist', async (req, res) => {
const { url } = req.body;
if (!url) return res.status(400).json({ error: 'url required' });
// Extract gist ID from URL
const match = url.match(/gist\.github\.com\/(?:[^/]+\/)?([a-f0-9]+)/);
if (!match) return res.status(400).json({ error: 'Invalid Gist URL' });
const gistId = match[1];
try {
const resp = await fetch(`https://api.github.com/gists/${gistId}`, {
headers: { 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'Fiddle-App' },
});
if (!resp.ok) throw new Error(`GitHub API returned ${resp.status}`);
const gist = await resp.json();
let html = '', css = '', js = '', mode = 'html-css-js', css_type = 'css';
const files = Object.values(gist.files);
for (const file of files) {
const ext = file.filename.split('.').pop().toLowerCase();
const content = file.content || '';
if (ext === 'html' || ext === 'htm') {
html = content;
} else if (ext === 'css') {
css = content;
} else if (ext === 'scss') {
css = content;
css_type = 'scss';
} else if (ext === 'less') {
css = content;
css_type = 'less';
} else if (ext === 'js' || ext === 'mjs') {
js = content;
} else if (ext === 'ts') {
js = content;
mode = 'typescript';
} else if (ext === 'jsx') {
js = content;
mode = 'react';
} else if (ext === 'tsx') {
js = content;
mode = 'react-ts';
} else if (ext === 'vue') {
js = content;
mode = 'vue';
} else if (ext === 'svelte') {
js = content;
mode = 'svelte';
} else if (ext === 'md' || ext === 'markdown') {
js = content;
mode = 'markdown';
} else if (ext === 'py') {
js = content;
mode = 'python';
}
}
res.json({ html, css, js, mode, css_type, title: gist.description || 'Imported Gist' });
} catch (e) {
res.status(502).json({ error: e.message });
}
});
// ===================== Publish API =====================
app.post('/api/fiddles/:id/publish', (req, res) => {
const fiddle = stmts.get.get(req.params.id);
if (!fiddle) return res.status(404).json({ error: 'Not found' });
// Check if already published
const existing = stmts.getPublishStatus.get(req.params.id);
if (existing && existing.published_slug) {
// Re-publish with updated HTML
const { html } = req.body;
if (html) stmts.publishFiddle.run({ id: req.params.id, slug: existing.published_slug, html });
return res.json({ slug: existing.published_slug, url: `/p/${existing.published_slug}` });
}
const slug = nanoid(8);
const { html } = req.body;
if (!html) return res.status(400).json({ error: 'html body required' });
stmts.publishFiddle.run({ id: req.params.id, slug, html });
res.json({ slug, url: `/p/${slug}` });
});
app.delete('/api/fiddles/:id/publish', (req, res) => {
const fiddle = stmts.get.get(req.params.id);
if (!fiddle) return res.status(404).json({ error: 'Not found' });
stmts.unpublishFiddle.run(req.params.id);
res.json({ ok: true });
});
app.get('/api/fiddles/:id/publish', (req, res) => {
const fiddle = stmts.get.get(req.params.id);
if (!fiddle) return res.status(404).json({ error: 'Not found' });
const status = stmts.getPublishStatus.get(req.params.id);
res.json({ published: !!(status && status.published_slug), slug: status?.published_slug || null });
});
// ===================== Version History API =====================
app.get('/api/fiddles/:id/versions', (req, res) => {
@@ -220,5 +330,39 @@ app.delete('/api/collections/:id/fiddles/:fid', (req, res) => {
res.json({ ok: true });
});
// ===================== Presentation Slides API =====================
app.get('/api/fiddles/:id/slides', (req, res) => {
const fiddle = stmts.get.get(req.params.id);
if (!fiddle) return res.status(404).json({ error: 'Not found' });
const slides = stmts.listSlides.all(req.params.id);
res.json({ slides });
});
app.post('/api/fiddles/:id/slides', (req, res) => {
const fiddle = stmts.get.get(req.params.id);
if (!fiddle) return res.status(404).json({ error: 'Not found' });
const id = nanoid(10);
const { html = '', css = '', js = '', notes = '' } = req.body;
const { max_order } = stmts.getMaxSlideOrder.get(req.params.id);
stmts.insertSlide.run({ id, fiddle_id: req.params.id, slide_order: max_order + 1, html, css, js, notes });
res.json({ id, fiddle_id: req.params.id, slide_order: max_order + 1, html, css, js, notes });
});
app.put('/api/slides/:slideId', (req, res) => {
const slide = stmts.getSlide.get(req.params.slideId);
if (!slide) return res.status(404).json({ error: 'Slide not found' });
const { html = slide.html, css = slide.css, js = slide.js, notes = slide.notes, slide_order = slide.slide_order } = req.body;
stmts.updateSlide.run({ id: req.params.slideId, html, css, js, notes, slide_order });
res.json({ ...slide, html, css, js, notes, slide_order });
});
app.delete('/api/slides/:slideId', (req, res) => {
const slide = stmts.getSlide.get(req.params.slideId);
if (!slide) return res.status(404).json({ error: 'Slide not found' });
stmts.deleteSlide.run(req.params.slideId);
res.json({ ok: true });
});
const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`Fiddle server running on http://localhost:${port}`));