This commit is contained in:
inaseem 2021-10-02 19:07:44 +05:30
parent 85afa783e5
commit 1b54a08dc1
26 changed files with 5413 additions and 602 deletions

3
frontend/.eslintrc Normal file
View File

@ -0,0 +1,3 @@
{
"extends": ["next", "next/core-web-vitals"]
}

34
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel

20
frontend/README.md Normal file
View File

@ -0,0 +1,20 @@
![home](https://user-images.githubusercontent.com/6770106/133267560-0eea8701-ac94-45f8-8ba8-eead83d83622.png)
![editor](https://user-images.githubusercontent.com/6770106/133267600-85073cdc-9ebf-4b7c-bbb8-f728318b26a5.png)
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.

View File

@ -0,0 +1,41 @@
import { alpha, Button } from "@mui/material";
import { useRouter } from "next/router";
import React from "react";
const LanguageLink = (props) => {
const { link } = props;
const router = useRouter();
const handleClick = () => {
router.push(`/editor/${link.id}`);
};
return (
<>
<Button
variant="contained"
onClick={handleClick}
disableElevation
sx={{
fontSize: 12,
minHeight: 70,
minWidth: "calc(calc(100vw - 120px) / 6)",
color: (theme) => theme.palette.text.primary,
background: (theme) => theme.palette.common.white,
border: `1px solid rgb(226,232,240)`,
":hover": {
background: (theme) => theme.palette.common.white,
borderColor: (theme) => theme.palette.primary.main,
color: (theme) => theme.palette.primary.main,
boxShadow: (theme) =>
`0 4px 4px ${alpha(theme.palette.primary.main, 0.25)}`,
},
}}
>
{link.name}
</Button>
</>
);
};
export default LanguageLink;

View File

@ -0,0 +1,47 @@
import Box from "@mui/material/Box";
import React, { useEffect } from "react";
import { Terminal } from "xterm";
import { FitAddon } from "xterm-addon-fit";
import "xterm/css/xterm.css";
import { EventEmitter } from "../utils/EventEmitter";
function RijuTerminal() {
useEffect(() => {
const term = new Terminal({
fontFamily: "Fira Code",
theme: {
background: "#292D3E",
},
});
term.open(document.getElementById("riju-term"));
term.write("Connecting to server...");
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
window.addEventListener("resize", () => fitAddon.fit());
EventEmitter.subscribe("terminal", (payload) => {
if (!payload) return;
const { type, data } = payload;
switch (type) {
case "terminalClear":
term.reset();
break;
case "terminalOutput":
term.write(data);
break;
default:
term.write(data);
break;
}
});
term.onData((data) => {
EventEmitter.dispatch("send", { event: "terminalInput", input: data });
});
}, []);
return (
<Box id="riju-term" sx={{ height: `calc(100% - 8px)`, mt: 1, ml: 2 }} />
);
}
export default RijuTerminal;

48
frontend/components/UI.js Normal file
View File

@ -0,0 +1,48 @@
import { alpha, InputBase, styled } from "@mui/material";
export const Search = styled("div")(({ theme }) => ({
borderRadius: theme.shape.borderRadius,
backgroundColor: alpha(theme.palette.common.white, 0.15),
"&:hover": {
backgroundColor: alpha(theme.palette.common.white, 0.25),
},
width: "100%",
[theme.breakpoints.up("sm")]: {
width: "auto",
},
display: "flex",
alignItems: "center",
// boxShadow: theme.shadows[3],
minHeight: 70,
border: `1px solid`,
borderColor: theme.palette.primary.main,
boxShadow: `0 2px 5px ${alpha(theme.palette.primary.main, 0.5)}`,
}));
export const SearchIconWrapper = styled("div")(() => ({
position: "absolute",
pointerEvents: "none",
display: "flex",
alignItems: "center",
justifyContent: "center",
marginLeft: 16,
}));
export const StyledInputBase = styled(InputBase)(({ theme }) => ({
color: "inherit",
height: "100%",
width: "100%",
"& .MuiInputBase-input": {
fontSize: 30,
// minHeight: 70,
// fontWeight: "bolder",
// vertical padding + font size from searchIcon
paddingLeft: 32 + 24,
transition: theme.transitions.create("width"),
width: "100%",
[theme.breakpoints.up("md")]: {
// width: "20ch",
},
},
flexGrow: 1,
}));

View File

@ -0,0 +1,5 @@
import createCache from "@emotion/cache";
export default function createEmotionCache() {
return createCache({ key: "css" });
}

49
frontend/pages/_app.js Normal file
View File

@ -0,0 +1,49 @@
import { ThemeProvider } from "@mui/material/styles";
import "setimmediate";
import Head from "next/head";
import Router from "next/router";
import NProgress from "nprogress";
import "../../node_modules/nprogress/nprogress.css";
import React from "react";
import "../styles/globals.css";
import { theme } from "../theme";
Router.events.on("routeChangeStart", () => {
console.log("routeChangeStart");
NProgress.start();
});
Router.events.on("routeChangeComplete", () => {
console.log("routeChangeComplete");
NProgress.done();
});
Router.events.on("routeChangeError", () => {
console.log("routeChangeError");
NProgress.done();
});
function MyApp({ Component, pageProps }) {
React.useEffect(() => {
// Remove the server-side injected CSS.
const jssStyles = document.querySelector("#jss-server-side");
if (jssStyles) {
jssStyles.parentElement.removeChild(jssStyles);
}
}, []);
return (
<React.Fragment>
<Head>
<title>Riju</title>
<meta
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width"
/>
</Head>
<ThemeProvider theme={theme}>
<Component {...pageProps} />
</ThemeProvider>
</React.Fragment>
);
}
export default MyApp;

View File

@ -0,0 +1,83 @@
import * as React from "react";
import Document, { Html, Head, Main, NextScript } from "next/document";
import createEmotionServer from "@emotion/server/create-instance";
import { theme } from "../theme";
import createEmotionCache from "../createEmotionCache";
export default class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head>
{/* PWA primary color */}
<meta name="theme-color" content={theme.palette.primary.main} />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
// `getInitialProps` belongs to `_document` (instead of `_app`),
// it's compatible with static-site generation (SSG).
MyDocument.getInitialProps = async (ctx) => {
// Resolution order
//
// On the server:
// 1. app.getInitialProps
// 2. page.getInitialProps
// 3. document.getInitialProps
// 4. app.render
// 5. page.render
// 6. document.render
//
// On the server with error:
// 1. document.getInitialProps
// 2. app.render
// 3. page.render
// 4. document.render
//
// On the client
// 1. app.getInitialProps
// 2. page.getInitialProps
// 3. app.render
// 4. page.render
const originalRenderPage = ctx.renderPage;
// You can consider sharing the same emotion cache between all the SSR requests to speed up performance.
// However, be aware that it can have global side effects.
const cache = createEmotionCache();
const { extractCriticalToChunks } = createEmotionServer(cache);
ctx.renderPage = () =>
originalRenderPage({
// eslint-disable-next-line react/display-name
enhanceApp: (App) => (props) => <App emotionCache={cache} {...props} />,
});
const initialProps = await Document.getInitialProps(ctx);
// This is important. It prevents emotion to render invalid HTML.
// See https://github.com/mui-org/material-ui/issues/26561#issuecomment-855286153
const emotionStyles = extractCriticalToChunks(initialProps.html);
const emotionStyleTags = emotionStyles.styles.map((style) => (
<style
data-emotion={`${style.key} ${style.ids.join(" ")}`}
key={style.key}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: style.css }}
/>
));
return {
...initialProps,
// Styles fragment is rendered after the app and page rendering finish.
styles: [
...React.Children.toArray(initialProps.styles),
...emotionStyleTags,
],
};
};

