Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Rust edition #1100

Closed
wants to merge 2 commits into from
Closed
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
1 change: 1 addition & 0 deletions book-example/book.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title = "mdBook Documentation"
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
authors = ["Mathieu David", "Michael-F-Bryan"]
language = "en"
edition = "2018"

[output.html]
mathjax-support = true
Expand Down
5 changes: 4 additions & 1 deletion book-example/src/format/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Here is an example of what a ***book.toml*** file might look like:
title = "Example book"
author = "John Doe"
description = "The example book covers examples."
edition = "2018"

[build]
build-dir = "my-example-book"
Expand Down Expand Up @@ -43,6 +44,7 @@ This is general information about your book.
`src` directly under the root folder. But this is configurable with the `src`
key in the configuration file.
- **language:** The main language of the book, which is used as a language attribute `<html lang="en">` for example.
- **edition**: Rust edition to use by default for the code snippets. Defaults to `rustdoc` defaults (2015).

**book.toml**
```toml
Expand All @@ -52,6 +54,7 @@ authors = ["John Doe", "Jane Doe"]
description = "The example book covers examples."
src = "my-src" # the source files will be found in `root/my-src` instead of `root/src`
language = "en"
edition = "2018"
```

### Build options
Expand Down Expand Up @@ -178,7 +181,7 @@ The following configuration options are available:
an icon link will be output in the menu bar of the book.
- **git-repository-icon:** The FontAwesome icon class to use for the git
repository link. Defaults to `fa-github`.

Available configuration options for the `[output.html.fold]` table:

- **enable:** Enable section-folding. When off, all folds are open.
Expand Down
22 changes: 16 additions & 6 deletions src/book/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use crate::preprocess::{
use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderContext, Renderer};
use crate::utils;

use crate::config::Config;
use crate::config::{Config, RustEdition};

