feat(renderer): sandboxed code runner for HTML/JS/CSS (M10.11)
- 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>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
7f67f0863d
commit
7fa06ce500
@@ -0,0 +1,133 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user