Skip to content

Commit ee250fc

Browse files
authored
Add LiveCodes as playground and test runner (#269)
* add LiveCodes as playground and test runner * add Ruby to LiveCodes playground * add Lua to LiveCodes playground and run tests * disable LiveCodes autoupdate * add Jupyter to LiveCodes playground * fix setting code in LiveCodes * set theme of LiveCodes * use stable LiveCodes release * add a comment for the source of lua test runner * use LiveCodes for PHP, Go & C * remove pyodide (used from within LiveCodes) * upgrade LiveCodes
1 parent 059a8dd commit ee250fc

File tree

18 files changed

+512
-3244
lines changed

18 files changed

+512
-3244
lines changed
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import { Dispatch, SetStateAction, useEffect, useState } from "react";
2+
import type { Config, Playground } from "livecodes";
3+
import LiveCodesPlayground from "livecodes/react";
4+
import { luaTestRunner, type Language } from "lib/playground/livecodes";
5+
import { useDarkTheme } from "hooks/darkTheme";
6+
7+
export default function LiveCodes({
8+
language,
9+
code,
10+
setCode,
11+
tests,
12+
}: {
13+
language: Language;
14+
code: string;
15+
setCode: Dispatch<SetStateAction<string>>;
16+
tests: string;
17+
}) {
18+
const [playground, setPlayground] = useState<Playground | undefined>();
19+
const [darkTheme] = useDarkTheme();
20+
21+
const onReady = (sdk: Playground) => {
22+
setPlayground(sdk);
23+
sdk.watch("ready", async () => {
24+
await sdk.run();
25+
if (language === "javascript" || language === "typescript") {
26+
await sdk.runTests();
27+
}
28+
});
29+
sdk.watch("code", (changed) => {
30+
setCode(changed.code.script.content);
31+
});
32+
};
33+
34+
useEffect(() => {
35+
playground?.setConfig({ theme: darkTheme ? "dark" : "light" });
36+
}, [playground, darkTheme]);
37+
38+
const baseConfig: Partial<Config> = {
39+
autoupdate: false,
40+
languages: [language === "jupyter" ? "python-wasm" : language],
41+
script: {
42+
language: language === "jupyter" ? "python-wasm" : language,
43+
content: code,
44+
},
45+
tools: {
46+
enabled: ["console"],
47+
active: "console",
48+
status: "full",
49+
},
50+
};
51+
52+
const getJSTSConfig = (
53+
lang: "javascript" | "typescript",
54+
jsCode: string,
55+
test: string
56+
): Partial<Config> => {
57+
const editTest = (src: string) =>
58+
src.replace(
59+
/import\s+((?:.|\s)*?)\s+from\s+('|").*?('|")/g,
60+
"import $1 from './script'"
61+
);
62+
return {
63+
...baseConfig,
64+
script: {
65+
language: lang,
66+
content: jsCode,
67+
},
68+
tests: {
69+
language: lang,
70+
content: editTest(test),
71+
},
72+
tools: {
73+
enabled: [
74+
"console",
75+
"tests",
76+
...(lang === "typescript" ? ["compiled"] : []),
77+
] as Config["tools"]["enabled"],
78+
active: "tests",
79+
status: "full",
80+
},
81+
autotest: true,
82+
};
83+
};
84+
85+
const getPythonConfig = (pyCode: string): Partial<Config> => {
86+
const addTestRunner = (src: string) => {
87+
const sep = 'if __name__ == "__main__":\n';
88+
const [algCode, run] = src.split(sep);
89+
const comment =
90+
run
91+
?.split("\n")
92+
.map((line) => `# ${line}`)
93+
.join("\n") || "";
94+
const testRunner = `\n import doctest\n doctest.testmod(verbose=True)`;
95+
return `${algCode}${sep}${comment}${testRunner}`;
96+
};
97+
return {
98+
...baseConfig,
99+
languages: ["python-wasm"],
100+
script: {
101+
language: "python-wasm",
102+
content: addTestRunner(pyCode),
103+
},
104+
};
105+
};
106+
107+
const getJupyterConfig = (jsonCode: string): Partial<Config> => {
108+
const getPyCode = (src: string) => {
109+
try {
110+
const nb: {
111+
cells: Array<{ ["cell_type"]: string; source: string[] }>;
112+
} = JSON.parse(src);
113+
return nb.cells
114+
.filter((c) => c.cell_type === "code")
115+
.map((c) => c.source.join(""))
116+
.join("\n\n");
117+
} catch {
118+
return "";
119+
}
120+
};
121+
return {
122+
...baseConfig,
123+
languages: ["python-wasm"],
124+
script: {
125+
language: "python-wasm",
126+
content: getPyCode(jsonCode),
127+
},
128+
tools: {
129+
enabled: ["console"],
130+
active: "console",
131+
status: "open",
132+
},
133+
};
134+
};
135+
136+
const getRConfig = (rCode: string): Partial<Config> => {
137+
const editCode = (src: string) =>
138+
src.replace(/# Example:\n# /g, "# Example:\n");
139+
return {
140+
...baseConfig,
141+
script: {
142+
language: "r",
143+
content: editCode(rCode),
144+
},
145+
tools: {
146+
enabled: ["console"],
147+
active: "console",
148+
status: "open",
149+
},
150+
};
151+
};
152+
153+
const getRubyConfig = (rubyCode: string): Partial<Config> => ({
154+
...baseConfig,
155+
script: {
156+
language: "ruby",
157+
content: rubyCode,
158+
},
159+
});
160+
161+
const getLuaConfig = (luaCode: string, test: string): Partial<Config> => {
162+
const pattern = /\n\s*local\s+(\S+)\s+=\s+require.*\n/g;
163+
const matches = test.matchAll(pattern);
164+
const fnName = [...matches][0]?.[1] || "return";
165+
const content = `
166+
${luaCode.replace("return", `local ${fnName} =`)}
167+
168+
169+
${test.replace(pattern, "\n")}`.trimStart();
170+
171+
return {
172+
...baseConfig,
173+
languages: ["lua-wasm"],
174+
script: {
175+
language: "lua-wasm",
176+
content,
177+
hiddenContent: luaTestRunner,
178+
},
179+
};
180+
};
181+
182+
const getPhpConfig = (phpCode: string): Partial<Config> => ({
183+
...baseConfig,
184+
languages: ["php-wasm"],
185+
script: {
186+
language: "php-wasm",
187+
content: phpCode,
188+
},
189+
tools: {
190+
enabled: ["console"],
191+
active: "console",
192+
status: "open",
193+
},
194+
});
195+
196+
const getCConfig = (cCode: string): Partial<Config> => ({
197+
...baseConfig,
198+
languages: ["cpp-wasm"],
199+
script: {
200+
language: "cpp-wasm",
201+
content: cCode,
202+
},
203+
});
204+
205+
const config: Partial<Config> =
206+
language === "javascript" || language === "typescript"
207+
? getJSTSConfig(language, code, tests)
208+
: language === "python"
209+
? getPythonConfig(code)
210+
: language === "jupyter"
211+
? getJupyterConfig(code)
212+
: language === "r"
213+
? getRConfig(code)
214+
: language === "ruby"
215+
? getRubyConfig(code)
216+
: language === "lua"
217+
? getLuaConfig(code, tests)
218+
: language === "php"
219+
? getPhpConfig(code)
220+
: language === "c"
221+
? getCConfig(code)
222+
: baseConfig;
223+
224+
return (
225+
<LiveCodesPlayground
226+
appUrl="https://v17.livecodes.io/"
227+
loading="eager"
228+
config={config}
229+
style={{ borderRadius: "0", resize: "none" }}
230+
sdkReady={onReady}
231+
/>
232+
);
233+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/* eslint-disable no-alert */
2+
import { Button, LinearProgress } from "@material-ui/core";
3+
import Editor from "@monaco-editor/react";
4+
import React, {
5+
useEffect,
6+
Dispatch,
7+
SetStateAction,
8+
useState,
9+
useRef,
10+
useMemo,
11+
} from "react";
12+
import useTranslation from "hooks/translation";
13+
import PlayArrow from "@material-ui/icons/PlayArrow";
14+
import { useDarkTheme } from "hooks/darkTheme";
15+
import { XTerm } from "xterm-for-react";
16+
import { FitAddon } from "xterm-addon-fit";
17+
import CodeRunner from "lib/playground/codeRunner";
18+
import PistonCodeRunner from "lib/playground/pistonCodeRunner";
19+
import classes from "./PlaygroundEditor.module.css";
20+
21+
function getMonacoLanguageName(language: string) {
22+
switch (language) {
23+
case "c-plus-plus":
24+
return "cpp";
25+
case "c-sharp":
26+
return "cs";
27+
default:
28+
return language;
29+
}
30+
}
31+
32+
export default function PlaygroundEditor({
33+
language,
34+
code,
35+
setCode,
36+
}: {
37+
language: string;
38+
code: string;
39+
setCode: Dispatch<SetStateAction<string>>;
40+
}) {
41+
const t = useTranslation();
42+
const [darkTheme] = useDarkTheme();
43+
const xtermRef = useRef<XTerm>();
44+
const [ready, setReady] = useState(false);
45+
const [disabled, setDisabled] = useState(false);
46+
47+
const codeRunner = useMemo<CodeRunner>(() => {
48+
const runner = new PistonCodeRunner(xtermRef, t);
49+
setTimeout(() => {
50+
runner.load(code, language).then((r) => {
51+
setReady(true);
52+
setDisabled(!r);
53+
});
54+
});
55+
return runner;
56+
// eslint-disable-next-line react-hooks/exhaustive-deps
57+
}, []);
58+
const fitAddon = useMemo(() => new FitAddon(), []);
59+
60+
useEffect(() => {
61+
function resizeHandler() {
62+
fitAddon.fit();
63+
}
64+
resizeHandler();
65+
window.addEventListener("resize", resizeHandler);
66+
return () => {
67+
window.removeEventListener("resize", resizeHandler);
68+
};
69+
}, [fitAddon]);
70+
71+
useEffect(() => {
72+
(async () => {
73+
xtermRef.current.terminal.writeln(`${t("playgroundWelcome")}\n`);
74+
})();
75+
// eslint-disable-next-line react-hooks/exhaustive-deps
76+
}, []);
77+
78+
return (
79+
<div className={classes.root}>
80+
<LinearProgress
81+
style={{
82+
opacity: ready ? 0 : 1,
83+
position: "absolute",
84+
width: "100%",
85+
zIndex: 10000,
86+
}}
87+
/>
88+
<div className={classes.editor}>
89+
<Editor
90+
language={getMonacoLanguageName(language)}
91+
value={code}
92+
onChange={setCode}
93+
options={{
94+
automaticLayout: true,
95+
padding: {
96+
top: 15,
97+
bottom: 15,
98+
},
99+
}}
100+
theme={darkTheme ? "vs-dark" : "vs-light"}
101+
/>
102+
<Button
103+
disabled={!ready || disabled}
104+
onClick={() => {
105+
setDisabled(true);
106+
codeRunner.run(code).finally(() => {
107+
setDisabled(false);
108+
});
109+
}}
110+
className={classes.runBtn}
111+
variant="contained"
112+
color="primary"
113+
startIcon={<PlayArrow />}
114+
>
115+
{t("playgroundRunCode")}
116+
</Button>
117+
</div>
118+
<div className={classes.output}>
119+
<XTerm
120+
className={classes.xterm}
121+
ref={xtermRef}
122+
options={{ convertEol: true }}
123+
addons={[fitAddon]}
124+
onKey={(event) => {
125+
if (event.domEvent.ctrlKey && event.domEvent.code === "KeyC") {
126+
event.domEvent.preventDefault();
127+
navigator.clipboard.writeText(
128+
xtermRef.current.terminal.getSelection()
129+
);
130+
}
131+
}}
132+
/>
133+
</div>
134+
</div>
135+
);
136+
}

0 commit comments

Comments
 (0)