dropped iframe

This commit is contained in:
Francesco
2025-06-02 17:46:03 +02:00
committed by Alessandro Pignotti
parent a07d52aeed
commit 45e932dc83
13 changed files with 121 additions and 222 deletions

View File

@@ -1,40 +1,30 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { browser } from '$app/environment';
import { tryPlausible } from './plausible';
import { files, loading, type File } from './repl/state';
import { onDestroy, onMount } from 'svelte';
import { autoRun, compileLog, files, isRunning, isSaved, runCode, type File } from './repl/state';
import { debounceFunction } from './utilities';
const dispatch = createEventDispatcher<{ ready: undefined }>();
export let display: HTMLElement;
export let consoleEl: HTMLPreElement;
async function onLoad() {
let cjConsole: HTMLElement;
let cjOutput: HTMLElement;
let cjOutputObserver: MutationObserver;
async function startCheerpj() {
await cheerpjInit({
status: 'none',
javaProperties: ['java.library.path=/app/cheerpj-natives/natives']
});
const display = document.getElementById("output");
cheerpjCreateDisplay(-1, -1, display);
dispatch('ready');
}
if (browser) { // so it doesn't run server-side
onLoad();
}
export async function compileAndRun() {
if (!browser) return;
async function runCheerpj() {
if ($isRunning) return;
console.info('compileAndRun');
// custom event tracking for analytics
tryPlausible('Compile');
consoleEl.innerHTML = '';
$isRunning = true;
cjConsole.innerHTML = '';
cjOutput.innerHTML = '';
const classPath = '/app/tools.jar:/app/lwjgl-2.9.0.jar:/app/lwjgl_util-2.9.0.jar:/files/';
const sourceFiles = $files.map((file) => '/str/' + file.path);
const code = await cheerpjRunMain(
'com.sun.tools.javac.Main',
@@ -44,17 +34,12 @@
'/files/',
'-Xlint'
);
const compileLog = consoleEl.innerText;
if (code != 0) {
$loading = false;
window.top?.postMessage({ action: 'compile_error', compileLog }, window.location.origin);
throw new Error('Compilation failed');
}
if (code === 0) await cheerpjRunMain(deriveMainClass($files[0]), classPath);
consoleEl.innerHTML = '';
cheerpjRunMain(deriveMainClass($files[0]), classPath);
$loading = false;
window.top?.postMessage({ action: 'running', compileLog }, window.location.origin);
// in case nothing is written on cjConsole and cjOutput
// manually unflag $isRunning
if ($isRunning) $isRunning = false;
$compileLog = cjConsole.innerText;
}
function deriveMainClass(file: File) {
@@ -68,18 +53,65 @@
}
}
// Persist files to CheerpJ filesystem
files.subscribe(($files) => {
if ('cheerpjAddStringFile' in globalThis) {
const debounceRunCheerpj = debounceFunction(runCheerpj, 500);
let unsubSaveFiles: () => void;
let unsubRunCode: () => void;
onMount(async () => {
await startCheerpj();
cjConsole = document.getElementById("console");
cjOutput = document.getElementById("cheerpjDisplay");
// remove useless loading screen
cjOutput.classList.remove("cheerpjLoading");
unsubSaveFiles = files.subscribe(() => {
if ($isRunning) {
$isSaved = false;
} else {
try {
const encoder = new TextEncoder();
for (const file of $files) {
cheerpjAddStringFile('/str/' + file.path, encoder.encode(file.content));
}
console.info('wrote files');
$isSaved = true;
if ($autoRun) $runCode = true;
} catch (error) {
console.error('Error writing files to CheerpJ', error);
}
}
});
unsubRunCode = runCode.subscribe(() => {
if ($runCode) {
$runCode = false;
($autoRun) ? debounceRunCheerpj() : runCheerpj();
}
});
// code execution (flagged by isRunning) is considered over
// when cjConsole or cjOutput are updated
cjOutputObserver = new MutationObserver(() => {
if ($isRunning && (cjConsole.innerHTML || cjOutput.innerHTML)) {
$isRunning = false;
if (!$isSaved) files.update((files) => files);
}
});
cjOutputObserver.observe((cjConsole), {
childList: true,
subtree: true,
});
cjOutputObserver.observe((cjOutput), {
childList: true,
subtree: true,
});
await runCheerpj();
});
onDestroy(() => {
if (unsubSaveFiles) unsubSaveFiles();
if (unsubRunCode) unsubRunCode();
if (cjOutputObserver) cjOutputObserver.disconnect();
});
</script>

View File

@@ -2,86 +2,24 @@
import Menu from './repl/Menu.svelte';
import Sidebar from './repl/Sidebar.svelte';
import Editor from './repl/Editor.svelte';
import { files, autoRun, loading } from './repl/state';
import { isSaved, runCode } from './repl/state';
import FileTabs from './repl/FileTabs.svelte';
import Loading from './Loading.svelte';
import { SplitPane } from '@rich_harris/svelte-split-pane';
import { theme } from './settings/store';
import { onMount } from 'svelte';
import { tryPlausible } from './plausible';
import { tryPlausible } from './utilities';
import Output from './repl/Output.svelte';
export let outputUrl: string;
export let enableSidebar: boolean = true;
export let enableMenu: boolean = true;
let isSaved = true;
let iframe: HTMLIFrameElement;
let compileLog = '';
files.subscribe(() => {
isSaved = false;
if ($autoRun) run();
});
// files is set by +layout.svelte on load, but we want to keep isSaved true on load
// i.e. undo above subscription
onMount(() => {
isSaved = true;
});
function run() {
if (!$loading) {
$loading = true;
iframe?.contentWindow?.postMessage(
{
action: 'reload'
},
window.location.origin
);
}
}
function onMessage(event: MessageEvent) {
if (event.origin !== window.location.origin) return;
const { action } = event.data;
console.log('recv from iframe', event.data);
if (action === 'ready') {
iframe?.contentWindow?.postMessage(
{
action: 'run',
files: $files
},
window.location.origin
);
$loading = false; // once files are sent, any changes to files will trigger a reload
} else if (action === 'running') {
compileLog = event.data.compileLog;
} else if (action === 'compile_error') {
compileLog = event.data.compileLog;
}
}
async function share() {
// custom event tracking for analytics
tryPlausible('Share');
isSaved = true;
await navigator.clipboard.writeText(window.location.toString());
}
// Notify iframe of theme changes so it can reload its theme from localStorage
$: {
$theme;
iframe?.contentWindow?.postMessage(
{
action: 'theme_change'
},
window.location.origin
);
// only used when the users presses the button RUN
async function run() {
tryPlausible('Compile');
$runCode = true;
}
function onBeforeUnload(evt: BeforeUnloadEvent) {
@@ -92,7 +30,7 @@
}
</script>
<svelte:window on:message={onMessage} on:beforeunload={isSaved ? undefined : onBeforeUnload} />
<svelte:window on:beforeunload={$isSaved ? undefined : onBeforeUnload} />
<div class="w-full h-screen font-sans flex flex-col overflow-hidden">
{#if enableMenu}
@@ -109,21 +47,10 @@
<FileTabs />
</div>
<Editor {compileLog} />
<Editor />
</section>
<section slot="b" class="border-t border-stone-200 dark:border-stone-700 overflow-hidden">
<div class="w-full h-full" class:hidden={!$loading}>
<Loading />
</div>
<iframe
bind:this={iframe}
src={outputUrl}
class="w-full h-full"
class:hidden={$loading}
title="Output"
allowtransparency={true}
frameborder={0}
/>
<Output />
</section>
</SplitPane>
</div>

View File

@@ -1,5 +0,0 @@
// for adblockers protection
export function tryPlausible(msg: string) {
if (self.plausible)
plausible(msg)
}

View File

@@ -7,7 +7,7 @@
import { indentUnit } from '@codemirror/language';
import { lintGutter } from '@codemirror/lint';
import { java } from '@codemirror/lang-java';
import { files, fiddleTitle, fiddleUpdated, selectedFilePath, type File } from './state';
import { files, fiddleTitle, fiddleUpdated, selectedFilePath, type File, compileLog } from './state';
import './codemirror.css';
import { compartment, diagnostic, parseCompileLog } from './linter';
import { effectiveTheme } from '$lib/settings/store';
@@ -111,15 +111,6 @@
editorView?.destroy();
};
});
/*
beforeNavigate(() => {
skipReset = true;
});
afterNavigate(() => {
skipReset = false;
editorStates.clear();
reset(files);
});*/
// Look at the selected file
$: {
@@ -130,9 +121,8 @@
}
// Linter
export let compileLog: string;
$: {
const diagnostics = parseCompileLog(compileLog, $files);
const diagnostics = parseCompileLog($compileLog, $files);
for (let fileIndex = 0; fileIndex < diagnostics.length; fileIndex++) {
const diagnosticsForFile = diagnostics[fileIndex];
const path = $files[fileIndex].path;

View File

@@ -1,54 +1,20 @@
<script lang="ts">
import CheerpJ from '$lib/CheerpJ.svelte';
import Icon from '@iconify/svelte';
import { files, loading, type File } from './state';
import { theme } from '$lib/settings/store';
import { isRunning } from './state';
import Loading from '$lib/Loading.svelte';
export let showLink: boolean;
let consoleEl: HTMLPreElement;
let display: HTMLElement;
let cjConsole: HTMLPreElement;
let lwjglCanvas: HTMLCanvasElement;
$: if (lwjglCanvas) window.lwjglCanvasElement = lwjglCanvas;
let cheerpj: CheerpJ;
async function ready() {
if (window.parent !== window && window.parent) {
// Tell parent frame we are ready to recieve files
window.parent.postMessage({ action: 'ready' }, window.location.origin);
} else {
// Load files from load function
files.set($files); // Force file write
cheerpj?.compileAndRun();
}
}
function onMessage(event: MessageEvent) {
if (event.origin !== window.location.origin) return;
const { action } = event.data;
console.log('recv from top', event.data);
if (action === 'reload') {
window.location.reload();
} else if (action === 'run') {
files.set(event.data.files);
cheerpj?.compileAndRun();
} else if (action === 'theme_change') {
$theme = JSON.parse(localStorage['theme']);
}
}
</script>
<svelte:window on:message={onMessage} />
<div class="w-screen h-screen" class:hidden={!$loading}>
<div class="w-full h-full" class:hidden={!$isRunning}>
<Loading />
</div>
<div class="grid grid-cols-2 grow">
<div class="w-full h-full grid grid-cols-2 grow">
<section class="border-r border-stone-200 dark:border-stone-700">
<div class="p-3 h-full overflow-scroll text-stone-800 dark:text-stone-100">
<div class="flex text-stone-500 text-sm select-none pb-3">
@@ -56,19 +22,19 @@
<button
class="ml-auto text-xs hover:underline text-stone-400 dark:text-stone-600"
on:click={() => (consoleEl.innerText = '')}
on:click={() => (cjConsole.innerText = '')}
>
Clear
</button>
</div>
<!-- CheerpJ implicitly looks for a #console to write to -->
<pre class="font-mono text-sm h-0" bind:this={consoleEl} id="console" />
<pre class="font-mono text-sm h-0" bind:this={cjConsole} id="console" />
</div>
</section>
<section class="flex flex-col">
<div class="p-3 text-stone-500 text-sm select-none">Result</div>
<div class="grow relative" bind:this={display}>
<div class="grow relative" id="output">
<canvas bind:this={lwjglCanvas} class="absolute inset-0 w-full h-full" />
<!-- #cheerpjDisplay will be inserted here -->
</div>
@@ -76,12 +42,10 @@
</div>
<div class="absolute top-0 right-0 text-stone-500 text-sm flex items-center select-none">
{#if showLink}
<!-- svelte-ignore a11y-invalid-attribute -->
<a href="" target="_blank" rel="noreferrer" class="px-2 py-2" title="Open in new tab">
<Icon icon="mi:external-link" class="w-5 h-5" />
</a>
{/if}
<!-- svelte-ignore a11y-invalid-attribute -->
<a href="" target="_blank" rel="noreferrer" class="px-2 py-2" title="Open in new tab">
<Icon icon="mi:external-link" class="w-5 h-5" />
</a>
</div>
<style>
@@ -90,4 +54,4 @@
}
</style>
<CheerpJ bind:this={cheerpj} on:ready={ready} {display} {consoleEl} />
<CheerpJ/>

View File

@@ -29,4 +29,10 @@ export const description = writable<string>('JavaFiddle is an online, browser-ba
export const autoRun = persist(writable<boolean>(false), createLocalStorage(), 'autoRun');
export const loading = writable<boolean>(false);
export const isRunning = writable<boolean>(false);
export const isSaved = writable<boolean>(true);
export const runCode = writable<boolean>(false);
export const compileLog = writable<string>('');

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import ThemeSwitcher from './ThemeSwitcher.svelte';
import { autoRun } from '$lib/repl/state';
import { autoRun, isRunning, runCode } from '$lib/repl/state';
</script>
<!-- triangle pointing above -->
@@ -27,7 +27,10 @@
<h3 class="font-semibold">Behaviour</h3>
<div class="flex items-center gap-1.5">
<input type="checkbox" bind:checked={$autoRun} id="auto-run" />
<input type="checkbox" bind:checked={$autoRun} on:change={() => {
// if autorun is set force re-run by updating files
$runCode = $autoRun && !$isRunning;
}} id="auto-run" />
<label for="auto-run" class="grow">Run code automatically</label>
</div>
</div>

13
src/lib/utilities.ts Normal file
View File

@@ -0,0 +1,13 @@
// for adblockers protection
export function tryPlausible(msg: string) {
if (self.plausible)
plausible(msg)
}
export function debounceFunction(fn: () => void, delay: number) {
let timeoutId: number;
return(() => {
clearInterval(timeoutId);
timeoutId = setTimeout(() => fn(), delay);
});
}

View File

@@ -1,7 +1,5 @@
<script lang="ts">
import Repl from '$lib/Repl.svelte';
export let data;
</script>
<Repl outputUrl={data.outputUrl} />
<Repl/>

View File

@@ -1,8 +0,0 @@
export function load({ url }) {
let outputUrl = url.pathname;
if (!outputUrl.endsWith('/')) outputUrl += '/';
outputUrl += 'output';
return {
outputUrl
};
}

View File

@@ -1,7 +1,5 @@
<script lang="ts">
import Repl from '$lib/Repl.svelte';
export let data;
</script>
<Repl outputUrl={data.outputUrl} enableMenu={false} enableSidebar={false} />
<Repl enableMenu={false} enableSidebar={false} />

View File

@@ -1,8 +0,0 @@
export function load({ url }) {
let outputUrl = url.pathname.replace('/embed', '');
if (!outputUrl.endsWith('/')) outputUrl += '/';
outputUrl += 'output';
return {
outputUrl
};
}

View File

@@ -1,16 +1,5 @@
<script lang="ts">
import Output from '$lib/repl/Output.svelte';
import { loading } from '$lib/repl/state';
import { onMount } from 'svelte';
let isTop = false;
let isShared = false;
onMount(() => {
isTop = window.top === window;
isShared = window.location.pathname !== '/output';
});
</script>
<div class="flex flex-col w-screen h-screen overflow-hidden" class:hidden={$loading}>
<Output showLink={!isTop && isShared} />
</div>
<Output />