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:
144
server.js
144
server.js
@@ -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}`));
|
||||
|
||||
Reference in New Issue
Block a user