/// The object used to manage and build a book.
pub struct MDBook {
Expand Down Expand Up @@ -262,11 +262,21 @@ impl MDBook {
let mut tmpf = utils::fs::create_file(&path)?;
tmpf.write_all(ch.content.as_bytes())?;

let output = Command::new("rustdoc")
.arg(&path)
.arg("--test")
.args(&library_args)
.output()?;
let mut cmd = Command::new("rustdoc");
cmd.arg(&path).arg("--test").args(&library_args);

if let Some(edition) = self.config.book.edition {
match edition {
RustEdition::E2015 => {
cmd.args(&["--edition", "2015"]);
}
RustEdition::E2018 => {
cmd.args(&["--edition", "2018"]);
}
}
}

let output = cmd.output()?;

if !output.status.success() {
bail!(ErrorKind::Subprocess(
Expand Down
105 changes: 105 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,8 @@ pub struct BookConfig {
pub multilingual: bool,
/// The main language of the book.
pub language: Option<String>,
/// Rust edition to use for the code.
pub edition: Option<RustEdition>,
}

impl Default for BookConfig {
Expand All @@ -404,10 +406,60 @@ impl Default for BookConfig {
src: PathBuf::from("src"),
multilingual: false,
language: Some(String::from("en")),
edition: None,
}
}
}

#[derive(Debug, Copy, Clone, PartialEq)]
/// Rust edition to use for the code.
pub enum RustEdition {
/// The 2018 edition of Rust
E2018,
/// The 2015 edition of Rust
E2015,
}

impl Serialize for RustEdition {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
RustEdition::E2015 => serializer.serialize_str("2015"),
RustEdition::E2018 => serializer.serialize_str("2018"),
}
}
}

impl<'de> Deserialize<'de> for RustEdition {
fn deserialize<D>(de: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;

let raw = Value::deserialize(de)?;

let edition = match raw {
Value::String(s) => s,
_ => {
return Err(D::Error::custom("Rust edition should be a string"));
}
};

let edition = match edition.as_str() {
"2018" => RustEdition::E2018,
"2015" => RustEdition::E2015,
_ => {
return Err(D::Error::custom("Unknown Rust edition"));
}
};

Ok(edition)
}
}

/// Configuration for the build procedure.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
Expand Down Expand Up @@ -647,6 +699,7 @@ mod tests {
multilingual: true,
src: PathBuf::from("source"),
language: Some(String::from("ja")),
edition: None,
};
let build_should_be = BuildConfig {
build_dir: PathBuf::from("outputs"),
Expand Down Expand Up @@ -678,6 +731,58 @@ mod tests {
assert_eq!(got.html_config().unwrap(), html_should_be);
}

#[test]
fn edition_2015() {
let src = r#"
[book]
title = "mdBook Documentation"
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
authors = ["Mathieu David"]
src = "./source"
edition = "2015"
"#;

let book_should_be = BookConfig {
title: Some(String::from("mdBook Documentation")),
description: Some(String::from(
"Create book from markdown files. Like Gitbook but implemented in Rust",
)),
authors: vec![String::from("Mathieu David")],
src: PathBuf::from("./source"),
edition: Some(RustEdition::E2015),
..Default::default()
};

let got = Config::from_str(src).unwrap();
assert_eq!(got.book, book_should_be);
}

#[test]
fn edition_2018() {
let src = r#"
[book]
title = "mdBook Documentation"
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
authors = ["Mathieu David"]
src = "./source"
edition = "2018"
"#;

let book_should_be = BookConfig {
title: Some(String::from("mdBook Documentation")),
description: Some(String::from(
"Create book from markdown files. Like Gitbook but implemented in Rust",
)),
authors: vec![String::from("Mathieu David")],
src: PathBuf::from("./source"),
edition: Some(RustEdition::E2018),
..Default::default()
};

let got = Config::from_str(src).unwrap();
assert_eq!(got.book, book_should_be);
}

#[test]
fn load_arbitrary_output_type() {
#[derive(Debug, Deserialize, PartialEq)]
Expand Down
87 changes: 79 additions & 8 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::{Config, HtmlConfig, Playpen};
use crate::config::{Config, HtmlConfig, Playpen, RustEdition};
use crate::errors::*;
use crate::renderer::html_handlebars::helpers;
use crate::renderer::{RenderContext, Renderer};
Expand Down Expand Up @@ -81,7 +81,7 @@ impl HtmlHandlebars {
debug!("Render template");
let rendered = ctx.handlebars.render("index", &ctx.data)?;

let rendered = self.post_process(rendered, &ctx.html_config.playpen);
let rendered = self.post_process(rendered, &ctx.html_config.playpen, ctx.edition);

// Write to file
debug!("Creating {}", filepath.display());
Expand All @@ -92,7 +92,8 @@ impl HtmlHandlebars {
ctx.data.insert("path_to_root".to_owned(), json!(""));
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.playpen);
let rendered_index =
self.post_process(rendered_index, &ctx.html_config.playpen, ctx.edition);
debug!("Creating index.html from {}", path);
utils::fs::write_file(&ctx.destination, "index.html", rendered_index.as_bytes())?;
}
Expand All @@ -102,10 +103,15 @@ impl HtmlHandlebars {
}

#[cfg_attr(feature = "cargo-clippy", allow(clippy::let_and_return))]
fn post_process(&self, rendered: String, playpen_config: &Playpen) -> String {
fn post_process(
&self,
rendered: String,
playpen_config: &Playpen,
edition: Option<RustEdition>,
) -> String {
let rendered = build_header_links(&rendered);
let rendered = fix_code_blocks(&rendered);
let rendered = add_playpen_pre(&rendered, playpen_config);
let rendered = add_playpen_pre(&rendered, playpen_config, edition);

rendered
}
Expand Down Expand Up @@ -338,6 +344,7 @@ impl Renderer for HtmlHandlebars {
data: data.clone(),
is_index,
html_config: html_config.clone(),
edition: ctx.config.book.edition,
};
self.render_item(item, ctx, &mut print_content)?;
is_index = false;
Expand All @@ -353,7 +360,7 @@ impl Renderer for HtmlHandlebars {
debug!("Render template");
let rendered = handlebars.render("index", &data)?;

let rendered = self.post_process(rendered, &html_config.playpen);
let rendered = self.post_process(rendered, &html_config.playpen, ctx.config.book.edition);

utils::fs::write_file(&destination, "print.html", rendered.as_bytes())?;
debug!("Creating print.html ✓");
Expand Down Expand Up @@ -600,7 +607,7 @@ fn fix_code_blocks(html: &str) -> String {
.into_owned()
}

fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
fn add_playpen_pre(html: &str, playpen_config: &Playpen, edition: Option<RustEdition>) -> String {
let regex = Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
regex
.replace_all(html, |caps: &Captures<'_>| {
Expand All @@ -612,10 +619,24 @@ fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
if (!classes.contains("ignore") && !classes.contains("noplaypen"))
|| classes.contains("mdbook-runnable")
{
let contains_e2015 = classes.contains("edition2015");
let contains_e2018 = classes.contains("edition2018");
let edition_class = if contains_e2015 || contains_e2018 {
// the user forced edition, we should not overwrite it
kpp marked this conversation as resolved.
Show resolved Hide resolved
""
} else {
match edition {
Some(RustEdition::E2015) => " edition2015",
Some(RustEdition::E2018) => " edition2018",
None => "",
}
};

// wrap the contents in an external pre block
format!(
"<pre class=\"playpen\"><code class=\"{}\">{}</code></pre>",
"<pre class=\"playpen\"><code class=\"{}{}\">{}</code></pre>",
classes,
edition_class,
{
let content: Cow<'_, str> = if playpen_config.editable
&& classes.contains("editable")
Expand Down Expand Up @@ -706,6 +727,7 @@ struct RenderItemContext<'a> {
data: serde_json::Map<String, serde_json::Value>,
is_index: bool,
html_config: HtmlConfig,
edition: Option<RustEdition>,
}

#[cfg(test)]
Expand Down Expand Up @@ -772,6 +794,55 @@ mod tests {
editable: true,
..Playpen::default()
},
None,
);
assert_eq!(&*got, *should_be);
}
}
#[test]
fn add_playpen_edition2015() {
let inputs = [
("<code class=\"language-rust\">x()</code>",
"<pre class=\"playpen\"><code class=\"language-rust edition2015\">\n<span class=\"boring\">#![allow(unused_variables)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
("<code class=\"language-rust\">fn main() {}</code>",
"<pre class=\"playpen\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
("<code class=\"language-rust edition2015\">fn main() {}</code>",
"<pre class=\"playpen\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
("<code class=\"language-rust edition2018\">fn main() {}</code>",
"<pre class=\"playpen\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
];
for (src, should_be) in &inputs {
let got = add_playpen_pre(
src,
&Playpen {
editable: true,
..Playpen::default()
},
Some(RustEdition::E2015),
);
assert_eq!(&*got, *should_be);
}
}
#[test]
fn add_playpen_edition2018() {
let inputs = [
("<code class=\"language-rust\">x()</code>",
"<pre class=\"playpen\"><code class=\"language-rust edition2018\">\n<span class=\"boring\">#![allow(unused_variables)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
("<code class=\"language-rust\">fn main() {}</code>",
"<pre class=\"playpen\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
("<code class=\"language-rust edition2015\">fn main() {}</code>",
"<pre class=\"playpen\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
("<code class=\"language-rust edition2018\">fn main() {}</code>",
"<pre class=\"playpen\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
];
for (src, should_be) in &inputs {
let got = add_playpen_pre(
src,
&Playpen {
editable: true,
..Playpen::default()
},
Some(RustEdition::E2018),
);
assert_eq!(&*got, *should_be);
}
Expand Down