View File

@ -1,84 +0,0 @@
<!DOCTYPE html>
<html lang="en" style="overflow: hidden">
<head>
<meta charset="utf-8" />
<title><%= config.name %> - Riju</title>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.9.3/css/bulma.min.css"
integrity="sha512-IgmDkwzs96t4SrChW29No3NXBIBv8baW490zk5aXvhCD8vuZM3yUSkbyTBcXohkySecyzIrUwiF/qV0cuPcL3Q=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"
integrity="sha512-iBBXm8fW90+nuLcSKlbmrPcLa0OT92xO1BIsZ+ywDWZCvqsWgccV3gFoRBv0z+8dLJgyAHIhR35VZc2oM/gI1w=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<link rel="stylesheet" href="/css/app.css" />
<script>
window.rijuConfig = <%- JSON.stringify(config) %>;
</script>
<script src="/js/app.js" defer></script>
<% if (fathomSiteId) { %>
<script src="https://cdn.usefathom.com/script.js" data-site="<%= fathomSiteId %>" defer></script>
<% } %>
</head>
<body>
<div class="columns" style="height: 100vh; margin: 0">
<div class="column" style="padding: 0">
<div id="header" style="border-bottom-style: solid; border-bottom-width: 1px; border-bottom-color: lightgray">
<a href="/" class="button is-small is-info">
<span class="icon is-small">
<i class="fas fa-home"></i>
</span>
</a>
<span style="display: inline-block; vertical-align: middle; margin-left: 6px; padding-top: 2px">
<b>Riju :: <%= config.name %></b>
</span>
<span style="display: inline-block; vertical-align: middle; margin-left: 6px; padding-top: 2px">
<i id="connectionStatus" style="color: lightgray"></i>
</span>
<span style="display: inline-block; vertical-align: middle; height: 100%"></span>
<button id="runButton" type="button" class="button is-small is-success is-pulled-right">
<span>Run</span>
<span class="icon is-small">
<i class="fas fa-play"></i>
</span>
</button>
<button id="formatButton" type="button" class="button is-small is-info is-pulled-right is-hidden">
<span>Prettify</span>
<span class="icon is-small">
<i class="fas fa-code"></i>
</span>
</button>
<button id="lspButton" type="button" class="button is-small is-warning is-light is-pulled-right is-hidden">
<span>Autocomplete <span id="lspButtonState">OFF</span></span>
<span class="icon is-small">
<i class="fas fa-bolt"></i>
</span>
</button>
</div>
<div style="height: 100%">
<div id="editor" style="height: 100%; margin: 12px; margin-left: 0"></div>
</div>
</div>
<div class="column" id="terminal" style="background: black; padding: 0">
</div>
</div>
<div class="modal" id="modal">
<div class="modal-background will-close-modal"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title" id="modal-title"></p>
<button class="delete will-close-modal" aria-label="close"></button>
</header>
<section class="modal-card-body">
<pre id="modal-data"></pre>
</section>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,495 @@
import MonacoEditor, { useMonaco } from "@monaco-editor/react";
import { Circle, Code as Format, Home, PlayArrow } from "@mui/icons-material";
import { LoadingButton } from "@mui/lab";
import { Box, Button, Divider, Stack, Typography } from "@mui/material";
import ansi from "ansicolor";
import dynamic from "next/dynamic";
import Head from "next/head";
import { useRouter } from "next/router";
import React, { useEffect, useRef, useState } from "react";
import { createMessageConnection } from "vscode-jsonrpc";
import langs from "../../static/langs.json";
import { EventEmitter } from "../../utils/EventEmitter";
ansi.rgb = {
green: "#00FD61",
};
const RijuTerminal = dynamic(() => import("../../components/RijuTerminal"), {
ssr: false,
});
const DEBUG = true;
let clientDisposable = null;
let servicesDisposable = null;
const serviceLogBuffers = {};
const CodeRunner = (props) => {
const router = useRouter();
const { langConfig } = props;
const editorRef = useRef(null);
const [config, setConfig] = useState(langConfig);
const [mounted, setMounted] = useState(false);
const [isRunning, setRunning] = useState(false);
const [isFormatting, setFormatting] = useState(false);
const [isLspStarted, setLspStarted] = useState(false);
const [isLspRequested, setIsLspRequested] = useState(false);
const monaco = useMonaco();
function sendToTerminal(type, data) {
EventEmitter.dispatch("terminal", { type, data });
}
function connect() {
const socket = new WebSocket(
// (document.location.protocol === "http:" ? "ws://" : "wss://") +
"wss://" +
"riju.codes" +
`/api/v1/ws?lang=${encodeURIComponent(config.id)}`
);
socket.addEventListener("open", () => {
console.log("Successfully connected to server playground");
});
EventEmitter.subscribe("send", (payload) => {
if (DEBUG) {
console.log("SEND:", payload);
}
if (socket) {
socket.send(JSON.stringify(payload));
}
});
socket.addEventListener("message", async (event) => {
let message;
try {
message = JSON.parse(event.data);
} catch (err) {
console.error("Malformed message from server:", event.data);
return;
}
if (
DEBUG &&
message &&
message.event !== "lspOutput" &&
message.event !== "serviceLog"
) {
console.log("RECEIVE:", message);
}
if (message && message.event && message.event !== "error") {
// retryDelayMs = initialRetryDelayMs;
}
switch (message && message.event) {
case "terminalClear":
// term.reset();
sendToTerminal("terminalClear");
return;
case "terminalOutput":
if (typeof message.output !== "string") {
console.error("Unexpected message from server:", message);
return;
}
sendToTerminal("terminalOutput", ansi.white(message.output));
setRunning(false);
return;
case "formattedCode":
if (
typeof message.code !== "string" ||
typeof message.originalCode !== "string"
) {
console.error("Unexpected message from server:", message);
return;
}
if (editorRef.current?.getValue() === message.originalCode) {
editorRef.current?.setValue(message.code);
}
setFormatting(false);
return;
case "lspStopped":
setIsLspRequested(false);
setLspStarted(false);
if (clientDisposable) {
clientDisposable.dispose();
clientDisposable = null;
}
if (servicesDisposable) {
servicesDisposable.dispose();
servicesDisposable = null;
}
break;
case "lspStarted":
setLspStarted(true);
setIsLspRequested(false);
if (typeof message.root !== "string") {
console.error("Unexpected message from server:", message);
return;
}
console.log("Started", message.root, config.main);
// EventEmitter.dispatch("lspStarted", message);
const {
createConnection,
MonacoLanguageClient,
MonacoServices,
Services,
} = await import("monaco-languageclient");
const services = MonacoServices.create(editorRef.current, {
rootUri: `file://${message.root}`,
});
servicesDisposable = Services.install(services);
const newURI = `file://${message.root}/${config.main}`;
const oldModel = editorRef.current.getModel();
console.log("Check 4", oldModel.uri, newURI);
if (oldModel.uri.toString() !== newURI) {
// This code is likely to be buggy as it will probably
// never run and has thus never been tested.
editorRef.current.setModel(
monaco.editor.createModel(
oldModel.getValue(),
undefined,
monaco.Uri.parse(newURI)
)
);
oldModel.dispose();
}
const RijuMessageReader = (
await import("../../services/RijuMessageReader")
).default;
const RijuMessageWriter = (
await import("../../services/RijuMessageWriter")
).default;
const connection = createMessageConnection(
new RijuMessageReader(socket),
new RijuMessageWriter(socket, config)
);
const client = new MonacoLanguageClient({
name: "Riju",
clientOptions: {
documentSelector: [{ pattern: "**" }],
middleware: {
workspace: {
configuration: (params, token, configuration) => {
return Array(configuration(params, token).length).fill(
config.lsp.config !== undefined ? config.lsp.config : {}
);
},
},
},
initializationOptions: config.lsp.init || {},
},
connectionProvider: {
get: (errorHandler, closeHandler) =>
Promise.resolve(
createConnection(connection, errorHandler, closeHandler)
),
},
});
clientDisposable = client.start();
return;
case "lspOutput":
// Should be handled by RijuMessageReader
return;
case "serviceLog":
if (
typeof message.service !== "string" ||
typeof message.output !== "string"
) {
console.error("Unexpected message from server:", message);
return;
}
let buffer = serviceLogBuffers[message.service] || "";
let lines = serviceLogLines[message.service] || [];
buffer += message.output;
while (buffer.includes("\n")) {
const idx = buffer.indexOf("\n");
const line = buffer.slice(0, idx);
buffer = buffer.slice(idx + 1);
lines.push(line);
if (DEBUG) {
console.log(`${message.service.toUpperCase()} || ${line}`);
}
}
serviceLogBuffers[message.service] = buffer;
serviceLogLines[message.service] = lines;
return;
case "serviceFailed":
if (
typeof message.service !== "string" ||
typeof message.error !== "string"
) {
console.error("Unexpected message from server:", message);
return;
}
switch (message.service) {
case "formatter":
setFormatting(false);
// showError({
// message: "Could not prettify code!",
// data: serviceLogLines["formatter"].join("\n"),
// });
break;
case "lsp":
setLspStarted(false);
setIsLspRequested(false);
// lspButton.classList.add("is-light");
// lspButtonState.innerText = "CRASHED";
break;
case "terminal":
sendToTerminal(
"terminalOutput",
ansi.red(`\r\n[${message.error}]`)
);
break;
}
return;
default:
console.error("Unexpected message from server:", message);
}
});
socket.addEventListener("close", (event) => {
if (event.wasClean) {
console.log("Connection closed cleanly");
} else {
console.error("Connection died");
}
if (clientDisposable) {
clientDisposable.dispose();
clientDisposable = null;
}
if (servicesDisposable) {
servicesDisposable.dispose();
servicesDisposable = null;
}
setRunning(false);
setLspStarted(false);
setIsLspRequested(false);
});
return () => socket && socket.close();
}
useEffect(() => {
if (!config || !mounted) return;
const socket = connect();
return () => socket && socket.close();
}, [config, mounted]);
function showValue() {
setRunning(true);
EventEmitter.dispatch("send", {
event: "runCode",
code: editorRef.current.getValue(),
});
}
function sendFormat() {
setFormatting(true);
serviceLogBuffers["formatter"] = "";
serviceLogLines["formatter"] = [];
EventEmitter.dispatch("send", {
event: "formatCode",
code: editorRef.current.getValue(),
});
}
function editorDidMount(editor, monaco) {
editorRef.current = editor;
editor.addAction({
id: "runCode",
label: "Run",
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],
contextMenuGroupId: "2_execution",
run: () => {
showValue();
},
});
// editor.getModel().onDidChangeContent(() => recordActivity());
// window.addEventListener("resize", () => editor.layout());
editor.getModel().setValue(config.template + "\n");
monaco.editor.setModelLanguage(
editor.getModel(),
config.monacoLang || "plaintext"
);
setMounted(true);
}
const handleLspClick = () => {
setIsLspRequested(true);
if (isLspStarted) {
EventEmitter.dispatch("send", {
event: "lspStop",
});
} else {
EventEmitter.dispatch("send", {
event: "lspStart",
});
}
};
return (
<>
<Head>
<title>Riju</title>
<meta
name="description"
content="Riju - fast playground for any language"
/>
<link rel="icon" href="/favicon.ico" />
<meta
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width"
/>
</Head>
<Box
sx={{
display: "flex",
flexDirection: "column",
height: "100vh",
bgcolor: "#fff",
}}
component="main"
>
<Box
sx={{
display: "flex",
flexDirection: "row",
alignItems: "center",
boxShadow: `0 2px 4px rgb(0 0 0 / 2%)`,
width: "60%",
}}
>
<Box sx={{ flexGrow: 1, display: "flex", alignItems: "center" }}>
<Button
variant="contained"
sx={{ borderRadius: 0, minWidth: 0 }}
disableElevation
size="small"
color="primary"
onClick={() => {
router.push("/");
}}
>
<Home fontSize={"small"} />
</Button>
<Typography sx={{ fontSize: 14, px: 2, fontWeight: 600 }}>
{config.name}
</Typography>
</Box>
<LoadingButton
onClick={handleLspClick}
size="small"
variant="text"
loading={isLspRequested}
sx={{
borderRadius: 0,
visibility: config.lsp ? "visible" : "hidden",
mr: 1,
color: (t) =>
isLspStarted ? t.palette.success.main : t.palette.text.disabled,
}}
disableElevation
endIcon={
<Circle
fontSize="small"
sx={{
color: "inherit",
fontSize: "0.6em !important",
}}
/>
}
>
<Typography sx={{ fontSize: 12 }}>Autocomplete</Typography>
</LoadingButton>
<Button
onClick={sendFormat}
disabled={isFormatting || isRunning}
size="small"
color="primary"
variant="contained"
sx={{
borderRadius: 0,
visibility: config.format ? "visible" : "hidden",
}}
disableElevation
>
<Stack direction="row" gap={1} alignItems="center">
<Typography sx={{ fontSize: 12 }}>Prettify</Typography>
<Format fontSize="small" />
</Stack>
</Button>
<Divider orientation="vertical" />
<Button
onClick={showValue}
disabled={isRunning || isRunning}
size="small"
color="success"
variant="contained"
sx={{ borderRadius: 0 }}
disableElevation
>
<Stack direction="row" gap={1} alignItems="center">
<Typography sx={{ fontSize: 12, color: "#fff" }}>Run</Typography>
<PlayArrow fontSize="small" htmlColor="#fff" />
</Stack>
</Button>
</Box>
<Divider />
<Box
sx={{
display: "flex",
flexDirection: "row",
flexGrow: 1,
alignItems: "stretch",
}}
>
<Box sx={{ backgroundColor: "white", width: "60%" }}>
<Box
component={MonacoEditor}
wrapperClassName={"rijuEditor"}
height="90vh"
defaultLanguage="javascript"
defaultValue="// some comment"
options={{
minimap: { enabled: false },
scrollbar: { verticalScrollbarSize: 0 },
fontLigatures: true,
fontFamily: "Fira Code",
}}
onMount={editorDidMount}
/>
</Box>
<Box
sx={{
overflow: "hidden",
backgroundColor: "#292D3E",
width: "40%",
}}
>
<RijuTerminal />
</Box>
</Box>
</Box>
</>
);
};
CodeRunner.getInitialProps = async (ctx) => {
const { req, query } = ctx;
console.log("Query", query);
let config = langs.javascript;
if (query.lang) config = langs[query.lang];
return {
langConfig: config, // will be passed to the page component as props
};
};
// export async function getServerSideProps(ctx) {
// // TODO: Fetch language details using api route
// const { req, query } = ctx;
// let lsp = langs.javascript;
// if (query.lang) lsp = langs[query.lang];
// return {
// props: { lsp }, // will be passed to the page component as props
// };
// }
export default CodeRunner;

