- Device breakpoint toggles (mobile 375px / tablet 768px / desktop 100%) - Editor theme selector with 6 themes (VS Dark/Light, High Contrast, Monokai, Dracula, GitHub Dark) - Starter template gallery with 8 pre-built templates (Todo, API Fetch, CSS Animation, etc.) - Code autocomplete with DOM/React type definitions and snippet completions - Devtools panels: console, network, elements, performance - Code formatter (Prettier), diff view, and linter integration
91 lines
2.9 KiB
JavaScript
91 lines
2.9 KiB
JavaScript
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, tailwind = false, renderedHtml = null, previewTheme = 'light', resources = [] }) {
|
|
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 = finalHtml ? `${finalHtml}\n${runtime.bodyHtml}` : runtime.bodyHtml;
|
|
} else if (runtime.bodyHtml) {
|
|
bodyContent = `${finalHtml}\n${runtime.bodyHtml}`;
|
|
} else {
|
|
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 (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${finalJs}\n<\/script>`;
|
|
}
|
|
}
|
|
|
|
const tailwindScript = tailwind
|
|
? `<script>var _tw=console.warn;console.warn=function(){if(typeof arguments[0]==='string'&&arguments[0].indexOf('cdn.tailwindcss.com')!==-1)return;_tw.apply(console,arguments)}<\/script>\n<script src="https://cdn.tailwindcss.com"><\/script>\n<script>console.warn=_tw<\/script>\n`
|
|
: '';
|
|
|
|
const darkCss = previewTheme === 'dark'
|
|
? `<style>body { background: #1e1e1e; color: #ccc; }</style>\n`
|
|
: '';
|
|
|
|
let resourceTags = '';
|
|
if (resources && resources.length) {
|
|
for (const r of resources) {
|
|
if (r.type === 'css') resourceTags += `<link rel="stylesheet" href="${r.url}">\n`;
|
|
else if (r.type === 'js') resourceTags += `<script src="${r.url}"><\/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>
|
|
${darkCss}${resourceTags}${tailwindScript}<style>
|
|
${allCss}
|
|
</style>
|
|
${importMapTag}
|
|
</head>
|
|
<body>
|
|
${bodyContent}
|
|
${scripts}
|
|
</body>
|
|
</html>`;
|
|
|
|
const blob = new Blob([doc], { type: 'text/html' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `${title.replace(/[^a-zA-Z0-9_-]/g, '_')}.html`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
function escHtml(str) {
|
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|