diff --git a/.gitignore b/.gitignore index a23c771e89..7a278c29c7 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,7 @@ test_book/book/ # Ignore Vim temporary and swap files. *.sw? *~ + +node_modules +*-lock.json +build \ No newline at end of file diff --git a/README.md b/README.md index b2177cf307..f97dbfb20c 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,48 @@ 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 +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 new file mode 100644 index 0000000000..97670c8f64 --- /dev/null +++ b/code-executor/README.md @@ -0,0 +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 new file mode 100644 index 0000000000..7be0a10a29 --- /dev/null +++ b/code-executor/package.json @@ -0,0 +1,21 @@ +{ + "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": { + "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/src/config.rs b/src/config.rs index 0c367d8481..f397bf810e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -628,6 +628,10 @@ 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, + /// The string used to mark lines as hidden. + pub hidden_str: String } impl Default for Playground { @@ -638,6 +642,8 @@ impl Default for Playground { copy_js: true, line_numbers: false, runnable: true, + endpoint: "https://play.rust-lang.org/evaluate.json".to_string(), + hidden_str: "!!".to_string() } } } @@ -783,6 +789,8 @@ mod tests { copy_js: true, line_numbers: false, runnable: true, + 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 1b648dac10..efd91be2db 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}; @@ -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)] @@ -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,71 +837,49 @@ 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!( - "
{}
", + "
{}
", + playground_config.endpoint, 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) + hide_lines(&content, &playground_config.hidden_str) } ) } else { - format!("{}", classes, hide_lines(code)) + format!("{}", classes, hide_lines(code, &playground_config.hidden_str)) } } else { - // not language-rust, so no-op text.to_owned() } }) .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]; @@ -932,27 +903,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, @@ -960,7 +910,6 @@ struct RenderItemContext<'a> { is_index: bool, book_config: BookConfig, html_config: HtmlConfig, - edition: Option, chapter_titles: &'a HashMap, } @@ -1002,107 +951,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); - } - } } diff --git a/src/theme/book.js b/src/theme/book.js index e303ebb451..c8144cf0bd 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_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,163 +42,37 @@ 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 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")).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"); @@ -215,16 +106,16 @@ function 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); @@ -238,58 +129,147 @@ function 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_rust_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); + }); + + /** + * 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; } + + 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..."; + + 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).trim() + } + + 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', + 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'); - 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']"), @@ -315,7 +295,7 @@ function 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; @@ -355,7 +335,7 @@ function playground_text(playground, hidden = true) { }); } - var previousTheme = get_theme(); + let previousTheme = get_theme(); if (store) { try { localStorage.setItem('mdbook-theme', theme); } catch (e) { } @@ -367,7 +347,7 @@ function playground_text(playground, hidden = true) { } // Set theme - var theme = get_theme(); + let theme = get_theme(); set_theme(theme, false); @@ -380,7 +360,7 @@ function 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") { @@ -440,13 +420,15 @@ function 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') @@ -460,22 +442,20 @@ function 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) { } @@ -484,7 +464,7 @@ function 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'); @@ -509,7 +489,7 @@ function 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 { @@ -527,19 +507,19 @@ function 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) { @@ -553,39 +533,43 @@ function 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' }); } })(); +/*===========================================================================*/ + (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 = ""; @@ -597,58 +581,58 @@ function 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 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) { @@ -675,14 +659,16 @@ function 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 }); })(); + })();