View File

@ -1,37 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Riju</title>
<link rel="stylesheet" href="/css/index.css" />
</head>
<body>
<h1>Riju: <i>fast</i> online playground for every programming language</h1>
<% if (Object.keys(langs).length > 0) { %>
<i>Pick your favorite language to get started:</i>
<div class="grid">
<% for (const [id, {name}] of Object.entries(langs).sort(
([id1, {name: name1}], [id2, {name: name2}]) => name1.toLowerCase().localeCompare(name2.toLowerCase()))) { %>
<a href=<%= "/" + encodeURIComponent(id) %> class="language">
<div class="language">
<%= name %>
</div>
</a>
<% } %>
</div>
<p>
<i>
Created by
<a href="https://github.com/raxod502">Radon Rosborough</a>.
Check out the project
<a href="https://github.com/raxod502/riju">on GitHub</a>.
</i>
</p>
<% } else { %>
<i>Riju is loading language configuration...</i>
<% } %>
<% if (fathomSiteId) { %>
<script src="https://cdn.usefathom.com/script.js" data-site="<%= fathomSiteId %>" defer></script>
<% } %>
</body>
</html>

123
frontend/pages/index.js Normal file
View File

@ -0,0 +1,123 @@
import SearchIcon from "@mui/icons-material/Search";
import { Autocomplete, Box, Typography } from "@mui/material";
import Head from "next/head";
import { useRouter } from "next/router";
import React, { useRef, useState } from "react";
import LanguageLink from "../components/LanguageLink";
import { Search, SearchIconWrapper, StyledInputBase } from "../components/UI";
import langs from "../static/data.json";
export default function Home() {
const [selected, setSelected] = useState(null);
const router = useRouter();
const search = useRef();
const moveToEditor = (link) => {
router.push(`/editor/${link.id}`);
};
React.useEffect(() => {
if (search.current) {
search.current.focus();
}
}, []);
return (
<>
<Head>
<title>Riju</title>
<meta
name="description"
content="Riju - fast playground for any language"
/>
<link rel="icon" href="/favicon.ico" />
<meta
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width"
/>
</Head>
<Box component="main" sx={{ m: 3 }}>
<Box
sx={{
flexGrow: 1,
display: "flex",
alignItems: "center",
flexDirection: "column",
width: "100%",
}}
>
<Typography
variant="h2"
component="h2"
sx={{ fontWeight: "bolder", color: "#000000" }}
>
Riju
</Typography>
<Typography variant="subtitle2" sx={{ color: "rgb(113,128,150)" }}>
fast online playground for every programming language
</Typography>
<Box
sx={{
width: "70%",
m: 3,
position: "sticky",
top: 0,
zIndex: (theme) => theme.zIndex.appBar + 1,
borderRadius: (theme) => theme.shape.borderRadius,
backgroundColor: (theme) =>
theme.palette.type === "dark"
? theme.palette.common.black
: theme.palette.common.white,
}}
id="search"
>
<Search>
<SearchIconWrapper>
<SearchIcon fontSize="large" color="action" />
</SearchIconWrapper>
<Autocomplete
sx={{ width: "100%" }}
options={langs}
getOptionLabel={(option) => option.name}
value={selected}
onChange={(event, newValue) => {
if (!newValue) return;
setSelected(newValue);
moveToEditor(newValue);
}}
renderInput={(params) => (
<StyledInputBase
inputRef={(e) => {
params.InputProps.ref(e);
search.current = e;
return e;
}}
placeholder="Search…"
inputProps={{
...params.inputProps,
"aria-label": "search",
}}
/>
)}
/>
</Search>
</Box>
<Box
sx={{
mt: 1,
width: "100%",
display: "flex",
flexWrap: "wrap",
gap: 1,
justifyContent: "center",
}}
>
{langs.map((link, i) => (
<LanguageLink key={i} link={link} />
))}
</Box>
</Box>
</Box>
</>
);
}

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,4 @@
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,49 @@
import { AbstractMessageReader } from "vscode-jsonrpc/lib/messageReader";
const DEBUG = window.location.hash === "#debug";
class RijuMessageReader extends AbstractMessageReader {
constructor(socket) {
super();
this.state = "initial";
this.callback = null;
this.messageQueue = [];
this.socket = socket;
this.socket.addEventListener("message", (event) => {
this.readMessage(event.data);
});
}
listen(callback) {
if (this.state === "initial") {
this.state = "listening";
this.callback = callback;
while (this.messageQueue.length > 0) {
this.readMessage(this.messageQueue.pop());
}
}
}
readMessage(rawMessage) {
if (this.state === "initial") {
this.messageQueue.splice(0, 0, rawMessage);
} else if (this.state === "listening") {
let message;
try {
message = JSON.parse(rawMessage);
} catch (err) {
return;
}
switch (message && message.event) {
case "lspOutput":
if (DEBUG) {
console.log("RECEIVE LSP:", message.output);
}
this.callback(message.output);
break;
}
}
}
}
export default RijuMessageReader;

