diff --git a/Cargo.lock b/Cargo.lock index 299e8daa53..bb7eea9070 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -152,6 +152,15 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "basic-toml" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8" +dependencies = [ + "serde", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -627,21 +636,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "handlebars" -version = "6.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd4ccde012831f9a071a637b0d4e31df31c0f6c525784b35ae76a9ac6bc1e315" -dependencies = [ - "log", - "num-order", - "pest", - "pest_derive", - "serde", - "serde_json", - "thiserror", -] - [[package]] name = "hashbrown" version = "0.15.1" @@ -751,6 +745,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + [[package]] name = "humantime" version = "2.1.0" @@ -1046,6 +1049,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + [[package]] name = "libredox" version = "0.1.3" @@ -1150,7 +1159,6 @@ dependencies = [ "elasticlunr-rs", "env_logger", "futures-util", - "handlebars", "ignore", "log", "memchr", @@ -1163,6 +1171,7 @@ dependencies = [ "pretty_assertions", "pulldown-cmark 0.10.3", "regex", + "rinja", "select", "semver", "serde", @@ -1208,6 +1217,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.0" @@ -1247,6 +1262,16 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -1292,21 +1317,6 @@ dependencies = [ "notify", ] -[[package]] -name = "num-modular" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" - -[[package]] -name = "num-order" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" -dependencies = [ - "num-modular", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -1378,51 +1388,6 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "pest" -version = "2.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879952a81a83930934cbf1786752d6dedc3b1f29e8f8fb2ad1d0a36f377cf442" -dependencies = [ - "memchr", - "thiserror", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d214365f632b123a47fd913301e14c946c61d1c183ee245fa76eb752e59a02dd" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb55586734301717aea2ac313f50b2eb8f60d2fc3dc01d190eefa2e625f60c4e" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.87", -] - -[[package]] -name = "pest_meta" -version = "2.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75da2a70cf4d9cb76833c990ac9cd3923c9a8905a8929789ce347c84564d03d" -dependencies = [ - "once_cell", - "pest", - "sha2", -] - [[package]] name = "phf" version = "0.10.1" @@ -1716,12 +1681,59 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rinja" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dc4940d00595430b3d7d5a01f6222b5e5b51395d1120bdb28d854bb8abb17a5" +dependencies = [ + "humansize", + "itoa", + "percent-encoding", + "rinja_derive", +] + +[[package]] +name = "rinja_derive" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d9ed0146aef6e2825f1b1515f074510549efba38d71f4554eec32eb36ba18b" +dependencies = [ + "basic-toml", + "memchr", + "mime", + "mime_guess", + "proc-macro2", + "quote", + "rinja_parser", + "rustc-hash", + "serde", + "syn 2.0.87", +] + +[[package]] +name = "rinja_parser" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f9a866e2e00a7a1fb27e46e9e324a6f7c0e7edc4543cae1d38f4e4a100c610" +dependencies = [ + "memchr", + "nom", + "serde", +] + [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + [[package]] name = "rustix" version = "0.38.39" @@ -1834,17 +1846,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "shlex" version = "1.3.0" @@ -2147,12 +2148,6 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - [[package]] name = "unicase" version = "2.8.0" diff --git a/Cargo.toml b/Cargo.toml index 5d593b2700..ab5940fa3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,12 +26,12 @@ clap = { version = "4.3.12", features = ["cargo", "wrap_help"] } clap_complete = "4.3.2" once_cell = "1.17.1" env_logger = "0.11.1" -handlebars = "6.0" log = "0.4.17" memchr = "2.5.0" opener = "0.7.0" pulldown-cmark = { version = "0.10.0", default-features = false, features = ["html"] } # Do not update, part of the public api. regex = "1.8.1" +rinja = "0.3.5" serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" shlex = "1.3.0" diff --git a/guide/src/format/theme/index-hbs.md b/guide/src/format/theme/index-hbs.md index 5139dbff9d..ba3fff18f5 100644 --- a/guide/src/format/theme/index-hbs.md +++ b/guide/src/format/theme/index-hbs.md @@ -1,6 +1,6 @@ -# index.hbs +# index.html -`index.hbs` is the handlebars template that is used to render the book. The +`index.html` is the jinja template that is used to render the book. The markdown files are processed to html and then injected in that template. If you want to change the layout or style of your book, chances are that you @@ -11,7 +11,7 @@ will have to modify this template a little bit. Here is what you need to know. A lot of data is exposed to the handlebars template with the "context". In the handlebars template you can access this information by using -```handlebars +```jinja {{name_of_property}} ``` diff --git a/rinja.toml b/rinja.toml new file mode 100644 index 0000000000..f1a7ff7565 --- /dev/null +++ b/rinja.toml @@ -0,0 +1,3 @@ +[[escaper]] +path = "rinja::filters::Text" +extensions = ["js"] diff --git a/src/book/init.rs b/src/book/init.rs index faca1d09aa..c9fefd3d18 100644 --- a/src/book/init.rs +++ b/src/book/init.rs @@ -122,9 +122,6 @@ impl BookBuilder { fs::create_dir(&themedir)?; } - let mut index = File::create(themedir.join("index.hbs"))?; - index.write_all(theme::INDEX)?; - let cssdir = themedir.join("css"); if !cssdir.exists() { fs::create_dir(&cssdir)?; diff --git a/src/book/mod.rs b/src/book/mod.rs index 608ed16652..e2cfd3f1eb 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -27,7 +27,7 @@ use crate::errors::*; use crate::preprocess::{ CmdPreprocessor, IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext, }; -use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderContext, Renderer}; +use crate::renderer::{CmdRenderer, HtmlRenderer, MarkdownRenderer, RenderContext, Renderer}; use crate::utils; use crate::config::{Config, RustEdition}; @@ -435,7 +435,7 @@ fn determine_renderers(config: &Config) -> Vec> { if let Some(output_table) = config.get("output").and_then(Value::as_table) { renderers.extend(output_table.iter().map(|(key, table)| { if key == "html" { - Box::new(HtmlHandlebars::new()) as Box + Box::new(HtmlRenderer::new()) as Box } else if key == "markdown" { Box::new(MarkdownRenderer::new()) as Box } else { @@ -446,7 +446,7 @@ fn determine_renderers(config: &Config) -> Vec> { // if we couldn't find anything, add the HTML renderer as a default if renderers.is_empty() { - renderers.push(Box::new(HtmlHandlebars::new())); + renderers.push(Box::new(HtmlRenderer::new())); } renderers @@ -858,7 +858,7 @@ mod tests { .and_then(Value::as_str) .unwrap(); assert_eq!(html, "html"); - let html_renderer = HtmlHandlebars::default(); + let html_renderer = HtmlRenderer::default(); let pre = LinkPreprocessor::new(); let should_run = preprocessor_should_run(&pre, &html_renderer, &cfg); @@ -883,7 +883,7 @@ mod tests { #[test] fn preprocessor_should_run_falls_back_to_supports_renderer_method() { let cfg = Config::default(); - let html = HtmlHandlebars::new(); + let html = HtmlRenderer::new(); let should_be = true; let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg); diff --git a/src/config.rs b/src/config.rs index b87ad27644..3c27e1315b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -464,6 +464,14 @@ impl TextDirection { _ => TextDirection::LeftToRight, } } + + /// Convert the text representation. + pub fn as_str(&self) -> &'static str { + match self { + Self::LeftToRight => "ltr", + Self::RightToLeft => "rtl", + } + } } /// Configuration for the build procedure. diff --git a/src/renderer/html_handlebars/helpers/mod.rs b/src/renderer/html_handlebars/helpers/mod.rs deleted file mode 100644 index 52be6d204b..0000000000 --- a/src/renderer/html_handlebars/helpers/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod navigation; -pub mod theme; -pub mod toc; diff --git a/src/renderer/html_handlebars/helpers/navigation.rs b/src/renderer/html_handlebars/helpers/navigation.rs deleted file mode 100644 index 12c69027f3..0000000000 --- a/src/renderer/html_handlebars/helpers/navigation.rs +++ /dev/null @@ -1,302 +0,0 @@ -use std::collections::BTreeMap; -use std::path::Path; - -use handlebars::{ - Context, Handlebars, Helper, Output, RenderContext, RenderError, RenderErrorReason, Renderable, -}; - -use crate::utils; -use log::{debug, trace}; -use serde_json::json; - -type StringMap = BTreeMap; - -/// Target for `find_chapter`. -enum Target { - Previous, - Next, -} - -impl Target { - /// Returns target if found. - fn find( - &self, - base_path: &str, - current_path: &str, - current_item: &StringMap, - previous_item: &StringMap, - ) -> Result, RenderError> { - match *self { - Target::Next => { - let previous_path = previous_item.get("path").ok_or_else(|| { - RenderErrorReason::Other("No path found for chapter in JSON data".to_owned()) - })?; - - if previous_path == base_path { - return Ok(Some(current_item.clone())); - } - } - - Target::Previous => { - if current_path == base_path { - return Ok(Some(previous_item.clone())); - } - } - } - - Ok(None) - } -} - -fn find_chapter( - ctx: &Context, - rc: &mut RenderContext<'_, '_>, - target: Target, -) -> Result, RenderError> { - debug!("Get data from context"); - - let chapters = rc.evaluate(ctx, "@root/chapters").and_then(|c| { - serde_json::value::from_value::>(c.as_json().clone()).map_err(|_| { - RenderErrorReason::Other("Could not decode the JSON data".to_owned()).into() - }) - })?; - - let base_path = rc - .evaluate(ctx, "@root/path")? - .as_json() - .as_str() - .ok_or_else(|| { - RenderErrorReason::Other("Type error for `path`, string expected".to_owned()) - })? - .replace('\"', ""); - - if !rc.evaluate(ctx, "@root/is_index")?.is_missing() { - // Special case for index.md which may be a synthetic page. - // Target::find won't match because there is no page with the path - // "index.md" (unless there really is an index.md in SUMMARY.md). - match target { - Target::Previous => return Ok(None), - Target::Next => match chapters - .iter() - .filter(|chapter| { - // Skip things like "spacer" - chapter.contains_key("path") - }) - .nth(1) - { - Some(chapter) => return Ok(Some(chapter.clone())), - None => return Ok(None), - }, - } - } - - let mut previous: Option = None; - - debug!("Search for chapter"); - - for item in chapters { - match item.get("path") { - Some(path) if !path.is_empty() => { - if let Some(previous) = previous { - if let Some(item) = target.find(&base_path, path, &item, &previous)? { - return Ok(Some(item)); - } - } - - previous = Some(item); - } - _ => continue, - } - } - - Ok(None) -} - -fn render( - _h: &Helper<'_>, - r: &Handlebars<'_>, - ctx: &Context, - rc: &mut RenderContext<'_, '_>, - out: &mut dyn Output, - chapter: &StringMap, -) -> Result<(), RenderError> { - trace!("Creating BTreeMap to inject in context"); - - let mut context = BTreeMap::new(); - let base_path = rc - .evaluate(ctx, "@root/path")? - .as_json() - .as_str() - .ok_or_else(|| { - RenderErrorReason::Other("Type error for `path`, string expected".to_owned()) - })? - .replace('\"', ""); - - context.insert( - "path_to_root".to_owned(), - json!(utils::fs::path_to_root(base_path)), - ); - - chapter - .get("name") - .ok_or_else(|| { - RenderErrorReason::Other("No title found for chapter in JSON data".to_owned()) - }) - .map(|name| context.insert("title".to_owned(), json!(name)))?; - - chapter - .get("path") - .ok_or_else(|| { - RenderErrorReason::Other("No path found for chapter in JSON data".to_owned()) - }) - .and_then(|p| { - Path::new(p) - .with_extension("html") - .to_str() - .ok_or_else(|| { - RenderErrorReason::Other("Link could not be converted to str".to_owned()) - }) - .map(|p| context.insert("link".to_owned(), json!(p.replace('\\', "/")))) - })?; - - trace!("Render template"); - - let t = _h - .template() - .ok_or_else(|| RenderErrorReason::Other("Error with the handlebars template".to_owned()))?; - let local_ctx = Context::wraps(&context)?; - let mut local_rc = rc.clone(); - t.render(r, &local_ctx, &mut local_rc, out) -} - -pub fn previous( - _h: &Helper<'_>, - r: &Handlebars<'_>, - ctx: &Context, - rc: &mut RenderContext<'_, '_>, - out: &mut dyn Output, -) -> Result<(), RenderError> { - trace!("previous (handlebars helper)"); - - if let Some(previous) = find_chapter(ctx, rc, Target::Previous)? { - render(_h, r, ctx, rc, out, &previous)?; - } - - Ok(()) -} - -pub fn next( - _h: &Helper<'_>, - r: &Handlebars<'_>, - ctx: &Context, - rc: &mut RenderContext<'_, '_>, - out: &mut dyn Output, -) -> Result<(), RenderError> { - trace!("next (handlebars helper)"); - - if let Some(next) = find_chapter(ctx, rc, Target::Next)? { - render(_h, r, ctx, rc, out, &next)?; - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - static TEMPLATE: &str = - "{{#previous}}{{title}}: {{link}}{{/previous}}|{{#next}}{{title}}: {{link}}{{/next}}"; - - #[test] - fn test_next_previous() { - let data = json!({ - "name": "two", - "path": "two.path", - "chapters": [ - { - "name": "one", - "path": "one.path" - }, - { - "name": "two", - "path": "two.path", - }, - { - "name": "three", - "path": "three.path" - } - ] - }); - - let mut h = Handlebars::new(); - h.register_helper("previous", Box::new(previous)); - h.register_helper("next", Box::new(next)); - - assert_eq!( - h.render_template(TEMPLATE, &data).unwrap(), - "one: one.html|three: three.html" - ); - } - - #[test] - fn test_first() { - let data = json!({ - "name": "one", - "path": "one.path", - "chapters": [ - { - "name": "one", - "path": "one.path" - }, - { - "name": "two", - "path": "two.path", - }, - { - "name": "three", - "path": "three.path" - } - ] - }); - - let mut h = Handlebars::new(); - h.register_helper("previous", Box::new(previous)); - h.register_helper("next", Box::new(next)); - - assert_eq!( - h.render_template(TEMPLATE, &data).unwrap(), - "|two: two.html" - ); - } - #[test] - fn test_last() { - let data = json!({ - "name": "three", - "path": "three.path", - "chapters": [ - { - "name": "one", - "path": "one.path" - }, - { - "name": "two", - "path": "two.path", - }, - { - "name": "three", - "path": "three.path" - } - ] - }); - - let mut h = Handlebars::new(); - h.register_helper("previous", Box::new(previous)); - h.register_helper("next", Box::new(next)); - - assert_eq!( - h.render_template(TEMPLATE, &data).unwrap(), - "two: two.html|" - ); - } -} diff --git a/src/renderer/html_handlebars/helpers/theme.rs b/src/renderer/html_handlebars/helpers/theme.rs deleted file mode 100644 index 29fe98a2e8..0000000000 --- a/src/renderer/html_handlebars/helpers/theme.rs +++ /dev/null @@ -1,38 +0,0 @@ -use handlebars::{ - Context, Handlebars, Helper, Output, RenderContext, RenderError, RenderErrorReason, -}; -use log::trace; - -pub fn theme_option( - h: &Helper<'_>, - _r: &Handlebars<'_>, - ctx: &Context, - rc: &mut RenderContext<'_, '_>, - out: &mut dyn Output, -) -> Result<(), RenderError> { - trace!("theme_option (handlebars helper)"); - - let param = h.param(0).and_then(|v| v.value().as_str()).ok_or_else(|| { - RenderErrorReason::ParamTypeMismatchForName( - "theme_option", - "0".to_owned(), - "string".to_owned(), - ) - })?; - - let default_theme = rc.evaluate(ctx, "@root/default_theme")?; - let default_theme_name = default_theme.as_json().as_str().ok_or_else(|| { - RenderErrorReason::ParamTypeMismatchForName( - "theme_option", - "default_theme".to_owned(), - "string".to_owned(), - ) - })?; - - out.write(param)?; - if param.to_lowercase() == default_theme_name.to_lowercase() { - out.write(" (default)")?; - } - - Ok(()) -} diff --git a/src/renderer/html_handlebars/helpers/toc.rs b/src/renderer/html_handlebars/helpers/toc.rs deleted file mode 100644 index d43c617fcd..0000000000 --- a/src/renderer/html_handlebars/helpers/toc.rs +++ /dev/null @@ -1,189 +0,0 @@ -use std::path::Path; -use std::{cmp::Ordering, collections::BTreeMap}; - -use crate::utils::special_escape; - -use handlebars::{ - Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError, RenderErrorReason, -}; - -// Handlebars helper to construct TOC -#[derive(Clone, Copy)] -pub struct RenderToc { - pub no_section_label: bool, -} - -impl HelperDef for RenderToc { - fn call<'reg: 'rc, 'rc>( - &self, - _h: &Helper<'rc>, - _r: &'reg Handlebars<'_>, - ctx: &'rc Context, - rc: &mut RenderContext<'reg, 'rc>, - out: &mut dyn Output, - ) -> Result<(), RenderError> { - // get value from context data - // rc.get_path() is current json parent path, you should always use it like this - // param is the key of value you want to display - let chapters = rc.evaluate(ctx, "@root/chapters").and_then(|c| { - serde_json::value::from_value::>>(c.as_json().clone()) - .map_err(|_| { - RenderErrorReason::Other("Could not decode the JSON data".to_owned()).into() - }) - })?; - - let fold_enable = rc - .evaluate(ctx, "@root/fold_enable")? - .as_json() - .as_bool() - .ok_or_else(|| { - RenderErrorReason::Other("Type error for `fold_enable`, bool expected".to_owned()) - })?; - - let fold_level = rc - .evaluate(ctx, "@root/fold_level")? - .as_json() - .as_u64() - .ok_or_else(|| { - RenderErrorReason::Other("Type error for `fold_level`, u64 expected".to_owned()) - })?; - - // If true, then this is the iframe and we need target="_parent" - let is_toc_html = rc - .evaluate(ctx, "@root/is_toc_html")? - .as_json() - .as_bool() - .unwrap_or(false); - - out.write("
    ")?; - - let mut current_level = 1; - - for item in chapters { - let (_section, level) = if let Some(s) = item.get("section") { - (s.as_str(), s.matches('.').count()) - } else { - ("", 1) - }; - - // Expand if folding is disabled, or if levels that are larger than this would not - // be folded. - let is_expanded = !fold_enable || level - 1 < (fold_level as usize); - - match level.cmp(¤t_level) { - Ordering::Greater => { - while level > current_level { - out.write("
  1. ")?; - out.write("
      ")?; - current_level += 1; - } - write_li_open_tag(out, is_expanded, false)?; - } - Ordering::Less => { - while level < current_level { - out.write("
    ")?; - out.write("
  2. ")?; - current_level -= 1; - } - write_li_open_tag(out, is_expanded, false)?; - } - Ordering::Equal => { - write_li_open_tag(out, is_expanded, !item.contains_key("section"))?; - } - } - - // Spacer - if item.contains_key("spacer") { - out.write("
  3. ")?; - continue; - } - - // Part title - if let Some(title) = item.get("part") { - out.write("
  4. ")?; - out.write(&special_escape(title))?; - out.write("
  5. ")?; - continue; - } - - // Link - let path_exists: bool; - match item.get("path") { - Some(path) if !path.is_empty() => { - out.write("" - } else { - "\">" - })?; - path_exists = true; - } - _ => { - out.write("")?; - } - - // Render expand/collapse toggle - if let Some(flag) = item.get("has_sub_items") { - let has_sub_items = flag.parse::().unwrap_or_default(); - if fold_enable && has_sub_items { - out.write("
    ")?; - } - } - out.write("")?; - } - while current_level > 1 { - out.write("
