"#,
- before = before,
- classes = classes,
- after = after
- )
- })
- .into_owned()
-}
-
-fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
- let regex = Regex::new(r##"((?s)]?class="([^"]+)".*?>(.*?)
)"##).unwrap();
- regex
- .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("noplaypen"))
- || classes.contains("mdbook-runnable")
- {
- // wrap the contents in an external pre block
- if playpen_config.editable && classes.contains("editable")
- || text.contains("fn main")
- || text.contains("quick_main!")
- {
- format!("{}
", text)
- } else {
- // we need to inject our own main
- let (attrs, code) = partition_source(code);
-
- format!(
- "\n# \
- #![allow(unused_variables)]\n{}#fn main() {{\n{}#}}
",
- classes, attrs, code
- )
- }
- } else {
- // not language-rust, so no-op
- text.to_owned()
- }
- })
- .into_owned()
-}
-
-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_str("\n");
- } else {
- before.push_str(line);
- before.push_str("\n");
- }
- }
-
- (before, after)
-}
-
struct RenderItemContext<'a> {
- handlebars: &'a Handlebars,
+ handlebars: &'a HbsWrapper,
destination: PathBuf,
data: serde_json::Map,
is_index: bool,
html_config: HtmlConfig,
}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn original_build_header_links() {
- let inputs = vec![
- (
- "blah blah Foo
",
- r##"blah blah "##,
- ),
- (
- "Foo
",
- r##""##,
- ),
- (
- "Foo^bar
",
- r##""##,
- ),
- (
- "",
- r##""##,
- ),
- (
- "Hï
",
- r##""##,
- ),
- (
- "Foo
Foo
",
- r##""##,
- ),
- ];
-
- for (src, should_be) in inputs {
- let got = build_header_links(&src);
- assert_eq!(got, should_be);
- }
- }
-}
diff --git a/src/renderer/html_handlebars/hbs_wrapper.rs b/src/renderer/html_handlebars/hbs_wrapper.rs
new file mode 100644
index 0000000000..1f3d84d106
--- /dev/null
+++ b/src/renderer/html_handlebars/hbs_wrapper.rs
@@ -0,0 +1,278 @@
+use super::helpers;
+use crate::config::Playpen;
+use crate::errors::Result;
+use crate::utils;
+use handlebars::Handlebars;
+use regex::{Captures, Regex};
+use serde::Serialize;
+use std::collections::HashMap;
+
+#[derive(Debug)]
+pub struct HbsConfig {
+ pub index_template: String,
+ pub header_template: String,
+ pub no_section_label: bool,
+}
+
+#[derive(Debug)]
+pub struct HbsWrapper {
+ handlebars: Handlebars,
+}
+
+impl HbsWrapper {
+ /// Factory function for create configured handlebars
+ pub fn with_config(cfg: HbsConfig) -> Result {
+ let mut handlebars = Handlebars::new();
+ debug!("Register the index handlebars template");
+ handlebars.register_template_string("index", cfg.index_template)?;
+
+ debug!("Register the header handlebars template");
+ handlebars.register_partial("header", cfg.header_template)?;
+
+ debug!("Register handlebars helpers");
+
+ handlebars.register_helper(
+ "toc",
+ Box::new(helpers::toc::RenderToc {
+ no_section_label: cfg.no_section_label,
+ }),
+ );
+ handlebars.register_helper("previous", Box::new(helpers::navigation::previous));
+ handlebars.register_helper("next", Box::new(helpers::navigation::next));
+ handlebars.register_helper("theme_option", Box::new(helpers::theme::theme_option));
+
+ Ok(HbsWrapper { handlebars })
+ }
+
+ pub fn render(&self, name: &str, data: &T, playpen: &Playpen) -> Result
+ where
+ T: Serialize,
+ {
+ let rendered = self.handlebars.render(name, data)?;
+ let rendered = post_process(rendered, &playpen);
+
+ Ok(rendered)
+ }
+}
+
+#[cfg_attr(feature = "cargo-clippy", allow(clippy::let_and_return))]
+fn post_process(rendered: String, playpen_config: &Playpen) -> String {
+ let rendered = build_header_links(&rendered);
+ let rendered = fix_code_blocks(&rendered);
+ let rendered = add_playpen_pre(&rendered, playpen_config);
+
+ rendered
+}
+
+/// Goes through the rendered HTML, making sure all header tags have
+/// an anchor respectively so people can link to sections directly.
+fn build_header_links(html: &str) -> String {
+ let regex = Regex::new(r"(.*?)").unwrap();
+ let mut id_counter = HashMap::new();
+
+ regex
+ .replace_all(html, |caps: &Captures<'_>| {
+ let level = caps[1]
+ .parse()
+ .expect("Regex should ensure we only ever get numbers here");
+
+ insert_link_into_header(level, &caps[2], &mut id_counter)
+ })
+ .into_owned()
+}
+
+/// Insert a single link into a header, making sure each link gets its own
+/// unique ID by appending an auto-incremented number (if necessary).
+fn insert_link_into_header(
+ level: usize,
+ content: &str,
+ id_counter: &mut HashMap,
+) -> String {
+ let raw_id = utils::id_from_content(content);
+
+ let id_count = id_counter.entry(raw_id.clone()).or_insert(0);
+
+ let id = match *id_count {
+ 0 => raw_id,
+ other => format!("{}-{}", raw_id, other),
+ };
+
+ *id_count += 1;
+
+ format!(
+ r##""##,
+ level = level,
+ id = id,
+ text = content
+ )
+}
+
+// The rust book uses annotations for rustdoc to test code snippets,
+// like the following:
+// ```rust,should_panic
+// fn main() {
+// // Code here
+// }
+// ```
+// This function replaces all commas by spaces in the code block classes
+fn fix_code_blocks(html: &str) -> String {
+ let regex = Regex::new(r##"]+)class="([^"]+)"([^>]*)>"##).unwrap();
+ regex
+ .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()
+}
+
+fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
+ let regex = Regex::new(r##"((?s)]?class="([^"]+)".*?>(.*?)
)"##).unwrap();
+ regex
+ .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("noplaypen"))
+ || classes.contains("mdbook-runnable")
+ {
+ // wrap the contents in an external pre block
+ if playpen_config.editable && classes.contains("editable")
+ || text.contains("fn main")
+ || text.contains("quick_main!")
+ {
+ format!("{}
", text)
+ } else {
+ // we need to inject our own main
+ let (attrs, code) = partition_source(code);
+
+ format!(
+ "\n# \
+ #![allow(unused_variables)]\n{}#fn main() {{\n{}#}}
",
+ classes, attrs, code
+ )
+ }
+ } else {
+ // not language-rust, so no-op
+ text.to_owned()
+ }
+ })
+ .into_owned()
+}
+
+/// Split source code to module-level attributes (from head of text) and rest text
+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.is_empty() || trimline.starts_with("#![");
+ if !header || after_header {
+ after_header = true;
+ after.push_str(line);
+ after.push_str("\n");
+ } else {
+ before.push_str(line);
+ before.push_str("\n");
+ }
+ }
+
+ (before, after)
+}
+
+#[cfg(test)]
+mod partition_source_tests {
+ use super::partition_source;
+
+ macro_rules! run_tests {
+ ($($name:ident, $initial:expr => ($before:expr, $after:expr)),+) => {
+ $(
+ #[test]
+ fn $name() {
+ let sources = partition_source($initial);
+ assert_eq!(sources, (String::from($before), String::from($after)));
+ }
+ )+
+ };
+ }
+
+ run_tests!(
+ without_header, "some text" => ("", "some text\n"),
+ two_string_without_header, "some\ntext" => ("", "some\ntext\n"),
+ three_string_separated_by_empty_string, "some\n\ntext" => ("", "some\n\ntext\n"),
+
+ some_empty_lines_before_text, "\n\n\nsome text" => ("\n\n\n", "some text\n"),
+
+ only_header, "#![allow(unused_variables)]" => (
+ "#![allow(unused_variables)]\n",
+ ""
+ ),
+
+ one_line_header, "#![allow(unused_variables)]\n#fn main() {\nlet a = 5;\n#}
" => (
+ "#![allow(unused_variables)]\n",
+ "#fn main() {\nlet a = 5;\n#}
\n"
+ ),
+
+ multiline_header, "#![allow(unused_variables)]\n#![allow(missing_docs)]\n#fn main() {\nlet a = 5;\n#}
" => (
+ "#![allow(unused_variables)]\n#![allow(missing_docs)]\n",
+ "#fn main() {\nlet a = 5;\n#}