View File

@ -0,0 +1,42 @@
import { AbstractMessageWriter } from "vscode-jsonrpc/lib/messageWriter";
const DEBUG = window.location.hash === "#debug";
class RijuMessageWriter extends AbstractMessageWriter {
constructor(socket, config) {
super();
this.socket = socket;
this.config = config;
}
write(msg) {
switch (msg.method) {
case "initialize":
msg.params.processId = null;
if (this.config.lsp.disableDynamicRegistration) {
this.disableDynamicRegistration(msg);
}
break;
case "textDocument/didOpen":
if (this.config.lsp.lang) {
msg.params.textDocument.languageId = this.config.lsp.lang;
}
}
if (DEBUG) {
console.log("SEND LSP:", msg);
}
this.socket.send(JSON.stringify({ event: "lspInput", input: msg }));
}
disableDynamicRegistration(msg) {
if (!msg || typeof msg !== "object") return;
for (const [key, val] of Object.entries(msg)) {
if (key === "dynamicRegistration" && val === true) {
msg.dynamicRegistration = false;
}
this.disableDynamicRegistration(val);
}
}
}
export default RijuMessageWriter;

View File

@ -1,441 +0,0 @@
import * as monaco from "monaco-editor";
import {
createConnection,
MonacoLanguageClient,
MonacoServices,
Services,
} from "monaco-languageclient";
import { createMessageConnection } from "vscode-jsonrpc";
import { AbstractMessageReader } from "vscode-jsonrpc/lib/messageReader.js";
import { AbstractMessageWriter } from "vscode-jsonrpc/lib/messageWriter.js";
import { Terminal } from "xterm";
import { FitAddon } from "xterm-addon-fit";
import "xterm/css/xterm.css";
const DEBUG = window.location.hash === "#debug";
const config = window.rijuConfig;
const formatButton = document.getElementById("formatButton");
const lspButton = document.getElementById("lspButton");
const lspButtonState = document.getElementById("lspButtonState");
const connectionStatus = document.getElementById("connectionStatus");
function closeModal() {
document.querySelector("html").classList.remove("is-clipped");
document.getElementById("modal").classList.remove("is-active");
}
function showError({ message, data }) {
document.getElementById("modal-title").innerText = message;
document.getElementById("modal-data").innerText =
data || "(no output on stderr)";
document.getElementById("modal").classList.add("is-active");
document.querySelector("html").classList.add("is-clipped");
}
class RijuMessageReader extends AbstractMessageReader {
constructor(socket) {
super();
this.state = "initial";
this.callback = null;
this.messageQueue = [];
this.socket = socket;
this.socket.addEventListener("message", (event) => {
this.readMessage(event.data);
});
}
listen(callback) {
if (this.state === "initial") {
this.state = "listening";
this.callback = callback;
while (this.messageQueue.length > 0) {
this.readMessage(this.messageQueue.pop());
}
}
}
readMessage(rawMessage) {
if (this.state === "initial") {
this.messageQueue.splice(0, 0, rawMessage);
} else if (this.state === "listening") {
let message;
try {
message = JSON.parse(rawMessage);
} catch (err) {
return;
}
switch (message && message.event) {
case "lspOutput":
if (DEBUG) {
console.log("RECEIVE LSP:", message.output);
}
this.callback(message.output);
break;
}
}
}
}
class RijuMessageWriter extends AbstractMessageWriter {
constructor(socket) {
super();
this.socket = socket;
}
write(msg) {
switch (msg.method) {
case "initialize":
msg.params.processId = null;
if (config.lsp.disableDynamicRegistration) {
this.disableDynamicRegistration(msg);
}
break;
case "textDocument/didOpen":
if (config.lsp.lang) {
msg.params.textDocument.languageId = config.lsp.lang;
}
}
if (DEBUG) {
console.log("SEND LSP:", msg);
}
this.socket.send(JSON.stringify({ event: "lspInput", input: msg }));
}
disableDynamicRegistration(msg) {
if (!msg || typeof msg !== "object") return;
for (const [key, val] of Object.entries(msg)) {
if (key === "dynamicRegistration" && val === true)
msg.dynamicRegistration = false;
this.disableDynamicRegistration(val);
}
}
}
async function main() {
let serviceLogBuffers = {};
let serviceLogLines = {};
let lastActivityTimestamp = new Date();
let idleDueToInactivity = false;
function recordActivity() {
lastActivityTimestamp = new Date();
if (idleDueToInactivity) {
scheduleConnect();
}
}
const term = new Terminal();
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
term.open(document.getElementById("terminal"));
fitAddon.fit();
window.addEventListener("resize", () => fitAddon.fit());
await new Promise((resolve) =>
term.write("Connecting to server...", resolve)
);
const initialRetryDelayMs = 200;
let retryDelayMs = initialRetryDelayMs;
function sendMessage(message) {
if (DEBUG) {
console.log("SEND:", message);
}
if (socket) {
socket.send(JSON.stringify(message));
}
}
function tryConnect() {
serviceLogBuffers = {};
serviceLogLines = {};
let clientDisposable = null;
let servicesDisposable = null;
connectionStatus.innerText = "connecting...";
console.log("Connecting to server...");
socket = new WebSocket(
(document.location.protocol === "http:" ? "ws://" : "wss://") +
document.location.host +
`/api/v1/ws?lang=${encodeURIComponent(config.id)}`
);
socket.addEventListener("open", () => {
connectionStatus.innerText = "connected";
console.log("Successfully connected to server");
});
socket.addEventListener("message", (event) => {
let message;
try {
message = JSON.parse(event.data);
} catch (err) {
console.error("Malformed message from server:", event.data);
return;
}
if (
DEBUG &&
message &&
message.event !== "lspOutput" &&
message.event !== "serviceLog"
) {
console.log("RECEIVE:", message);
}
if (message && message.event && message.event !== "error") {
retryDelayMs = initialRetryDelayMs;
}
switch (message && message.event) {
case "terminalClear":
term.reset();
return;
case "terminalOutput":
if (typeof message.output !== "string") {
console.error("Unexpected message from server:", message);
return;
}
term.write(message.output);
return;
case "formattedCode":
formatButton.disabled = false;
formatButton.classList.remove("is-loading");
if (
typeof message.code !== "string" ||
typeof message.originalCode !== "string"
) {
console.error("Unexpected message from server:", message);
return;
}
if (editor.getValue() === message.originalCode) {
editor.setValue(message.code);
}
return;
case "lspStopped":
lspButton.disabled = false;
lspButton.classList.remove("is-loading");
lspButton.classList.add("is-light");
lspButtonState.innerText = "OFF";
if (clientDisposable) {
clientDisposable.dispose();
clientDisposable = null;
}
if (servicesDisposable) {
servicesDisposable.dispose();
servicesDisposable = null;
}
break;
case "lspStarted":
lspButton.disabled = false;
lspButton.classList.remove("is-loading");
lspButton.classList.remove("is-light");
lspButtonState.innerText = "ON";
if (typeof message.root !== "string") {
console.error("Unexpected message from server:", message);
return;
}
const services = MonacoServices.create(editor, {
rootUri: `file://${message.root}`,
});
servicesDisposable = Services.install(services);
const newURI = `file://${message.root}/${config.main}`;
const oldModel = editor.getModel();
if (oldModel.uri.toString() !== newURI) {
// This code is likely to be buggy as it will probably
// never run and has thus never been tested.
editor.setModel(
monaco.editor.createModel(
oldModel.getValue(),
undefined,
monaco.Uri.parse(newURI)
)
);
oldModel.dispose();
}
const connection = createMessageConnection(
new RijuMessageReader(socket),
new RijuMessageWriter(socket)
);
const client = new MonacoLanguageClient({
name: "Riju",
clientOptions: {
documentSelector: [{ pattern: "**" }],
middleware: {
workspace: {
configuration: (params, token, configuration) => {
return Array(configuration(params, token).length).fill(
config.lsp.config !== undefined ? config.lsp.config : {}
);
},
},
},
initializationOptions: config.lsp.init || {},
},
connectionProvider: {
get: (errorHandler, closeHandler) =>
Promise.resolve(
createConnection(connection, errorHandler, closeHandler)
),
},
});
clientDisposable = client.start();
return;
case "lspOutput":
// Should be handled by RijuMessageReader
return;
case "serviceLog":
if (
typeof message.service !== "string" ||
typeof message.output !== "string"
) {
console.error("Unexpected message from server:", message);
return;
}
let buffer = serviceLogBuffers[message.service] || "";
let lines = serviceLogLines[message.service] || [];
buffer += message.output;
while (buffer.includes("\n")) {
const idx = buffer.indexOf("\n");
const line = buffer.slice(0, idx);
buffer = buffer.slice(idx + 1);
lines.push(line);
if (DEBUG) {
console.log(`${message.service.toUpperCase()} || ${line}`);
}
}
serviceLogBuffers[message.service] = buffer;
serviceLogLines[message.service] = lines;
return;
case "serviceFailed":
if (
typeof message.service !== "string" ||
typeof message.error !== "string"
) {
console.error("Unexpected message from server:", message);
return;
}
switch (message.service) {
case "formatter":
formatButton.disabled = false;
formatButton.classList.remove("is-loading");
showError({
message: "Could not prettify code!",
data: serviceLogLines["formatter"].join("\n"),
});
break;
case "lsp":
lspButton.disabled = false;
lspButton.classList.remove("is-loading");
lspButton.classList.add("is-light");
lspButtonState.innerText = "CRASHED";
break;
case "terminal":
term.write(`\r\n[${message.error}]`);
break;
}
return;
default:
console.error("Unexpected message from server:", message);
return;
}
});
socket.addEventListener("close", (event) => {
if (event.wasClean) {
console.log("Connection closed cleanly");
} else {
console.error("Connection died");
}
if (clientDisposable) {
clientDisposable.dispose();
clientDisposable = null;
}
if (servicesDisposable) {
servicesDisposable.dispose();
servicesDisposable = null;
}
if (lspButtonState.innerText === "ON") {
lspButton.disabled = false;
lspButton.classList.remove("is-loading");
lspButton.classList.add("is-light");
lspButtonState.innerText = "DISCONNECTED";
}
scheduleConnect();
});
}
function scheduleConnect() {
idleDueToInactivity = new Date() - lastActivityTimestamp > 10 * 60 * 1000;
if (idleDueToInactivity) {
connectionStatus.innerText = "idle";
return;
}
const delay = retryDelayMs * Math.random();
console.log(`Trying to reconnect in ${Math.floor(delay)}ms`);
setTimeout(tryConnect, delay);
retryDelayMs *= 2;
}
let socket = null;
tryConnect();
term.onData((data) => {
sendMessage({ event: "terminalInput", input: data });
recordActivity();
});
const editor = monaco.editor.create(document.getElementById("editor"), {
minimap: { enabled: false },
scrollbar: { verticalScrollbarSize: 0 },
});
editor.addAction({
id: "runCode",
label: "Run",
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],
contextMenuGroupId: "2_execution",
run: () => {
sendMessage({ event: "runCode", code: editor.getValue() });
},
});
editor.getModel().onDidChangeContent(() => recordActivity());
window.addEventListener("resize", () => editor.layout());
editor.getModel().setValue(config.template + "\n");
monaco.editor.setModelLanguage(
editor.getModel(),
config.monacoLang || "plaintext"
);
document.getElementById("runButton").addEventListener("click", () => {
sendMessage({ event: "runCode", code: editor.getValue() });
});
if (config.format) {
formatButton.classList.remove("is-hidden");
formatButton.addEventListener("click", () => {
formatButton.classList.add("is-loading");
formatButton.disabled = true;
serviceLogBuffers["formatter"] = "";
serviceLogLines["formatter"] = [];
sendMessage({ event: "formatCode", code: editor.getValue() });
});
}
if (config.lsp) {
lspButton.classList.remove("is-hidden");
lspButton.addEventListener("click", () => {
lspButton.classList.add("is-loading");
lspButton.disabled = true;
lspButton.classList.remove("is-light");
if (lspButtonState.innerText === "ON") {
sendMessage({ event: "lspStop" });
} else {
serviceLogBuffers["lsp"] = "";
serviceLogLines["lsp"] = [];
sendMessage({ event: "lspStart" });
}
});
}
for (const elt of document.querySelectorAll(".will-close-modal")) {
elt.addEventListener("click", closeModal);
}
}
main().catch(console.error);

