- CodeRunner.vue: sandboxed iframe with srcdoc, allow-scripts only - Console capture via postMessage (log + error) - Run button, clear output, code preview - Extracts runnable code blocks from markdown Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
134 lines
4.0 KiB
Vue
134 lines
4.0 KiB
Vue
<template>
|
|
<div class="code-runner rounded-xl bg-white/5 border border-white/10 overflow-hidden">
|
|
<!-- Toolbar -->
|
|
<div class="flex items-center gap-2 px-3 py-2 border-b border-white/5">
|
|
<span class="text-[10px] text-white/30 uppercase tracking-wider">{{ language }}</span>
|
|
<div class="flex-1" />
|
|
<button
|
|
class="text-[10px] px-2.5 py-1 rounded bg-accent/15 text-accent/80 hover:bg-accent/25 transition-colors"
|
|
@click="runCode"
|
|
>
|
|
Run
|
|
</button>
|
|
<button
|
|
v-if="consoleOutput.length > 0"
|
|
class="text-[10px] px-2 py-1 rounded bg-white/5 text-white/40 hover:text-white/60 hover:bg-white/10 transition-colors"
|
|
@click="clearOutput"
|
|
>
|
|
Clear
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Code display -->
|
|
<pre class="px-3 py-2 text-xs text-white/70 overflow-x-auto max-h-48 bg-black/20"><code>{{ code }}</code></pre>
|
|
|
|
<!-- Output iframe (hidden, used for execution) -->
|
|
<iframe
|
|
v-if="showIframe"
|
|
ref="iframeRef"
|
|
:srcdoc="srcdoc"
|
|
sandbox="allow-scripts"
|
|
class="w-full border-t border-white/5"
|
|
:class="isHtml ? 'h-48' : 'h-0 invisible'"
|
|
title="Code output"
|
|
/>
|
|
|
|
<!-- Console output -->
|
|
<div
|
|
v-if="consoleOutput.length > 0"
|
|
class="border-t border-white/5 bg-black/30 px-3 py-2 max-h-32 overflow-y-auto"
|
|
>
|
|
<p class="text-[9px] text-white/20 uppercase tracking-wider mb-1">Console</p>
|
|
<div
|
|
v-for="(entry, i) in consoleOutput"
|
|
:key="i"
|
|
class="text-[11px] font-mono leading-relaxed"
|
|
:class="entry.type === 'error' ? 'text-red-400/80' : 'text-white/60'"
|
|
>
|
|
{{ entry.text }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
|
|
|
const props = defineProps<{
|
|
code: string
|
|
language: string
|
|
}>()
|
|
|
|
interface ConsoleEntry {
|
|
type: 'log' | 'error'
|
|
text: string
|
|
}
|
|
|
|
const consoleOutput = ref<ConsoleEntry[]>([])
|
|
const showIframe = ref(false)
|
|
const iframeRef = ref<HTMLIFrameElement | null>(null)
|
|
|
|
const isHtml = computed(() => props.language === 'html')
|
|
|
|
const srcdoc = computed(() => {
|
|
if (isHtml.value) {
|
|
// For HTML, wrap with console capture
|
|
return `<!DOCTYPE html>
|
|
<html><head><style>body{background:#0a0a0a;color:#e0e0e0;font-family:system-ui;margin:8px}</style>
|
|
<script>
|
|
const _post = (type, args) => parent.postMessage({type:'code-runner-console',level:type,text:args.map(a=>typeof a==='object'?JSON.stringify(a):String(a)).join(' ')},'*');
|
|
console.log = (...a) => _post('log', a);
|
|
console.error = (...a) => _post('error', a);
|
|
window.onerror = (m) => _post('error', [m]);
|
|
<\/script></head><body>${props.code}</body></html>`
|
|
}
|
|
|
|
// For JS/CSS, wrap in HTML
|
|
if (props.language === 'javascript' || props.language === 'js') {
|
|
return `<!DOCTYPE html><html><head>
|
|
<script>
|
|
const _post = (type, args) => parent.postMessage({type:'code-runner-console',level:type,text:args.map(a=>typeof a==='object'?JSON.stringify(a):String(a)).join(' ')},'*');
|
|
console.log = (...a) => _post('log', a);
|
|
console.error = (...a) => _post('error', a);
|
|
window.onerror = (m) => _post('error', [m]);
|
|
<\/script></head><body><script>${props.code}<\/script></body></html>`
|
|
}
|
|
|
|
if (props.language === 'css') {
|
|
return `<!DOCTYPE html><html><head><style>${props.code}</style></head><body style="background:#0a0a0a"><div style="padding:16px;color:#e0e0e0;font-family:system-ui">CSS Preview</div></body></html>`
|
|
}
|
|
|
|
return ''
|
|
})
|
|
|
|
function runCode() {
|
|
consoleOutput.value = []
|
|
showIframe.value = false
|
|
// Force re-mount iframe
|
|
requestAnimationFrame(() => {
|
|
showIframe.value = true
|
|
})
|
|
}
|
|
|
|
function clearOutput() {
|
|
consoleOutput.value = []
|
|
}
|
|
|
|
function handleMessage(e: MessageEvent) {
|
|
if (e.data?.type === 'code-runner-console') {
|
|
consoleOutput.value.push({
|
|
type: e.data.level === 'error' ? 'error' : 'log',
|
|
text: e.data.text,
|
|
})
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
window.addEventListener('message', handleMessage)
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
window.removeEventListener('message', handleMessage)
|
|
})
|
|
</script>
|