use crate::book::{Book, BookItem}; use crate::config::{BookConfig, Code, Config, HtmlConfig, Playground, RustEdition}; use crate::errors::*; use crate::renderer::html_handlebars::helpers; use crate::renderer::{RenderContext, Renderer}; use crate::theme::{self, playground_editor, Theme}; use crate::utils; use std::borrow::Cow; use std::collections::BTreeMap; use std::collections::HashMap; use std::fs::{self, File}; use std::path::{Path, PathBuf}; 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 serde_json::json; #[derive(Default)] pub struct HtmlHandlebars; impl HtmlHandlebars { pub fn new() -> Self { HtmlHandlebars } fn render_item( &self, item: &BookItem, mut ctx: RenderItemContext<'_>, print_content: &mut String, ) -> Result<()> { // FIXME: This should be made DRY-er and rely less on mutable state let (ch, path) = match item { BookItem::Chapter(ch) if !ch.is_draft_chapter() => (ch, ch.path.as_ref().unwrap()), _ => return Ok(()), }; if let Some(ref edit_url_template) = ctx.html_config.edit_url_template { let full_path = ctx.book_config.src.to_str().unwrap_or_default().to_owned() + "/" + ch.source_path .clone() .unwrap_or_default() .to_str() .unwrap_or_default(); let edit_url = edit_url_template.replace("{path}", &full_path); ctx.data .insert("git_repository_edit_url".to_owned(), json!(edit_url)); } let content = utils::render_markdown(&ch.content, ctx.html_config.smart_punctuation()); let fixed_content = utils::render_markdown_with_path( &ch.content, ctx.html_config.smart_punctuation(), Some(path), ); if !ctx.is_index && ctx.html_config.print.page_break { // Add page break between chapters // See https://developer.mozilla.org/en-US/docs/Web/CSS/break-before and https://developer.mozilla.org/en-US/docs/Web/CSS/page-break-before // Add both two CSS properties because of the compatibility issue print_content .push_str(r#"
"#); } print_content.push_str(&fixed_content); // Update the context with data for this file let ctx_path = path .to_str() .with_context(|| "Could not convert path to str")?; let filepath = Path::new(&ctx_path).with_extension("html"); // "print.html" is used for the print page. if path == Path::new("print.md") { bail!("{} is reserved for internal use", path.display()); }; let book_title = ctx .data .get("book_title") .and_then(serde_json::Value::as_str) .unwrap_or(""); let title = if let Some(title) = ctx.chapter_titles.get(path) { title.clone() } else if book_title.is_empty() { ch.name.clone() } else { ch.name.clone() + " - " + book_title }; ctx.data.insert("path".to_owned(), json!(path)); ctx.data.insert("content".to_owned(), json!(content)); ctx.data.insert("chapter_title".to_owned(), json!(ch.name)); ctx.data.insert("title".to_owned(), json!(title)); ctx.data.insert( "path_to_root".to_owned(), json!(utils::fs::path_to_root(path)), ); if let Some(ref section) = ch.number { ctx.data .insert("section".to_owned(), json!(section.to_string())); } // Render the handlebars template with the data debug!("Render template"); let rendered = ctx.handlebars.render("index", &ctx.data)?; let rendered = self.post_process( rendered, &ctx.html_config.playground, &ctx.html_config.code, ctx.edition, ); // Write to file debug!("Creating {}", filepath.display()); utils::fs::write_file(&ctx.destination, &filepath, rendered.as_bytes())?; if ctx.is_index { ctx.data.insert("path".to_owned(), json!("index.md")); 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.playground, &ctx.html_config.code, ctx.edition, ); debug!("Creating index.html from {}", ctx_path); utils::fs::write_file(&ctx.destination, "index.html", rendered_index.as_bytes())?; } Ok(()) } fn render_404( &self, ctx: &RenderContext, html_config: &HtmlConfig, src_dir: &Path, handlebars: &mut Handlebars<'_>, data: &mut serde_json::Map]+)class="([^"]+)"([^>]*)>"##).unwrap());
FIX_CODE_BLOCKS
.replace_all(html, |caps: &Captures<'_>| {
let before = &caps[1];
let classes = &caps[2].replace(',', " ");
let after = &caps[3];
format!(
r#""#,
before = before,
classes = classes,
after = after
)
})
.into_owned()
}
static CODE_BLOCK_RE: Lazy =
Lazy::new(|| Regex::new(r##"((?s)]?class="([^"]+)".*?>(.*?))"##).unwrap());
fn add_playground_pre(
html: &str,
playground_config: &Playground,
edition: Option,
) -> String {
CODE_BLOCK_RE
.replace_all(html, |caps: &Captures<'_>| {
let text = &caps[1];
let classes = &caps[2];
let code = &caps[3];
if classes.contains("language-rust")
&& ((!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!")
{
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()
};
content
}
)
} else {
// not language-rust, so no-op
text.to_owned()
}
})
.into_owned()
}
/// Modifies all `` blocks to convert "hidden" lines and to wrap them in
/// a ``.
fn hide_lines(html: &str, code_config: &Code) -> String {
static LANGUAGE_REGEX: Lazy = Lazy::new(|| Regex::new(r"\blanguage-(\w+)\b").unwrap());
static HIDELINES_REGEX: Lazy = Lazy::new(|| Regex::new(r"\bhidelines=(\S+)").unwrap());
CODE_BLOCK_RE
.replace_all(html, |caps: &Captures<'_>| {
let text = &caps[1];
let classes = &caps[2];
let code = &caps[3];
if classes.contains("language-rust") {
format!(
"{}",
classes,
hide_lines_rust(code)
)
} else {
// First try to get the prefix from the code block
let hidelines_capture = HIDELINES_REGEX.captures(classes);
let hidelines_prefix = match &hidelines_capture {
Some(capture) => Some(&capture[1]),
None => {
// Then look up the prefix by language
LANGUAGE_REGEX.captures(classes).and_then(|capture| {
code_config.hidelines.get(&capture[1]).map(|p| p.as_str())
})
}
};
match hidelines_prefix {
Some(prefix) => format!(
"{}",
classes,
hide_lines_with_prefix(code, prefix)
),
None => text.to_owned(),
}
}
})
.into_owned()
}
fn hide_lines_rust(content: &str) -> String {
static BORING_LINES_REGEX: Lazy = Lazy::new(|| Regex::new(r"^(\s*)#(.?)(.*)$").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] == "#" {
result += &caps[1];
result += &caps[2];
result += &caps[3];
result += newline;
continue;
} else if &caps[2] != "!" && &caps[2] != "[" {
result += "";
result += &caps[1];
if &caps[2] != " " {
result += &caps[2];
}
result += &caps[3];
result += newline;
result += "";
continue;
}
}
result += line;
result += newline;
}
result
}
fn hide_lines_with_prefix(content: &str, prefix: &str) -> String {
let mut result = String::with_capacity(content.len());
for line in content.lines() {
if line.trim_start().starts_with(prefix) {
let pos = line.find(prefix).unwrap();
let (ws, rest) = (&line[..pos], &line[pos + prefix.len()..]);
result += "";
result += ws;
result += rest;
result += "\n";
result += "";
continue;
}
result += line;
result += "\n";
}
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,
is_index: bool,
book_config: BookConfig,
html_config: HtmlConfig,
edition: Option,
chapter_titles: &'a HashMap,
}
#[cfg(test)]
mod tests {
use crate::config::TextDirection;
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn original_build_header_links() {
let inputs = vec![
(
"blah blah Foo
",
r##"blah blah Foo
"##,
),
(
"Foo
",
r##"Foo
"##,
),
(
"Foo^bar
",
r##"Foo^bar
"##,
),
(
"",
r##"
"##,
),
(
"Hï
",
r##"Hï
"##,
),
(
"Foo
Foo
",
r##"Foo
Foo
"##,
),
// id only
(
r##"Foo
"##,
r##"Foo
"##,
),
// class only
(
r##"Foo
"##,
r##"Foo
"##,
),
// both id and class
(
r##"Foo
"##,
r##"Foo
"##,
),
];
for (src, should_be) in inputs {
let got = build_header_links(src);
assert_eq!(got, should_be);
}
}
#[test]
fn add_playground() {
let inputs = [
("x()",
"# #![allow(unused)]\n#fn 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)]\n#fn 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)]\n#fn 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)]\n#fn 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);
}
}
#[test]
fn hide_lines_language_rust() {
let inputs = [
(
"\n# #![allow(unused)]\n#fn main() {\nx()\n#}
",
"\n#![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 = hide_lines(src, &Code::default());
assert_eq!(&*got, *should_be);
}
}
#[test]
fn hide_lines_language_other() {
let inputs = [
(
"~hidden()\nnothidden():\n~ hidden()\n ~hidden()\n nothidden()",
"hidden()\nnothidden():\n hidden()\n hidden()\n nothidden()\n",),
(
"!!!hidden()\nnothidden():\n!!! hidden()\n !!!hidden()\n nothidden()",
"hidden()\nnothidden():\n hidden()\n hidden()\n nothidden()\n",),
];
for (src, should_be) in &inputs {
let got = hide_lines(
src,
&Code {
hidelines: {
let mut map = HashMap::new();
map.insert("python".to_string(), "~".to_string());
map
},
},
);
assert_eq!(&*got, *should_be);
}
}
#[test]
fn test_json_direction() {
assert_eq!(json!(TextDirection::RightToLeft), json!("rtl"));
assert_eq!(json!(TextDirection::LeftToRight), json!("ltr"));
}
}