Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae8dbafb20 |
43
db.js
43
db.js
@@ -59,6 +59,15 @@ try {
|
|||||||
db.exec(`ALTER TABLE fiddles ADD COLUMN screenshot TEXT`);
|
db.exec(`ALTER TABLE fiddles ADD COLUMN screenshot TEXT`);
|
||||||
} catch (_) { /* column already exists */ }
|
} catch (_) { /* column already exists */ }
|
||||||
|
|
||||||
|
// Migration: add publishing columns
|
||||||
|
try {
|
||||||
|
db.exec(`ALTER TABLE fiddles ADD COLUMN published_slug TEXT`);
|
||||||
|
} catch (_) { /* column already exists */ }
|
||||||
|
try {
|
||||||
|
db.exec(`ALTER TABLE fiddles ADD COLUMN published_html TEXT`);
|
||||||
|
} catch (_) { /* column already exists */ }
|
||||||
|
db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_fiddles_published_slug ON fiddles(published_slug) WHERE published_slug IS NOT NULL`);
|
||||||
|
|
||||||
// Version history table
|
// Version history table
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS fiddle_versions (
|
CREATE TABLE IF NOT EXISTS fiddle_versions (
|
||||||
@@ -95,6 +104,21 @@ db.exec(`
|
|||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// Presentation slides table
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS fiddle_slides (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
fiddle_id TEXT NOT NULL,
|
||||||
|
slide_order INTEGER NOT NULL,
|
||||||
|
html TEXT DEFAULT '',
|
||||||
|
css TEXT DEFAULT '',
|
||||||
|
js TEXT DEFAULT '',
|
||||||
|
notes TEXT DEFAULT '',
|
||||||
|
FOREIGN KEY (fiddle_id) REFERENCES fiddles(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_slides_fiddle ON fiddle_slides(fiddle_id, slide_order)`);
|
||||||
|
|
||||||
export const stmts = {
|
export const stmts = {
|
||||||
insert: db.prepare(`
|
insert: db.prepare(`
|
||||||
INSERT INTO fiddles (id, title, html, css, css_type, js, js_type, listed, options)
|
INSERT INTO fiddles (id, title, html, css, css_type, js, js_type, listed, options)
|
||||||
@@ -145,6 +169,12 @@ export const stmts = {
|
|||||||
// Screenshot
|
// Screenshot
|
||||||
updateScreenshot: db.prepare('UPDATE fiddles SET screenshot = ? WHERE id = ?'),
|
updateScreenshot: db.prepare('UPDATE fiddles SET screenshot = ? WHERE id = ?'),
|
||||||
|
|
||||||
|
// Publishing
|
||||||
|
publishFiddle: db.prepare('UPDATE fiddles SET published_slug = @slug, published_html = @html WHERE id = @id'),
|
||||||
|
getPublishedFiddle: db.prepare('SELECT id, title, published_html FROM fiddles WHERE published_slug = ?'),
|
||||||
|
unpublishFiddle: db.prepare('UPDATE fiddles SET published_slug = NULL, published_html = NULL WHERE id = ?'),
|
||||||
|
getPublishStatus: db.prepare('SELECT published_slug FROM fiddles WHERE id = ?'),
|
||||||
|
|
||||||
// Collections
|
// Collections
|
||||||
insertCollection: db.prepare(`
|
insertCollection: db.prepare(`
|
||||||
INSERT INTO collections (id, name, description) VALUES (@id, @name, @description)
|
INSERT INTO collections (id, name, description) VALUES (@id, @name, @description)
|
||||||
@@ -179,6 +209,19 @@ export const stmts = {
|
|||||||
JOIN collection_fiddles cf ON cf.collection_id = c.id
|
JOIN collection_fiddles cf ON cf.collection_id = c.id
|
||||||
WHERE cf.fiddle_id = ?
|
WHERE cf.fiddle_id = ?
|
||||||
`),
|
`),
|
||||||
|
|
||||||
|
// Slides
|
||||||
|
insertSlide: db.prepare(`
|
||||||
|
INSERT INTO fiddle_slides (id, fiddle_id, slide_order, html, css, js, notes)
|
||||||
|
VALUES (@id, @fiddle_id, @slide_order, @html, @css, @js, @notes)
|
||||||
|
`),
|
||||||
|
listSlides: db.prepare('SELECT * FROM fiddle_slides WHERE fiddle_id = ? ORDER BY slide_order'),
|
||||||
|
getSlide: db.prepare('SELECT * FROM fiddle_slides WHERE id = ?'),
|
||||||
|
updateSlide: db.prepare(`
|
||||||
|
UPDATE fiddle_slides SET html = @html, css = @css, js = @js, notes = @notes, slide_order = @slide_order WHERE id = @id
|
||||||
|
`),
|
||||||
|
deleteSlide: db.prepare('DELETE FROM fiddle_slides WHERE id = ?'),
|
||||||
|
getMaxSlideOrder: db.prepare('SELECT COALESCE(MAX(slide_order), 0) as max_order FROM fiddle_slides WHERE fiddle_id = ?'),
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -766,3 +766,50 @@ body.resizing iframe { pointer-events: none; }
|
|||||||
.npm-result-version { font-size: 10px; color: var(--text-dim); font-weight: 400; }
|
.npm-result-version { font-size: 10px; color: var(--text-dim); font-weight: 400; }
|
||||||
.npm-result-desc { font-size: 11px; color: var(--text-dim); margin-top: 2px; }
|
.npm-result-desc { font-size: 11px; color: var(--text-dim); margin-top: 2px; }
|
||||||
.npm-no-results { padding: 12px; text-align: center; color: var(--text-dim); font-size: 12px; }
|
.npm-no-results { padding: 12px; text-align: center; color: var(--text-dim); font-size: 12px; }
|
||||||
|
|
||||||
|
/* Slide list (presentation manager) */
|
||||||
|
.slide-list { max-height: 300px; overflow-y: auto; }
|
||||||
|
.slide-item {
|
||||||
|
display: flex; align-items: center; gap: 8px; padding: 8px 16px;
|
||||||
|
border-bottom: 1px solid var(--border); font-size: 12px;
|
||||||
|
}
|
||||||
|
.slide-item:hover { background: rgba(255,255,255,0.03); }
|
||||||
|
.slide-number {
|
||||||
|
width: 24px; height: 24px; border-radius: 50%; background: var(--accent);
|
||||||
|
color: #fff; display: flex; align-items: center; justify-content: center;
|
||||||
|
font-size: 11px; font-weight: 600; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.slide-preview-text { flex: 1; color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.slide-delete-btn { flex-shrink: 0; }
|
||||||
|
|
||||||
|
/* Presentation overlay (fullscreen) */
|
||||||
|
.presentation-overlay {
|
||||||
|
position: fixed; inset: 0; z-index: 10000;
|
||||||
|
background: #111; display: flex; flex-direction: column;
|
||||||
|
}
|
||||||
|
.presentation-overlay.hidden { display: none; }
|
||||||
|
.pres-header {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
padding: 8px 16px; background: #1a1a1a; border-bottom: 1px solid #333;
|
||||||
|
}
|
||||||
|
.pres-counter { color: #aaa; font-size: 13px; font-weight: 500; }
|
||||||
|
.pres-exit-btn {
|
||||||
|
background: transparent; color: #aaa; border: 1px solid #444;
|
||||||
|
padding: 4px 12px; font-size: 12px; border-radius: 4px;
|
||||||
|
}
|
||||||
|
.pres-exit-btn:hover { background: #333; color: #fff; }
|
||||||
|
.pres-content { flex: 1; display: flex; overflow: hidden; }
|
||||||
|
.pres-iframe { width: 100%; height: 100%; border: none; background: #fff; }
|
||||||
|
.pres-footer {
|
||||||
|
display: flex; align-items: center; gap: 12px;
|
||||||
|
padding: 8px 16px; background: #1a1a1a; border-top: 1px solid #333;
|
||||||
|
}
|
||||||
|
.pres-nav-btn {
|
||||||
|
background: transparent; color: #aaa; border: 1px solid #444;
|
||||||
|
padding: 6px 16px; font-size: 12px; border-radius: 4px; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.pres-nav-btn:hover { background: #333; color: #fff; }
|
||||||
|
.pres-notes {
|
||||||
|
flex: 1; color: #888; font-size: 12px; font-style: italic;
|
||||||
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: center;
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
<option value="svelte">Svelte</option>
|
<option value="svelte">Svelte</option>
|
||||||
<option value="markdown">Markdown</option>
|
<option value="markdown">Markdown</option>
|
||||||
<option value="wasm">WASM</option>
|
<option value="wasm">WASM</option>
|
||||||
|
<option value="python">Python</option>
|
||||||
</select>
|
</select>
|
||||||
<input type="text" id="title-input" placeholder="Untitled" spellcheck="false">
|
<input type="text" id="title-input" placeholder="Untitled" spellcheck="false">
|
||||||
<div class="tags-input-wrap">
|
<div class="tags-input-wrap">
|
||||||
@@ -54,6 +55,9 @@
|
|||||||
<button id="btn-templates" class="btn-icon" title="Starter templates">
|
<button id="btn-templates" class="btn-icon" title="Starter templates">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button id="btn-import-gist" class="btn-icon" title="Import from GitHub Gist">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg>
|
||||||
|
</button>
|
||||||
<button id="btn-resources" class="btn-icon" title="External resources">
|
<button id="btn-resources" class="btn-icon" title="External resources">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
||||||
</button>
|
</button>
|
||||||
@@ -63,12 +67,18 @@
|
|||||||
<button id="btn-embed" class="btn-icon" title="Embed code">
|
<button id="btn-embed" class="btn-icon" title="Embed code">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button id="btn-publish" class="btn-icon" title="Publish to clean URL">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="M12 15l-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></svg>
|
||||||
|
</button>
|
||||||
<button id="btn-export" class="btn-icon" title="Export HTML">
|
<button id="btn-export" class="btn-icon" title="Export HTML">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button id="btn-collection" class="btn-icon" title="Collections">
|
<button id="btn-collection" class="btn-icon" title="Collections">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button id="btn-presentation" class="btn-icon" title="Presentation mode">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21l4-4 4 4"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
|
||||||
|
</button>
|
||||||
<button id="btn-qr" class="btn-icon" title="QR code">
|
<button id="btn-qr" class="btn-icon" title="QR code">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="8" height="8" rx="1"/><rect x="14" y="2" width="8" height="8" rx="1"/><rect x="2" y="14" width="8" height="8" rx="1"/><rect x="14" y="14" width="4" height="4"/><line x1="22" y1="14" x2="22" y2="22"/><line x1="14" y1="22" x2="22" y2="22"/></svg>
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="8" height="8" rx="1"/><rect x="14" y="2" width="8" height="8" rx="1"/><rect x="2" y="14" width="8" height="8" rx="1"/><rect x="14" y="14" width="4" height="4"/><line x1="22" y1="14" x2="22" y2="22"/><line x1="14" y1="22" x2="22" y2="22"/></svg>
|
||||||
</button>
|
</button>
|
||||||
@@ -306,6 +316,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="presentation-modal" class="modal-overlay hidden">
|
||||||
|
<div class="modal-content" style="min-width:380px">
|
||||||
|
<div class="modal-header">
|
||||||
|
<span>Presentation Mode</span>
|
||||||
|
<button id="presentation-modal-close" class="btn-small">×</button>
|
||||||
|
</div>
|
||||||
|
<div style="padding:8px 16px;display:flex;gap:8px;align-items:center">
|
||||||
|
<button id="btn-add-slide" class="btn-small">+ Add Current State as Slide</button>
|
||||||
|
<span id="slide-count" style="color:var(--text-dim);font-size:12px"></span>
|
||||||
|
</div>
|
||||||
|
<div id="slide-list" class="slide-list"></div>
|
||||||
|
<div style="padding:8px 16px">
|
||||||
|
<button id="btn-start-presentation" style="width:100%">Start Presentation</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="presentation-overlay" class="presentation-overlay hidden">
|
||||||
|
<div class="pres-header">
|
||||||
|
<span id="pres-counter" class="pres-counter"></span>
|
||||||
|
<button id="pres-exit" class="pres-exit-btn">Exit (Esc)</button>
|
||||||
|
</div>
|
||||||
|
<div class="pres-content">
|
||||||
|
<iframe id="pres-preview-frame" class="pres-iframe" sandbox="allow-scripts allow-same-origin"></iframe>
|
||||||
|
</div>
|
||||||
|
<div class="pres-footer">
|
||||||
|
<button id="pres-prev" class="pres-nav-btn">← Prev</button>
|
||||||
|
<div id="pres-notes" class="pres-notes"></div>
|
||||||
|
<button id="pres-next" class="pres-nav-btn">Next →</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="share-toast" class="toast hidden"></div>
|
<div id="share-toast" class="toast hidden"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -53,6 +53,41 @@ export function revertVersion(id, ver) {
|
|||||||
return request(`${BASE}/${id}/revert/${ver}`, { method: 'POST' });
|
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
|
// Collections
|
||||||
export function createCollection(data) {
|
export function createCollection(data) {
|
||||||
return request('/api/collections', { method: 'POST', body: JSON.stringify(data) });
|
return request('/api/collections', { method: 'POST', body: JSON.stringify(data) });
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ import {
|
|||||||
createFiddle, loadFiddle, updateFiddle, listTags,
|
createFiddle, loadFiddle, updateFiddle, listTags,
|
||||||
listVersions, getVersion, revertVersion,
|
listVersions, getVersion, revertVersion,
|
||||||
listCollections, createCollection, addToCollection,
|
listCollections, createCollection, addToCollection,
|
||||||
|
publishFiddle, unpublishFiddle, getPublishStatus,
|
||||||
|
importGist,
|
||||||
} from './api.js';
|
} from './api.js';
|
||||||
import { getPref, setPref } from './preferences.js';
|
import { getPref, setPref } from './preferences.js';
|
||||||
import { initEmmet } from './emmet.js';
|
import { initEmmet } from './emmet.js';
|
||||||
import { initKeybindings } from './keybindings.js';
|
import { initKeybindings } from './keybindings.js';
|
||||||
import { initResizer, clearInlineSizes } from './resizer.js';
|
import { initResizer, clearInlineSizes } from './resizer.js';
|
||||||
import { exportHtml } from './export.js';
|
import { exportHtml, generateStandaloneHtml } from './export.js';
|
||||||
import { showQrModal } from './qr.js';
|
import { showQrModal } from './qr.js';
|
||||||
import { initDevtools } from './devtools.js';
|
import { initDevtools } from './devtools.js';
|
||||||
import { initNetwork, clearNetwork } from './network-panel.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 { GALLERY_TEMPLATES } from './templates.js';
|
||||||
import { configureTypeDefaults, registerSnippetProviders } from './autocomplete.js';
|
import { configureTypeDefaults, registerSnippetProviders } from './autocomplete.js';
|
||||||
import { initNpmSearch } from './npm-search.js';
|
import { initNpmSearch } from './npm-search.js';
|
||||||
|
import { openSlideManager, addCurrentSlide, startPresentation, stopPresentation } from './presentation.js';
|
||||||
|
|
||||||
let currentId = null;
|
let currentId = null;
|
||||||
let debounceTimer = 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 |`,
|
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; }`,
|
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': {
|
'wasm': {
|
||||||
html: '<h1>WebAssembly Demo</h1>\n<div id="output"></div>',
|
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; }`,
|
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', () => {
|
$('#btn-qr').addEventListener('click', () => {
|
||||||
const url = currentId ? `${location.origin}/f/${currentId}` : location.href;
|
const url = currentId ? `${location.origin}/f/${currentId}` : location.href;
|
||||||
showQrModal(url);
|
showQrModal(url);
|
||||||
@@ -531,6 +595,21 @@ async function init() {
|
|||||||
openCollectionModal(); // refresh list
|
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
|
// npm search in resources modal
|
||||||
initNpmSearch((pkg) => {
|
initNpmSearch((pkg) => {
|
||||||
currentResources.push({ type: pkg.type, url: pkg.url });
|
currentResources.push({ type: pkg.type, url: pkg.url });
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const editorOpts = {
|
|||||||
autoClosingQuotes: 'always',
|
autoClosingQuotes: 'always',
|
||||||
autoSurround: 'languageDefined',
|
autoSurround: 'languageDefined',
|
||||||
bracketPairColorization: { enabled: true },
|
bracketPairColorization: { enabled: true },
|
||||||
|
colorDecorators: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MODE_TABS = {
|
export const MODE_TABS = {
|
||||||
@@ -73,6 +74,10 @@ export const MODE_TABS = {
|
|||||||
{ id: 'css', label: 'CSS', lang: 'css', ext: 'css' },
|
{ id: 'css', label: 'CSS', lang: 'css', ext: 'css' },
|
||||||
{ id: 'js', label: 'JavaScript', lang: 'javascript', ext: 'js' },
|
{ 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
|
// Map mode names to js_type values stored in DB
|
||||||
@@ -85,6 +90,7 @@ export const MODE_TO_JS_TYPE = {
|
|||||||
'svelte': 'svelte',
|
'svelte': 'svelte',
|
||||||
'markdown': 'markdown',
|
'markdown': 'markdown',
|
||||||
'wasm': 'wasm',
|
'wasm': 'wasm',
|
||||||
|
'python': 'python',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const JS_TYPE_TO_MODE = Object.fromEntries(
|
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';
|
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 runtime = getFrameworkRuntime(mode);
|
||||||
const allCss = extraCss ? `${css}\n${extraCss}` : css;
|
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';
|
const effectiveIsModule = isModule || mode === 'svelte';
|
||||||
|
|
||||||
// Build importmap for module scripts with bare imports
|
|
||||||
let importMapTag = '';
|
let importMapTag = '';
|
||||||
if (effectiveIsModule && finalJs) {
|
if (effectiveIsModule && finalJs) {
|
||||||
const bareImports = extractBareImports(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">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
@@ -75,12 +74,18 @@ ${bodyContent}
|
|||||||
${scripts}
|
${scripts}
|
||||||
</body>
|
</body>
|
||||||
</html>`;
|
</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 blob = new Blob([doc], { type: 'text/html' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
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();
|
a.click();
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,9 @@ export async function compileJs(code, mode) {
|
|||||||
return { js: code, isModule: hasModuleSyntax || hasTopLevelAwait };
|
return { js: code, isModule: hasModuleSyntax || hasTopLevelAwait };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'python':
|
||||||
|
return compilePython(code);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return { js: code };
|
return { js: code };
|
||||||
}
|
}
|
||||||
@@ -188,6 +191,34 @@ async function compileSvelte(code) {
|
|||||||
return { js, warnings, isModule: true };
|
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.
|
* Get runtime scripts and body HTML to inject into the preview iframe.
|
||||||
*/
|
*/
|
||||||
@@ -223,6 +254,12 @@ export function getFrameworkRuntime(mode) {
|
|||||||
case 'wasm':
|
case 'wasm':
|
||||||
return { scripts: [], bodyHtml: '' };
|
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:
|
default:
|
||||||
return { scripts: [], bodyHtml: '' };
|
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();
|
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 ---
|
// --- Performance: timing metrics ---
|
||||||
window.__fiddle_scriptStart = performance.now();
|
window.__fiddle_scriptStart = performance.now();
|
||||||
window.addEventListener('load', function() {
|
window.addEventListener('load', function() {
|
||||||
|
|||||||
144
server.js
144
server.js
@@ -22,6 +22,13 @@ app.get('/f/:id', (_req, res) => {
|
|||||||
res.sendFile('index.html', { root: 'public' });
|
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 }));
|
app.use(express.static('public', { index: false }));
|
||||||
|
|
||||||
// API: Create fiddle
|
// API: Create fiddle
|
||||||
@@ -124,6 +131,109 @@ app.get('/api/tags', (_req, res) => {
|
|||||||
res.json({ tags: stmts.listTags.all() });
|
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 =====================
|
// ===================== Version History API =====================
|
||||||
|
|
||||||
app.get('/api/fiddles/:id/versions', (req, res) => {
|
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 });
|
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;
|
const port = process.env.PORT || 3000;
|
||||||
app.listen(port, () => console.log(`Fiddle server running on http://localhost:${port}`));
|
app.listen(port, () => console.log(`Fiddle server running on http://localhost:${port}`));
|
||||||
|
|||||||
Reference in New Issue
Block a user