Skip to content
This repository was archived by the owner on Mar 28, 2024. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ test_book/book/
# Ignore Vim temporary and swap files.
*.sw?
*~

node_modules
*-lock.json
build
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,30 @@

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.
Checkout our own custom playground to get started: [Here](https://github.com/codam-coding-college/code-playground)

Then in the `.toml` config file of your book set the endpoint:
```toml
[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
```

The client will display the appropriate message depending on the server's response.

## License

Expand Down
8 changes: 8 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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()
}
}
}
Expand Down Expand Up @@ -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,
Expand Down
200 changes: 23 additions & 177 deletions src/renderer/html_handlebars/hbs_renderer.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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)]
Expand Down Expand Up @@ -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());
Expand All @@ -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())?;
}
Expand Down Expand Up @@ -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 ✓");
Expand All @@ -195,11 +194,10 @@ impl HtmlHandlebars {
&self,
rendered: String,
playground_config: &Playground,
edition: Option<RustEdition>,
) -> 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
}
Expand Down Expand Up @@ -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)?;
Expand All @@ -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 ✓");
Expand Down Expand Up @@ -830,11 +826,8 @@ fn fix_code_blocks(html: &str) -> String {
.into_owned()
}

fn add_playground_pre(
html: &str,
playground_config: &Playground,
edition: Option<RustEdition>,
) -> String {
// NOTE: Rust devs cry in agony
fn add_playground_pre(html: &str, playground_config: &Playground) -> String {
static ADD_PLAYGROUND_PRE: Lazy<Regex> =
Lazy::new(|| Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap());

Expand All @@ -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!(
"<pre class=\"playground\"><code class=\"{}{}\">{}</code></pre>",
"<pre data-endpoint={} class=\"playground\"><code class=\"{}\">{}</code></pre>",
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!("<code class=\"{}\">{}</code>", classes, hide_lines(code))
format!("<code class=\"{}\">{}</code>", 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<Regex> = 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];
Expand All @@ -932,35 +903,13 @@ 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,
data: serde_json::Map<String, serde_json::Value>,
is_index: bool,
book_config: BookConfig,
html_config: HtmlConfig,
edition: Option<RustEdition>,
chapter_titles: &'a HashMap<PathBuf, String>,
}

Expand Down Expand Up @@ -1002,107 +951,4 @@ mod tests {
assert_eq!(got, should_be);
}
}

#[test]
fn add_playground() {
let inputs = [
("<code class=\"language-rust\">x()</code>",
"<pre class=\"playground\"><code class=\"language-rust\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}</span></code></pre>"),
("<code class=\"language-rust\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}</code></pre>"),
("<code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code>",
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";</code></pre>"),
("<code class=\"language-rust editable\">let s = \"foo\n ## bar\n\";</code>",
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code></pre>"),
("<code class=\"language-rust editable\">let s = \"foo\n # bar\n#\n\";</code>",
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span><span class=\"boring\">\n</span>\";</code></pre>"),
("<code class=\"language-rust ignore\">let s = \"foo\n # bar\n\";</code>",
"<code class=\"language-rust ignore\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";</code>"),
("<code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code>",
"<pre class=\"playground\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code></pre>"),
];
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 = [
("<code class=\"language-rust\">x()</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2015\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}</span></code></pre>"),
("<code class=\"language-rust\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}</code></pre>"),
("<code class=\"language-rust edition2015\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}</code></pre>"),
("<code class=\"language-rust edition2018\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}</code></pre>"),
];
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 = [
("<code class=\"language-rust\">x()</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2018\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}</span></code></pre>"),
("<code class=\"language-rust\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}</code></pre>"),
("<code class=\"language-rust edition2015\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}</code></pre>"),
("<code class=\"language-rust edition2018\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}</code></pre>"),
];
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 = [
("<code class=\"language-rust\">x()</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2021\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}</span></code></pre>"),
("<code class=\"language-rust\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2021\">fn main() {}</code></pre>"),
("<code class=\"language-rust edition2015\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}</code></pre>"),
("<code class=\"language-rust edition2018\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}</code></pre>"),
];
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);
}
}
}
Loading