226
frontend/static/data.json Normal file
View File

@ -0,0 +1,226 @@
[
{ "name": "><>", "id": "fishlang" },
{ "name": "A+", "id": "aplus" },
{ "name": "ABC", "id": "abc" },
{ "name": "Ada", "id": "ada" },
{ "name": "Afnix", "id": "afnix" },
{ "name": "ALGOL 68", "id": "algol" },
{ "name": "Ante", "id": "ante" },
{ "name": "Ante (Cards)", "id": "antecards" },
{ "name": "APL", "id": "apl" },
{ "name": "ARM", "id": "arm" },
{ "name": "AsciiDoc", "id": "asciidoc" },
{ "name": "AspectC++", "id": "aspectcpp" },
{ "name": "AspectJ", "id": "aspectj" },
{ "name": "Asymptote", "id": "asymptote" },
{ "name": "ATS", "id": "ats" },
{ "name": "Awk", "id": "awk" },
{ "name": "Bash", "id": "bash" },
{ "name": "BASIC", "id": "basic" },
{ "name": "Battlestar", "id": "battlestar" },
{ "name": "bc", "id": "bc" },
{ "name": "Beanshell", "id": "beanshell" },
{ "name": "Beatnik", "id": "beatnik" },
{ "name": "Befunge", "id": "befunge" },
{ "name": "Binary Lambda Calculus", "id": "blc" },
{ "name": "Boo", "id": "boo" },
{ "name": "Brainf***", "id": "brainf" },
{ "name": "Bython", "id": "bython" },
{ "name": "C", "id": "c" },
{ "name": "C#", "id": "csharp" },
{ "name": "C++", "id": "cpp" },
{ "name": "Carp", "id": "carp" },
{ "name": "Cat", "id": "cat" },
{ "name": "Ceylon", "id": "ceylon" },
{ "name": "Chef", "id": "chef" },
{ "name": "CIL", "id": "cil" },
{ "name": "Clean", "id": "clean" },
{ "name": "Clojure", "id": "clojure" },
{ "name": "ClojureScript", "id": "clojurescript" },
{ "name": "CMake", "id": "cmake" },
{ "name": "Cmd", "id": "cmd" },
{ "name": "COBOL", "id": "cobol" },
{ "name": "Coconut", "id": "coconut" },
{ "name": "CoffeeScript", "id": "coffeescript" },
{ "name": "Common Lisp", "id": "commonlisp" },
{ "name": "Confluence", "id": "confluence" },
{ "name": "Crystal", "id": "crystal" },
{ "name": "Curry", "id": "curry" },
{ "name": "D", "id": "d" },
{ "name": "Dafny", "id": "dafny" },
{ "name": "Dart", "id": "dart" },
{ "name": "dc", "id": "dc" },
{ "name": "Dhall", "id": "dhall" },
{ "name": "Dogescript", "id": "dogescript" },
{ "name": "DokuWiki", "id": "dokuwiki" },
{ "name": "Dylan", "id": "dylan" },
{ "name": "eC", "id": "ec" },
{ "name": "Elixir", "id": "elixir" },
{ "name": "Elm", "id": "elm" },
{ "name": "Elvish", "id": "elvish" },
{ "name": "Emacs Lisp", "id": "emacslisp" },
{ "name": "Emojicode", "id": "emojicode" },
{ "name": "Entropy", "id": "entropy" },
{ "name": "Erlang", "id": "erlang" },
{ "name": "Euphoria", "id": "euphoria" },
{ "name": "F#", "id": "fsharp" },
{ "name": "Factor", "id": "factor" },
{ "name": "FALSE", "id": "false" },
{ "name": "Fish", "id": "fish" },
{ "name": "Flex", "id": "flex" },
{ "name": "Forth", "id": "forth" },
{ "name": "FORTRAN", "id": "fortran" },
{ "name": "Gambas", "id": "gambas" },
{ "name": "GAP", "id": "gap" },
{ "name": "GDB", "id": "gdb" },
{ "name": "GEL", "id": "gel" },
{ "name": "Gnuplot", "id": "gnuplot" },
{ "name": "Go", "id": "go" },
{ "name": "GolfScript", "id": "golfscript" },
{ "name": "Grass", "id": "grass" },
{ "name": "Groovy", "id": "groovy" },
{ "name": "Hack", "id": "hack" },
{ "name": "Haskell", "id": "haskell" },
{ "name": "Haxe", "id": "haxe" },
{ "name": "HCL", "id": "hcl" },
{ "name": "Hexagony", "id": "hexagony" },
{ "name": "HMMM", "id": "hmmm" },
{ "name": "Hy", "id": "hy" },
{ "name": "Icon", "id": "icon" },
{ "name": "Idris", "id": "idris" },
{ "name": "Ink", "id": "ink" },
{ "name": "INTERCAL", "id": "intercal" },
{ "name": "Io", "id": "io" },
{ "name": "Ioke", "id": "ioke" },
{ "name": "J", "id": "j" },
{ "name": "Jasmin", "id": "jasmin" },
{ "name": "Java", "id": "java" },
{ "name": "JavaScript", "id": "javascript" },
{ "name": "jq", "id": "jq" },
{ "name": "JSF***", "id": "jsf" },
{ "name": "Julia", "id": "julia" },
{ "name": "Kalyn", "id": "kalyn" },
{ "name": "Kitten", "id": "kitten" },
{ "name": "Kotlin", "id": "kotlin" },
{ "name": "Ksh", "id": "ksh" },
{ "name": "Lazy K", "id": "lazyk" },
{ "name": "Less", "id": "less" },
{ "name": "Limbo", "id": "limbo" },
{ "name": "Lisaac", "id": "lisaac" },
{ "name": "LiveScript", "id": "livescript" },
{ "name": "LLVM", "id": "llvm" },
{ "name": "LOLCODE", "id": "lolcode" },
{ "name": "Lua", "id": "lua" },
{ "name": "m4", "id": "m4" },
{ "name": "Make", "id": "make" },
{ "name": "Malbolge", "id": "malbolge" },
{ "name": "MariaDB", "id": "mariadb" },
{ "name": "Markdown", "id": "markdown" },
{ "name": "MediaWiki", "id": "mediawiki" },
{ "name": "MiniZinc", "id": "minizinc" },
{ "name": "MIPS", "id": "mips" },
{ "name": "Miranda", "id": "miranda" },
{ "name": "MongoDB", "id": "mongodb" },
{ "name": "MUMPS", "id": "mumps" },
{ "name": "MySQL", "id": "mysql" },
{ "name": "Neko", "id": "neko" },
{ "name": "Nelua", "id": "nelua" },
{ "name": "Nickle", "id": "nickle" },
{ "name": "Nim", "id": "nim" },
{ "name": "Oberon", "id": "oberon" },
{ "name": "Objective-C", "id": "objectivec" },
{ "name": "Objective-C++", "id": "objectivecpp" },
{ "name": "OCaml", "id": "ocaml" },
{ "name": "Octave", "id": "octave" },
{ "name": "Odin", "id": "odin" },
{ "name": "Omgrofl", "id": "omgrofl" },
{ "name": "Ook", "id": "ook" },
{ "name": "OpenSCAD", "id": "openscad" },
{ "name": "Org", "id": "org" },
{ "name": "Oz", "id": "oz" },
{ "name": "PARI/GP", "id": "parigp" },
{ "name": "Parser3", "id": "parser3" },
{ "name": "Pascal", "id": "pascal" },
{ "name": "PAWN", "id": "pawn" },
{ "name": "Perl", "id": "perl" },
{ "name": "PHP", "id": "php" },
{ "name": "Pikachu", "id": "pikachu" },
{ "name": "Pike", "id": "pike" },
{ "name": "PostgreSQL", "id": "postgresql" },
{ "name": "PostScript", "id": "postscript" },
{ "name": "PowerShell", "id": "powershell" },
{ "name": "Prolog", "id": "prolog" },
{ "name": "PROMELA", "id": "promela" },
{ "name": "PSeInt", "id": "pseint" },
{ "name": "Pug", "id": "pug" },
{ "name": "PureScript", "id": "purescript" },
{ "name": "Python", "id": "python" },
{ "name": "Q#", "id": "qsharp" },
{ "name": "R", "id": "r" },
{ "name": "Racket", "id": "racket" },
{ "name": "Ratfor", "id": "ratfor" },
{ "name": "rc", "id": "rc" },
{ "name": "ReasonML", "id": "reasonml" },
{ "name": "REBOL", "id": "rebol" },
{ "name": "Red", "id": "red" },
{ "name": "Redis", "id": "redis" },
{ "name": "reStructuredText", "id": "restructuredtext" },
{ "name": "REXX", "id": "rexx" },
{ "name": "RISC-V", "id": "riscv" },
{ "name": "roff", "id": "roff" },
{ "name": "Ruby", "id": "ruby" },
{ "name": "Rust", "id": "rust" },
{ "name": "S-Lang", "id": "slang" },
{ "name": "SageMath", "id": "sagemath" },
{ "name": "Sass", "id": "sass" },
{ "name": "Scala", "id": "scala" },
{ "name": "Scheme", "id": "scheme" },
{ "name": "Scilab", "id": "scilab" },
{ "name": "SCSS", "id": "scss" },
{ "name": "Sed", "id": "sed" },
{ "name": "SETL", "id": "setl" },
{ "name": "Sh", "id": "sh" },
{ "name": "Shakespeare", "id": "shakespeare" },
{ "name": "Slick", "id": "slick" },
{ "name": "Smalltalk", "id": "smalltalk" },
{ "name": "SNOBOL", "id": "snobol" },
{ "name": "SQLite", "id": "sqlite" },
{ "name": "Squirrel", "id": "squirrel" },
{ "name": "Standard ML", "id": "standardml" },
{ "name": "Subleq", "id": "subleq" },
{ "name": "Swift", "id": "swift" },
{ "name": "Tabloid", "id": "tabloid" },
{ "name": "Tcl", "id": "tcl" },
{ "name": "Tcsh", "id": "tcsh" },
{ "name": "TECO", "id": "teco" },
{ "name": "TeX", "id": "tex" },
{ "name": "Textile", "id": "textile" },
{ "name": "Thue", "id": "thue" },
{ "name": "Tiki Wiki", "id": "tikiwiki" },
{ "name": "TOML", "id": "toml" },
{ "name": "TWiki", "id": "twiki" },
{ "name": "TypeScript", "id": "typescript" },
{ "name": "Unison", "id": "unison" },
{ "name": "Unlambda", "id": "unlambda" },
{ "name": "Vala", "id": "vala" },
{ "name": "Velato", "id": "velato" },
{ "name": "Verilog", "id": "verilog" },
{ "name": "Vimscript", "id": "vimscript" },
{ "name": "Vimwiki", "id": "vimwiki" },
{ "name": "Visual Basic", "id": "visualbasic" },
{ "name": "Whitespace", "id": "whitespace" },
{ "name": "Wolfram Language", "id": "wolframlanguage" },
{ "name": "x86", "id": "x86" },
{ "name": "XSLT", "id": "xslt" },
{ "name": "YAML", "id": "yaml" },
{ "name": "YoptaScript", "id": "yoptascript" },
{ "name": "Yorick", "id": "yorick" },
{ "name": "Zig", "id": "zig" },
{ "name": "Zoem", "id": "zoem" },
{ "name": "Zot", "id": "zot" },
{ "name": "Zsh", "id": "zsh" },
{ "name": "Рапира", "id": "rapira" },
{ "name": "قلب", "id": "qalb" },
{ "name": "எழில்", "id": "ezhil" },
{ "name": "아희", "id": "aheui" }
]

