Skip to content

Removed Iframe #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 105 additions & 10 deletions src/lib/CheerpJ.svelte
Original file line number Diff line number Diff line change
@@ -1,22 +1,117 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { browser } from '$app/environment';
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;

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();
async function runCheerpj() {
if ($isRunning) return;

console.info('compileAndRun');
$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',
classPath,
...sourceFiles,
'-d',
'/files/',
'-Xlint'
);
if (code === 0) await cheerpjRunMain(deriveMainClass($files[0]), classPath);

// in case nothing is written on cjConsole and cjOutput
// manually unflag $isRunning
if ($isRunning) $isRunning = false;
$compileLog = cjConsole.innerText;
}

function deriveMainClass(file: File) {
const className = file.path.split('/').pop()!.replace('.java', '');
const match = file.content.match(/package\s+(.+);/);
if (match && match.length > 1) {
const packageName = match[1];
return `${packageName}.${className}`;
} else {
return className;
}
}

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));
}
$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>
94 changes: 10 additions & 84 deletions src/lib/Repl.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,87 +2,24 @@
import Menu from './repl/Menu.svelte';
import Sidebar from './repl/Sidebar.svelte';
import Editor from './repl/Editor.svelte';
import { files } 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 { autoRun, 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 loading = false;
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) {
Expand All @@ -93,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}
Expand All @@ -110,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>
Expand Down
5 changes: 0 additions & 5 deletions src/lib/plausible.ts

This file was deleted.

14 changes: 2 additions & 12 deletions src/lib/repl/Editor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -111,15 +111,6 @@
editorView?.destroy();
};
});
/*
beforeNavigate(() => {
skipReset = true;
});
afterNavigate(() => {
skipReset = false;
editorStates.clear();
reset(files);
});*/

// Look at the selected file
$: {
Expand All @@ -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;
Expand Down
3 changes: 1 addition & 2 deletions src/lib/repl/Menu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
import { relativeTime } from 'svelte-relative-time';
import FiddleTitle from './menu/FiddleTitle.svelte';
import SettingsButton from './menu/SettingsButton.svelte';
import { autoRun } from '$lib/settings/store';
import { blur } from 'svelte/transition';
import FavouriteButton from './menu/FavouriteButton.svelte';
import { files, fiddleTitle, fiddleUpdated, favouriteIndex } from './state';
import { files, fiddleTitle, fiddleUpdated, favouriteIndex, autoRun } from './state';
import { defaultFiddle } from '$lib/compress-fiddle';
import { goto } from '$app/navigation';
import { page } from '$app/stores'
Expand Down
35 changes: 21 additions & 14 deletions src/lib/repl/Output.svelte
Original file line number Diff line number Diff line change
@@ -1,50 +1,57 @@
<script lang="ts">
import CheerpJ from '$lib/CheerpJ.svelte';
import Icon from '@iconify/svelte';
import { isRunning } from './state';
import Loading from '$lib/Loading.svelte';

export let console: HTMLPreElement;
export let display: HTMLElement;
export let lwjglCanvas: HTMLCanvasElement;
export let showLink: boolean;
let cjConsole: HTMLPreElement;
let lwjglCanvas: HTMLCanvasElement;

$: if (lwjglCanvas) window.lwjglCanvasElement = lwjglCanvas;
</script>

<div class="grid grid-cols-2 grow">
<div class="w-full h-full" class:hidden={!$isRunning}>
<Loading />
</div>

<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">
Console

<button
class="ml-auto text-xs hover:underline text-stone-400 dark:text-stone-600"
on:click={() => (console.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={console} 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>
</section>
</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>
:global(#cheerpjDisplay) {
box-shadow: none;
}
</style>

<CheerpJ/>
Loading