")?; - out.write("")?; - current_level -= 1; - } - - out.write("")?; - Ok(()) - } -} - -fn write_li_open_tag( - out: &mut dyn Output, - is_expanded: bool, - is_affix: bool, -) -> Result<(), std::io::Error> { - let mut li = String::from("
  • "); - out.write(&li) -} diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_renderer/hbs_renderer.rs similarity index 73% rename from src/renderer/html_handlebars/hbs_renderer.rs rename to src/renderer/html_renderer/hbs_renderer.rs index d0149fb523..52a25bc697 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_renderer/hbs_renderer.rs @@ -1,67 +1,328 @@ use crate::book::{Book, BookItem}; -use crate::config::{BookConfig, Code, Config, HtmlConfig, Playground, RustEdition}; +use crate::config::{BookConfig, Code, 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 crate::utils::fs::get_404_output_file; use std::borrow::Cow; -use std::collections::BTreeMap; use std::collections::HashMap; use std::fs::{self, File}; -use std::path::{Path, PathBuf}; +use std::path::Path; -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; +use rinja::{Error, Template}; + +/// Target for `find_chapter`. +enum Target { + Previous, + Next, +} + +impl Target { + /// Returns target if found. + fn find_path<'a>( + &self, + base_path: &str, + current_path: &'a str, + previous_path: Option<&'a str>, + ) -> Option<&'a str> { + match *self { + Target::Next => { + if previous_path.is_some_and(|previous_path| previous_path == base_path) { + return Some(current_path); + } + } + + Target::Previous => { + if current_path == base_path { + return previous_path; + } + } + } + + None + } +} + +fn get_chapter(chapter_path: Option<&str>) -> rinja::Result> { + let Some(path) = chapter_path else { + return Ok(None); + }; + Path::new(path) + .with_extension("html") + .to_str() + .ok_or_else(|| Error::Custom("Link could not be converted to str".into())) + .map(|p| Some(p.replace('\\', "/"))) +} + +fn get_chapter_path( + target: Target, + chapters: &[Chapter], + base_path: &str, + is_index: bool, +) -> rinja::Result> { + if is_index { + // Special case for index.md which may be a synthetic page. + // Target::find won't match because there is no page with the path + // "index.md" (unless there really is an index.md in SUMMARY.md). + match target { + Target::Previous => return Ok(None), + Target::Next => match chapters + .iter() + .filter_map(|chapter| match chapter { + Chapter::Chapter { + path: Some(path), .. + } => Some(path), + _ => None, + }) + .nth(1) + { + Some(path) => return get_chapter(Some(path)), + None => return Ok(None), + }, + } + } + + let mut previous: Option<&str> = None; + + debug!("Search for chapter"); + + for item in chapters { + if let Chapter::Chapter { + path: Some(path), .. + } = item + { + if !path.is_empty() { + if let Some(path) = target.find_path(base_path, path, previous) { + return get_chapter(Some(path)); + } + previous = Some(path); + } + } else { + continue; + } + } + + Ok(None) +} + +fn get_chapter_paths( + base_path: &str, + chapters: &[Chapter], + is_index: bool, +) -> rinja::Result<(Option, Option)> { + let next_chapter = get_chapter_path(Target::Next, chapters, base_path, is_index)?; + let prev_chapter = get_chapter_path(Target::Previous, chapters, base_path, is_index)?; + Ok((next_chapter, prev_chapter)) +} + +fn get_default_theme(html_config: &HtmlConfig) -> String { + if let Some(ref theme) = html_config.default_theme { + theme.to_lowercase() + } else { + "light".to_string() + } +} + +fn get_preferred_dark_theme(html_config: &HtmlConfig) -> String { + if let Some(ref theme) = html_config.preferred_dark_theme { + theme.to_lowercase() + } else { + "navy".to_string() + } +} + +#[derive(Template)] +#[template(path = "index.html")] +struct Index<'a> { + path_to_root: String, + next_chapter: Option, + previous_chapter: Option, + book_config: &'a BookConfig, + html_config: &'a HtmlConfig, + title: &'a str, + copy_fonts: bool, + git_repository_edit_url: Option<&'a str>, + content: &'a str, + base_url: Option<&'a str>, + template_root: &'a Path, + is_print: bool, + has_favicon_png: bool, + has_favicon_svg: bool, + default_theme: String, + preferred_dark_theme: String, +} + +impl<'a> Index<'a> { + fn new( + ctx: &'a RenderContext, + html_config: &'a HtmlConfig, + is_index: bool, + base_path: &'a str, + theme: &Theme, + title: &'a str, + is_print: bool, + git_repository_edit_url: Option<&'a str>, + path_to_root: String, + content: &'a str, + base_url: Option<&'a str>, + chapters: &[Chapter], + ) -> rinja::Result { + let (next_chapter, previous_chapter) = get_chapter_paths(base_path, chapters, is_index)?; + + let has_favicon_png = theme.favicon_png.is_some(); + let has_favicon_svg = theme.favicon_svg.is_some(); + let default_theme = get_default_theme(html_config); + let preferred_dark_theme = get_preferred_dark_theme(html_config); + + // This `matches!` checks for a non-empty file. + let copy_fonts = + html_config.copy_fonts || matches!(theme.fonts_css.as_deref(), Some([_, ..])); + Ok(Self { + path_to_root, + next_chapter, + previous_chapter, + book_config: &ctx.config.book, + html_config, + has_favicon_png, + has_favicon_svg, + title, + copy_fonts, + is_print, + git_repository_edit_url, + content, + base_url, + template_root: &ctx.root, + default_theme, + preferred_dark_theme, + }) + } + + fn additional_css<'b>(&'b self) -> impl Iterator { + self.html_config.additional_css.iter().map(|style| { + match style.strip_prefix(self.template_root) { + Ok(p) => p.to_str().expect("Could not convert to str"), + Err(_) => style.to_str().expect("Could not convert to str"), + } + }) + } + + fn additional_js<'b>(&'b self) -> impl Iterator { + self.html_config.additional_js.iter().map(|script| { + match script.strip_prefix(self.template_root) { + Ok(p) => p.to_str().expect("Could not convert to str"), + Err(_) => script.to_str().expect("Could not convert to str"), + } + }) + } +} + +#[derive(Template)] +#[template(path = "redirect.html")] +struct Redirect<'a> { + url: &'a str, +} + +#[derive(Template)] +#[template(path = "toc.html")] +struct Toc<'a> { + default_theme: String, + book_config: &'a BookConfig, + html_config: &'a HtmlConfig, + copy_fonts: bool, + path_to_root: &'static str, + chapters: &'a [Chapter], +} + +impl<'a> Toc<'a> { + fn new( + html_config: &'a HtmlConfig, + book_config: &'a BookConfig, + theme: &Theme, + chapters: &'a [Chapter], + ) -> Self { + let default_theme = get_default_theme(&html_config); + // This `matches!` checks for a non-empty file. + let copy_fonts = + html_config.copy_fonts || matches!(theme.fonts_css.as_deref(), Some([_, ..])); + + Self { + default_theme, + book_config, + html_config, + path_to_root: "", + copy_fonts, + chapters, + } + } + + fn additional_css<'b>(&'b self) -> impl Iterator { + self.html_config + .additional_css + .iter() + .map(|p| p.to_str().expect("Could not convert to str")) + } +} + +#[derive(Template)] +#[template(path = "toc.js", ext = "txt")] +struct TocJS<'a> { + html_config: &'a HtmlConfig, + chapters: &'a [Chapter], +} #[derive(Default)] -pub struct HtmlHandlebars; +pub struct HtmlRenderer; -impl HtmlHandlebars { +impl HtmlRenderer { pub fn new() -> Self { - HtmlHandlebars + HtmlRenderer } fn render_item( &self, item: &BookItem, - mut ctx: RenderItemContext<'_>, + html_config: &HtmlConfig, + is_index: bool, + ctx: &RenderContext, + theme: &Theme, print_content: &mut String, + chapters: &[Chapter], ) -> 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()), + BookItem::Chapter(ch) if !ch.is_draft_chapter() => (ch, ch.path.as_deref().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 git_repository_edit_url = + if let Some(ref edit_url_template) = html_config.edit_url_template { + let full_path = ctx.config.book.src.to_str().unwrap_or_default().to_owned() + + "/" + + ch.source_path + .clone() + .unwrap_or_default() + .to_str() + .unwrap_or_default(); + + Some(edit_url_template.replace("{path}", &full_path)) + } else { + None + }; - let content = utils::render_markdown(&ch.content, ctx.html_config.smart_punctuation()); + let content = utils::render_markdown(&ch.content, html_config.smart_punctuation()); let fixed_content = utils::render_markdown_with_path( &ch.content, - ctx.html_config.smart_punctuation(), + html_config.smart_punctuation(), Some(path), ); - if !ctx.is_index && ctx.html_config.print.page_break { + if !is_index && 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 @@ -81,58 +342,74 @@ impl HtmlHandlebars { 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 + let book_title = ctx.config.book.title.as_deref().unwrap_or(""); + 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())); - } + // FIXME: is it actually needed? + // if let Some(ref section) = ch.number { + // ctx.data + // .insert("section".to_owned(), json!(section.to_string())); + // } + let path_to_root = utils::fs::path_to_root(path); // Render the handlebars template with the data debug!("Render template"); - let rendered = ctx.handlebars.render("index", &ctx.data)?; + let rendered = Index::new( + &ctx, + html_config, + is_index, + path.to_str().expect("failed to convert path to str"), + theme, + &title, + false, + git_repository_edit_url.as_deref(), + path_to_root, + &content, + None, + chapters, + )? + .render()?; let rendered = self.post_process( rendered, - &ctx.html_config.playground, - &ctx.html_config.code, - ctx.edition, + &html_config.playground, + &html_config.code, + ctx.config.rust.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)?; + if is_index { + let rendered_index = Index::new( + &ctx, + html_config, + true, + "index.md", + theme, + &title, + false, + git_repository_edit_url.as_deref(), + String::new(), + &content, + None, + chapters, + )? + .render()?; let rendered_index = self.post_process( rendered_index, - &ctx.html_config.playground, - &ctx.html_config.code, - ctx.edition, + &html_config.playground, + &html_config.code, + ctx.config.rust.edition, ); debug!("Creating index.html from {}", ctx_path); utils::fs::write_file(&ctx.destination, "index.html", rendered_index.as_bytes())?; @@ -146,8 +423,7 @@ impl HtmlHandlebars { ctx: &RenderContext, html_config: &HtmlConfig, src_dir: &Path, - handlebars: &mut Handlebars<'_>, - data: &mut serde_json::Map, + theme: &Theme, ) -> Result<()> { let destination = &ctx.destination; let content_404 = if let Some(ref filename) = html_config.input_404 { @@ -170,8 +446,7 @@ impl HtmlHandlebars { let html_content_404 = utils::render_markdown(&content_404, html_config.smart_punctuation()); - let mut data_404 = data.clone(); - let base_url = if let Some(site_url) = &html_config.site_url { + let base_url = Some(if let Some(site_url) = &html_config.site_url { site_url } else { debug!( @@ -180,19 +455,28 @@ impl HtmlHandlebars { subdirectory on the HTTP server." ); "/" - }; - data_404.insert("base_url".to_owned(), json!(base_url)); - // Set a dummy path to ensure other paths (e.g. in the TOC) are generated correctly - data_404.insert("path".to_owned(), json!("404.md")); - data_404.insert("content".to_owned(), json!(html_content_404)); + }); let mut title = String::from("Page not found"); if let Some(book_title) = &ctx.config.book.title { title.push_str(" - "); title.push_str(book_title); } - data_404.insert("title".to_owned(), json!(title)); - let rendered = handlebars.render("index", &data_404)?; + let rendered = Index::new( + ctx, + html_config, + false, + "404.md", + theme, + &title, + false, + None, + String::new(), + &html_content_404, + base_url, + &[], + )? + .render()?; let rendered = self.post_process( rendered, @@ -350,37 +634,6 @@ impl HtmlHandlebars { Ok(()) } - /// Update the context with data for this file - fn configure_print_version( - &self, - data: &mut serde_json::Map, - print_content: &str, - ) { - // Make sure that the Print chapter does not display the title from - // the last rendered chapter by removing it from its context - data.remove("title"); - data.insert("is_print".to_owned(), json!(true)); - data.insert("path".to_owned(), json!("print.md")); - data.insert("content".to_owned(), json!(print_content)); - data.insert( - "path_to_root".to_owned(), - json!(utils::fs::path_to_root(Path::new("print.md"))), - ); - } - - fn register_hbs_helpers(&self, handlebars: &mut Handlebars<'_>, html_config: &HtmlConfig) { - handlebars.register_helper( - "toc", - Box::new(helpers::toc::RenderToc { - no_section_label: html_config.no_section_label, - }), - ); - handlebars.register_helper("previous", Box::new(helpers::navigation::previous)); - handlebars.register_helper("next", Box::new(helpers::navigation::next)); - // TODO: remove theme_option in 0.5, it is not needed. - handlebars.register_helper("theme_option", Box::new(helpers::theme::theme_option)); - } - /// Copy across any additional CSS and JavaScript files which the book /// has been configured to use. fn copy_additional_css_and_js( @@ -418,12 +671,7 @@ impl HtmlHandlebars { Ok(()) } - fn emit_redirects( - &self, - root: &Path, - handlebars: &Handlebars<'_>, - redirects: &HashMap, - ) -> Result<()> { + fn emit_redirects(&self, root: &Path, redirects: &HashMap) -> Result<()> { if redirects.is_empty() { return Ok(()); } @@ -437,18 +685,13 @@ impl HtmlHandlebars { // up `root.join(original)`). let original = original.trim_start_matches('/'); let filename = root.join(original); - self.emit_redirect(handlebars, &filename, new)?; + self.emit_redirect(&filename, new)?; } Ok(()) } - fn emit_redirect( - &self, - handlebars: &Handlebars<'_>, - original: &Path, - destination: &str, - ) -> Result<()> { + fn emit_redirect(&self, original: &Path, destination: &str) -> Result<()> { if original.exists() { // sanity check to avoid accidentally overwriting a real file. let msg = format!( @@ -456,7 +699,7 @@ impl HtmlHandlebars { original.display(), destination, ); - return Err(Error::msg(msg)); + return Err(Error::Custom(msg.into()).into()); } if let Some(parent) = original.parent() { @@ -464,24 +707,20 @@ impl HtmlHandlebars { .with_context(|| format!("Unable to ensure \"{}\" exists", parent.display()))?; } - let ctx = json!({ - "url": destination, - }); - let f = File::create(original)?; - handlebars - .render_to_write("redirect", &ctx, f) - .with_context(|| { - format!( - "Unable to create a redirect file at \"{}\"", - original.display() - ) - })?; + let mut f = File::create(original)?; + let r = Redirect { url: destination }; + r.write_into(&mut f).with_context(|| { + format!( + "Unable to create a redirect file at \"{}\"", + original.display() + ) + })?; Ok(()) } } -impl Renderer for HtmlHandlebars { +impl Renderer for HtmlRenderer { fn name(&self) -> &str { "html" } @@ -500,8 +739,6 @@ impl Renderer for HtmlHandlebars { } trace!("render"); - let mut handlebars = Handlebars::new(); - let theme_dir = match html_config.theme { Some(ref theme) => { let dir = ctx.root.join(theme); @@ -515,28 +752,7 @@ impl Renderer for HtmlHandlebars { let theme = theme::Theme::new(theme_dir); - debug!("Register the index handlebars template"); - handlebars.register_template_string("index", String::from_utf8(theme.index.clone())?)?; - - debug!("Register the head handlebars template"); - handlebars.register_partial("head", String::from_utf8(theme.head.clone())?)?; - - debug!("Register the redirect handlebars template"); - handlebars - .register_template_string("redirect", String::from_utf8(theme.redirect.clone())?)?; - - debug!("Register the header handlebars template"); - handlebars.register_partial("header", String::from_utf8(theme.header.clone())?)?; - - debug!("Register the toc handlebars template"); - handlebars.register_template_string("toc_js", String::from_utf8(theme.toc_js.clone())?)?; - handlebars - .register_template_string("toc_html", String::from_utf8(theme.toc_html.clone())?)?; - - debug!("Register handlebars helpers"); - self.register_hbs_helpers(&mut handlebars, &html_config); - - let mut data = make_data(&ctx.root, book, &ctx.config, &html_config, &theme)?; + let chapters = make_chapters(book, &html_config)?; // Print version let mut print_content = String::new(); @@ -546,36 +762,45 @@ impl Renderer for HtmlHandlebars { let mut is_index = true; for item in book.iter() { - let ctx = RenderItemContext { - handlebars: &handlebars, - destination: destination.to_path_buf(), - data: data.clone(), + self.render_item( + item, + &html_config, 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)?; + ctx, + &theme, + &mut print_content, + &chapters, + )?; // Only the first non-draft chapter item should be treated as the "index" is_index &= !matches!(item, BookItem::Chapter(ch) if !ch.is_draft_chapter()); } // Render 404 page if html_config.input_404 != Some("".to_string()) { - self.render_404(ctx, &html_config, &src_dir, &mut handlebars, &mut data)?; + self.render_404(ctx, &html_config, &src_dir, &theme)?; } // Print version - self.configure_print_version(&mut data, &print_content); - if let Some(ref title) = ctx.config.book.title { - data.insert("title".to_owned(), json!(title)); - } - // Render the handlebars template with the data if html_config.print.enable { debug!("Render template"); - let rendered = handlebars.render("index", &data)?; + let path_root = utils::fs::path_to_root(Path::new("print.md")); + + let rendered = Index::new( + ctx, + &html_config, + false, + "print.md", + &theme, + ctx.config.book.title.as_deref().unwrap_or(""), + true, + None, + path_root, + &print_content, + None, + &chapters, + )? + .render()?; let rendered = self.post_process( rendered, @@ -590,14 +815,18 @@ impl Renderer for HtmlHandlebars { debug!("Render toc"); { - let rendered_toc = handlebars.render("toc_js", &data)?; + let rendered_toc = TocJS { + html_config: &html_config, + chapters: &chapters, + } + .render()?; utils::fs::write_file(destination, "toc.js", rendered_toc.as_bytes())?; debug!("Creating toc.js ✓"); - data.insert("is_toc_html".to_owned(), json!(true)); - let rendered_toc = handlebars.render("toc_html", &data)?; + // data.insert("is_toc_html".to_owned(), json!(true)); + let rendered_toc = Toc::new(&html_config, &book_config, &theme, &chapters).render()?; utils::fs::write_file(destination, "toc.html", rendered_toc.as_bytes())?; debug!("Creating toc.html ✓"); - data.remove("is_toc_html"); + // data.remove("is_toc_html"); } debug!("Copy static files"); @@ -615,7 +844,7 @@ impl Renderer for HtmlHandlebars { } } - self.emit_redirects(&ctx.destination, &handlebars, &html_config.redirect) + self.emit_redirects(&ctx.destination, &html_config.redirect) .context("Unable to emit redirects")?; // Copy all remaining files, avoid a recursive copy from/to the book build dir @@ -625,122 +854,21 @@ impl Renderer for HtmlHandlebars { } } -fn make_data( - root: &Path, - book: &Book, - config: &Config, - html_config: &HtmlConfig, - theme: &Theme, -) -> Result> { - trace!("make_data"); - - let mut data = serde_json::Map::new(); - data.insert( - "language".to_owned(), - json!(config.book.language.clone().unwrap_or_default()), - ); - data.insert( - "text_direction".to_owned(), - json!(config.book.realized_text_direction()), - ); - data.insert( - "book_title".to_owned(), - json!(config.book.title.clone().unwrap_or_default()), - ); - data.insert( - "description".to_owned(), - json!(config.book.description.clone().unwrap_or_default()), - ); - if theme.favicon_png.is_some() { - data.insert("favicon_png".to_owned(), json!("favicon.png")); - } - if theme.favicon_svg.is_some() { - data.insert("favicon_svg".to_owned(), json!("favicon.svg")); - } - if let Some(ref live_reload_endpoint) = html_config.live_reload_endpoint { - data.insert( - "live_reload_endpoint".to_owned(), - json!(live_reload_endpoint), - ); - } - - // TODO: remove default_theme in 0.5, it is not needed. - let default_theme = match html_config.default_theme { - Some(ref theme) => theme.to_lowercase(), - None => "light".to_string(), - }; - data.insert("default_theme".to_owned(), json!(default_theme)); - - let preferred_dark_theme = match html_config.preferred_dark_theme { - Some(ref theme) => theme.to_lowercase(), - None => "navy".to_string(), - }; - data.insert( - "preferred_dark_theme".to_owned(), - json!(preferred_dark_theme), - ); - - // Add google analytics tag - if let Some(ref ga) = html_config.google_analytics { - data.insert("google_analytics".to_owned(), json!(ga)); - } - - if html_config.mathjax_support { - data.insert("mathjax_support".to_owned(), json!(true)); - } - - // This `matches!` checks for a non-empty file. - if html_config.copy_fonts || matches!(theme.fonts_css.as_deref(), Some([_, ..])) { - data.insert("copy_fonts".to_owned(), json!(true)); - } - - // Add check to see if there is an additional style - if !html_config.additional_css.is_empty() { - let mut css = Vec::new(); - for style in &html_config.additional_css { - match style.strip_prefix(root) { - Ok(p) => css.push(p.to_str().expect("Could not convert to str")), - Err(_) => css.push(style.to_str().expect("Could not convert to str")), - } - } - data.insert("additional_css".to_owned(), json!(css)); - } - - // Add check to see if there is an additional script - if !html_config.additional_js.is_empty() { - let mut js = Vec::new(); - for script in &html_config.additional_js { - match script.strip_prefix(root) { - Ok(p) => js.push(p.to_str().expect("Could not convert to str")), - Err(_) => js.push(script.to_str().expect("Could not convert to str")), - } - } - data.insert("additional_js".to_owned(), json!(js)); - } +pub enum Chapter { + PartTitle(String), + Chapter { + section: Option, + has_sub_items: bool, + name: String, + path: Option, + }, + Separator, +} - if html_config.playground.editable && html_config.playground.copy_js { - data.insert("playground_js".to_owned(), json!(true)); - if html_config.playground.line_numbers { - data.insert("playground_line_numbers".to_owned(), json!(true)); - } - } - if html_config.playground.copyable { - data.insert("playground_copyable".to_owned(), json!(true)); - } +fn make_chapters(book: &Book, html_config: &HtmlConfig) -> Result> { + trace!("make_chapters"); - data.insert("print_enable".to_owned(), json!(html_config.print.enable)); - data.insert("fold_enable".to_owned(), json!(html_config.fold.enable)); - data.insert("fold_level".to_owned(), json!(html_config.fold.level)); - - let search = html_config.search.clone(); - if cfg!(feature = "search") { - let search = search.unwrap_or_default(); - data.insert("search_enabled".to_owned(), json!(search.enable)); - data.insert( - "search_js".to_owned(), - json!(search.enable && search.copy_js), - ); - } else if search.is_some() { + if !cfg!(feature = "search") && html_config.search.is_some() { warn!("mdBook compiled without search support, ignoring `output.html.search` table"); warn!( "please reinstall with `cargo install mdbook --force --features search`to use the \ @@ -748,56 +876,41 @@ fn make_data( ) } - if let Some(ref git_repository_url) = html_config.git_repository_url { - data.insert("git_repository_url".to_owned(), json!(git_repository_url)); - } - - let git_repository_icon = match html_config.git_repository_icon { - Some(ref git_repository_icon) => git_repository_icon, - None => "fa-github", - }; - data.insert("git_repository_icon".to_owned(), json!(git_repository_icon)); - let mut chapters = vec![]; for item in book.iter() { - // Create the data to inject in the template - let mut chapter = BTreeMap::new(); - match *item { BookItem::PartTitle(ref title) => { - chapter.insert("part".to_owned(), json!(title)); + chapters.push(Chapter::PartTitle(title.to_string())); } BookItem::Chapter(ref ch) => { - if let Some(ref section) = ch.number { - chapter.insert("section".to_owned(), json!(section.to_string())); - } - - chapter.insert( - "has_sub_items".to_owned(), - json!((!ch.sub_items.is_empty()).to_string()), - ); - - chapter.insert("name".to_owned(), json!(ch.name)); - if let Some(ref path) = ch.path { - let p = path - .to_str() - .with_context(|| "Could not convert path to str")?; - chapter.insert("path".to_owned(), json!(p)); - } + let section = ch.number.as_ref().map(|section| section.to_string()); + let has_sub_items = !ch.sub_items.is_empty(); + + let path = if let Some(ref path) = ch.path { + Some( + path.to_str() + .with_context(|| "Could not convert path to str")? + .to_string(), + ) + } else { + None + }; + chapters.push(Chapter::Chapter { + section, + has_sub_items, + name: ch.name.clone(), + path, + }); } BookItem::Separator => { - chapter.insert("spacer".to_owned(), json!("_spacer_")); + chapters.push(Chapter::Separator); } } - - chapters.push(chapter); } - data.insert("chapters".to_owned(), json!(chapters)); - - debug!("[*]: JSON constructed"); - Ok(data) + debug!("[*]: chapters constructed"); + Ok(chapters) } /// Goes through the rendered HTML, making sure all header tags have @@ -1062,17 +1175,6 @@ fn partition_source(s: &str) -> (String, String) { (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; @@ -1291,7 +1393,7 @@ mod tests { #[test] fn test_json_direction() { - assert_eq!(json!(TextDirection::RightToLeft), json!("rtl")); - assert_eq!(json!(TextDirection::LeftToRight), json!("ltr")); + assert_eq!(TextDirection::RightToLeft.as_str(), "rtl"); + assert_eq!(TextDirection::LeftToRight.as_str(), "ltr"); } } diff --git a/src/renderer/html_handlebars/mod.rs b/src/renderer/html_renderer/mod.rs similarity index 65% rename from src/renderer/html_handlebars/mod.rs rename to src/renderer/html_renderer/mod.rs index f1155ed759..c2df432bf2 100644 --- a/src/renderer/html_handlebars/mod.rs +++ b/src/renderer/html_renderer/mod.rs @@ -1,9 +1,9 @@ #![allow(missing_docs)] // FIXME: Document this -pub use self::hbs_renderer::HtmlHandlebars; +pub use self::hbs_renderer::HtmlRenderer; mod hbs_renderer; -mod helpers; +mod toc; #[cfg(feature = "search")] mod search; diff --git a/src/renderer/html_handlebars/search.rs b/src/renderer/html_renderer/search.rs similarity index 100% rename from src/renderer/html_handlebars/search.rs rename to src/renderer/html_renderer/search.rs diff --git a/src/renderer/html_renderer/toc.rs b/src/renderer/html_renderer/toc.rs new file mode 100644 index 0000000000..bbb84e0405 --- /dev/null +++ b/src/renderer/html_renderer/toc.rs @@ -0,0 +1,152 @@ +use std::cmp::Ordering; +use std::path::Path; + +use super::hbs_renderer::Chapter; +use crate::config::HtmlConfig; +use crate::utils::special_escape; + +pub fn render( + html_config: &HtmlConfig, + chapters: &[Chapter], + // If true, then this is the iframe and we need target="_parent" + is_toc_html: bool, +) -> String { + let mut out = String::new(); + + let fold_enable = html_config.fold.enable; + let fold_level = html_config.fold.level; + + out.push_str("
      "); + + let mut current_level = 1; + + for item in chapters { + let (has_section, level) = if let Chapter::Chapter { + section: Some(s), .. + } = item + { + (true, s.matches('.').count()) + } else { + (false, 1) + }; + + // Expand if folding is disabled, or if levels that are larger than this would not + // be folded. + let is_expanded = !fold_enable || level - 1 < (fold_level as usize); + + match level.cmp(¤t_level) { + Ordering::Greater => { + while level > current_level { + out.push_str("
    1. "); + out.push_str("
        "); + current_level += 1; + } + write_li_open_tag(&mut out, is_expanded, false); + } + Ordering::Less => { + while level < current_level { + out.push_str("
      "); + out.push_str("
    2. "); + current_level -= 1; + } + write_li_open_tag(&mut out, is_expanded, false); + } + Ordering::Equal => { + write_li_open_tag(&mut out, is_expanded, !has_section); + } + } + + // Spacer + if matches!(item, Chapter::Separator) { + out.push_str("
    3. "); + continue; + } + + // Part title + if let Chapter::PartTitle(title) = item { + out.push_str("
    4. "); + out.push_str(&special_escape(title)); + out.push_str("
    5. "); + continue; + } + + // Link + let path_exists: bool; + match item { + Chapter::Chapter { + path: Some(path), .. + } if !path.is_empty() => { + out.push_str("" + } else { + "\">" + }); + path_exists = true; + } + _ => { + out.push_str("" }); + + // Render expand/collapse toggle + if fold_enable { + if let Chapter::Chapter { + has_sub_items: true, + .. + } = item + { + out.push_str("
      "); + } + } + out.push_str(""); + } + while current_level > 1 { + out.push_str("
    "); + out.push_str("
  • "); + current_level -= 1; + } + + out.push_str(""); + out +} + +fn write_li_open_tag(out: &mut String, is_expanded: bool, is_affix: bool) { + out.push_str("
  • "); +} diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 1c97f8f221..0ce0fcbd04 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -11,10 +11,10 @@ //! [For Developers]: https://rust-lang.github.io/mdBook/for_developers/index.html //! [RenderContext]: struct.RenderContext.html -pub use self::html_handlebars::HtmlHandlebars; +pub use self::html_renderer::HtmlRenderer; pub use self::markdown_renderer::MarkdownRenderer; -mod html_handlebars; +mod html_renderer; mod markdown_renderer; use shlex::Shlex; @@ -38,7 +38,7 @@ use serde::{Deserialize, Serialize}; /// provide your own renderer, there are two main renderer implementations that /// 99% of users will ever use: /// -/// - [`HtmlHandlebars`] - the built-in HTML renderer +/// - [`HtmlRenderer`] - the built-in HTML renderer /// - [`CmdRenderer`] - a generic renderer which shells out to a program to do the /// actual rendering pub trait Renderer { diff --git a/src/theme/head.hbs b/src/theme/head.hbs deleted file mode 100644 index cb1be1876c..0000000000 --- a/src/theme/head.hbs +++ /dev/null @@ -1 +0,0 @@ -{{!-- Put your head HTML text here --}} diff --git a/src/theme/header.hbs b/src/theme/header.hbs deleted file mode 100644 index 26fa2d2efe..0000000000 --- a/src/theme/header.hbs +++ /dev/null @@ -1 +0,0 @@ -{{!-- Put your header HTML text here --}} \ No newline at end of file diff --git a/src/theme/index.hbs b/src/theme/index.hbs deleted file mode 100644 index 7775f262d6..0000000000 --- a/src/theme/index.hbs +++ /dev/null @@ -1,325 +0,0 @@ - - - - - - {{ title }} - {{#if is_print }} - - {{/if}} - {{#if base_url}} - - {{/if}} - - - - {{> head}} - - - - - - {{#if favicon_svg}} - - {{/if}} - {{#if favicon_png}} - - {{/if}} - - - - {{#if print_enable}} - - {{/if}} - - - - {{#if copy_fonts}} - - {{/if}} - - - - - - - - {{#each additional_css}} - - {{/each}} - - {{#if mathjax_support}} - - - {{/if}} - - - - - - - -
    - - - - - - - - - - - - - -
    - -
    - {{> header}} - - - - {{#if search_enabled}} - - {{/if}} - - - - -
    -
    - {{{ content }}} -
    - - -
    -
    - - - -
    - - {{#if live_reload_endpoint}} - - - {{/if}} - - {{#if google_analytics}} - - - {{/if}} - - {{#if playground_line_numbers}} - - {{/if}} - - {{#if playground_copyable}} - - {{/if}} - - {{#if playground_js}} - - - - - - {{/if}} - - {{#if search_js}} - - - - {{/if}} - - - - - - - {{#each additional_js}} - - {{/each}} - - {{#if is_print}} - {{#if mathjax_support}} - - {{else}} - - {{/if}} - {{/if}} - -
    - - diff --git a/src/theme/mod.rs b/src/theme/mod.rs index b173bd4a19..fa214eb67a 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -13,12 +13,6 @@ use std::path::{Path, PathBuf}; use crate::errors::*; use log::warn; -pub static INDEX: &[u8] = include_bytes!("index.hbs"); -pub static HEAD: &[u8] = include_bytes!("head.hbs"); -pub static REDIRECT: &[u8] = include_bytes!("redirect.hbs"); -pub static HEADER: &[u8] = include_bytes!("header.hbs"); -pub static TOC_JS: &[u8] = include_bytes!("toc.js.hbs"); -pub static TOC_HTML: &[u8] = include_bytes!("toc.html.hbs"); pub static CHROME_CSS: &[u8] = include_bytes!("css/chrome.css"); pub static GENERAL_CSS: &[u8] = include_bytes!("css/general.css"); pub static PRINT_CSS: &[u8] = include_bytes!("css/print.css"); @@ -48,12 +42,6 @@ pub static FONT_AWESOME_OTF: &[u8] = include_bytes!("FontAwesome/fonts/FontAweso /// override the user's theme with the defaults. #[derive(Debug, PartialEq)] pub struct Theme { - pub index: Vec, - pub head: Vec, - pub redirect: Vec, - pub header: Vec, - pub toc_js: Vec, - pub toc_html: Vec, pub chrome_css: Vec, pub general_css: Vec, pub print_css: Vec, @@ -85,12 +73,6 @@ impl Theme { // Check for individual files, if they exist copy them across { let files = vec![ - (theme_dir.join("index.hbs"), &mut theme.index), - (theme_dir.join("head.hbs"), &mut theme.head), - (theme_dir.join("redirect.hbs"), &mut theme.redirect), - (theme_dir.join("header.hbs"), &mut theme.header), - (theme_dir.join("toc.js.hbs"), &mut theme.toc_js), - (theme_dir.join("toc.html.hbs"), &mut theme.toc_html), (theme_dir.join("book.js"), &mut theme.js), (theme_dir.join("css/chrome.css"), &mut theme.chrome_css), (theme_dir.join("css/general.css"), &mut theme.general_css), @@ -176,12 +158,6 @@ impl Theme { impl Default for Theme { fn default() -> Theme { Theme { - index: INDEX.to_owned(), - head: HEAD.to_owned(), - redirect: REDIRECT.to_owned(), - header: HEADER.to_owned(), - toc_js: TOC_JS.to_owned(), - toc_html: TOC_HTML.to_owned(), chrome_css: CHROME_CSS.to_owned(), general_css: GENERAL_CSS.to_owned(), print_css: PRINT_CSS.to_owned(), @@ -236,12 +212,6 @@ mod tests { #[test] fn theme_dir_overrides_defaults() { let files = [ - "index.hbs", - "head.hbs", - "redirect.hbs", - "header.hbs", - "toc.js.hbs", - "toc.html.hbs", "favicon.png", "favicon.svg", "css/chrome.css", @@ -269,12 +239,6 @@ mod tests { let got = Theme::new(temp.path()); let empty = Theme { - index: Vec::new(), - head: Vec::new(), - redirect: Vec::new(), - header: Vec::new(), - toc_js: Vec::new(), - toc_html: Vec::new(), chrome_css: Vec::new(), general_css: Vec::new(), print_css: Vec::new(), diff --git a/templates/head.html b/templates/head.html new file mode 100644 index 0000000000..33cd06e4f6 --- /dev/null +++ b/templates/head.html @@ -0,0 +1 @@ +{# Put your head HTML text here #} diff --git a/templates/header.html b/templates/header.html new file mode 100644 index 0000000000..f88e1c133b --- /dev/null +++ b/templates/header.html @@ -0,0 +1 @@ +{# Put your header HTML text here #} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000000..d739487119 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,342 @@ + + + + {# Book generated using mdBook #} + + {{ title }} + {%- if is_print -%} + + {%- endif -%} + {%- if let Some(base_url) = base_url -%} + + {%- endif -%} + + {# Custom HTML head #} + {% include "head.html" %} + + {%- if let Some(description) = book_config.description -%} + + {%- endif -%} + + + + {%- if has_favicon_svg -%} + + {%- endif -%} + {%- if has_favicon_png -%} + + {%- endif -%} + + + + {%- if html_config.print.enable -%} + + {%- endif -%} + + {# Fonts #} + + {%- if copy_fonts -%} + + {%- endif -%} + + {# Highlight.js Stylesheets #} + + + + + {# Custom theme stylesheets #} + {%- for extra_css in additional_css() -%} + + {%- endfor -%} + + {%- if html_config.mathjax_support -%} + {# MathJax #} + + {%- endif -%} + + {# Provide site root to javascript #} + + {# Start loading toc.js asap #} + + + +
    + {# Work around some values being stored in localStorage wrapped in quotes #} + + + {# Set the theme before any content is loaded, prevents flash #} + + + + + {# Hide / unhide sidebar before it is displayed #} + + + + +
    +
    + {% include "header.html" %} + + + + {%- if cfg!(feature = "search") -%} + {%- if let Some(search) = html_config.search -%} + {%- if search.enable -%} + + {% endif %} + {% endif %} + {% endif %} + {# Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM #} + + +
    +
    + {{ content|safe }} +
    + + +
    +
    + + + +
    + + {%- if let Some(live_reload_endpoint) = html_config.live_reload_endpoint -%} + {#- Livereload script (if served using the cli tool) -#} + + {%- endif -%} + + {%- if let Some(google_analytics) = html_config.google_analytics -%} + {#- Google Analytics Tag -#} + + {%- endif -%} + + {%- if html_config.playground.editable && html_config.playground.copy_js && html_config.playground.line_numbers -%} + + {%- endif -%} + + {%- if html_config.playground.copyable -%} + + {%- endif -%} + + {%- if html_config.playground.editable && html_config.playground.copy_js -%} + + + + + + {%- endif -%} + + {%- if cfg!(feature = "search") -%} + {%- if let Some(search) = html_config.search -%} + {%- if search.enable && search.copy_js -%} + + + + {%- endif -%} + {%- endif -%} + {%- endif -%} + + + + + + {#- Custom JS scripts #} + {%- for extra_js in additional_js() -%} + + {%- endfor -%} + + {%- if is_print -%} + {%- if html_config.mathjax_support -%} + + {%- else -%} + + {%- endif -%} + {%- endif -%} + +
    + + diff --git a/src/theme/redirect.hbs b/templates/redirect.html similarity index 100% rename from src/theme/redirect.hbs rename to templates/redirect.html diff --git a/src/theme/toc.html.hbs b/templates/toc.html similarity index 51% rename from src/theme/toc.html.hbs rename to templates/toc.html index f8fca87353..591a520120 100644 --- a/src/theme/toc.html.hbs +++ b/templates/toc.html @@ -1,7 +1,7 @@ - + - + -#} - {{#if base_url}} - - {{/if}} - - {{> head}} + {% if let Some(base_url) = html_config.site_url %} + + {%- endif -%} + {# Custom HTML head #} + {%- include "head.html" -%} - {{#if print_enable}} - - {{/if}} - + {%- if html_config.print.enable -%} + + {%- endif -%} + {# Fonts #} - {{#if copy_fonts}} - - {{/if}} - - {{#each additional_css}} - - {{/each}} + {%- if copy_fonts -%} + + {%- endif -%} + {# Custom theme stylesheets #} + {% for extra_css in additional_css() %} + + {% endfor %} - - {{#toc}}{{/toc}} - + {{ crate::renderer::html_renderer::toc::render(html_config, chapters, true)|safe }} diff --git a/src/theme/toc.js.hbs b/templates/toc.js similarity index 96% rename from src/theme/toc.js.hbs rename to templates/toc.js index 7adf0c2789..a21133cc1c 100644 --- a/src/theme/toc.js.hbs +++ b/templates/toc.js @@ -8,7 +8,7 @@ class MDBookSidebarScrollbox extends HTMLElement { super(); } connectedCallback() { - this.innerHTML = '{{#toc}}{{/toc}}'; + this.innerHTML = '{{ crate::renderer::html_renderer::toc::render(html_config, chapters, false) }}'; // Set the current, active page, and reveal it if it's hidden let current_page = document.location.href.toString(); if (current_page.endsWith("/")) { diff --git a/tests/init.rs b/tests/init.rs index e952ed1991..3c5fac76d3 100644 --- a/tests/init.rs +++ b/tests/init.rs @@ -136,7 +136,6 @@ fn copy_theme() { "fonts/source-code-pro-v11-all-charsets-500.woff2", "highlight.css", "highlight.js", - "index.hbs", ]; let theme_dir = temp.path().join("theme"); let mut actual: Vec<_> = walkdir::WalkDir::new(&theme_dir) diff --git a/tests/rendered_output.rs b/tests/rendered_output.rs index 707b997db6..8d9d02a1db 100644 --- a/tests/rendered_output.rs +++ b/tests/rendered_output.rs @@ -494,24 +494,6 @@ fn first_chapter_is_copied_as_index_even_if_not_first_elem() { pretty_assertions::assert_eq!(chapter, index); } -#[test] -fn theme_dir_overrides_work_correctly() { - let book_dir = dummy_book::new_copy_of_example_book().unwrap(); - let book_dir = book_dir.path(); - let theme_dir = book_dir.join("theme"); - - let mut index = mdbook::theme::INDEX.to_vec(); - index.extend_from_slice(b"\n"); - - write_file(&theme_dir, "index.hbs", &index).unwrap(); - - let md = MDBook::load(book_dir).unwrap(); - md.build().unwrap(); - - let built_index = book_dir.join("book").join("index.html"); - dummy_book::assert_contains_strings(built_index, &["This is a modified index.hbs!"]); -} - #[test] fn no_index_for_print_html() { let temp = DummyBook::new().build().unwrap();