3951
frontend/static/langs.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,121 @@
.container {
min-height: 100vh;
padding: 0 0.5rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
}
.main {
padding: 5rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.footer {
width: 100%;
height: 100px;
border-top: 1px solid #eaeaea;
display: flex;
justify-content: center;
align-items: center;
}
.footer a {
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
}
.title a {
color: #0070f3;
text-decoration: none;
}
.title a:hover,
.title a:focus,
.title a:active {
text-decoration: underline;
}
.title {
margin: 0;
line-height: 1.15;
font-size: 4rem;
}
.title,
.description {
text-align: center;
}
.description {
line-height: 1.5;
font-size: 1.5rem;
}
.code {
background: #fafafa;
border-radius: 5px;
padding: 0.75rem;
font-size: 1.1rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}
.grid {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
max-width: 800px;
margin-top: 3rem;
}
.card {
margin: 1rem;
padding: 1.5rem;
text-align: left;
color: inherit;
text-decoration: none;
border: 1px solid #eaeaea;
border-radius: 10px;
transition: color 0.15s ease, border-color 0.15s ease;
width: 45%;
}
.card:hover,
.card:focus,
.card:active {
color: #0070f3;
border-color: #0070f3;
}
.card h2 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
}
.card p {
margin: 0;
font-size: 1.25rem;
line-height: 1.5;
}
.logo {
height: 1em;
margin-left: 0.5rem;
}
@media (max-width: 600px) {
.grid {
width: 100%;
flex-direction: column;
}
}

