- 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
206 lines
7.3 KiB
JavaScript
206 lines
7.3 KiB
JavaScript
import { getFrameworkRuntime } from './js-preprocessors.js';
|
|
import { extractBareImports, buildImportMapTag } from './import-map.js';
|
|
|
|
const consoleInterceptor = `
|
|
<script>
|
|
(function() {
|
|
const methods = ['log', 'warn', 'error', 'info', 'debug', 'clear'];
|
|
methods.forEach(function(method) {
|
|
var original = console[method];
|
|
console[method] = function() {
|
|
if (method === 'clear') {
|
|
parent.postMessage({ type: 'console', method: 'clear' }, '*');
|
|
if (original) original.apply(console, arguments);
|
|
return;
|
|
}
|
|
var args = Array.from(arguments).map(function(a) {
|
|
if (a === null) return 'null';
|
|
if (a === undefined) return 'undefined';
|
|
if (typeof a === 'object') {
|
|
try { return JSON.stringify(a, null, 2); } catch(e) { return String(a); }
|
|
}
|
|
return String(a);
|
|
});
|
|
parent.postMessage({ type: 'console', method: method, args: args }, '*');
|
|
if (original) original.apply(console, arguments);
|
|
};
|
|
});
|
|
window.onerror = function(msg, url, line, col) {
|
|
parent.postMessage({ type: 'console', method: 'error', args: ['Error: ' + msg + ' (line ' + line + ')'] }, '*');
|
|
};
|
|
|
|
// --- Network: PerformanceObserver for resource loads ---
|
|
try {
|
|
var netObserver = new PerformanceObserver(function(list) {
|
|
var entries = list.getEntries().map(function(e) {
|
|
return { name: e.name, initiatorType: e.initiatorType, duration: e.duration, transferSize: e.transferSize || 0, startTime: e.startTime };
|
|
});
|
|
if (entries.length) parent.postMessage({ type: 'devtools', tab: 'network', entries: entries }, '*');
|
|
});
|
|
netObserver.observe({ type: 'resource', buffered: true });
|
|
} catch(e) {}
|
|
|
|
// --- Elements: serialize DOM tree on DOMContentLoaded ---
|
|
function serializeNode(node, depth) {
|
|
if (depth > 15) return null;
|
|
if (node.nodeType === 3) {
|
|
var t = node.textContent;
|
|
if (!t.trim()) return null;
|
|
return { type: 'text', text: t };
|
|
}
|
|
if (node.nodeType !== 1) return null;
|
|
var tag = node.tagName.toLowerCase();
|
|
if (tag === 'script' || tag === 'style') {
|
|
return { type: 'element', tag: tag, attrs: getAttrs(node), children: [] };
|
|
}
|
|
var children = [];
|
|
for (var i = 0; i < node.childNodes.length; i++) {
|
|
var c = serializeNode(node.childNodes[i], depth + 1);
|
|
if (c) children.push(c);
|
|
}
|
|
return { type: 'element', tag: tag, attrs: getAttrs(node), children: children };
|
|
}
|
|
function getAttrs(el) {
|
|
var arr = [];
|
|
for (var i = 0; i < el.attributes.length; i++) {
|
|
arr.push({ name: el.attributes[i].name, value: el.attributes[i].value });
|
|
}
|
|
return arr;
|
|
}
|
|
function sendElements() {
|
|
var tree = serializeNode(document.documentElement, 0);
|
|
parent.postMessage({ type: 'devtools', tab: 'elements', tree: tree }, '*');
|
|
}
|
|
document.addEventListener('DOMContentLoaded', function() { setTimeout(sendElements, 50); });
|
|
window.addEventListener('message', function(e) {
|
|
if (e.data && e.data.type === 'devtools-request' && e.data.tab === 'elements') sendElements();
|
|
});
|
|
|
|
// --- Performance: timing metrics ---
|
|
window.__fiddle_scriptStart = performance.now();
|
|
window.addEventListener('load', function() {
|
|
var scriptEnd = window.__fiddle_scriptEnd || performance.now();
|
|
var metrics = {
|
|
scriptDuration: scriptEnd - window.__fiddle_scriptStart,
|
|
domNodes: document.getElementsByTagName('*').length,
|
|
resourceCount: performance.getEntriesByType('resource').length
|
|
};
|
|
var nav = performance.getEntriesByType('navigation');
|
|
if (nav && nav.length) {
|
|
metrics.domContentLoaded = nav[0].domContentLoadedEventEnd;
|
|
metrics.loadEvent = nav[0].loadEventEnd || performance.now();
|
|
}
|
|
parent.postMessage({ type: 'devtools', tab: 'performance', metrics: metrics }, '*');
|
|
});
|
|
})();
|
|
<\/script>
|
|
`;
|
|
|
|
/**
|
|
* Escape </script in user code to prevent premature HTML script tag closing.
|
|
*/
|
|
function escapeScriptClose(code) {
|
|
return code.replace(/<\/script/gi, '<\\/script');
|
|
}
|
|
|
|
/**
|
|
* Build script tags for the preview iframe.
|
|
* Uses static <script src> tags for runtime libraries (parser-blocking per HTML spec)
|
|
* followed by an inline <script> for user code.
|
|
*/
|
|
function buildLoaderScript(runtimeUrls, userJs, isModule) {
|
|
const endMarker = 'window.__fiddle_scriptEnd = performance.now();';
|
|
if (isModule) {
|
|
return `\n<script type="module">\n${escapeScriptClose(userJs)}\n${endMarker}\n<\/script>`;
|
|
}
|
|
|
|
let parts = '';
|
|
for (const url of runtimeUrls) {
|
|
parts += `<script src="${url}"><\/script>\n`;
|
|
}
|
|
parts += `<script>\n${escapeScriptClose(userJs)}\n${endMarker}\n<\/script>`;
|
|
return parts;
|
|
}
|
|
|
|
/**
|
|
* 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 = finalHtml ? `${finalHtml}\n${runtime.bodyHtml}` : runtime.bodyHtml;
|
|
} else if (runtime.bodyHtml) {
|
|
bodyContent = `${finalHtml}\n${runtime.bodyHtml}`;
|
|
} else {
|
|
bodyContent = finalHtml;
|
|
}
|
|
|
|
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 (suppress production warning)
|
|
const tailwindScript = options.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`
|
|
: '';
|
|
|
|
// Dark preview theme
|
|
const darkCss = options.previewTheme === 'dark'
|
|
? `<style>body { background: #1e1e1e; color: #ccc; }</style>\n`
|
|
: '';
|
|
|
|
// External resources
|
|
let resourceTags = '';
|
|
if (options.resources && options.resources.length) {
|
|
for (const r of options.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>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
${consoleInterceptor}
|
|
${darkCss}${resourceTags}${tailwindScript}<style>${allCss}</style>
|
|
${importMapTag}
|
|
</head>
|
|
<body>
|
|
${bodyContent}
|
|
${loaderScript}
|
|
</body>
|
|
</html>`;
|
|
// Update iframe bg class
|
|
frame.classList.toggle('preview-dark', options.previewTheme === 'dark');
|
|
frame.srcdoc = doc;
|
|
}
|