Skip to content

Commit

Permalink
Sandbox web languages using iframes (#268)
Browse files Browse the repository at this point in the history
Fixes #239 

Load web libraries in separate pages and embed them using iframes. This
way, these libraries do not pollute the global Window from the session
page and there are no collisions between the globals each library
define.

Incidentally, this also fixes #193: Because the web language libraries
are only loaded inside the iframe and thus in a separate JS context,
when unloading the Iframe DOM element, sound and visuals stop.

Strudel also adds some extensions to the Codemirror editor and has an
animation frame to update decorators. For some reason, these updates
need to run in the same context as the editor, and can't be modified
from within the Iframe page.
  • Loading branch information
munshkr authored Apr 3, 2024
1 parent d263680 commit 205232c
Show file tree
Hide file tree
Showing 16 changed files with 419 additions and 255 deletions.
31 changes: 13 additions & 18 deletions packages/web/src/components/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@ import {
} from "@/settings.json";
import { javascript } from "@codemirror/lang-javascript";
import { python } from "@codemirror/lang-python";
import { EditorState, Prec, Compartment, Extension } from "@codemirror/state";
import { Compartment, EditorState, Extension, Prec } from "@codemirror/state";
import { EditorView, keymap, lineNumbers } from "@codemirror/view";
import { evalKeymap, flashField, remoteEvalFlash } from "@flok-editor/cm-eval";
import { tidal } from "@flok-editor/lang-tidal";
import type { Document } from "@flok-editor/session";
import { highlightExtension } from "@strudel/codemirror";
import CodeMirror, {
ReactCodeMirrorProps,
ReactCodeMirrorRef,
} from "@uiw/react-codemirror";
import React, { useEffect, useState } from "react";
import { yCollab } from "y-codemirror.next";
import { UndoManager } from "yjs";
import { highlightExtension } from '@strudel/codemirror';

const defaultLanguage = "javascript";
const langByTarget = langByTargetUntyped as { [lang: string]: string };
Expand Down Expand Up @@ -121,28 +121,25 @@ const flokSetup = (
];
};

export interface EditorProps extends ReactCodeMirrorProps {
document?: Document;
}

// Code example from:
// https://codemirror.net/examples/config/#dynamic-configuration
// Allows toggling of extensions based on string shortkey
//
//
const toggleWith = (key: string, extension: Extension) => {
let comp = new Compartment;
let comp = new Compartment();

function toggle(view: EditorView) {
let on = comp.get(view.state) == extension;
view.dispatch({
effects: comp.reconfigure(on ? [] : extension)
})
effects: comp.reconfigure(on ? [] : extension),
});
return true;
}
return [
comp.of([]),
keymap.of([{key, run: toggle}])
]
return [comp.of([]), keymap.of([{ key, run: toggle }])];
};

export interface EditorProps extends ReactCodeMirrorProps {
document?: Document;
}

export const Editor = React.forwardRef(
Expand Down Expand Up @@ -175,10 +172,8 @@ export const Editor = React.forwardRef(
languageExtension(),
highlightExtension,
readOnly ? EditorState.readOnly.of(true) : [],
// toggle linenumbers on/off
toggleWith('shift-ctrl-l', lineNumbers()),
// toggle linewrapping on/off
toggleWith('shift-ctrl-w', EditorView.lineWrapping)
toggleWith("shift-ctrl-l", lineNumbers()), // toggle linenumbers on/off
toggleWith("shift-ctrl-w", EditorView.lineWrapping), // toggle linewrapping on/off
];

// If it's read-only, put a div in front of the editor so that the user
Expand Down
47 changes: 47 additions & 0 deletions packages/web/src/components/web-target-iframe.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useQuery } from "@/hooks/use-query";
import { EvalMessage, Session } from "@flok-editor/session";
import { useEffect, useRef } from "react";

export interface WebTargetIframeProps {
target: string;
session: Session | null;
}

export const WebTargetIframe = ({ target, session }: WebTargetIframeProps) => {
const ref = useRef<HTMLIFrameElement | null>(null);

const query = useQuery();
const noWebEval = query.get("noWebEval")?.split(",") || [];

// Check if we should load the target
if (noWebEval.includes(target) || noWebEval.includes("*")) {
return null;
}

// Handle evaluation messages from session
useEffect(() => {
if (!session || !ref.current) return;

const handler = (msg: EvalMessage) => {
const payload = {
type: "eval",
body: msg,
};
ref.current?.contentWindow?.postMessage(payload, "*");
};

session.on(`eval:${target}`, handler);

return () => {
session.off(`eval:${target}`, handler);
};
}, [session, ref]);

return (
<iframe
ref={ref}
src={`/frames/${target}`}
className="absolute inset-0 w-full h-full"
/>
);
};
21 changes: 21 additions & 0 deletions packages/web/src/hooks/use-animation-frame.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useRef, useEffect, useCallback } from "react";

export function useAnimationFrame(callback: (timestamp: number) => void) {
const requestId = useRef<number>();

const animate = useCallback(
(timestamp: number) => {
callback(timestamp);
requestId.current = requestAnimationFrame(animate);
},
[callback]
);

useEffect(() => {
requestId.current = requestAnimationFrame(animate);

return () => {
cancelAnimationFrame(requestId.current!);
};
}, [animate]);
}
21 changes: 21 additions & 0 deletions packages/web/src/hooks/use-eval-handler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { EvalMessage } from "@flok-editor/session";
import { useEffect } from "react";

export function useEvalHandler(callback: (message: EvalMessage) => void) {
useEffect(() => {
const handleEval = (event: MessageEvent) => {
if (event.data.type === "eval") {
const msg = event.data.body as EvalMessage;
callback(msg);
}
};

window.addEventListener("message", handleEval);

return () => {
window.removeEventListener("message", handleEval);
};
}, [callback]);

return;
}
40 changes: 0 additions & 40 deletions packages/web/src/hooks/use-hydra.tsx

This file was deleted.

25 changes: 0 additions & 25 deletions packages/web/src/hooks/use-mercury.tsx

This file was deleted.

31 changes: 31 additions & 0 deletions packages/web/src/hooks/use-strudel-codemirror-extensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Session } from "@flok-editor/session";
import {
highlightMiniLocations,
updateMiniLocations,
} from "@strudel/codemirror";
import { ReactCodeMirrorRef } from "@uiw/react-codemirror";
import { useCallback } from "react";
import { useAnimationFrame } from "./use-animation-frame";
import { forEachDocumentContext } from "@/lib/utils";

export function useStrudelCodemirrorExtensions(
session: Session | null,
editorRefs: React.RefObject<ReactCodeMirrorRef>[]
) {
useAnimationFrame(
useCallback(() => {
if (!session) return;

forEachDocumentContext(
(ctx, editor) => {
const view = editor?.view;
if (!view) return;
updateMiniLocations(view, ctx.miniLocations || []);
highlightMiniLocations(view, ctx.phase || 0, ctx.haps || []);
},
session,
editorRefs
);
}, [session, editorRefs])
);
}
34 changes: 0 additions & 34 deletions packages/web/src/hooks/use-strudel.tsx

This file was deleted.

1 change: 0 additions & 1 deletion packages/web/src/lib/hydra-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ export class HydraWrapper {

try {
await eval?.(`(async () => {\n${code}\n})()`);
this._onError("");
} catch (error) {
console.error(error);
this._onError(`${error}`);
Expand Down
Loading

0 comments on commit 205232c

Please sign in to comment.