View File

@ -1,7 +0,0 @@
.xterm {
padding: 12px;
}
#header .button {
border-radius: 0;
}

View File

@ -0,0 +1,25 @@
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
background-image: radial-gradient(#ddd 1px, transparent 0),
radial-gradient(#ddd 1px, transparent 0);
background-position: 0 0, 25px 25px;
background-size: 50px 50px;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}
.rijuEdutor {
overflow: hidden;
height: calc(100% - 8px);
}

View File

@ -1,33 +0,0 @@
body {
display: flex;
flex-direction: column;
align-items: center;
font-family: sans-serif;
text-align: center;
padding-bottom: 20px;
}
.grid {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-top: 20px;
}
div.language {
width: 140px;
height: 60px;
border: solid;
margin: 5px;
padding: 5px;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
font-size: 18px;
}
a.language {
text-decoration: none;
color: black;
}

29
frontend/theme.js Normal file
View File

@ -0,0 +1,29 @@
import { createTheme } from "@mui/material/styles";
export const theme = createTheme({
palette: {
mode: "light",
primary: {
main: "#4d4dff",
},
background: {
default: "white",
},
success: {
main: "#48c78e",
},
},
typography: {
fontFamily: "Fira Code",
},
components: {
MuiButton: {
styleOverrides: {
root: {
textTransform: "none",
},
disableElevation: true,
},
},
},
});

View File

@ -0,0 +1,18 @@
export const EventEmitter = {
events: {},
dispatch: function (event, data) {
if (!this.events[event]) return;
this.events[event].forEach((callback) => callback(data));
},
subscribe: function (event, callback) {
if (!this.events[event]) this.events[event] = [];
this.events[event].push(callback);
},
isSubscribed: function (event) {
if (!Array.isArray(this.events[event])) return false;
else {
if (this.events[event].length == 0) return false;
else return true;
}
},
};