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 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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