From d69583eda31c2b489f175bcfb9f66aa5b1a0f3b6 Mon Sep 17 00:00:00 2001 From: "W2.Wizard" <63303990+W2Wizard@users.noreply.github.com> Date: Fri, 13 Jan 2023 15:57:31 +0100 Subject: [PATCH 01/14] Partial refactor of code snippet --- .gitignore | 3 + c-playground/package.json | 15 +++ c-playground/webserv.js | 30 +++++ src/theme/book.js | 258 ++++++++++++++++++-------------------- 4 files changed, 171 insertions(+), 135 deletions(-) create mode 100644 c-playground/package.json create mode 100644 c-playground/webserv.js diff --git a/.gitignore b/.gitignore index a23c771e89..e3ecdaa32c 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ test_book/book/ # Ignore Vim temporary and swap files. *.sw? *~ + +node_modules +*-lock.json \ No newline at end of file diff --git a/c-playground/package.json b/c-playground/package.json new file mode 100644 index 0000000000..593c61dcbc --- /dev/null +++ b/c-playground/package.json @@ -0,0 +1,15 @@ +{ + "name": "c-playground", + "version": "1.0.0", + "description": "A simple server that takes a post request and executes the C code locally.", + "main": "webserv.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "W2Wizard", + "license": "MIT", + "dependencies": { + "cors": "^2.8.5", + "express": "^4.18.2" + } +} diff --git a/c-playground/webserv.js b/c-playground/webserv.js new file mode 100644 index 0000000000..278fe6af9f --- /dev/null +++ b/c-playground/webserv.js @@ -0,0 +1,30 @@ +// This is merely a dummy backend to receive the incoming requests. + +const express = require("express"); +const cors = require('cors'); +const app = express(); +const port = 4242; + +/*============================================================================*/ + +app.use(cors()); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use((err, req, res, next) => { + if (err.statusCode === 400 && "body" in err) + res.status(400).send({ status: 400, message: err.message }); + next(); +}); + +/*============================================================================*/ + +app.post('/playground', (req, res) => { + console.log("[Playground] body:", req.body); + console.log(req.headers); + + res.json({ result: "Request received!\n", error: null }); +}) + +app.listen(port, () => { + console.log(`[Playground] Running on: ${port}`); +}); \ No newline at end of file diff --git a/src/theme/book.js b/src/theme/book.js index e303ebb451..ceec413282 100644 --- a/src/theme/book.js +++ b/src/theme/book.js @@ -3,21 +3,38 @@ // Fix back button cache problem window.onunload = function () { }; -// Global variable, shared between modules -function playground_text(playground, hidden = true) { +/*===========================================================================*/ + +/** + * Retrieves the playground text. + * Global variable, shared between modules. + * @param {*} playground The playground itself. + * @param {boolean} hidden Is the playground hidden. + * @returns The code within the playground. + */ +function get_get_playground_text(playground, hidden = true) { let code_block = playground.querySelector("code"); - if (window.ace && code_block.classList.contains("editable")) { - let editor = window.ace.edit(code_block); - return editor.getValue(); - } else if (hidden) { + if (window.ace && code_block.classList.contains("editable")) + return window.ace.edit(code_block).getValue(); + else if (hidden) return code_block.textContent; - } else { + else return code_block.innerText; - } } +/*===========================================================================*/ + +/** Configure the code snippets. */ (function codeSnippets() { + + /** + * Fetch wrapper that uses a timeout to avoid indefinite or stuck requests. + * @param {string} url The URL to make the requst to. + * @param {*} options The fetch options. + * @param {number} timeout The amount of ms until timeout. + * @returns + */ function fetch_with_timeout(url, options, timeout = 6000) { return Promise.race([ fetch(url, options), @@ -25,131 +42,6 @@ function playground_text(playground, hidden = true) { ]); } - var playgrounds = Array.from(document.querySelectorAll(".playground")); - if (playgrounds.length > 0) { - fetch_with_timeout("https://play.rust-lang.org/meta/crates", { - headers: { - 'Content-Type': "application/json", - }, - method: 'POST', - mode: 'cors', - }) - .then(response => response.json()) - .then(response => { - // get list of crates available in the rust playground - let playground_crates = response.crates.map(item => item["id"]); - playgrounds.forEach(block => handle_crate_list_update(block, playground_crates)); - }); - } - - function handle_crate_list_update(playground_block, playground_crates) { - // update the play buttons after receiving the response - update_play_button(playground_block, playground_crates); - - // and install on change listener to dynamically update ACE editors - if (window.ace) { - let code_block = playground_block.querySelector("code"); - if (code_block.classList.contains("editable")) { - let editor = window.ace.edit(code_block); - editor.addEventListener("change", function (e) { - update_play_button(playground_block, playground_crates); - }); - // add Ctrl-Enter command to execute rust code - editor.commands.addCommand({ - name: "run", - bindKey: { - win: "Ctrl-Enter", - mac: "Ctrl-Enter" - }, - exec: _editor => run_rust_code(playground_block) - }); - } - } - } - - // updates the visibility of play button based on `no_run` class and - // used crates vs ones available on http://play.rust-lang.org - function update_play_button(pre_block, playground_crates) { - var play_button = pre_block.querySelector(".play-button"); - - // skip if code is `no_run` - if (pre_block.querySelector('code').classList.contains("no_run")) { - play_button.classList.add("hidden"); - return; - } - - // get list of `extern crate`'s from snippet - var txt = playground_text(pre_block); - var re = /extern\s+crate\s+([a-zA-Z_0-9]+)\s*;/g; - var snippet_crates = []; - var item; - while (item = re.exec(txt)) { - snippet_crates.push(item[1]); - } - - // check if all used crates are available on play.rust-lang.org - var all_available = snippet_crates.every(function (elem) { - return playground_crates.indexOf(elem) > -1; - }); - - if (all_available) { - play_button.classList.remove("hidden"); - } else { - play_button.classList.add("hidden"); - } - } - - function run_rust_code(code_block) { - var result_block = code_block.querySelector(".result"); - if (!result_block) { - result_block = document.createElement('code'); - result_block.className = 'result hljs language-bash'; - - code_block.append(result_block); - } - - let text = playground_text(code_block); - let classes = code_block.querySelector('code').classList; - let edition = "2015"; - if(classes.contains("edition2018")) { - edition = "2018"; - } else if(classes.contains("edition2021")) { - edition = "2021"; - } - var params = { - version: "stable", - optimize: "0", - code: text, - edition: edition - }; - - if (text.indexOf("#![feature") !== -1) { - params.version = "nightly"; - } - - result_block.innerText = "Running..."; - - fetch_with_timeout("https://play.rust-lang.org/evaluate.json", { - headers: { - 'Content-Type': "application/json", - }, - method: 'POST', - mode: 'cors', - body: JSON.stringify(params) - }) - .then(response => response.json()) - .then(response => { - if (response.result.trim() === '') { - result_block.innerText = "No output"; - result_block.classList.add("result-no-output"); - } else { - result_block.innerText = response.result; - result_block.classList.remove("result-no-output"); - } - }) - .catch(error => result_block.innerText = "Playground Communication: " + error.message); - } - // Syntax highlighting Configuration hljs.configure({ tabReplace: ' ', // 4 spaces @@ -253,7 +145,7 @@ function playground_text(playground, hidden = true) { buttons.insertBefore(runCodeButton, buttons.firstChild); runCodeButton.addEventListener('click', function (e) { - run_rust_code(pre_block); + run_code(pre_block); }); if (window.playground_copyable) { @@ -282,8 +174,94 @@ function playground_text(playground, hidden = true) { }); } }); + + /** + * Updates the visibility of play button based on `no_run` + * TODO: For rust: Toggle visiblity based on if the crates are available. + * @param {*} pre_block The playground block. + */ + function update_play(pre_block) { + const play_button = pre_block.querySelector(".play-button"); + + if (pre_block.querySelector('code').classList.contains("no_run")) { + play_button.classList.add("hidden"); + return; + } + play_button.classList.remove("hidden"); + } + + // Construct the playgrounds appropriately. + const playgrounds = Array.from(document.querySelectorAll(".playground")); + playgrounds.forEach(playground_block => { + update_play(playground_block); + if (!window.ace) { + return; + } + + const code_block = playground_block.querySelector("code"); + if (!code_block.classList.contains("editable")) { + return; + } + + // NOTE: Re-apply for rust later, since the code change might introduce new crates. + // const editor = window.ace.edit(code_block); + // editor.addEventListener("change", function (e) { + // update_play_button(playground_block); + // }); + + editor.commands.addCommand({ + name: "run", + bindKey: { + win: "Ctrl-Enter", + mac: "Ctrl-Enter" + }, + exec: _editor => run_code(playground_block) + }); + }); + + /** + * 'Executes' the code in the code block by POSTing it to the configured playground. + * @param {*} code_block The code block. + */ + function run_code(code_block) { + let result_block = code_block.querySelector(".result"); + if (!result_block) { + result_block = document.createElement('code'); + result_block.className = 'result hljs language-bash'; + code_block.append(result_block); + } + + result_block.innerText = "Running..."; + + // TODO: Pass markdown params as classes or use a data tag to cary over information. + let classes = code_block.querySelector('code').classList; + const params = { + code: get_playground_text(code_block) + } + + // TODO: Somehow fetch the configured URL from the TOML. + fetch_with_timeout("http://localhost:4242/evaluate", { + headers: { 'Content-Type': "application/json" }, + method: 'POST', + mode: 'cors', + body: JSON.stringify(params) + }) + .then(response => response.json()) + .then(response => { + if (response.result.trim() === '') { + result_block.innerText = "No output"; + result_block.classList.add("result-no-output"); + } else { + result_block.innerText = response.result; + result_block.classList.remove("result-no-output"); + } + }) + .catch(error => result_block.innerText = "Playground Communication: " + error.message); + } })(); +/*===========================================================================*/ + (function themes() { var html = document.querySelector('html'); var themeToggleButton = document.getElementById('theme-toggle'); @@ -440,6 +418,8 @@ function playground_text(playground, hidden = true) { }); })(); +/*===========================================================================*/ + (function sidebar() { var html = document.querySelector("html"); var sidebar = document.getElementById("sidebar"); @@ -560,6 +540,8 @@ function playground_text(playground, hidden = true) { } })(); +/*===========================================================================*/ + (function chapterNavigation() { document.addEventListener('keydown', function (e) { if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; } @@ -584,6 +566,8 @@ function playground_text(playground, hidden = true) { }); })(); +/*===========================================================================*/ + (function clipboard() { var clipButtons = document.querySelectorAll('.clip-button'); @@ -601,7 +585,7 @@ function playground_text(playground, hidden = true) { text: function (trigger) { hideTooltip(trigger); let playground = trigger.closest("pre"); - return playground_text(playground, false); + return get_playground_text(playground, false); } }); @@ -621,6 +605,8 @@ function playground_text(playground, hidden = true) { }); })(); +/*===========================================================================*/ + (function scrollToTop () { var menuTitle = document.querySelector('.menu-title'); @@ -629,6 +615,8 @@ function playground_text(playground, hidden = true) { }); })(); +/*===========================================================================*/ + (function controllMenu() { var menu = document.getElementById('menu-bar'); From 6e7cc8880f49fdd71113d1f80ed211c96f168ab4 Mon Sep 17 00:00:00 2001 From: "W2.Wizard" <63303990+W2Wizard@users.noreply.github.com> Date: Mon, 16 Jan 2023 12:46:43 +0100 Subject: [PATCH 02/14] Additional minor refactoring --- src/theme/book.js | 41 ++++++++++++++++++----------------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/src/theme/book.js b/src/theme/book.js index ceec413282..e0c19f3274 100644 --- a/src/theme/book.js +++ b/src/theme/book.js @@ -12,7 +12,7 @@ window.onunload = function () { }; * @param {boolean} hidden Is the playground hidden. * @returns The code within the playground. */ -function get_get_playground_text(playground, hidden = true) { +function get_playground_text(playground, hidden = true) { let code_block = playground.querySelector("code"); if (window.ace && code_block.classList.contains("editable")) @@ -48,32 +48,31 @@ function get_get_playground_text(playground, hidden = true) { languages: [], // Languages used for auto-detection }); - let code_nodes = Array - .from(document.querySelectorAll('code')) - // Don't highlight `inline code` blocks in headers. - .filter(function (node) {return !node.parentElement.classList.contains("header"); }); + // Don't highlight `inline code` blocks in headers. + const code_nodes = Array.from(document.querySelectorAll('code')).filter((node) => { + return !node.parentElement.classList.contains("header"); + }); + // Languange class needs to be removed for editable or highlightjs will capture events. if (window.ace) { - // language-rust class needs to be removed for editable - // blocks or highlightjs will capture events code_nodes - .filter(function (node) {return node.classList.contains("editable"); }) - .forEach(function (block) { block.classList.remove('language-rust'); }); + .filter((node) => node.classList.contains("editable")) + .forEach((block) => block.classList.remove("languange-rust")); code_nodes - .filter(function (node) {return !node.classList.contains("editable"); }) - .forEach(function (block) { hljs.highlightBlock(block); }); - } else { + .filter((node) => !node.classList.contains("editable")) + .forEach((block) => hljs.highlightBlock(block)); + } + else { code_nodes.forEach(function (block) { hljs.highlightBlock(block); }); } - // Adding the hljs class gives code blocks the color css - // even if highlighting doesn't apply + // Adding the hljs class gives code blocks the color css even if highlighting doesn't apply code_nodes.forEach(function (block) { block.classList.add('hljs'); }); - Array.from(document.querySelectorAll("code.language-rust")).forEach(function (block) { + Array.from(document.querySelectorAll("code.language-rust")).forEach((block) => { + const lines = Array.from(block.querySelectorAll('.boring')); - var lines = Array.from(block.querySelectorAll('.boring')); // If no lines were hidden, return if (!lines.length) { return; } block.classList.add("hide-boring"); @@ -194,14 +193,10 @@ function get_get_playground_text(playground, hidden = true) { const playgrounds = Array.from(document.querySelectorAll(".playground")); playgrounds.forEach(playground_block => { update_play(playground_block); - if (!window.ace) { - return; - } + if (!window.ace) { return; } const code_block = playground_block.querySelector("code"); - if (!code_block.classList.contains("editable")) { - return; - } + if (!code_block.classList.contains("editable")) { return; } // NOTE: Re-apply for rust later, since the code change might introduce new crates. // const editor = window.ace.edit(code_block); @@ -240,7 +235,7 @@ function get_get_playground_text(playground, hidden = true) { } // TODO: Somehow fetch the configured URL from the TOML. - fetch_with_timeout("http://localhost:4242/evaluate", { + fetch_with_timeout("http://localhost:4242/playground", { headers: { 'Content-Type': "application/json" }, method: 'POST', mode: 'cors', From a25dbdb170bd6c08efd28373e963430f555e011b Mon Sep 17 00:00:00 2001 From: "W2.Wizard" <63303990+W2Wizard@users.noreply.github.com> Date: Tue, 17 Jan 2023 12:07:05 +0100 Subject: [PATCH 03/14] Remove hidden lines feature, as well as rust stuff. --- c-playground/webserv.js | 1 - src/renderer/html_handlebars/hbs_renderer.rs | 60 +++++--------------- src/theme/book.js | 34 ----------- 3 files changed, 14 insertions(+), 81 deletions(-) diff --git a/c-playground/webserv.js b/c-playground/webserv.js index 278fe6af9f..efd3419efe 100644 --- a/c-playground/webserv.js +++ b/c-playground/webserv.js @@ -20,7 +20,6 @@ app.use((err, req, res, next) => { app.post('/playground', (req, res) => { console.log("[Playground] body:", req.body); - console.log(req.headers); res.json({ result: "Request received!\n", error: null }); }) diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 1b648dac10..037c8d0951 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -1,5 +1,5 @@ use crate::book::{Book, BookItem}; -use crate::config::{BookConfig, Config, HtmlConfig, Playground, RustEdition}; +use crate::config::{BookConfig, Config, HtmlConfig, Playground}; use crate::errors::*; use crate::renderer::html_handlebars::helpers; use crate::renderer::{RenderContext, Renderer}; @@ -110,7 +110,7 @@ impl HtmlHandlebars { debug!("Render template"); let rendered = ctx.handlebars.render("index", &ctx.data)?; - let rendered = self.post_process(rendered, &ctx.html_config.playground, ctx.edition); + let rendered = self.post_process(rendered, &ctx.html_config.playground); // Write to file debug!("Creating {}", filepath.display()); @@ -122,7 +122,7 @@ impl HtmlHandlebars { ctx.data.insert("is_index".to_owned(), json!(true)); let rendered_index = ctx.handlebars.render("index", &ctx.data)?; let rendered_index = - self.post_process(rendered_index, &ctx.html_config.playground, ctx.edition); + self.post_process(rendered_index, &ctx.html_config.playground); debug!("Creating index.html from {}", ctx_path); utils::fs::write_file(&ctx.destination, "index.html", rendered_index.as_bytes())?; } @@ -182,8 +182,7 @@ impl HtmlHandlebars { data_404.insert("title".to_owned(), json!(title)); let rendered = handlebars.render("index", &data_404)?; - let rendered = - self.post_process(rendered, &html_config.playground, ctx.config.rust.edition); + let rendered = self.post_process(rendered, &html_config.playground); let output_file = get_404_output_file(&html_config.input_404); utils::fs::write_file(destination, output_file, rendered.as_bytes())?; debug!("Creating 404.html ✓"); @@ -195,11 +194,10 @@ impl HtmlHandlebars { &self, rendered: String, playground_config: &Playground, - edition: Option, ) -> String { let rendered = build_header_links(&rendered); let rendered = fix_code_blocks(&rendered); - let rendered = add_playground_pre(&rendered, playground_config, edition); + let rendered = add_playground_pre(&rendered, playground_config); rendered } @@ -540,7 +538,6 @@ impl Renderer for HtmlHandlebars { is_index, book_config: book_config.clone(), html_config: html_config.clone(), - edition: ctx.config.rust.edition, chapter_titles: &ctx.chapter_titles, }; self.render_item(item, ctx, &mut print_content)?; @@ -564,8 +561,7 @@ impl Renderer for HtmlHandlebars { debug!("Render template"); let rendered = handlebars.render("index", &data)?; - let rendered = - self.post_process(rendered, &html_config.playground, ctx.config.rust.edition); + let rendered = self.post_process(rendered, &html_config.playground); utils::fs::write_file(destination, "print.html", rendered.as_bytes())?; debug!("Creating print.html ✓"); @@ -830,11 +826,8 @@ fn fix_code_blocks(html: &str) -> String { .into_owned() } -fn add_playground_pre( - html: &str, - playground_config: &Playground, - edition: Option, -) -> String { +// NOTE: Rust devs cry in agony +fn add_playground_pre(html: &str, playground_config: &Playground) -> String { static ADD_PLAYGROUND_PRE: Lazy = Lazy::new(|| Regex::new(r##"((?s)]?class="([^"]+)".*?>(.*?))"##).unwrap()); @@ -844,46 +837,23 @@ fn add_playground_pre( let classes = &caps[2]; let code = &caps[3]; - if classes.contains("language-rust") { + // TODO: Revisit this as right now its a bit of a mess + if classes.contains("language") { if (!classes.contains("ignore") && !classes.contains("noplayground") && !classes.contains("noplaypen") && playground_config.runnable) || classes.contains("mdbook-runnable") { - let contains_e2015 = classes.contains("edition2015"); - let contains_e2018 = classes.contains("edition2018"); - let contains_e2021 = classes.contains("edition2021"); - let edition_class = if contains_e2015 || contains_e2018 || contains_e2021 { - // the user forced edition, we should not overwrite it - "" - } else { - match edition { - Some(RustEdition::E2015) => " edition2015", - Some(RustEdition::E2018) => " edition2018", - Some(RustEdition::E2021) => " edition2021", - None => "", - } - }; - - // wrap the contents in an external pre block format!( - "
{}
", + "
{}
", classes, - edition_class, { - let content: Cow<'_, str> = if playground_config.editable - && classes.contains("editable") - || text.contains("fn main") - || text.contains("quick_main!") - { + // I have no idea what im doing, this syntax is god awful, but it works :) + let content: Cow<'_, str> = if playground_config.editable && classes.contains("editable"){ code.into() } else { - // we need to inject our own main - let (attrs, code) = partition_source(code); - - format!("# #![allow(unused)]\n{}#fn main() {{\n{}#}}", attrs, code) - .into() + format!("{}", code).into() }; hide_lines(&content) } @@ -892,7 +862,6 @@ fn add_playground_pre( format!("{}", classes, hide_lines(code)) } } else { - // not language-rust, so no-op text.to_owned() } }) @@ -960,7 +929,6 @@ struct RenderItemContext<'a> { is_index: bool, book_config: BookConfig, html_config: HtmlConfig, - edition: Option, chapter_titles: &'a HashMap, } diff --git a/src/theme/book.js b/src/theme/book.js index e0c19f3274..0537dc128b 100644 --- a/src/theme/book.js +++ b/src/theme/book.js @@ -70,40 +70,6 @@ function get_playground_text(playground, hidden = true) { // Adding the hljs class gives code blocks the color css even if highlighting doesn't apply code_nodes.forEach(function (block) { block.classList.add('hljs'); }); - Array.from(document.querySelectorAll("code.language-rust")).forEach((block) => { - const lines = Array.from(block.querySelectorAll('.boring')); - - // If no lines were hidden, return - if (!lines.length) { return; } - block.classList.add("hide-boring"); - - var buttons = document.createElement('div'); - buttons.className = 'buttons'; - buttons.innerHTML = ""; - - // add expand button - var pre_block = block.parentNode; - pre_block.insertBefore(buttons, pre_block.firstChild); - - pre_block.querySelector('.buttons').addEventListener('click', function (e) { - if (e.target.classList.contains('fa-eye')) { - e.target.classList.remove('fa-eye'); - e.target.classList.add('fa-eye-slash'); - e.target.title = 'Hide lines'; - e.target.setAttribute('aria-label', e.target.title); - - block.classList.remove('hide-boring'); - } else if (e.target.classList.contains('fa-eye-slash')) { - e.target.classList.remove('fa-eye-slash'); - e.target.classList.add('fa-eye'); - e.target.title = 'Show hidden lines'; - e.target.setAttribute('aria-label', e.target.title); - - block.classList.add('hide-boring'); - } - }); - }); - if (window.playground_copyable) { Array.from(document.querySelectorAll('pre code')).forEach(function (block) { var pre_block = block.parentNode; From f738dcc1f3845a15e9360e8238a733b7da7d4c50 Mon Sep 17 00:00:00 2001 From: "W2.Wizard" <63303990+W2Wizard@users.noreply.github.com> Date: Tue, 17 Jan 2023 12:28:47 +0100 Subject: [PATCH 04/14] Allow to configure custom endpoint --- src/config.rs | 3 +++ src/renderer/html_handlebars/hbs_renderer.rs | 24 ++------------------ src/theme/book.js | 8 ++++--- 3 files changed, 10 insertions(+), 25 deletions(-) diff --git a/src/config.rs b/src/config.rs index 0c367d8481..f46a2ff5d6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -628,6 +628,8 @@ pub struct Playground { pub line_numbers: bool, /// Display the run button. Default: `true` pub runnable: bool, + /// Submits the code to the following endpoint for running. + pub endpoint: String } impl Default for Playground { @@ -638,6 +640,7 @@ impl Default for Playground { copy_js: true, line_numbers: false, runnable: true, + endpoint: "https://play.rust-lang.org/evaluate.json".to_string() } } } diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 037c8d0951..ab66e80255 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -846,7 +846,8 @@ fn add_playground_pre(html: &str, playground_config: &Playground) -> String { || classes.contains("mdbook-runnable") { format!( - "
{}
", + "
{}
", + playground_config.endpoint, classes, { // I have no idea what im doing, this syntax is god awful, but it works :) @@ -901,27 +902,6 @@ fn hide_lines(content: &str) -> String { result } -fn partition_source(s: &str) -> (String, String) { - let mut after_header = false; - let mut before = String::new(); - let mut after = String::new(); - - for line in s.lines() { - let trimline = line.trim(); - let header = trimline.chars().all(char::is_whitespace) || trimline.starts_with("#!["); - if !header || after_header { - after_header = true; - after.push_str(line); - after.push('\n'); - } else { - before.push_str(line); - before.push('\n'); - } - } - - (before, after) -} - struct RenderItemContext<'a> { handlebars: &'a Handlebars<'a>, destination: PathBuf, diff --git a/src/theme/book.js b/src/theme/book.js index 0537dc128b..fc3a8cb3c0 100644 --- a/src/theme/book.js +++ b/src/theme/book.js @@ -195,13 +195,15 @@ function get_playground_text(playground, hidden = true) { result_block.innerText = "Running..."; // TODO: Pass markdown params as classes or use a data tag to cary over information. - let classes = code_block.querySelector('code').classList; + // let classes = code_block.querySelector('code').classList; const params = { code: get_playground_text(code_block) } - // TODO: Somehow fetch the configured URL from the TOML. - fetch_with_timeout("http://localhost:4242/playground", { + const endpoint = code_block.dataset.endpoint; + console.log("Sending code to:", endpoint); + + fetch_with_timeout(endpoint, { headers: { 'Content-Type': "application/json" }, method: 'POST', mode: 'cors', From 5825751e7fc2afac8f08301860fbbcba79018663 Mon Sep 17 00:00:00 2001 From: "W2.Wizard" <63303990+W2Wizard@users.noreply.github.com> Date: Tue, 17 Jan 2023 14:00:48 +0100 Subject: [PATCH 05/14] Update readme --- README.md | 45 ++++++++++++++++++++++++++++++++++++++--- c-playground/webserv.js | 15 ++++++++++---- src/theme/book.js | 24 ++++++++++++++-------- 3 files changed, 69 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index b2177cf307..95d2128072 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,49 @@ mdBook is a utility to create modern online books from Markdown files. -Check out the **[User Guide]** for a list of features and installation and usage information. -The User Guide also serves as a demonstration to showcase what a book looks like. +This is a modified version that lets you support a custom playground backend other than rust. +Its entirely a "bring your own backend" situation so in the end you are responsible to safeguard against what kind of code +is running on your backend. -If you are interested in contributing to the development of mdBook, check out the [Contribution Guide]. +May or may not have some features broken due to mdBook being *VERY* hardcoded with rust in mind but should work just fine. + +## Custom playground backend + +For a custom playground backend you simply need a webserver with a `POST` route open. Call it whatever you like. + +Then in the `.toml` config file of your book set the endpoint: +```toml +[output.html.playground] +editable = false # allows editing the source code +copyable = true # include the copy button for copying code snippets +copy-js = true # includes the JavaScript for the code editor +line-numbers = true # displays line numbers for editable code +runnable = true # displays a run button for rust code +endpoint = "http://localhost:4242/playground/" # send the code to this url for execution +``` + +A clients incoming request looks as follows: +```json +{ + "lang": "cpp", + "code": "..." +} +``` + +> **Note**: The hidden code lines feature has been entirely ripped out. Because I deemed it useless. + +[See supported languanges](/guide/src/format/theme/syntax-highlighting.md) for syntax highlighting. As well as the `lang` options for incoming client requests. + + +A servers outgoing response should look as follows: +```json +{ + "result": "Request received!\n", + "error": null +} +``` + +The client will display the appropriate message depending on the server's response. ## License diff --git a/c-playground/webserv.js b/c-playground/webserv.js index efd3419efe..9d60ff45b8 100644 --- a/c-playground/webserv.js +++ b/c-playground/webserv.js @@ -5,24 +5,31 @@ const cors = require('cors'); const app = express(); const port = 4242; +// Middleware /*============================================================================*/ app.use(cors()); app.use(express.json()); app.use(express.urlencoded({ extended: true })); -app.use((err, req, res, next) => { +app.use((err, _, res, next) => { if (err.statusCode === 400 && "body" in err) res.status(400).send({ status: 400, message: err.message }); next(); }); +// Routes /*============================================================================*/ -app.post('/playground', (req, res) => { - console.log("[Playground] body:", req.body); +app.post('/playground/', (req, res) => { + const body = req.body; + + console.log(`[Playground] [${body.lang}] body:`, body.code); res.json({ result: "Request received!\n", error: null }); -}) +}); + +// Entry point +/*============================================================================*/ app.listen(port, () => { console.log(`[Playground] Running on: ${port}`); diff --git a/src/theme/book.js b/src/theme/book.js index fc3a8cb3c0..d9cff795c7 100644 --- a/src/theme/book.js +++ b/src/theme/book.js @@ -164,12 +164,6 @@ function get_playground_text(playground, hidden = true) { const code_block = playground_block.querySelector("code"); if (!code_block.classList.contains("editable")) { return; } - // NOTE: Re-apply for rust later, since the code change might introduce new crates. - // const editor = window.ace.edit(code_block); - // editor.addEventListener("change", function (e) { - // update_play_button(playground_block); - // }); - editor.commands.addCommand({ name: "run", bindKey: { @@ -194,9 +188,23 @@ function get_playground_text(playground, hidden = true) { result_block.innerText = "Running..."; - // TODO: Pass markdown params as classes or use a data tag to cary over information. - // let classes = code_block.querySelector('code').classList; + let lang = ""; + const classes = code_block.querySelector('code').classList; + for (const value of classes) { + if (value.startsWith("language-")) { + lang = value.split("-")[1]; + break; + } + } + + // TODO: Not quite sure what to do here ? + if (lang == "") { + result_block.innerText = "Not supported!"; + return; + } + const params = { + lang: lang, code: get_playground_text(code_block) } From 98c500a3ab7a87da7ea5e757a3a1fa09ffc85d88 Mon Sep 17 00:00:00 2001 From: "W2.Wizard" <63303990+W2Wizard@users.noreply.github.com> Date: Tue, 17 Jan 2023 14:10:17 +0100 Subject: [PATCH 06/14] Remove old hardcoded tests --- src/config.rs | 1 + src/renderer/html_handlebars/hbs_renderer.rs | 103 ------------------- 2 files changed, 1 insertion(+), 103 deletions(-) diff --git a/src/config.rs b/src/config.rs index f46a2ff5d6..d815a4b518 100644 --- a/src/config.rs +++ b/src/config.rs @@ -786,6 +786,7 @@ mod tests { copy_js: true, line_numbers: false, runnable: true, + endpoint: "".to_string() }; let html_should_be = HtmlConfig { curly_quotes: true, diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index ab66e80255..518bcf2d6c 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -950,107 +950,4 @@ mod tests { assert_eq!(got, should_be); } } - - #[test] - fn add_playground() { - let inputs = [ - ("x()", - "
#![allow(unused)]\nfn main() {\nx()\n}
"), - ("fn main() {}", - "
fn main() {}
"), - ("let s = \"foo\n # bar\n\";", - "
let s = \"foo\n bar\n\";
"), - ("let s = \"foo\n ## bar\n\";", - "
let s = \"foo\n # bar\n\";
"), - ("let s = \"foo\n # bar\n#\n\";", - "
let s = \"foo\n bar\n\n\";
"), - ("let s = \"foo\n # bar\n\";", - "let s = \"foo\n bar\n\";"), - ("#![no_std]\nlet s = \"foo\";\n #[some_attr]", - "
#![no_std]\nlet s = \"foo\";\n #[some_attr]
"), - ]; - for (src, should_be) in &inputs { - let got = add_playground_pre( - src, - &Playground { - editable: true, - ..Playground::default() - }, - None, - ); - assert_eq!(&*got, *should_be); - } - } - #[test] - fn add_playground_edition2015() { - let inputs = [ - ("x()", - "
#![allow(unused)]\nfn main() {\nx()\n}
"), - ("fn main() {}", - "
fn main() {}
"), - ("fn main() {}", - "
fn main() {}
"), - ("fn main() {}", - "
fn main() {}
"), - ]; - for (src, should_be) in &inputs { - let got = add_playground_pre( - src, - &Playground { - editable: true, - ..Playground::default() - }, - Some(RustEdition::E2015), - ); - assert_eq!(&*got, *should_be); - } - } - #[test] - fn add_playground_edition2018() { - let inputs = [ - ("x()", - "
#![allow(unused)]\nfn main() {\nx()\n}
"), - ("fn main() {}", - "
fn main() {}
"), - ("fn main() {}", - "
fn main() {}
"), - ("fn main() {}", - "
fn main() {}
"), - ]; - for (src, should_be) in &inputs { - let got = add_playground_pre( - src, - &Playground { - editable: true, - ..Playground::default() - }, - Some(RustEdition::E2018), - ); - assert_eq!(&*got, *should_be); - } - } - #[test] - fn add_playground_edition2021() { - let inputs = [ - ("x()", - "
#![allow(unused)]\nfn main() {\nx()\n}
"), - ("fn main() {}", - "
fn main() {}
"), - ("fn main() {}", - "
fn main() {}
"), - ("fn main() {}", - "
fn main() {}
"), - ]; - for (src, should_be) in &inputs { - let got = add_playground_pre( - src, - &Playground { - editable: true, - ..Playground::default() - }, - Some(RustEdition::E2021), - ); - assert_eq!(&*got, *should_be); - } - } } From 2242091efc15e3e75a3990ed074f576f358c8c14 Mon Sep 17 00:00:00 2001 From: "W2.Wizard" <63303990+W2Wizard@users.noreply.github.com> Date: Tue, 17 Jan 2023 14:27:35 +0100 Subject: [PATCH 07/14] Refactoring --- src/theme/book.js | 183 ++++++++++++++++++++++------------------------ 1 file changed, 88 insertions(+), 95 deletions(-) diff --git a/src/theme/book.js b/src/theme/book.js index d9cff795c7..c4a4c32140 100644 --- a/src/theme/book.js +++ b/src/theme/book.js @@ -72,16 +72,16 @@ function get_playground_text(playground, hidden = true) { if (window.playground_copyable) { Array.from(document.querySelectorAll('pre code')).forEach(function (block) { - var pre_block = block.parentNode; + let pre_block = block.parentNode; if (!pre_block.classList.contains('playground')) { - var buttons = pre_block.querySelector(".buttons"); + let buttons = pre_block.querySelector(".buttons"); if (!buttons) { buttons = document.createElement('div'); buttons.className = 'buttons'; pre_block.insertBefore(buttons, pre_block.firstChild); } - var clipButton = document.createElement('button'); + let clipButton = document.createElement('button'); clipButton.className = 'fa fa-copy clip-button'; clipButton.title = 'Copy to clipboard'; clipButton.setAttribute('aria-label', clipButton.title); @@ -95,49 +95,46 @@ function get_playground_text(playground, hidden = true) { // Process playground code blocks Array.from(document.querySelectorAll(".playground")).forEach(function (pre_block) { // Add play button - var buttons = pre_block.querySelector(".buttons"); + let buttons = pre_block.querySelector(".buttons"); if (!buttons) { buttons = document.createElement('div'); buttons.className = 'buttons'; pre_block.insertBefore(buttons, pre_block.firstChild); } - var runCodeButton = document.createElement('button'); + let runCodeButton = document.createElement('button'); runCodeButton.className = 'fa fa-play play-button'; runCodeButton.hidden = true; runCodeButton.title = 'Run this code'; runCodeButton.setAttribute('aria-label', runCodeButton.title); - + runCodeButton.addEventListener('click', (e) => run_code(pre_block)); buttons.insertBefore(runCodeButton, buttons.firstChild); - runCodeButton.addEventListener('click', function (e) { - run_code(pre_block); - }); if (window.playground_copyable) { - var copyCodeClipboardButton = document.createElement('button'); + let copyCodeClipboardButton = document.createElement('button'); copyCodeClipboardButton.className = 'fa fa-copy clip-button'; copyCodeClipboardButton.innerHTML = ''; copyCodeClipboardButton.title = 'Copy to clipboard'; copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title); - buttons.insertBefore(copyCodeClipboardButton, buttons.firstChild); } let code_block = pre_block.querySelector("code"); - if (window.ace && code_block.classList.contains("editable")) { - var undoChangesButton = document.createElement('button'); - undoChangesButton.className = 'fa fa-history reset-button'; - undoChangesButton.title = 'Undo changes'; - undoChangesButton.setAttribute('aria-label', undoChangesButton.title); - - buttons.insertBefore(undoChangesButton, buttons.firstChild); - - undoChangesButton.addEventListener('click', function () { - let editor = window.ace.edit(code_block); - editor.setValue(editor.originalCode); - editor.clearSelection(); - }); - } + if (!(window.ace && code_block.classList.contains("editable"))) + return; + + let undoChangesButton = document.createElement('button'); + undoChangesButton.className = 'fa fa-history reset-button'; + undoChangesButton.title = 'Undo changes'; + undoChangesButton.setAttribute('aria-label', undoChangesButton.title); + undoChangesButton.addEventListener('click', () => { + let editor = window.ace.edit(code_block); + + editor.setValue(editor.originalCode); + editor.clearSelection(); + }); + + buttons.insertBefore(undoChangesButton, buttons.firstChild); }); /** @@ -205,7 +202,7 @@ function get_playground_text(playground, hidden = true) { const params = { lang: lang, - code: get_playground_text(code_block) + code: get_playground_text(code_block).trim() } const endpoint = code_block.dataset.endpoint; @@ -234,11 +231,11 @@ function get_playground_text(playground, hidden = true) { /*===========================================================================*/ (function themes() { - var html = document.querySelector('html'); - var themeToggleButton = document.getElementById('theme-toggle'); - var themePopup = document.getElementById('theme-list'); - var themeColorMetaTag = document.querySelector('meta[name="theme-color"]'); - var stylesheets = { + let html = document.querySelector('html'); + let themeToggleButton = document.getElementById('theme-toggle'); + let themePopup = document.getElementById('theme-list'); + let themeColorMetaTag = document.querySelector('meta[name="theme-color"]'); + let stylesheets = { ayuHighlight: document.querySelector("[href$='ayu-highlight.css']"), tomorrowNight: document.querySelector("[href$='tomorrow-night.css']"), highlight: document.querySelector("[href$='highlight.css']"), @@ -264,7 +261,7 @@ function get_playground_text(playground, hidden = true) { } function get_theme() { - var theme; + let theme; try { theme = localStorage.getItem('mdbook-theme'); } catch (e) { } if (theme === null || theme === undefined) { return default_theme; @@ -304,7 +301,7 @@ function get_playground_text(playground, hidden = true) { }); } - var previousTheme = get_theme(); + let previousTheme = get_theme(); if (store) { try { localStorage.setItem('mdbook-theme', theme); } catch (e) { } @@ -316,7 +313,7 @@ function get_playground_text(playground, hidden = true) { } // Set theme - var theme = get_theme(); + let theme = get_theme(); set_theme(theme, false); @@ -329,7 +326,7 @@ function get_playground_text(playground, hidden = true) { }); themePopup.addEventListener('click', function (e) { - var theme; + let theme; if (e.target.className === "theme") { theme = e.target.id; } else if (e.target.parentElement.className === "theme") { @@ -392,12 +389,12 @@ function get_playground_text(playground, hidden = true) { /*===========================================================================*/ (function sidebar() { - var html = document.querySelector("html"); - var sidebar = document.getElementById("sidebar"); - var sidebarLinks = document.querySelectorAll('#sidebar a'); - var sidebarToggleButton = document.getElementById("sidebar-toggle"); - var sidebarResizeHandle = document.getElementById("sidebar-resize-handle"); - var firstContact = null; + let html = document.querySelector("html"); + let sidebar = document.getElementById("sidebar"); + let sidebarLinks = document.querySelectorAll('#sidebar a'); + let sidebarToggleButton = document.getElementById("sidebar-toggle"); + let sidebarResizeHandle = document.getElementById("sidebar-resize-handle"); + let firstContact = null; function showSidebar() { html.classList.remove('sidebar-hidden') @@ -411,22 +408,20 @@ function get_playground_text(playground, hidden = true) { } - var sidebarAnchorToggles = document.querySelectorAll('#sidebar a.toggle'); + let sidebarAnchorToggles = document.querySelectorAll('#sidebar a.toggle'); function toggleSection(ev) { ev.currentTarget.parentElement.classList.toggle('expanded'); } - Array.from(sidebarAnchorToggles).forEach(function (el) { - el.addEventListener('click', toggleSection); - }); + Array.from(sidebarAnchorToggles).forEach((el) => el.addEventListener('click', toggleSection)); function hideSidebar() { html.classList.remove('sidebar-visible') html.classList.add('sidebar-hidden'); - Array.from(sidebarLinks).forEach(function (link) { - link.setAttribute('tabIndex', -1); - }); + + Array.from(sidebarLinks).forEach((link) => link.setAttribute('tabIndex', -1)); + sidebarToggleButton.setAttribute('aria-expanded', false); sidebar.setAttribute('aria-hidden', true); try { localStorage.setItem('mdbook-sidebar', 'hidden'); } catch (e) { } @@ -435,7 +430,7 @@ function get_playground_text(playground, hidden = true) { // Toggle sidebar sidebarToggleButton.addEventListener('click', function sidebarToggle() { if (html.classList.contains("sidebar-hidden")) { - var current_width = parseInt( + let current_width = parseInt( document.documentElement.style.getPropertyValue('--sidebar-width'), 10); if (current_width < 150) { document.documentElement.style.setProperty('--sidebar-width', '150px'); @@ -460,7 +455,7 @@ function get_playground_text(playground, hidden = true) { html.classList.add('sidebar-resizing'); } function resize(e) { - var pos = (e.clientX - sidebar.offsetLeft); + let pos = (e.clientX - sidebar.offsetLeft); if (pos < 20) { hideSidebar(); } else { @@ -478,19 +473,19 @@ function get_playground_text(playground, hidden = true) { window.removeEventListener('mouseup', stopResize, false); } - document.addEventListener('touchstart', function (e) { + document.addEventListener('touchstart', (e) => { firstContact = { x: e.touches[0].clientX, time: Date.now() }; }, { passive: true }); - document.addEventListener('touchmove', function (e) { + document.addEventListener('touchmove', (e) => { if (!firstContact) return; - var curX = e.touches[0].clientX; - var xDiff = curX - firstContact.x, + let curX = e.touches[0].clientX; + let xDiff = curX - firstContact.x, tDiff = Date.now() - firstContact.time; if (tDiff < 250 && Math.abs(xDiff) >= 150) { @@ -504,7 +499,7 @@ function get_playground_text(playground, hidden = true) { }, { passive: true }); // Scroll sidebar to current active section - var activeSection = document.getElementById("sidebar").querySelector(".active"); + let activeSection = document.getElementById("sidebar").querySelector(".active"); if (activeSection) { // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView activeSection.scrollIntoView({ block: 'center' }); @@ -514,33 +509,33 @@ function get_playground_text(playground, hidden = true) { /*===========================================================================*/ (function chapterNavigation() { - document.addEventListener('keydown', function (e) { - if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; } - if (window.search && window.search.hasFocus()) { return; } + document.addEventListener('keydown', (e) => { + if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return; + if (window.search && window.search.hasFocus()) return; switch (e.key) { case 'ArrowRight': e.preventDefault(); - var nextButton = document.querySelector('.nav-chapters.next'); - if (nextButton) { + let nextButton = document.querySelector('.nav-chapters.next'); + if (nextButton) window.location.href = nextButton.href; - } break; + case 'ArrowLeft': e.preventDefault(); - var previousButton = document.querySelector('.nav-chapters.previous'); - if (previousButton) { + let previousButton = document.querySelector('.nav-chapters.previous'); + if (previousButton) window.location.href = previousButton.href; - } break; } }); + })(); /*===========================================================================*/ (function clipboard() { - var clipButtons = document.querySelectorAll('.clip-button'); + let clipButtons = document.querySelectorAll('.clip-button'); function hideTooltip(elem) { elem.firstChild.innerText = ""; @@ -552,62 +547,58 @@ function get_playground_text(playground, hidden = true) { elem.className = 'fa fa-copy tooltipped'; } - var clipboardSnippets = new ClipboardJS('.clip-button', { - text: function (trigger) { + let clipboardSnippets = new ClipboardJS('.clip-button', { + text: (trigger) => { hideTooltip(trigger); - let playground = trigger.closest("pre"); - return get_playground_text(playground, false); + return get_playground_text(trigger.closest("pre"), false); } }); - Array.from(clipButtons).forEach(function (clipButton) { - clipButton.addEventListener('mouseout', function (e) { - hideTooltip(e.currentTarget); - }); - }); + Array.from(clipButtons).forEach((clipButton) => + clipButton.addEventListener('mouseout', hideTooltip(e.currentTarget)) + ); - clipboardSnippets.on('success', function (e) { + clipboardSnippets.on('success', (e) => { e.clearSelection(); showTooltip(e.trigger, "Copied!"); }); - clipboardSnippets.on('error', function (e) { - showTooltip(e.trigger, "Clipboard error!"); - }); + clipboardSnippets.on('error', (e) => showTooltip(e.trigger, "Clipboard error!")); })(); /*===========================================================================*/ (function scrollToTop () { - var menuTitle = document.querySelector('.menu-title'); + let menuTitle = document.querySelector('.menu-title'); - menuTitle.addEventListener('click', function () { + menuTitle.addEventListener('click', () => { document.scrollingElement.scrollTo({ top: 0, behavior: 'smooth' }); }); })(); /*===========================================================================*/ +// wtf is this... (function controllMenu() { - var menu = document.getElementById('menu-bar'); + let menu = document.getElementById('menu-bar'); (function controllPosition() { - var scrollTop = document.scrollingElement.scrollTop; - var prevScrollTop = scrollTop; - var minMenuY = -menu.clientHeight - 50; - // When the script loads, the page can be at any scroll (e.g. if you reforesh it). + let scrollTop = document.scrollingElement.scrollTop; + let prevScrollTop = scrollTop; + let minMenuY = -menu.clientHeight - 50; + // When the script loads, the page can be at any scroll (e.g. if you refresh it). menu.style.top = scrollTop + 'px'; // Same as parseInt(menu.style.top.slice(0, -2), but faster - var topCache = menu.style.top.slice(0, -2); + let topCache = menu.style.top.slice(0, -2); menu.classList.remove('sticky'); - var stickyCache = false; // Same as menu.classList.contains('sticky'), but faster + let stickyCache = false; // Same as menu.classList.contains('sticky'), but faster document.addEventListener('scroll', function () { scrollTop = Math.max(document.scrollingElement.scrollTop, 0); // `null` means that it doesn't need to be updated - var nextSticky = null; - var nextTop = null; - var scrollDown = scrollTop > prevScrollTop; - var menuPosAbsoluteY = topCache - scrollTop; + let nextSticky = null; + let nextTop = null; + let scrollDown = scrollTop > prevScrollTop; + let menuPosAbsoluteY = topCache - scrollTop; if (scrollDown) { nextSticky = false; if (menuPosAbsoluteY > 0) { @@ -634,14 +625,16 @@ function get_playground_text(playground, hidden = true) { prevScrollTop = scrollTop; }, { passive: true }); })(); + (function controllBorder() { menu.classList.remove('bordered'); - document.addEventListener('scroll', function () { - if (menu.offsetTop === 0) { + + document.addEventListener('scroll', () => { + if (menu.offsetTop === 0) menu.classList.remove('bordered'); - } else { + else menu.classList.add('bordered'); - } }, { passive: true }); })(); + })(); From 84eb646be157c70d5b1f862dafc473ac2841e698 Mon Sep 17 00:00:00 2001 From: "W2.Wizard" <63303990+W2Wizard@users.noreply.github.com> Date: Tue, 17 Jan 2023 16:29:40 +0100 Subject: [PATCH 08/14] Hidden lines are back --- README.md | 3 +- code-executor/README.md | 2 ++ {c-playground => code-executor}/package.json | 0 {c-playground => code-executor}/webserv.js | 0 src/config.rs | 10 ++++-- src/renderer/html_handlebars/hbs_renderer.rs | 15 +++++---- src/theme/book.js | 34 ++++++++++++++++++++ 7 files changed, 52 insertions(+), 12 deletions(-) create mode 100644 code-executor/README.md rename {c-playground => code-executor}/package.json (100%) rename {c-playground => code-executor}/webserv.js (100%) diff --git a/README.md b/README.md index 95d2128072..f97dbfb20c 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ copy-js = true # includes the JavaScript for the line-numbers = true # displays line numbers for editable code runnable = true # displays a run button for rust code endpoint = "http://localhost:4242/playground/" # send the code to this url for execution +hidden-str = "#" # since different languange use certain chars ``` A clients incoming request looks as follows: @@ -35,8 +36,6 @@ A clients incoming request looks as follows: } ``` -> **Note**: The hidden code lines feature has been entirely ripped out. Because I deemed it useless. - [See supported languanges](/guide/src/format/theme/syntax-highlighting.md) for syntax highlighting. As well as the `lang` options for incoming client requests. diff --git a/code-executor/README.md b/code-executor/README.md new file mode 100644 index 0000000000..bf989648c0 --- /dev/null +++ b/code-executor/README.md @@ -0,0 +1,2 @@ +# Code executor + diff --git a/c-playground/package.json b/code-executor/package.json similarity index 100% rename from c-playground/package.json rename to code-executor/package.json diff --git a/c-playground/webserv.js b/code-executor/webserv.js similarity index 100% rename from c-playground/webserv.js rename to code-executor/webserv.js diff --git a/src/config.rs b/src/config.rs index d815a4b518..f397bf810e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -629,7 +629,9 @@ pub struct Playground { /// Display the run button. Default: `true` pub runnable: bool, /// Submits the code to the following endpoint for running. - pub endpoint: String + pub endpoint: String, + /// The string used to mark lines as hidden. + pub hidden_str: String } impl Default for Playground { @@ -640,7 +642,8 @@ impl Default for Playground { copy_js: true, line_numbers: false, runnable: true, - endpoint: "https://play.rust-lang.org/evaluate.json".to_string() + endpoint: "https://play.rust-lang.org/evaluate.json".to_string(), + hidden_str: "!!".to_string() } } } @@ -786,7 +789,8 @@ mod tests { copy_js: true, line_numbers: false, runnable: true, - endpoint: "".to_string() + endpoint: "".to_string(), + hidden_str: "".to_string() }; let html_should_be = HtmlConfig { curly_quotes: true, diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 518bcf2d6c..efd91be2db 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -16,7 +16,7 @@ use crate::utils::fs::get_404_output_file; use handlebars::Handlebars; use log::{debug, trace, warn}; use once_cell::sync::Lazy; -use regex::{Captures, Regex}; +use regex::{Captures, Regex, escape}; use serde_json::json; #[derive(Default)] @@ -856,11 +856,11 @@ fn add_playground_pre(html: &str, playground_config: &Playground) -> String { } else { format!("{}", code).into() }; - hide_lines(&content) + hide_lines(&content, &playground_config.hidden_str) } ) } else { - format!("{}", classes, hide_lines(code)) + format!("{}", classes, hide_lines(code, &playground_config.hidden_str)) } } else { text.to_owned() @@ -869,16 +869,17 @@ fn add_playground_pre(html: &str, playground_config: &Playground) -> String { .into_owned() } -fn hide_lines(content: &str) -> String { - static BORING_LINES_REGEX: Lazy = Lazy::new(|| Regex::new(r"^(\s*)#(.?)(.*)$").unwrap()); +fn hide_lines(content: &str, hidden: &str) -> String { + let hidden_regex = format!(r"^(\s*){}(.?)(.*)$", escape(hidden)); + let boring_lines_regex: Regex = Regex::new(&hidden_regex).unwrap(); let mut result = String::with_capacity(content.len()); let mut lines = content.lines().peekable(); while let Some(line) = lines.next() { // Don't include newline on the last line. let newline = if lines.peek().is_none() { "" } else { "\n" }; - if let Some(caps) = BORING_LINES_REGEX.captures(line) { - if &caps[2] == "#" { + if let Some(caps) = boring_lines_regex.captures(line) { + if &caps[2] == hidden { result += &caps[1]; result += &caps[2]; result += &caps[3]; diff --git a/src/theme/book.js b/src/theme/book.js index c4a4c32140..c8144cf0bd 100644 --- a/src/theme/book.js +++ b/src/theme/book.js @@ -70,6 +70,40 @@ function get_playground_text(playground, hidden = true) { // Adding the hljs class gives code blocks the color css even if highlighting doesn't apply code_nodes.forEach(function (block) { block.classList.add('hljs'); }); + Array.from(document.querySelectorAll("code")).forEach((block) => { + const lines = Array.from(block.querySelectorAll('.boring')); + + // If no lines were hidden, return + if (!lines.length) { return; } + block.classList.add("hide-boring"); + + var buttons = document.createElement('div'); + buttons.className = 'buttons'; + buttons.innerHTML = ""; + + // add expand button + var pre_block = block.parentNode; + pre_block.insertBefore(buttons, pre_block.firstChild); + + pre_block.querySelector('.buttons').addEventListener('click', function (e) { + if (e.target.classList.contains('fa-eye')) { + e.target.classList.remove('fa-eye'); + e.target.classList.add('fa-eye-slash'); + e.target.title = 'Hide lines'; + e.target.setAttribute('aria-label', e.target.title); + + block.classList.remove('hide-boring'); + } else if (e.target.classList.contains('fa-eye-slash')) { + e.target.classList.remove('fa-eye-slash'); + e.target.classList.add('fa-eye'); + e.target.title = 'Show hidden lines'; + e.target.setAttribute('aria-label', e.target.title); + + block.classList.add('hide-boring'); + } + }); + }); + if (window.playground_copyable) { Array.from(document.querySelectorAll('pre code')).forEach(function (block) { let pre_block = block.parentNode; From a04b514ebcef865db2364321238bfe62aa96a29e Mon Sep 17 00:00:00 2001 From: "W2.Wizard" <63303990+W2Wizard@users.noreply.github.com> Date: Tue, 17 Jan 2023 18:09:14 +0100 Subject: [PATCH 09/14] Start on new playground backend --- .gitignore | 3 +- code-executor/README.md | 1 + code-executor/package.json | 8 ++- code-executor/src/app.ts | 62 ++++++++++++++++++++++++ code-executor/src/executor.ts | 52 ++++++++++++++++++++ code-executor/src/modules/module.base.ts | 37 ++++++++++++++ code-executor/src/modules/module.c.ts | 32 ++++++++++++ code-executor/src/modules/module.cpp.ts | 23 +++++++++ code-executor/tsconfig.json | 30 ++++++++++++ code-executor/webserv.js | 36 -------------- 10 files changed, 246 insertions(+), 38 deletions(-) create mode 100644 code-executor/src/app.ts create mode 100644 code-executor/src/executor.ts create mode 100644 code-executor/src/modules/module.base.ts create mode 100644 code-executor/src/modules/module.c.ts create mode 100644 code-executor/src/modules/module.cpp.ts create mode 100644 code-executor/tsconfig.json delete mode 100644 code-executor/webserv.js diff --git a/.gitignore b/.gitignore index e3ecdaa32c..7a278c29c7 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,5 @@ test_book/book/ *~ node_modules -*-lock.json \ No newline at end of file +*-lock.json +build \ No newline at end of file diff --git a/code-executor/README.md b/code-executor/README.md index bf989648c0..97670c8f64 100644 --- a/code-executor/README.md +++ b/code-executor/README.md @@ -1,2 +1,3 @@ # Code executor +TODO: Re-write this in RUST because yes, for now typescript is faster to implement. \ No newline at end of file diff --git a/code-executor/package.json b/code-executor/package.json index 593c61dcbc..7be0a10a29 100644 --- a/code-executor/package.json +++ b/code-executor/package.json @@ -4,12 +4,18 @@ "description": "A simple server that takes a post request and executes the C code locally.", "main": "webserv.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "start": "node build/app.js", + "build": "./node_modules/typescript/bin/tsc" }, "author": "W2Wizard", "license": "MIT", "dependencies": { "cors": "^2.8.5", "express": "^4.18.2" + }, + "devDependencies": { + "@types/express": "^4.17.15", + "@types/node-fetch": "^2.6.2", + "typescript": "^4.9.4" } } diff --git a/code-executor/src/app.ts b/code-executor/src/app.ts new file mode 100644 index 0000000000..de78869dd0 --- /dev/null +++ b/code-executor/src/app.ts @@ -0,0 +1,62 @@ +// ----------------------------------------------------------------------------- +// Codam Coding College, Amsterdam @ 2023. +// See README in the root project for more information. +// ----------------------------------------------------------------------------- + +import cors from "cors"; +import express from "express"; +import { Request, Response, NextFunction } from "express"; +import { Execution } from "./executor"; + +// Globals +/*============================================================================*/ + +export const webserv = express(); +export const port = 4242; + +// Middleware +/*============================================================================*/ + +webserv.use(cors()); +webserv.use(express.json()); +webserv.use(express.urlencoded({ extended: true })); +webserv.use((err: any, req: Request, res: Response, next: NextFunction) => { + if (err.statusCode === 400 && "body" in err) + res.status(400).send({ status: 400, message: err.message }); +}); + +// Routes +/*============================================================================*/ + +webserv.post('/playground/', (req, res) => { + const code = req.body.code; + const flags = req.body.flags; + const languange = req.body.language; + + // Check request + if(!req.is("application/json")) + return res.status(400).json({ result: null, error: "Incorrect content type!" }); + if (code == null || languange == null || flags == null) + return res.status(400).json({ result: null, error: "Malformed body" }); + + // TODO: Check from which domain the request came from. + // TODO: Probs add a few more checks here for unwanted requests. + + // Find module + const module = Execution.modules[languange]; + if (module == undefined) + return res.status(404).json({ result: null, error: "Unsupported Language!" }); + + Execution.run(module, code, flags, res); + + console.log(`[Playground] [${languange}] body:`, code); + return res.json({ result: "Request received!\n", error: null }); +}); + + +// Entry point +/*============================================================================*/ + +webserv.listen(port, () => { + console.log(`[Playground] Running on: ${port}`); +}); diff --git a/code-executor/src/executor.ts b/code-executor/src/executor.ts new file mode 100644 index 0000000000..de4c23040c --- /dev/null +++ b/code-executor/src/executor.ts @@ -0,0 +1,52 @@ +// ----------------------------------------------------------------------------- +// Codam Coding College, Amsterdam @ 2023. +// See README in the root project for more information. +// ----------------------------------------------------------------------------- + +import { Response } from "express"; +import CExecutor from "./modules/module.c"; +import CPPExecutor from "./modules/module.cpp"; +import ExecutionModule from "./modules/module.base"; + +/*============================================================================*/ + +export namespace Execution { + export type ModuleEntry = { + executor: typeof ExecutionModule; + extensions: string; + } + + /** Map to associate languange with the correct executionModule */ + export const modules: { [name: string]: ModuleEntry } = { + "c": { + executor: CExecutor, + extensions: ".c" + }, + "cpp": { + executor: CPPExecutor, + extensions: ".cpp", + } + }; + + /** + * Spawns a child process for the given module and executes the code. + * @param module The specified module to run + */ + export function run(module: ModuleEntry, code: string, flags: string, response: Response) { + try { + const executor = new module.executor(code, flags); + + executor.execute((err, stderr, stdout) => { + if (err) + response.status(500).json({ result: null, error: err }); + else if (stderr != "") + response.status(204).json({ result: stderr, error: null }); + else + response.status(204).json({ result: stdout, error: null }); + }); + } catch (error) { + return response.status(500).json({ result: null, error: error }); + } + return; + } +} diff --git a/code-executor/src/modules/module.base.ts b/code-executor/src/modules/module.base.ts new file mode 100644 index 0000000000..9c09333053 --- /dev/null +++ b/code-executor/src/modules/module.base.ts @@ -0,0 +1,37 @@ +// ----------------------------------------------------------------------------- +// Codam Coding College, Amsterdam @ 2023. +// See README in the root project for more information. +// ----------------------------------------------------------------------------- + +import Shell from "child_process" + +/*============================================================================*/ + +/** + * An execution module describes the way a language should be compiled and run. + * + * For example in C you need to compile the language and then run the out file. + */ + class ExecutionModule { + protected code: string; + protected flags: string; + + /** + * Creates a new execution module. + * @param code The code to execute. + * @param flags Additional compiler flags + */ + constructor(code: string, flags: string) { + this.code = code; + this.flags = flags; + } + + /** + * Spawn a child process and + */ + execute(cb: (err: Shell.ExecException, stderr: string, stdout: string) => void): void { + cb(new Error("Invalid module"), "", ""); + } +} + +export default ExecutionModule; \ No newline at end of file diff --git a/code-executor/src/modules/module.c.ts b/code-executor/src/modules/module.c.ts new file mode 100644 index 0000000000..6b870081c5 --- /dev/null +++ b/code-executor/src/modules/module.c.ts @@ -0,0 +1,32 @@ +// ----------------------------------------------------------------------------- +// Codam Coding College, Amsterdam @ 2023. +// See README in the root project for more information. +// ----------------------------------------------------------------------------- + +import Shell from "child_process" +import ExecutionModule from "./module.base"; + +/*============================================================================*/ + +class CExecutor extends ExecutionModule { + constructor(code: string, flags: string) { + super(code, flags) + } + + /** + * Compiles and executes the code + */ + public execute(cb: (err, stderr, stdout) => void): void { + + // Create file with code in it. + // ... + + // Compile it + Shell.exec(`gcc ${this.flags} -o`, { timeout: 10000 }, (err, stdout: string, stderr: string) => cb(err, stderr, stdout)); + + // Run it + Shell.execFile(``, { timeout: 10000 }, (err, stdout, stderr) => cb(err, stderr, stdout)); + } +} + +export default CExecutor; \ No newline at end of file diff --git a/code-executor/src/modules/module.cpp.ts b/code-executor/src/modules/module.cpp.ts new file mode 100644 index 0000000000..a259644fd8 --- /dev/null +++ b/code-executor/src/modules/module.cpp.ts @@ -0,0 +1,23 @@ +// ----------------------------------------------------------------------------- +// Codam Coding College, Amsterdam @ 2023. +// See README in the root project for more information. +// ----------------------------------------------------------------------------- + +import ExecutionModule from "./module.base"; + +/*============================================================================*/ + +class CPPExecutor extends ExecutionModule { + constructor(code: string, flags: string) { + super(code, flags) + } + + /** + * Executes the code + */ + public execute(cb: (err, stderr, stdout) => void): void { + + } +} + +export default CPPExecutor; \ No newline at end of file diff --git a/code-executor/tsconfig.json b/code-executor/tsconfig.json new file mode 100644 index 0000000000..f90fcbe8e5 --- /dev/null +++ b/code-executor/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "commonjs", + "allowJs": false, + "outDir": "./build/", + "rootDir": "./src/", + "removeComments": true, + "downlevelIteration": false, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noPropertyAccessFromIndexSignature": true, + "esModuleInterop": true, + "preserveSymlinks": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "exclude": [ + "node_modules/", + ], + "include": ["src/"] +} \ No newline at end of file diff --git a/code-executor/webserv.js b/code-executor/webserv.js deleted file mode 100644 index 9d60ff45b8..0000000000 --- a/code-executor/webserv.js +++ /dev/null @@ -1,36 +0,0 @@ -// This is merely a dummy backend to receive the incoming requests. - -const express = require("express"); -const cors = require('cors'); -const app = express(); -const port = 4242; - -// Middleware -/*============================================================================*/ - -app.use(cors()); -app.use(express.json()); -app.use(express.urlencoded({ extended: true })); -app.use((err, _, res, next) => { - if (err.statusCode === 400 && "body" in err) - res.status(400).send({ status: 400, message: err.message }); - next(); -}); - -// Routes -/*============================================================================*/ - -app.post('/playground/', (req, res) => { - const body = req.body; - - - console.log(`[Playground] [${body.lang}] body:`, body.code); - res.json({ result: "Request received!\n", error: null }); -}); - -// Entry point -/*============================================================================*/ - -app.listen(port, () => { - console.log(`[Playground] Running on: ${port}`); -}); \ No newline at end of file From 58fc88f67203debfb5db64b708c32a98f563cc65 Mon Sep 17 00:00:00 2001 From: "W2.Wizard" <63303990+W2Wizard@users.noreply.github.com> Date: Tue, 17 Jan 2023 19:52:00 +0100 Subject: [PATCH 10/14] Refactor --- code-executor/package.json | 6 ++- code-executor/src/app.ts | 4 ++ code-executor/src/config.json | 9 +++++ code-executor/src/executor.ts | 48 ++++++++++++------------ code-executor/src/modules/module.base.ts | 9 +++-- code-executor/src/modules/module.c.ts | 17 +++++---- code-executor/src/modules/module.cpp.ts | 6 +-- code-executor/tsconfig.json | 3 +- 8 files changed, 59 insertions(+), 43 deletions(-) create mode 100644 code-executor/src/config.json diff --git a/code-executor/package.json b/code-executor/package.json index 7be0a10a29..44ce67b3d1 100644 --- a/code-executor/package.json +++ b/code-executor/package.json @@ -4,14 +4,16 @@ "description": "A simple server that takes a post request and executes the C code locally.", "main": "webserv.js", "scripts": { - "start": "node build/app.js", + "start": "node build/app.js", "build": "./node_modules/typescript/bin/tsc" }, "author": "W2Wizard", "license": "MIT", "dependencies": { + "@types/tmp": "^0.2.3", "cors": "^2.8.5", - "express": "^4.18.2" + "express": "^4.18.2", + "tmp": "^0.2.1" }, "devDependencies": { "@types/express": "^4.17.15", diff --git a/code-executor/src/app.ts b/code-executor/src/app.ts index de78869dd0..1d06279058 100644 --- a/code-executor/src/app.ts +++ b/code-executor/src/app.ts @@ -7,6 +7,7 @@ import cors from "cors"; import express from "express"; import { Request, Response, NextFunction } from "express"; import { Execution } from "./executor"; +// import config from "./config.json"; // Globals /*============================================================================*/ @@ -38,6 +39,9 @@ webserv.post('/playground/', (req, res) => { return res.status(400).json({ result: null, error: "Incorrect content type!" }); if (code == null || languange == null || flags == null) return res.status(400).json({ result: null, error: "Malformed body" }); + // TODO: Get from config. + if (req.headers.origin && !req.headers.origin.includes("codam.nl")) + return res.status(403).json({ result: null, error: "Non-valid origin" }); // TODO: Check from which domain the request came from. // TODO: Probs add a few more checks here for unwanted requests. diff --git a/code-executor/src/config.json b/code-executor/src/config.json new file mode 100644 index 0000000000..e4ad7023ec --- /dev/null +++ b/code-executor/src/config.json @@ -0,0 +1,9 @@ +{ + "port": 4242, + "timeout": 5, + "tmpDir": "/tmp/executor", + "validOrigins": [ + "*.42.fr", + "*.codam.nl" + ] +} \ No newline at end of file diff --git a/code-executor/src/executor.ts b/code-executor/src/executor.ts index de4c23040c..0bd818fc46 100644 --- a/code-executor/src/executor.ts +++ b/code-executor/src/executor.ts @@ -3,46 +3,46 @@ // See README in the root project for more information. // ----------------------------------------------------------------------------- +import FileSystem from "fs"; import { Response } from "express"; +import config from "./config.json"; import CExecutor from "./modules/module.c"; import CPPExecutor from "./modules/module.cpp"; import ExecutionModule from "./modules/module.base"; +import tmp from "tmp"; /*============================================================================*/ export namespace Execution { - export type ModuleEntry = { - executor: typeof ExecutionModule; - extensions: string; - } - /** Map to associate languange with the correct executionModule */ - export const modules: { [name: string]: ModuleEntry } = { - "c": { - executor: CExecutor, - extensions: ".c" - }, - "cpp": { - executor: CPPExecutor, - extensions: ".cpp", - } + export const modules: { [name: string]: typeof ExecutionModule } = { + "c": CExecutor, + "cpp": CPPExecutor }; /** * Spawns a child process for the given module and executes the code. * @param module The specified module to run */ - export function run(module: ModuleEntry, code: string, flags: string, response: Response) { + export function run(moduleType: typeof ExecutionModule, code: string, flags: string, response: Response) { try { - const executor = new module.executor(code, flags); - - executor.execute((err, stderr, stdout) => { - if (err) - response.status(500).json({ result: null, error: err }); - else if (stderr != "") - response.status(204).json({ result: stderr, error: null }); - else - response.status(204).json({ result: stdout, error: null }); + const module = new moduleType(code, flags); + + // Create temp file + tmp.file({ dir: config.tmpDir, postfix: module.extension }, (err, path) => { + // Write code into file + FileSystem.writeFile(path, code, (err) => { + if (err != null) throw err; + }); + + if (err != null) throw err; + + // Execute it. + module.execute(path, (err, stderr, stdout) => { + if (err) throw new Error(err.message); + + response.status(204).json({ result: stderr != "" ? stderr : stdout, error: null }); + }); }); } catch (error) { return response.status(500).json({ result: null, error: error }); diff --git a/code-executor/src/modules/module.base.ts b/code-executor/src/modules/module.base.ts index 9c09333053..8ca159892d 100644 --- a/code-executor/src/modules/module.base.ts +++ b/code-executor/src/modules/module.base.ts @@ -11,8 +11,11 @@ import Shell from "child_process" * An execution module describes the way a language should be compiled and run. * * For example in C you need to compile the language and then run the out file. + * TODO: Turn abstract, note lots of bs ahead. */ class ExecutionModule { + public extension: string = ""; + protected code: string; protected flags: string; @@ -26,10 +29,8 @@ import Shell from "child_process" this.flags = flags; } - /** - * Spawn a child process and - */ - execute(cb: (err: Shell.ExecException, stderr: string, stdout: string) => void): void { + /** Spawn a child process and */ + public execute(file: string, cb: (err: Shell.ExecException | null, stderr: string, stdout: string) => void): void { cb(new Error("Invalid module"), "", ""); } } diff --git a/code-executor/src/modules/module.c.ts b/code-executor/src/modules/module.c.ts index 6b870081c5..ef03a8fd66 100644 --- a/code-executor/src/modules/module.c.ts +++ b/code-executor/src/modules/module.c.ts @@ -3,7 +3,8 @@ // See README in the root project for more information. // ----------------------------------------------------------------------------- -import Shell from "child_process" +import Path from "path"; +import Shell from "child_process"; import ExecutionModule from "./module.base"; /*============================================================================*/ @@ -11,21 +12,21 @@ import ExecutionModule from "./module.base"; class CExecutor extends ExecutionModule { constructor(code: string, flags: string) { super(code, flags) + this.extension = ".c"; } /** * Compiles and executes the code */ - public execute(cb: (err, stderr, stdout) => void): void { - - // Create file with code in it. - // ... + public execute(file: string, cb: (err: Shell.ExecException | null, stderr: string, stdout: string) => void): void { // Compile it - Shell.exec(`gcc ${this.flags} -o`, { timeout: 10000 }, (err, stdout: string, stderr: string) => cb(err, stderr, stdout)); + Shell.exec(`gcc ${this.flags} ${file} -o output.out`, { + timeout: 10000 + }, (err, stdout: string, stderr: string) => cb(err, stderr, stdout)); - // Run it - Shell.execFile(``, { timeout: 10000 }, (err, stdout, stderr) => cb(err, stderr, stdout)); + // Execute it. + Shell.execFile("output.out", { timeout: 10000 }, (err, stdout, stderr) => cb(err, stderr, stdout)); } } diff --git a/code-executor/src/modules/module.cpp.ts b/code-executor/src/modules/module.cpp.ts index a259644fd8..8bc3cc7043 100644 --- a/code-executor/src/modules/module.cpp.ts +++ b/code-executor/src/modules/module.cpp.ts @@ -3,6 +3,7 @@ // See README in the root project for more information. // ----------------------------------------------------------------------------- +import Shell from "child_process" import ExecutionModule from "./module.base"; /*============================================================================*/ @@ -12,10 +13,7 @@ class CPPExecutor extends ExecutionModule { super(code, flags) } - /** - * Executes the code - */ - public execute(cb: (err, stderr, stdout) => void): void { + public execute(file: string, cb: (err: Shell.ExecException | null, stderr: string, stdout: string) => void): void { } } diff --git a/code-executor/tsconfig.json b/code-executor/tsconfig.json index f90fcbe8e5..d932f350b0 100644 --- a/code-executor/tsconfig.json +++ b/code-executor/tsconfig.json @@ -21,7 +21,8 @@ "esModuleInterop": true, "preserveSymlinks": true, "skipLibCheck": true, - "forceConsistentCasingInFileNames": true + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, }, "exclude": [ "node_modules/", From 635750e6a358ab0cf1fd68395506c154d47fe534 Mon Sep 17 00:00:00 2001 From: "W2.Wizard" <63303990+W2Wizard@users.noreply.github.com> Date: Wed, 18 Jan 2023 14:49:17 +0100 Subject: [PATCH 11/14] Fix the C playground executor --- code-executor/src/app.ts | 28 +++++++++---- code-executor/src/config.json | 2 +- code-executor/src/executor.ts | 51 ++++++++++-------------- code-executor/src/modules/module.base.ts | 33 ++------------- code-executor/src/modules/module.c.ts | 39 +++++++++--------- code-executor/src/modules/module.cpp.ts | 21 ---------- 6 files changed, 65 insertions(+), 109 deletions(-) delete mode 100644 code-executor/src/modules/module.cpp.ts diff --git a/code-executor/src/app.ts b/code-executor/src/app.ts index 1d06279058..f24af86a35 100644 --- a/code-executor/src/app.ts +++ b/code-executor/src/app.ts @@ -3,6 +3,9 @@ // See README in the root project for more information. // ----------------------------------------------------------------------------- +// import tmp from "tmp"; +// import Path from "path"; +// import crypto from "crypto" import cors from "cors"; import express from "express"; import { Request, Response, NextFunction } from "express"; @@ -29,32 +32,41 @@ webserv.use((err: any, req: Request, res: Response, next: NextFunction) => { // Routes /*============================================================================*/ -webserv.post('/playground/', (req, res) => { +webserv.post('/playground/', async (req, res) => { const code = req.body.code; const flags = req.body.flags; - const languange = req.body.language; + const language = req.body.language; // Check request if(!req.is("application/json")) return res.status(400).json({ result: null, error: "Incorrect content type!" }); - if (code == null || languange == null || flags == null) + if (code == null || language == null || flags == null) return res.status(400).json({ result: null, error: "Malformed body" }); + // TODO: Get from config. + // TODO: Check from which domain the request came from. if (req.headers.origin && !req.headers.origin.includes("codam.nl")) return res.status(403).json({ result: null, error: "Non-valid origin" }); - // TODO: Check from which domain the request came from. // TODO: Probs add a few more checks here for unwanted requests. // Find module - const module = Execution.modules[languange]; + const module = Execution.modules[language]; if (module == undefined) return res.status(404).json({ result: null, error: "Unsupported Language!" }); - Execution.run(module, code, flags, res); + console.log(`[Playground] [${language}] body:`, code); - console.log(`[Playground] [${languange}] body:`, code); - return res.json({ result: "Request received!\n", error: null }); + try { + const out = await Execution.run(module, code, flags); + res.status(201).json({ + result: out.stderr != "" ? out.stderr : out.stdout, + error: null + }); + } catch (error) { + res.status(500).json({ result: null, error: error }).end(); + } + return; }); diff --git a/code-executor/src/config.json b/code-executor/src/config.json index e4ad7023ec..b013364c30 100644 --- a/code-executor/src/config.json +++ b/code-executor/src/config.json @@ -1,7 +1,7 @@ { "port": 4242, "timeout": 5, - "tmpDir": "/tmp/executor", + "tmpDir": "./tmp", "validOrigins": [ "*.42.fr", "*.codam.nl" diff --git a/code-executor/src/executor.ts b/code-executor/src/executor.ts index 0bd818fc46..c16d1675b3 100644 --- a/code-executor/src/executor.ts +++ b/code-executor/src/executor.ts @@ -3,50 +3,41 @@ // See README in the root project for more information. // ----------------------------------------------------------------------------- -import FileSystem from "fs"; -import { Response } from "express"; -import config from "./config.json"; -import CExecutor from "./modules/module.c"; -import CPPExecutor from "./modules/module.cpp"; -import ExecutionModule from "./modules/module.base"; +import fs from "fs"; import tmp from "tmp"; +import crypto from "crypto" +import { Modules } from "./modules/module.base"; +import { ExecuteC } from "./modules/module.c"; /*============================================================================*/ export namespace Execution { /** Map to associate languange with the correct executionModule */ - export const modules: { [name: string]: typeof ExecutionModule } = { - "c": CExecutor, - "cpp": CPPExecutor + export const modules: { [name: string]: Modules.Function } = { + "c": ExecuteC, }; /** * Spawns a child process for the given module and executes the code. * @param module The specified module to run */ - export function run(moduleType: typeof ExecutionModule, code: string, flags: string, response: Response) { - try { - const module = new moduleType(code, flags); - - // Create temp file - tmp.file({ dir: config.tmpDir, postfix: module.extension }, (err, path) => { - // Write code into file - FileSystem.writeFile(path, code, (err) => { - if (err != null) throw err; - }); + export async function run(module: Modules.Function, code: string, flags: string): Promise<{ stdout: string; stderr: string; }> { + console.log("Running ..."); + const instanceID = crypto.randomBytes(5).toString('hex'); + return new Promise<{ stdout: string, stderr: string }>((resolve, reject) => { + tmp.file({ prefix: instanceID, postfix: ".c" }, async (err, path) => { if (err != null) throw err; - - // Execute it. - module.execute(path, (err, stderr, stdout) => { - if (err) throw new Error(err.message); - - response.status(204).json({ result: stderr != "" ? stderr : stdout, error: null }); - }); + + // Write source code into tmp file. + console.log("Writing to file:", path); + fs.writeFileSync(path, code); + + const [data, error] = await module(path, flags); + console.log("Done!", data); + if (error) return reject(error); + return resolve(data!); }); - } catch (error) { - return response.status(500).json({ result: null, error: error }); - } - return; + }); } } diff --git a/code-executor/src/modules/module.base.ts b/code-executor/src/modules/module.base.ts index 8ca159892d..08b0f2932b 100644 --- a/code-executor/src/modules/module.base.ts +++ b/code-executor/src/modules/module.base.ts @@ -3,36 +3,9 @@ // See README in the root project for more information. // ----------------------------------------------------------------------------- -import Shell from "child_process" - /*============================================================================*/ -/** - * An execution module describes the way a language should be compiled and run. - * - * For example in C you need to compile the language and then run the out file. - * TODO: Turn abstract, note lots of bs ahead. - */ - class ExecutionModule { - public extension: string = ""; - - protected code: string; - protected flags: string; - - /** - * Creates a new execution module. - * @param code The code to execute. - * @param flags Additional compiler flags - */ - constructor(code: string, flags: string) { - this.code = code; - this.flags = flags; - } - - /** Spawn a child process and */ - public execute(file: string, cb: (err: Shell.ExecException | null, stderr: string, stdout: string) => void): void { - cb(new Error("Invalid module"), "", ""); - } +export namespace Modules { + export type Function = (file: string, flags: string) => ReturnType; + export type ReturnType = Promise<[{ stdout: string, stderr: string } | null, any | null]>; } - -export default ExecutionModule; \ No newline at end of file diff --git a/code-executor/src/modules/module.c.ts b/code-executor/src/modules/module.c.ts index ef03a8fd66..4cff90a143 100644 --- a/code-executor/src/modules/module.c.ts +++ b/code-executor/src/modules/module.c.ts @@ -3,31 +3,32 @@ // See README in the root project for more information. // ----------------------------------------------------------------------------- +import fs from "fs"; import Path from "path"; import Shell from "child_process"; -import ExecutionModule from "./module.base"; +import { Modules } from "./module.base" /*============================================================================*/ -class CExecutor extends ExecutionModule { - constructor(code: string, flags: string) { - super(code, flags) - this.extension = ".c"; - } - - /** - * Compiles and executes the code - */ - public execute(file: string, cb: (err: Shell.ExecException | null, stderr: string, stdout: string) => void): void { +export async function ExecuteC(file: string, flags: string): Modules.ReturnType { + // Compile, execute and remove binary + const binary = Path.join(Path.dirname(file), Path.parse(file).name); + const execution = new Promise<{ stdout: string, stderr: string }>((resolve, reject) => { + Shell.execSync(`gcc ${flags} ${file} -o ${binary}`, { timeout: 10000 }); + Shell.execFile(binary, { timeout: 10000 }, (err, stdout, stderr) => { + if (err) return reject(err); - // Compile it - Shell.exec(`gcc ${this.flags} ${file} -o output.out`, { - timeout: 10000 - }, (err, stdout: string, stderr: string) => cb(err, stderr, stdout)); + fs.rmSync(binary, { force: true, recursive: true }); + return resolve({ stdout: stdout, stderr: stderr }); + }); + }); - // Execute it. - Shell.execFile("output.out", { timeout: 10000 }, (err, stdout, stderr) => cb(err, stderr, stdout)); + try { + const data = await execution; + return [data, null] + } + catch (error) { + console.error(error); + return [null, error]; } } - -export default CExecutor; \ No newline at end of file diff --git a/code-executor/src/modules/module.cpp.ts b/code-executor/src/modules/module.cpp.ts deleted file mode 100644 index 8bc3cc7043..0000000000 --- a/code-executor/src/modules/module.cpp.ts +++ /dev/null @@ -1,21 +0,0 @@ -// ----------------------------------------------------------------------------- -// Codam Coding College, Amsterdam @ 2023. -// See README in the root project for more information. -// ----------------------------------------------------------------------------- - -import Shell from "child_process" -import ExecutionModule from "./module.base"; - -/*============================================================================*/ - -class CPPExecutor extends ExecutionModule { - constructor(code: string, flags: string) { - super(code, flags) - } - - public execute(file: string, cb: (err: Shell.ExecException | null, stderr: string, stdout: string) => void): void { - - } -} - -export default CPPExecutor; \ No newline at end of file From 311f36aa9af34bc57fd59a4b6731a27b134642b2 Mon Sep 17 00:00:00 2001 From: "W2.Wizard" <63303990+W2Wizard@users.noreply.github.com> Date: Wed, 18 Jan 2023 15:51:18 +0100 Subject: [PATCH 12/14] Simplify executor return --- code-executor/src/app.ts | 33 +++++++----------------- code-executor/src/executor.ts | 22 +++++++--------- code-executor/src/modules/module.base.ts | 2 +- code-executor/src/modules/module.c.ts | 10 ++++--- src/theme/book.js | 19 +++++++++----- 5 files changed, 39 insertions(+), 47 deletions(-) diff --git a/code-executor/src/app.ts b/code-executor/src/app.ts index f24af86a35..b3ab355943 100644 --- a/code-executor/src/app.ts +++ b/code-executor/src/app.ts @@ -3,14 +3,11 @@ // See README in the root project for more information. // ----------------------------------------------------------------------------- -// import tmp from "tmp"; -// import Path from "path"; -// import crypto from "crypto" import cors from "cors"; import express from "express"; -import { Request, Response, NextFunction } from "express"; -import { Execution } from "./executor"; // import config from "./config.json"; +import { Execution } from "./executor"; +import { Request, Response, NextFunction } from "express"; // Globals /*============================================================================*/ @@ -32,7 +29,7 @@ webserv.use((err: any, req: Request, res: Response, next: NextFunction) => { // Routes /*============================================================================*/ -webserv.post('/playground/', async (req, res) => { +webserv.post('/playground/', (req, res) => { const code = req.body.code; const flags = req.body.flags; const language = req.body.language; @@ -45,8 +42,8 @@ webserv.post('/playground/', async (req, res) => { // TODO: Get from config. // TODO: Check from which domain the request came from. - if (req.headers.origin && !req.headers.origin.includes("codam.nl")) - return res.status(403).json({ result: null, error: "Non-valid origin" }); + // if (req.headers.origin && !req.headers.origin.includes(".codam.nl")) + // return res.status(403).json({ result: null, error: "Non-valid origin" }); // TODO: Probs add a few more checks here for unwanted requests. @@ -55,24 +52,14 @@ webserv.post('/playground/', async (req, res) => { if (module == undefined) return res.status(404).json({ result: null, error: "Unsupported Language!" }); - console.log(`[Playground] [${language}] body:`, code); - - try { - const out = await Execution.run(module, code, flags); - res.status(201).json({ - result: out.stderr != "" ? out.stderr : out.stdout, - error: null - }); - } catch (error) { - res.status(500).json({ result: null, error: error }).end(); - } - return; + console.log(`[Playground] Request for lang: ${language} using flags: ${flags}`); + return Execution.run(module, code, flags) + .then((output) => res.status(201).json({ result: output, error: null })) + .catch((error) => res.status(422).json({ result: null, error: error.message })); }); // Entry point /*============================================================================*/ -webserv.listen(port, () => { - console.log(`[Playground] Running on: ${port}`); -}); +webserv.listen(port, () => console.log(`[Playground] Running on: ${port}`)); diff --git a/code-executor/src/executor.ts b/code-executor/src/executor.ts index c16d1675b3..88f75469a0 100644 --- a/code-executor/src/executor.ts +++ b/code-executor/src/executor.ts @@ -5,7 +5,7 @@ import fs from "fs"; import tmp from "tmp"; -import crypto from "crypto" +// import crypto from "crypto" import { Modules } from "./modules/module.base"; import { ExecuteC } from "./modules/module.c"; @@ -14,29 +14,25 @@ import { ExecuteC } from "./modules/module.c"; export namespace Execution { /** Map to associate languange with the correct executionModule */ export const modules: { [name: string]: Modules.Function } = { - "c": ExecuteC, + "cpp": ExecuteC, }; /** * Spawns a child process for the given module and executes the code. * @param module The specified module to run */ - export async function run(module: Modules.Function, code: string, flags: string): Promise<{ stdout: string; stderr: string; }> { - console.log("Running ..."); - const instanceID = crypto.randomBytes(5).toString('hex'); - - return new Promise<{ stdout: string, stderr: string }>((resolve, reject) => { - tmp.file({ prefix: instanceID, postfix: ".c" }, async (err, path) => { - if (err != null) throw err; + export async function run(module: Modules.Function, code: string, flags: string): Promise { + return new Promise((resolve, reject) => { + tmp.file({postfix: ".c" }, async (err, path) => { + if (err) return reject(err.message); // Write source code into tmp file. - console.log("Writing to file:", path); fs.writeFileSync(path, code); - const [data, error] = await module(path, flags); - console.log("Done!", data); + // Execute it + const [output, error] = await module(path, flags); if (error) return reject(error); - return resolve(data!); + return resolve(output!); }); }); } diff --git a/code-executor/src/modules/module.base.ts b/code-executor/src/modules/module.base.ts index 08b0f2932b..acc2df154f 100644 --- a/code-executor/src/modules/module.base.ts +++ b/code-executor/src/modules/module.base.ts @@ -7,5 +7,5 @@ export namespace Modules { export type Function = (file: string, flags: string) => ReturnType; - export type ReturnType = Promise<[{ stdout: string, stderr: string } | null, any | null]>; + export type ReturnType = Promise<[string | null, string | null]>; } diff --git a/code-executor/src/modules/module.c.ts b/code-executor/src/modules/module.c.ts index 4cff90a143..4539b6e8cc 100644 --- a/code-executor/src/modules/module.c.ts +++ b/code-executor/src/modules/module.c.ts @@ -13,13 +13,16 @@ import { Modules } from "./module.base" export async function ExecuteC(file: string, flags: string): Modules.ReturnType { // Compile, execute and remove binary const binary = Path.join(Path.dirname(file), Path.parse(file).name); - const execution = new Promise<{ stdout: string, stderr: string }>((resolve, reject) => { + + const execution = new Promise((resolve, reject) => { Shell.execSync(`gcc ${flags} ${file} -o ${binary}`, { timeout: 10000 }); Shell.execFile(binary, { timeout: 10000 }, (err, stdout, stderr) => { + fs.rmSync(binary, { force: true, recursive: true }); + if (err) return reject(err); + if (stderr.length > 0) return reject(stderr); - fs.rmSync(binary, { force: true, recursive: true }); - return resolve({ stdout: stdout, stderr: stderr }); + return resolve(stdout); }); }); @@ -28,7 +31,6 @@ export async function ExecuteC(file: string, flags: string): Modules.ReturnType return [data, null] } catch (error) { - console.error(error); return [null, error]; } } diff --git a/src/theme/book.js b/src/theme/book.js index c8144cf0bd..2f5dbd0d63 100644 --- a/src/theme/book.js +++ b/src/theme/book.js @@ -234,8 +234,10 @@ function get_playground_text(playground, hidden = true) { return; } + // TODO: Fetch MD parameters and pass them here const params = { - lang: lang, + language: lang, + flags: "-Wextra -Werror -Wall", code: get_playground_text(code_block).trim() } @@ -248,13 +250,19 @@ function get_playground_text(playground, hidden = true) { mode: 'cors', body: JSON.stringify(params) }) - .then(response => response.json()) .then(response => { - if (response.result.trim() === '') { + if (response.status >= 500) + throw new Error(response.statusText); + return response.json(); + }) + .then(data => { + if ("error" in data) + throw new Error(data.error); + else if (typeof data.result == "string" && data.result.trim() === '') { result_block.innerText = "No output"; result_block.classList.add("result-no-output"); } else { - result_block.innerText = response.result; + result_block.innerText = data.result; result_block.classList.remove("result-no-output"); } }) @@ -589,8 +597,7 @@ function get_playground_text(playground, hidden = true) { }); Array.from(clipButtons).forEach((clipButton) => - clipButton.addEventListener('mouseout', hideTooltip(e.currentTarget)) - ); + clipButton.addEventListener('mouseout', (e) => hideTooltip(e.currentTarget))); clipboardSnippets.on('success', (e) => { e.clearSelection(); From 447d020bb095aff40a427283b8f66cce92e9ae04 Mon Sep 17 00:00:00 2001 From: "W2.Wizard" <63303990+W2Wizard@users.noreply.github.com> Date: Wed, 18 Jan 2023 16:15:06 +0100 Subject: [PATCH 13/14] Fix shenanigans regarding the module map --- code-executor/src/app.ts | 13 +++++++------ code-executor/src/executor.ts | 11 +++++++---- src/theme/book.js | 4 ++-- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/code-executor/src/app.ts b/code-executor/src/app.ts index b3ab355943..cd48c82dfa 100644 --- a/code-executor/src/app.ts +++ b/code-executor/src/app.ts @@ -30,9 +30,9 @@ webserv.use((err: any, req: Request, res: Response, next: NextFunction) => { /*============================================================================*/ webserv.post('/playground/', (req, res) => { - const code = req.body.code; - const flags = req.body.flags; - const language = req.body.language; + const code = req.body.code as string; + const flags = req.body.flags as string; + const language = req.body.language as string; // Check request if(!req.is("application/json")) @@ -48,12 +48,13 @@ webserv.post('/playground/', (req, res) => { // TODO: Probs add a few more checks here for unwanted requests. // Find module - const module = Execution.modules[language]; + let module = Execution.modules[language]; if (module == undefined) return res.status(404).json({ result: null, error: "Unsupported Language!" }); - console.log(`[Playground] Request for lang: ${language} using flags: ${flags}`); - return Execution.run(module, code, flags) + console.log(`[Playground] Request with: ${language} | Flags: ${flags.length > 0 ? flags : "None"}`); + + return Execution.run(module.executor, code, flags, module.extension) .then((output) => res.status(201).json({ result: output, error: null })) .catch((error) => res.status(422).json({ result: null, error: error.message })); }); diff --git a/code-executor/src/executor.ts b/code-executor/src/executor.ts index 88f75469a0..a33f883324 100644 --- a/code-executor/src/executor.ts +++ b/code-executor/src/executor.ts @@ -13,17 +13,20 @@ import { ExecuteC } from "./modules/module.c"; export namespace Execution { /** Map to associate languange with the correct executionModule */ - export const modules: { [name: string]: Modules.Function } = { - "cpp": ExecuteC, + export const modules: {[name: string]: { executor: Modules.Function, extension: string }} = { + "c": { + executor: ExecuteC, + extension: ".c" + }, }; /** * Spawns a child process for the given module and executes the code. * @param module The specified module to run */ - export async function run(module: Modules.Function, code: string, flags: string): Promise { + export async function run(module: Modules.Function, code: string, flags: string, extension: string): Promise { return new Promise((resolve, reject) => { - tmp.file({postfix: ".c" }, async (err, path) => { + tmp.file({postfix: extension }, async (err, path) => { if (err) return reject(err.message); // Write source code into tmp file. diff --git a/src/theme/book.js b/src/theme/book.js index 2f5dbd0d63..a53a00332c 100644 --- a/src/theme/book.js +++ b/src/theme/book.js @@ -237,7 +237,7 @@ function get_playground_text(playground, hidden = true) { // TODO: Fetch MD parameters and pass them here const params = { language: lang, - flags: "-Wextra -Werror -Wall", + flags: "", code: get_playground_text(code_block).trim() } @@ -256,7 +256,7 @@ function get_playground_text(playground, hidden = true) { return response.json(); }) .then(data => { - if ("error" in data) + if (data.error != null) throw new Error(data.error); else if (typeof data.result == "string" && data.result.trim() === '') { result_block.innerText = "No output"; From d9f2998467d9817393fb1f5e74d5bb6b2aa88c33 Mon Sep 17 00:00:00 2001 From: "W2.Wizard" <63303990+W2Wizard@users.noreply.github.com> Date: Wed, 18 Jan 2023 16:28:59 +0100 Subject: [PATCH 14/14] Remove code playground --- README.md | 20 +------ code-executor/README.md | 3 -- code-executor/package.json | 23 --------- code-executor/src/app.ts | 66 ------------------------ code-executor/src/config.json | 9 ---- code-executor/src/executor.ts | 42 --------------- code-executor/src/modules/module.base.ts | 11 ---- code-executor/src/modules/module.c.ts | 36 ------------- code-executor/tsconfig.json | 31 ----------- 9 files changed, 1 insertion(+), 240 deletions(-) delete mode 100644 code-executor/README.md delete mode 100644 code-executor/package.json delete mode 100644 code-executor/src/app.ts delete mode 100644 code-executor/src/config.json delete mode 100644 code-executor/src/executor.ts delete mode 100644 code-executor/src/modules/module.base.ts delete mode 100644 code-executor/src/modules/module.c.ts delete mode 100644 code-executor/tsconfig.json diff --git a/README.md b/README.md index f97dbfb20c..f9e388b14c 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ May or may not have some features broken due to mdBook being *VERY* hardcoded wi ## Custom playground backend For a custom playground backend you simply need a webserver with a `POST` route open. Call it whatever you like. +Checkout our own custom playground to get started: [Here](https://github.com/codam-coding-college/code-playground) Then in the `.toml` config file of your book set the endpoint: ```toml @@ -28,25 +29,6 @@ endpoint = "http://localhost:4242/playground/" # send the code to this url for e hidden-str = "#" # since different languange use certain chars ``` -A clients incoming request looks as follows: -```json -{ - "lang": "cpp", - "code": "..." -} -``` - -[See supported languanges](/guide/src/format/theme/syntax-highlighting.md) for syntax highlighting. As well as the `lang` options for incoming client requests. - - -A servers outgoing response should look as follows: -```json -{ - "result": "Request received!\n", - "error": null -} -``` - The client will display the appropriate message depending on the server's response. ## License diff --git a/code-executor/README.md b/code-executor/README.md deleted file mode 100644 index 97670c8f64..0000000000 --- a/code-executor/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Code executor - -TODO: Re-write this in RUST because yes, for now typescript is faster to implement. \ No newline at end of file diff --git a/code-executor/package.json b/code-executor/package.json deleted file mode 100644 index 44ce67b3d1..0000000000 --- a/code-executor/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "c-playground", - "version": "1.0.0", - "description": "A simple server that takes a post request and executes the C code locally.", - "main": "webserv.js", - "scripts": { - "start": "node build/app.js", - "build": "./node_modules/typescript/bin/tsc" - }, - "author": "W2Wizard", - "license": "MIT", - "dependencies": { - "@types/tmp": "^0.2.3", - "cors": "^2.8.5", - "express": "^4.18.2", - "tmp": "^0.2.1" - }, - "devDependencies": { - "@types/express": "^4.17.15", - "@types/node-fetch": "^2.6.2", - "typescript": "^4.9.4" - } -} diff --git a/code-executor/src/app.ts b/code-executor/src/app.ts deleted file mode 100644 index cd48c82dfa..0000000000 --- a/code-executor/src/app.ts +++ /dev/null @@ -1,66 +0,0 @@ -// ----------------------------------------------------------------------------- -// Codam Coding College, Amsterdam @ 2023. -// See README in the root project for more information. -// ----------------------------------------------------------------------------- - -import cors from "cors"; -import express from "express"; -// import config from "./config.json"; -import { Execution } from "./executor"; -import { Request, Response, NextFunction } from "express"; - -// Globals -/*============================================================================*/ - -export const webserv = express(); -export const port = 4242; - -// Middleware -/*============================================================================*/ - -webserv.use(cors()); -webserv.use(express.json()); -webserv.use(express.urlencoded({ extended: true })); -webserv.use((err: any, req: Request, res: Response, next: NextFunction) => { - if (err.statusCode === 400 && "body" in err) - res.status(400).send({ status: 400, message: err.message }); -}); - -// Routes -/*============================================================================*/ - -webserv.post('/playground/', (req, res) => { - const code = req.body.code as string; - const flags = req.body.flags as string; - const language = req.body.language as string; - - // Check request - if(!req.is("application/json")) - return res.status(400).json({ result: null, error: "Incorrect content type!" }); - if (code == null || language == null || flags == null) - return res.status(400).json({ result: null, error: "Malformed body" }); - - // TODO: Get from config. - // TODO: Check from which domain the request came from. - // if (req.headers.origin && !req.headers.origin.includes(".codam.nl")) - // return res.status(403).json({ result: null, error: "Non-valid origin" }); - - // TODO: Probs add a few more checks here for unwanted requests. - - // Find module - let module = Execution.modules[language]; - if (module == undefined) - return res.status(404).json({ result: null, error: "Unsupported Language!" }); - - console.log(`[Playground] Request with: ${language} | Flags: ${flags.length > 0 ? flags : "None"}`); - - return Execution.run(module.executor, code, flags, module.extension) - .then((output) => res.status(201).json({ result: output, error: null })) - .catch((error) => res.status(422).json({ result: null, error: error.message })); -}); - - -// Entry point -/*============================================================================*/ - -webserv.listen(port, () => console.log(`[Playground] Running on: ${port}`)); diff --git a/code-executor/src/config.json b/code-executor/src/config.json deleted file mode 100644 index b013364c30..0000000000 --- a/code-executor/src/config.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "port": 4242, - "timeout": 5, - "tmpDir": "./tmp", - "validOrigins": [ - "*.42.fr", - "*.codam.nl" - ] -} \ No newline at end of file diff --git a/code-executor/src/executor.ts b/code-executor/src/executor.ts deleted file mode 100644 index a33f883324..0000000000 --- a/code-executor/src/executor.ts +++ /dev/null @@ -1,42 +0,0 @@ -// ----------------------------------------------------------------------------- -// Codam Coding College, Amsterdam @ 2023. -// See README in the root project for more information. -// ----------------------------------------------------------------------------- - -import fs from "fs"; -import tmp from "tmp"; -// import crypto from "crypto" -import { Modules } from "./modules/module.base"; -import { ExecuteC } from "./modules/module.c"; - -/*============================================================================*/ - -export namespace Execution { - /** Map to associate languange with the correct executionModule */ - export const modules: {[name: string]: { executor: Modules.Function, extension: string }} = { - "c": { - executor: ExecuteC, - extension: ".c" - }, - }; - - /** - * Spawns a child process for the given module and executes the code. - * @param module The specified module to run - */ - export async function run(module: Modules.Function, code: string, flags: string, extension: string): Promise { - return new Promise((resolve, reject) => { - tmp.file({postfix: extension }, async (err, path) => { - if (err) return reject(err.message); - - // Write source code into tmp file. - fs.writeFileSync(path, code); - - // Execute it - const [output, error] = await module(path, flags); - if (error) return reject(error); - return resolve(output!); - }); - }); - } -} diff --git a/code-executor/src/modules/module.base.ts b/code-executor/src/modules/module.base.ts deleted file mode 100644 index acc2df154f..0000000000 --- a/code-executor/src/modules/module.base.ts +++ /dev/null @@ -1,11 +0,0 @@ -// ----------------------------------------------------------------------------- -// Codam Coding College, Amsterdam @ 2023. -// See README in the root project for more information. -// ----------------------------------------------------------------------------- - -/*============================================================================*/ - -export namespace Modules { - export type Function = (file: string, flags: string) => ReturnType; - export type ReturnType = Promise<[string | null, string | null]>; -} diff --git a/code-executor/src/modules/module.c.ts b/code-executor/src/modules/module.c.ts deleted file mode 100644 index 4539b6e8cc..0000000000 --- a/code-executor/src/modules/module.c.ts +++ /dev/null @@ -1,36 +0,0 @@ -// ----------------------------------------------------------------------------- -// Codam Coding College, Amsterdam @ 2023. -// See README in the root project for more information. -// ----------------------------------------------------------------------------- - -import fs from "fs"; -import Path from "path"; -import Shell from "child_process"; -import { Modules } from "./module.base" - -/*============================================================================*/ - -export async function ExecuteC(file: string, flags: string): Modules.ReturnType { - // Compile, execute and remove binary - const binary = Path.join(Path.dirname(file), Path.parse(file).name); - - const execution = new Promise((resolve, reject) => { - Shell.execSync(`gcc ${flags} ${file} -o ${binary}`, { timeout: 10000 }); - Shell.execFile(binary, { timeout: 10000 }, (err, stdout, stderr) => { - fs.rmSync(binary, { force: true, recursive: true }); - - if (err) return reject(err); - if (stderr.length > 0) return reject(stderr); - - return resolve(stdout); - }); - }); - - try { - const data = await execution; - return [data, null] - } - catch (error) { - return [null, error]; - } -} diff --git a/code-executor/tsconfig.json b/code-executor/tsconfig.json deleted file mode 100644 index d932f350b0..0000000000 --- a/code-executor/tsconfig.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "compilerOptions": { - "target": "esnext", - "module": "commonjs", - "allowJs": false, - "outDir": "./build/", - "rootDir": "./src/", - "removeComments": true, - "downlevelIteration": false, - "strictNullChecks": true, - "strictFunctionTypes": true, - "strictBindCallApply": true, - "strictPropertyInitialization": true, - "noImplicitThis": true, - "alwaysStrict": true, - "noUnusedLocals": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noPropertyAccessFromIndexSignature": true, - "esModuleInterop": true, - "preserveSymlinks": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - }, - "exclude": [ - "node_modules/", - ], - "include": ["src/"] -} \ No newline at end of file