Add Tailwind CSS toggle, Markdown/WASM modes, and npm import resolution
- Tailwind CSS: toolbar checkbox injects Play CDN into preview, persisted per-fiddle via new options JSON column - Markdown mode: uses marked.js CDN, renders markdown to HTML preview with CSS tab for custom styling - WASM mode: starter template with inline WebAssembly add function, supports top-level await via module detection - npm imports: auto-detect bare import specifiers in module code and inject importmap pointing to esm.sh CDN - Module auto-detection for html-css-js mode (import/export statements) - DB migration adds options column, server passes through all API endpoints - All features work across preview, export, and embed
This commit is contained in:
11
db.js
11
db.js
@@ -31,6 +31,11 @@ try {
|
||||
db.exec(`ALTER TABLE fiddles ADD COLUMN listed INTEGER NOT NULL DEFAULT 1`);
|
||||
} catch (_) { /* column already exists */ }
|
||||
|
||||
// Migration: add options column (JSON string for per-fiddle settings like tailwind)
|
||||
try {
|
||||
db.exec(`ALTER TABLE fiddles ADD COLUMN options TEXT NOT NULL DEFAULT '{}'`);
|
||||
} catch (_) { /* column already exists */ }
|
||||
|
||||
// Tags tables
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS tags (
|
||||
@@ -51,14 +56,14 @@ db.exec(`CREATE INDEX IF NOT EXISTS idx_fiddles_listed_updated ON fiddles(listed
|
||||
|
||||
export const stmts = {
|
||||
insert: db.prepare(`
|
||||
INSERT INTO fiddles (id, title, html, css, css_type, js, js_type, listed)
|
||||
VALUES (@id, @title, @html, @css, @css_type, @js, @js_type, @listed)
|
||||
INSERT INTO fiddles (id, title, html, css, css_type, js, js_type, listed, options)
|
||||
VALUES (@id, @title, @html, @css, @css_type, @js, @js_type, @listed, @options)
|
||||
`),
|
||||
get: db.prepare('SELECT * FROM fiddles WHERE id = ?'),
|
||||
update: db.prepare(`
|
||||
UPDATE fiddles SET title = @title, html = @html, css = @css,
|
||||
css_type = @css_type, js = @js, js_type = @js_type, listed = @listed,
|
||||
updated_at = datetime('now')
|
||||
options = @options, updated_at = datetime('now')
|
||||
WHERE id = @id
|
||||
`),
|
||||
list: db.prepare('SELECT id, title, css_type, js_type, created_at, updated_at FROM fiddles ORDER BY updated_at DESC LIMIT 50'),
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
<option value="react-ts">React + TS</option>
|
||||
<option value="vue">Vue</option>
|
||||
<option value="svelte">Svelte</option>
|
||||
<option value="markdown">Markdown</option>
|
||||
<option value="wasm">WASM</option>
|
||||
</select>
|
||||
<select id="filter-sort">
|
||||
<option value="updated">Recently Updated</option>
|
||||
|
||||
@@ -195,6 +195,18 @@ body.resizing iframe { pointer-events: none; }
|
||||
}
|
||||
.auto-run-toggle input { cursor: pointer; }
|
||||
|
||||
/* Tailwind toggle */
|
||||
.tailwind-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.tailwind-toggle input { cursor: pointer; }
|
||||
|
||||
/* Dividers */
|
||||
.divider { background: var(--border); transition: background 0.15s; z-index: 2; }
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
<option value="react-ts">React + TS</option>
|
||||
<option value="vue">Vue</option>
|
||||
<option value="svelte">Svelte</option>
|
||||
<option value="markdown">Markdown</option>
|
||||
<option value="wasm">WASM</option>
|
||||
</select>
|
||||
<input type="text" id="title-input" placeholder="Untitled" spellcheck="false">
|
||||
<select id="layout-mode">
|
||||
@@ -37,6 +39,10 @@
|
||||
<datalist id="tags-datalist"></datalist>
|
||||
<div id="tags-display" class="tags-display"></div>
|
||||
</div>
|
||||
<label class="tailwind-toggle" title="Enable Tailwind CSS">
|
||||
<input type="checkbox" id="tailwind-checkbox">
|
||||
Tailwind
|
||||
</label>
|
||||
<label class="listed-toggle" title="Show in browse page">
|
||||
<input type="checkbox" id="listed-checkbox" checked>
|
||||
Listed
|
||||
|
||||
@@ -47,8 +47,22 @@ const STARTER_TEMPLATES = {
|
||||
js: `<script>\n let count = 0;\n</script>\n\n<h1>Hello Svelte</h1>\n<button on:click={() => count++}>Count: {count}</button>\n\n<style>\n h1 { color: #ff3e00; }\n button { padding: 8px 16px; cursor: pointer; }\n</style>`,
|
||||
css: '',
|
||||
},
|
||||
'markdown': {
|
||||
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; }`,
|
||||
},
|
||||
'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; }`,
|
||||
js: `// Inline WebAssembly "add" module (no external URL needed)\n// WAT source: (module (func (export "add") (param i32 i32) (result i32) local.get 0 local.get 1 i32.add))\nconst wasmBytes = new Uint8Array([\n 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,\n 0x01, 0x07, 0x01, 0x60, 0x02, 0x7f, 0x7f, 0x01, 0x7f,\n 0x03, 0x02, 0x01, 0x00,\n 0x07, 0x07, 0x01, 0x03, 0x61, 0x64, 0x64, 0x00, 0x00,\n 0x0a, 0x09, 0x01, 0x07, 0x00, 0x20, 0x00, 0x20, 0x01, 0x6a, 0x0b\n]);\n\nconst out = document.getElementById('output');\n\ntry {\n const { instance } = await WebAssembly.instantiate(wasmBytes);\n const add = instance.exports.add;\n\n out.textContent = [\n \`WASM loaded successfully!\`,\n \`\`,\n \`add(2, 3) = \${add(2, 3)}\`,\n \`add(100, 200) = \${add(100, 200)}\`,\n \`add(-5, 10) = \${add(-5, 10)}\`,\n ].join('\\n');\n} catch (e) {\n out.textContent = 'Error: ' + e.message;\n}`,
|
||||
},
|
||||
};
|
||||
|
||||
function getTailwindChecked() {
|
||||
const cb = $('#tailwind-checkbox');
|
||||
return cb ? cb.checked : false;
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const mode = getCurrentMode();
|
||||
const { html, css, js } = getEditorValues();
|
||||
@@ -66,10 +80,16 @@ async function run() {
|
||||
});
|
||||
}
|
||||
|
||||
renderPreview(html, compiledCss, result.js, mode, result.extraCss || '');
|
||||
const options = {
|
||||
tailwind: getTailwindChecked(),
|
||||
isModule: result.isModule || false,
|
||||
renderedHtml: result.renderedHtml || null,
|
||||
};
|
||||
|
||||
renderPreview(html, compiledCss, result.js, mode, result.extraCss || '', options);
|
||||
} catch (e) {
|
||||
clearConsole();
|
||||
renderPreview(html, '', '', mode);
|
||||
renderPreview(html, '', '', mode, '', { tailwind: getTailwindChecked() });
|
||||
window.postMessage({ type: 'console', method: 'error', args: [`Compile error: ${e.message}`] }, '*');
|
||||
}
|
||||
}
|
||||
@@ -94,12 +114,13 @@ async function save() {
|
||||
const js_type = MODE_TO_JS_TYPE[getCurrentMode()] || 'javascript';
|
||||
const listed = $('#listed-checkbox').checked ? 1 : 0;
|
||||
const tags = currentTags.slice();
|
||||
const options = JSON.stringify({ tailwind: getTailwindChecked() });
|
||||
try {
|
||||
if (currentId) {
|
||||
await updateFiddle(currentId, { title, html, css, css_type, js, js_type, listed, tags });
|
||||
await updateFiddle(currentId, { title, html, css, css_type, js, js_type, listed, options, tags });
|
||||
showToast(`Saved! Share: ${location.origin}/f/${currentId}`);
|
||||
} else {
|
||||
const result = await createFiddle({ title, html, css, css_type, js, js_type, listed, tags });
|
||||
const result = await createFiddle({ title, html, css, css_type, js, js_type, listed, options, tags });
|
||||
currentId = result.id;
|
||||
history.pushState(null, '', `/f/${currentId}`);
|
||||
showToast(`Saved! Share: ${location.origin}/f/${currentId}`);
|
||||
@@ -116,8 +137,9 @@ async function fork() {
|
||||
const js_type = MODE_TO_JS_TYPE[getCurrentMode()] || 'javascript';
|
||||
const listed = $('#listed-checkbox').checked ? 1 : 0;
|
||||
const tags = currentTags.slice();
|
||||
const options = JSON.stringify({ tailwind: getTailwindChecked() });
|
||||
try {
|
||||
const result = await createFiddle({ title, html, css, css_type, js, js_type, listed, tags });
|
||||
const result = await createFiddle({ title, html, css, css_type, js, js_type, listed, options, tags });
|
||||
currentId = result.id;
|
||||
$('#title-input').value = title;
|
||||
history.pushState(null, '', `/f/${currentId}`);
|
||||
@@ -148,6 +170,11 @@ async function loadFromUrl() {
|
||||
currentTags = (fiddle.tags || []).map(t => t.name);
|
||||
renderTags();
|
||||
|
||||
// Restore options (tailwind checkbox)
|
||||
const opts = JSON.parse(fiddle.options || '{}');
|
||||
const twCb = $('#tailwind-checkbox');
|
||||
if (twCb) twCb.checked = !!opts.tailwind;
|
||||
|
||||
setEditorValues(fiddle);
|
||||
setTimeout(run, 100);
|
||||
} catch (e) {
|
||||
@@ -204,6 +231,12 @@ async function init() {
|
||||
autoRunCb.checked = getPref('autoRun');
|
||||
autoRunCb.addEventListener('change', (e) => setPref('autoRun', e.target.checked));
|
||||
|
||||
// Tailwind checkbox
|
||||
const twCb = $('#tailwind-checkbox');
|
||||
if (twCb) {
|
||||
twCb.addEventListener('change', () => scheduleRun());
|
||||
}
|
||||
|
||||
// Layout selector
|
||||
const layoutSel = $('#layout-mode');
|
||||
const savedLayout = getPref('layout') || 'default';
|
||||
@@ -259,7 +292,13 @@ async function init() {
|
||||
try {
|
||||
const compiledCss = await compileCss(css, cssType);
|
||||
const result = await compileJs(js, mode);
|
||||
exportHtml({ title, html, css: compiledCss, js: result.js, mode, extraCss: result.extraCss, isModule: result.isModule });
|
||||
exportHtml({
|
||||
title, html, css: compiledCss, js: result.js, mode,
|
||||
extraCss: result.extraCss,
|
||||
tailwind: getTailwindChecked(),
|
||||
isModule: result.isModule || false,
|
||||
renderedHtml: result.renderedHtml || null,
|
||||
});
|
||||
} catch (e) {
|
||||
showToast(`Export failed: ${e.message}`);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ const JS_TYPE_LABELS = {
|
||||
'react-ts': 'React+TS',
|
||||
vue: 'Vue',
|
||||
svelte: 'Svelte',
|
||||
markdown: 'MD',
|
||||
wasm: 'WASM',
|
||||
};
|
||||
|
||||
function relativeTime(dateStr) {
|
||||
|
||||
@@ -39,6 +39,15 @@ export const MODE_TABS = {
|
||||
{ id: 'js', label: 'Svelte', lang: 'html' },
|
||||
{ id: 'css', label: 'CSS', lang: 'css' },
|
||||
],
|
||||
'markdown': [
|
||||
{ id: 'js', label: 'Markdown', lang: 'markdown' },
|
||||
{ id: 'css', label: 'CSS', lang: 'css' },
|
||||
],
|
||||
'wasm': [
|
||||
{ id: 'html', label: 'HTML', lang: 'html' },
|
||||
{ id: 'css', label: 'CSS', lang: 'css' },
|
||||
{ id: 'js', label: 'JavaScript', lang: 'javascript' },
|
||||
],
|
||||
};
|
||||
|
||||
// Map mode names to js_type values stored in DB
|
||||
@@ -49,6 +58,8 @@ export const MODE_TO_JS_TYPE = {
|
||||
'react-ts': 'tsx',
|
||||
'vue': 'vue',
|
||||
'svelte': 'svelte',
|
||||
'markdown': 'markdown',
|
||||
'wasm': 'wasm',
|
||||
};
|
||||
|
||||
export const JS_TYPE_TO_MODE = Object.fromEntries(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { loadFiddle } from './api.js';
|
||||
import { getFrameworkRuntime } from './js-preprocessors.js';
|
||||
import { extractBareImports, buildImportMapTag } from './import-map.js';
|
||||
|
||||
const MODE_MAP = {
|
||||
javascript: 'html-css-js',
|
||||
@@ -8,6 +9,8 @@ const MODE_MAP = {
|
||||
'react-ts': 'react-ts',
|
||||
vue: 'vue',
|
||||
svelte: 'svelte',
|
||||
markdown: 'markdown',
|
||||
wasm: 'wasm',
|
||||
};
|
||||
|
||||
async function init() {
|
||||
@@ -26,6 +29,7 @@ async function init() {
|
||||
const fiddle = await loadFiddle(match[1]);
|
||||
const mode = MODE_MAP[fiddle.js_type] || 'html-css-js';
|
||||
const runtime = getFrameworkRuntime(mode);
|
||||
const opts = JSON.parse(fiddle.options || '{}');
|
||||
|
||||
// For embed, we compile client-side using the same preprocessors
|
||||
const { compileCss } = await import('./preprocessors.js');
|
||||
@@ -36,32 +40,51 @@ async function init() {
|
||||
|
||||
const allCss = result.extraCss ? `${compiledCss}\n${result.extraCss}` : compiledCss;
|
||||
|
||||
const finalHtml = result.renderedHtml || fiddle.html;
|
||||
const finalJs = result.renderedHtml ? '' : result.js;
|
||||
|
||||
let bodyContent;
|
||||
if (mode === 'vue' || mode === 'svelte') {
|
||||
bodyContent = fiddle.html ? `${fiddle.html}\n${runtime.bodyHtml}` : runtime.bodyHtml;
|
||||
bodyContent = finalHtml ? `${finalHtml}\n${runtime.bodyHtml}` : runtime.bodyHtml;
|
||||
} else if (runtime.bodyHtml) {
|
||||
bodyContent = `${fiddle.html}\n${runtime.bodyHtml}`;
|
||||
bodyContent = `${finalHtml}\n${runtime.bodyHtml}`;
|
||||
} else {
|
||||
bodyContent = fiddle.html;
|
||||
bodyContent = finalHtml;
|
||||
}
|
||||
|
||||
const isModule = result.isModule || mode === 'svelte';
|
||||
|
||||
// Build importmap for module scripts with bare imports
|
||||
let importMapTag = '';
|
||||
if (isModule && finalJs) {
|
||||
const bareImports = extractBareImports(finalJs);
|
||||
if (bareImports.length) {
|
||||
importMapTag = buildImportMapTag(bareImports);
|
||||
}
|
||||
}
|
||||
|
||||
let scripts = '';
|
||||
if (result.js) {
|
||||
if (result.isModule) {
|
||||
scripts = `<script type="module">\n${escapeScriptClose(result.js)}\n<\/script>`;
|
||||
if (finalJs) {
|
||||
if (isModule) {
|
||||
scripts = `<script type="module">\n${escapeScriptClose(finalJs)}\n<\/script>`;
|
||||
} else {
|
||||
for (const url of runtime.scripts) {
|
||||
scripts += `<script src="${url}"><\/script>\n`;
|
||||
}
|
||||
scripts += `<script>\n${escapeScriptClose(result.js)}\n<\/script>`;
|
||||
scripts += `<script>\n${escapeScriptClose(finalJs)}\n<\/script>`;
|
||||
}
|
||||
}
|
||||
|
||||
const tailwindScript = opts.tailwind
|
||||
? `<script src="https://cdn.tailwindcss.com"><\/script>\n`
|
||||
: '';
|
||||
|
||||
const doc = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>${allCss}</style>
|
||||
${tailwindScript}<style>${allCss}</style>
|
||||
${importMapTag}
|
||||
</head>
|
||||
<body>
|
||||
${bodyContent}
|
||||
|
||||
@@ -1,42 +1,62 @@
|
||||
import { getFrameworkRuntime } from './js-preprocessors.js';
|
||||
import { extractBareImports, buildImportMapTag } from './import-map.js';
|
||||
|
||||
/**
|
||||
* Export a fiddle as a standalone HTML file and trigger download.
|
||||
*/
|
||||
export function exportHtml({ title, html, css, js, mode, extraCss = '', isModule = false }) {
|
||||
export function exportHtml({ title, html, css, js, mode, extraCss = '', isModule = false, tailwind = false, renderedHtml = null }) {
|
||||
const runtime = getFrameworkRuntime(mode);
|
||||
const allCss = extraCss ? `${css}\n${extraCss}` : css;
|
||||
|
||||
const finalHtml = renderedHtml || html;
|
||||
const finalJs = renderedHtml ? '' : js;
|
||||
|
||||
let bodyContent;
|
||||
if (mode === 'vue' || mode === 'svelte') {
|
||||
bodyContent = html ? `${html}\n${runtime.bodyHtml}` : runtime.bodyHtml;
|
||||
bodyContent = finalHtml ? `${finalHtml}\n${runtime.bodyHtml}` : runtime.bodyHtml;
|
||||
} else if (runtime.bodyHtml) {
|
||||
bodyContent = `${html}\n${runtime.bodyHtml}`;
|
||||
bodyContent = `${finalHtml}\n${runtime.bodyHtml}`;
|
||||
} else {
|
||||
bodyContent = html;
|
||||
bodyContent = finalHtml;
|
||||
}
|
||||
|
||||
const effectiveIsModule = isModule || mode === 'svelte';
|
||||
|
||||
// Build importmap for module scripts with bare imports
|
||||
let importMapTag = '';
|
||||
if (effectiveIsModule && finalJs) {
|
||||
const bareImports = extractBareImports(finalJs);
|
||||
if (bareImports.length) {
|
||||
importMapTag = buildImportMapTag(bareImports);
|
||||
}
|
||||
}
|
||||
|
||||
let scripts = '';
|
||||
if (js) {
|
||||
if (isModule) {
|
||||
scripts = `<script type="module">\n${js}\n<\/script>`;
|
||||
if (finalJs) {
|
||||
if (effectiveIsModule) {
|
||||
scripts = `<script type="module">\n${finalJs}\n<\/script>`;
|
||||
} else {
|
||||
for (const url of runtime.scripts) {
|
||||
scripts += `<script src="${url}"><\/script>\n`;
|
||||
}
|
||||
scripts += `<script>\n${js}\n<\/script>`;
|
||||
scripts += `<script>\n${finalJs}\n<\/script>`;
|
||||
}
|
||||
}
|
||||
|
||||
const tailwindScript = tailwind
|
||||
? `<script src="https://cdn.tailwindcss.com"><\/script>\n`
|
||||
: '';
|
||||
|
||||
const doc = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${escHtml(title)}</title>
|
||||
<style>
|
||||
${tailwindScript}<style>
|
||||
${allCss}
|
||||
</style>
|
||||
${importMapTag}
|
||||
</head>
|
||||
<body>
|
||||
${bodyContent}
|
||||
|
||||
43
public/js/import-map.js
Normal file
43
public/js/import-map.js
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Extract bare import specifiers from JS code.
|
||||
* Matches `from 'pkg'`, `from "pkg"`, `import 'pkg'`, `import "pkg"`
|
||||
* where the specifier doesn't start with `.`, `/`, `http:`, or `https:`.
|
||||
*/
|
||||
export function extractBareImports(jsCode) {
|
||||
const imports = new Set();
|
||||
const re = /(?:import\s+(?:[\s\S]*?\s+from\s+)?['"])([^'"\s][^'"]*?)(?=['"])/g;
|
||||
let match;
|
||||
while ((match = re.exec(jsCode)) !== null) {
|
||||
const spec = match[1];
|
||||
// Skip relative, absolute, and URL imports
|
||||
if (spec.startsWith('.') || spec.startsWith('/') || spec.startsWith('http:') || spec.startsWith('https:')) {
|
||||
continue;
|
||||
}
|
||||
// Skip svelte internal imports (already rewritten by the compiler)
|
||||
if (spec.startsWith('svelte')) continue;
|
||||
imports.add(spec);
|
||||
}
|
||||
return Array.from(imports);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an HTML <script type="importmap"> tag from bare import specifiers.
|
||||
* Maps each bare import to its esm.sh URL.
|
||||
*/
|
||||
export function buildImportMapTag(bareImports) {
|
||||
if (!bareImports.length) return '';
|
||||
const imports = {};
|
||||
for (const spec of bareImports) {
|
||||
// Get the package name (handle scoped packages like @scope/pkg)
|
||||
const parts = spec.split('/');
|
||||
const pkgName = spec.startsWith('@') ? `${parts[0]}/${parts[1]}` : parts[0];
|
||||
// Map the full specifier (e.g. 'lodash/fp' -> 'https://esm.sh/lodash/fp')
|
||||
imports[spec] = `https://esm.sh/${spec}`;
|
||||
// Also map the base package if it's different
|
||||
if (spec !== pkgName) {
|
||||
imports[pkgName] = `https://esm.sh/${pkgName}`;
|
||||
}
|
||||
}
|
||||
const map = JSON.stringify({ imports }, null, 2);
|
||||
return `<script type="importmap">\n${map}\n<\/script>`;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { loadScript } from './utils.js';
|
||||
let tsLoaded = false;
|
||||
let babelLoaded = false;
|
||||
let svelteLoaded = false;
|
||||
let markedLoaded = false;
|
||||
|
||||
async function ensureTypeScript() {
|
||||
if (tsLoaded) return;
|
||||
@@ -31,6 +32,19 @@ async function ensureBabel() {
|
||||
babelLoaded = true;
|
||||
}
|
||||
|
||||
async function ensureMarked() {
|
||||
if (markedLoaded) return;
|
||||
await loadScript('https://cdnjs.cloudflare.com/ajax/libs/marked/15.0.7/marked.min.js');
|
||||
if (typeof marked === 'undefined') {
|
||||
await new Promise((resolve) => {
|
||||
const check = setInterval(() => {
|
||||
if (typeof marked !== 'undefined') { clearInterval(check); resolve(); }
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
markedLoaded = true;
|
||||
}
|
||||
|
||||
async function ensureSvelte() {
|
||||
if (svelteLoaded) return;
|
||||
await loadScript('https://unpkg.com/svelte@4.2.19/compiler.cjs');
|
||||
@@ -45,8 +59,10 @@ export async function compileJs(code, mode) {
|
||||
if (!code.trim()) return { js: '' };
|
||||
|
||||
switch (mode) {
|
||||
case 'html-css-js':
|
||||
return { js: code };
|
||||
case 'html-css-js': {
|
||||
const isModule = /(?:^|\n)\s*(?:import\s|export\s)/m.test(code);
|
||||
return { js: code, isModule };
|
||||
}
|
||||
|
||||
case 'typescript':
|
||||
return compileTypeScript(code);
|
||||
@@ -63,6 +79,16 @@ export async function compileJs(code, mode) {
|
||||
case 'svelte':
|
||||
return compileSvelte(code);
|
||||
|
||||
case 'markdown':
|
||||
return compileMarkdown(code);
|
||||
|
||||
case 'wasm': {
|
||||
// WASM starter uses top-level await, always treat as module
|
||||
const hasModuleSyntax = /(?:^|\n)\s*(?:import\s|export\s)/m.test(code);
|
||||
const hasTopLevelAwait = /(?:^|\n)\s*(?:const|let|var)\s+.*=\s*await\b/m.test(code) || /(?:^|\n)\s*await\s/m.test(code);
|
||||
return { js: code, isModule: hasModuleSyntax || hasTopLevelAwait };
|
||||
}
|
||||
|
||||
default:
|
||||
return { js: code };
|
||||
}
|
||||
@@ -125,6 +151,12 @@ function compileVue(code) {
|
||||
return { js, extraCss };
|
||||
}
|
||||
|
||||
async function compileMarkdown(code) {
|
||||
await ensureMarked();
|
||||
const renderedHtml = marked.parse(code);
|
||||
return { js: '', renderedHtml };
|
||||
}
|
||||
|
||||
async function compileSvelte(code) {
|
||||
await ensureSvelte();
|
||||
const result = svelte.compile(code, {
|
||||
@@ -185,6 +217,12 @@ export function getFrameworkRuntime(mode) {
|
||||
bodyHtml: '<div id="app"></div>',
|
||||
};
|
||||
|
||||
case 'markdown':
|
||||
return { scripts: [], bodyHtml: '' };
|
||||
|
||||
case 'wasm':
|
||||
return { scripts: [], bodyHtml: '' };
|
||||
|
||||
default:
|
||||
return { scripts: [], bodyHtml: '' };
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getFrameworkRuntime } from './js-preprocessors.js';
|
||||
import { extractBareImports, buildImportMapTag } from './import-map.js';
|
||||
|
||||
const consoleInterceptor = `
|
||||
<script>
|
||||
@@ -56,26 +57,54 @@ function buildLoaderScript(runtimeUrls, userJs, isModule) {
|
||||
return parts;
|
||||
}
|
||||
|
||||
export function renderPreview(html, css, js, mode = 'html-css-js', extraCss = '') {
|
||||
/**
|
||||
* Render compiled code into the preview iframe.
|
||||
* @param {string} html - HTML content
|
||||
* @param {string} css - Compiled CSS
|
||||
* @param {string} js - Compiled JS
|
||||
* @param {string} mode - Framework mode
|
||||
* @param {string} extraCss - Extra CSS (e.g. from Vue/Svelte)
|
||||
* @param {object} options - { tailwind, isModule, renderedHtml }
|
||||
*/
|
||||
export function renderPreview(html, css, js, mode = 'html-css-js', extraCss = '', options = {}) {
|
||||
const frame = document.getElementById('preview-frame');
|
||||
const runtime = getFrameworkRuntime(mode);
|
||||
|
||||
// Combine CSS
|
||||
const allCss = extraCss ? `${css}\n${extraCss}` : css;
|
||||
|
||||
// If renderedHtml is provided (e.g. Markdown), use it as the body and skip JS
|
||||
const finalHtml = options.renderedHtml || html;
|
||||
const finalJs = options.renderedHtml ? '' : js;
|
||||
|
||||
// Determine body content
|
||||
let bodyContent;
|
||||
if (mode === 'vue' || mode === 'svelte') {
|
||||
bodyContent = html ? `${html}\n${runtime.bodyHtml}` : runtime.bodyHtml;
|
||||
bodyContent = finalHtml ? `${finalHtml}\n${runtime.bodyHtml}` : runtime.bodyHtml;
|
||||
} else if (runtime.bodyHtml) {
|
||||
bodyContent = `${html}\n${runtime.bodyHtml}`;
|
||||
bodyContent = `${finalHtml}\n${runtime.bodyHtml}`;
|
||||
} else {
|
||||
bodyContent = html;
|
||||
bodyContent = finalHtml;
|
||||
}
|
||||
|
||||
const isModule = mode === 'svelte';
|
||||
const loaderScript = js
|
||||
? buildLoaderScript(runtime.scripts, js, isModule)
|
||||
const isModule = options.isModule || mode === 'svelte';
|
||||
|
||||
// Build importmap for module scripts with bare imports
|
||||
let importMapTag = '';
|
||||
if (isModule && finalJs) {
|
||||
const bareImports = extractBareImports(finalJs);
|
||||
if (bareImports.length) {
|
||||
importMapTag = buildImportMapTag(bareImports);
|
||||
}
|
||||
}
|
||||
|
||||
const loaderScript = finalJs
|
||||
? buildLoaderScript(runtime.scripts, finalJs, isModule)
|
||||
: '';
|
||||
|
||||
// Tailwind CDN injection
|
||||
const tailwindScript = options.tailwind
|
||||
? `<script src="https://cdn.tailwindcss.com"><\/script>\n`
|
||||
: '';
|
||||
|
||||
const doc = `<!DOCTYPE html>
|
||||
@@ -83,7 +112,8 @@ export function renderPreview(html, css, js, mode = 'html-css-js', extraCss = ''
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
${consoleInterceptor}
|
||||
<style>${allCss}</style>
|
||||
${tailwindScript}<style>${allCss}</style>
|
||||
${importMapTag}
|
||||
</head>
|
||||
<body>
|
||||
${bodyContent}
|
||||
|
||||
11
server.js
11
server.js
@@ -27,12 +27,12 @@ app.use(express.static('public', { index: false }));
|
||||
// API: Create fiddle
|
||||
app.post('/api/fiddles', (req, res) => {
|
||||
const id = nanoid(10);
|
||||
const { title = 'Untitled', html = '', css = '', css_type = 'css', js = '', js_type = 'javascript', listed = 1, tags = [] } = req.body;
|
||||
const { title = 'Untitled', html = '', css = '', css_type = 'css', js = '', js_type = 'javascript', listed = 1, options = '{}', tags = [] } = req.body;
|
||||
try {
|
||||
stmts.insert.run({ id, title, html, css, css_type, js, js_type, listed: listed ? 1 : 0 });
|
||||
stmts.insert.run({ id, title, html, css, css_type, js, js_type, listed: listed ? 1 : 0, options });
|
||||
if (tags.length) setFiddleTags(id, tags);
|
||||
const fiddleTags = stmts.getTagsForFiddle.all(id);
|
||||
res.json({ id, title, html, css, css_type, js, js_type, listed, tags: fiddleTags });
|
||||
res.json({ id, title, html, css, css_type, js, js_type, listed, options, tags: fiddleTags });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
@@ -104,12 +104,13 @@ app.put('/api/fiddles/:id', (req, res) => {
|
||||
js = existing.js,
|
||||
js_type = existing.js_type || 'javascript',
|
||||
listed = existing.listed,
|
||||
options = existing.options || '{}',
|
||||
tags,
|
||||
} = req.body;
|
||||
stmts.update.run({ id: req.params.id, title, html, css, css_type, js, js_type, listed: listed ? 1 : 0 });
|
||||
stmts.update.run({ id: req.params.id, title, html, css, css_type, js, js_type, listed: listed ? 1 : 0, options });
|
||||
if (Array.isArray(tags)) setFiddleTags(req.params.id, tags);
|
||||
const fiddleTags = stmts.getTagsForFiddle.all(req.params.id);
|
||||
res.json({ id: req.params.id, title, html, css, css_type, js, js_type, listed, tags: fiddleTags });
|
||||
res.json({ id: req.params.id, title, html, css, css_type, js, js_type, listed, options, tags: fiddleTags });
|
||||
});
|
||||
|
||||
// API: List tags
|
||||
|
||||
Reference in New Issue
Block a user