From a5086a1e585a0bde48468ef1b31460758c9f5dd7 Mon Sep 17 00:00:00 2001 From: Michael-F-Bryan Date: Wed, 27 May 2020 02:04:12 +0800 Subject: [PATCH 01/10] Added a redirect map to the HTML config --- src/config.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/config.rs b/src/config.rs index 4fb20be0..15c041cb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -50,6 +50,7 @@ #![deny(missing_docs)] use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::collections::HashMap; use std::env; use std::fs::File; use std::io::Read; @@ -514,6 +515,9 @@ pub struct HtmlConfig { /// This config item *should not be edited* by the end user. #[doc(hidden)] pub livereload_url: Option, + /// The mapping from old pages to new pages/URLs to use when generating + /// redirects. + pub redirect: HashMap, } impl Default for HtmlConfig { @@ -693,6 +697,10 @@ mod tests { editable = true editor = "ace" + [output.html.redirect] + "index.html" = "overview.html" + "nexted/page.md" = "https://rust-lang.org/" + [preprocessor.first] [preprocessor.second] @@ -731,6 +739,15 @@ mod tests { playpen: playpen_should_be, git_repository_url: Some(String::from("https://foo.com/")), git_repository_icon: Some(String::from("fa-code-fork")), + redirect: vec![ + (PathBuf::from("index.html"), String::from("overview.html")), + ( + PathBuf::from("nexted/page.md"), + String::from("https://rust-lang.org/"), + ), + ] + .into_iter() + .collect(), ..Default::default() }; From b2d50392eab891a177447225d69a46e97d30a5e8 Mon Sep 17 00:00:00 2001 From: Michael-F-Bryan Date: Wed, 27 May 2020 02:23:36 +0800 Subject: [PATCH 02/10] Emit redirects towards the end of the rendering process --- src/config.rs | 1 + src/renderer/html_handlebars/hbs_renderer.rs | 48 +++++++++++++++++++- src/theme/mod.rs | 6 +++ src/theme/redirect.hbs | 12 +++++ 4 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 src/theme/redirect.hbs diff --git a/src/config.rs b/src/config.rs index 15c041cb..71edbaf2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -539,6 +539,7 @@ impl Default for HtmlConfig { git_repository_url: None, git_repository_icon: None, livereload_url: None, + redirect: HashMap::new(), } } } diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index d58bc805..2953f098 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -9,7 +9,7 @@ use crate::utils; use std::borrow::Cow; use std::collections::BTreeMap; use std::collections::HashMap; -use std::fs; +use std::fs::{self, File}; use std::path::{Path, PathBuf}; use handlebars::Handlebars; @@ -279,6 +279,46 @@ impl HtmlHandlebars { Ok(()) } + + fn emit_redirects( + &self, + root: &Path, + handlebars: &Handlebars<'_>, + redirects: &HashMap, + ) -> Result<()> { + if redirects.is_empty() { + return Ok(()); + } + + log::debug!("Emitting redirects"); + + for (original, new) in redirects { + log::debug!("Redirecting \"{}\" → \"{}\"", original.display(), new); + let filename = root.join(original); + self.emit_redirect(handlebars, &filename, new)?; + } + + Ok(()) + } + + fn emit_redirect( + &self, + handlebars: &Handlebars<'_>, + original: &Path, + destination: &str, + ) -> Result<()> { + if let Some(parent) = original.parent() { + std::fs::create_dir_all(parent)?; + } + + let ctx = json!({ + "url": destination, + }); + let f = File::create(original)?; + handlebars.render_to_write("redirect", &ctx, f)?; + + Ok(()) + } } // TODO(mattico): Remove some time after the 0.1.8 release @@ -343,6 +383,10 @@ impl Renderer for HtmlHandlebars { 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())?)?; @@ -401,6 +445,8 @@ impl Renderer for HtmlHandlebars { } } + self.emit_redirects(&ctx.destination, &handlebars, &html_config.redirect)?; + // Copy all remaining files, avoid a recursive copy from/to the book build dir utils::fs::copy_files_except_ext(&src_dir, &destination, true, Some(&build_dir), &["md"])?; diff --git a/src/theme/mod.rs b/src/theme/mod.rs index b8c496c3..38aa22e9 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -15,6 +15,7 @@ use crate::errors::*; 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 CHROME_CSS: &[u8] = include_bytes!("css/chrome.css"); pub static GENERAL_CSS: &[u8] = include_bytes!("css/general.css"); @@ -46,6 +47,7 @@ pub static FONT_AWESOME_OTF: &[u8] = include_bytes!("FontAwesome/fonts/FontAweso pub struct Theme { pub index: Vec, pub head: Vec, + pub redirect: Vec, pub header: Vec, pub chrome_css: Vec, pub general_css: Vec, @@ -77,6 +79,7 @@ impl Theme { 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("book.js"), &mut theme.js), (theme_dir.join("css/chrome.css"), &mut theme.chrome_css), @@ -120,6 +123,7 @@ impl Default for Theme { Theme { index: INDEX.to_owned(), head: HEAD.to_owned(), + redirect: REDIRECT.to_owned(), header: HEADER.to_owned(), chrome_css: CHROME_CSS.to_owned(), general_css: GENERAL_CSS.to_owned(), @@ -175,6 +179,7 @@ mod tests { let files = [ "index.hbs", "head.hbs", + "redirect.hbs", "header.hbs", "favicon.png", "css/chrome.css", @@ -203,6 +208,7 @@ mod tests { let empty = Theme { index: Vec::new(), head: Vec::new(), + redirect: Vec::new(), header: Vec::new(), chrome_css: Vec::new(), general_css: Vec::new(), diff --git a/src/theme/redirect.hbs b/src/theme/redirect.hbs new file mode 100644 index 00000000..9f49e6d0 --- /dev/null +++ b/src/theme/redirect.hbs @@ -0,0 +1,12 @@ + + + + + Redirecting... + + + + +

Redirecting to... {{url}}.

+ + From 4c2318922f861ac4a452a50ab202d2bb7eb1d35d Mon Sep 17 00:00:00 2001 From: Michael-F-Bryan Date: Wed, 27 May 2020 02:35:15 +0800 Subject: [PATCH 03/10] Added integration tests to make sure files are redirected as intended --- tests/rendered_output.rs | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/tests/rendered_output.rs b/tests/rendered_output.rs index 82b07045..3b76cbd2 100644 --- a/tests/rendered_output.rs +++ b/tests/rendered_output.rs @@ -12,10 +12,11 @@ use mdbook::utils::fs::write_file; use mdbook::MDBook; use select::document::Document; use select::predicate::{Class, Name, Predicate}; +use std::collections::HashMap; use std::ffi::OsStr; use std::fs; use std::io::Write; -use std::path::Path; +use std::path::{Path, PathBuf}; use tempfile::Builder as TempFileBuilder; use walkdir::{DirEntry, WalkDir}; @@ -511,6 +512,32 @@ fn markdown_options() { ); } +#[test] +fn redirects_are_emitted_correctly() { + let temp = DummyBook::new().build().unwrap(); + let mut md = MDBook::load(temp.path()).unwrap(); + + // override the "outputs.html.redirect" table + let redirects: HashMap = vec![ + (PathBuf::from("index.html"), String::from("overview.html")), + ( + PathBuf::from("nexted/page.md"), + String::from("https://rust-lang.org/"), + ), + ] + .into_iter() + .collect(); + md.config.set("output.html.redirect", &redirects).unwrap(); + + md.build().unwrap(); + + for (original, redirect) in &redirects { + let redirect_file = md.build_dir_for("html").join(original); + let contents = fs::read_to_string(&redirect_file).unwrap(); + assert!(contents.contains(redirect)); + } +} + #[cfg(feature = "search")] mod search { use crate::dummy_book::DummyBook; From a6ab4d8402eaa3ffcd6ac955fbd054c31d6c07c2 Mon Sep 17 00:00:00 2001 From: Michael-F-Bryan Date: Wed, 27 May 2020 02:52:59 +0800 Subject: [PATCH 04/10] Explained how you can configure redirects --- book-example/src/format/config.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/book-example/src/format/config.md b/book-example/src/format/config.md index c1a97aa7..e8b7e9aa 100644 --- a/book-example/src/format/config.md +++ b/book-example/src/format/config.md @@ -198,6 +198,10 @@ 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`. +- **redirect:** A subtable used for generating redirects when a page is moved. + The table contains key-value pairs where the key is the path to the moved + page, relative to the build directory and the value can be an ordinary URL + or the *absolute* path to another page in the book Available configuration options for the `[output.html.fold]` table: @@ -281,6 +285,10 @@ boost-paragraph = 1 expand = true heading-split-level = 3 copy-js = true + +[output.html.redirect] +"other-installation-methods.html" = "/infra/other-installation-methods.html" +"bibliography.html" = "https://rustc-dev-guide.rust-lang.org/appendix/bibliography.html" ``` ### Markdown Renderer @@ -291,7 +299,7 @@ conjunction with `mdbook test` to see the Markdown that `mdbook` is passing to `rustdoc`. The Markdown renderer is included with `mdbook` but disabled by default. -Enable it by adding an emtpy table to your `book.toml` as follows: +Enable it by adding an empty table to your `book.toml` as follows: ```toml [output.markdown] From da55cf273f8f054a5837ba2804dd70976ea6a3a0 Mon Sep 17 00:00:00 2001 From: Michael-F-Bryan Date: Wed, 27 May 2020 03:12:57 +0800 Subject: [PATCH 05/10] Changed redirect mapping to HashMap and improved error handling --- src/config.rs | 6 ++-- src/renderer/html_handlebars/hbs_renderer.rs | 33 +++++++++++++++++--- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/config.rs b/src/config.rs index 71edbaf2..dc712bbb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -517,7 +517,7 @@ pub struct HtmlConfig { pub livereload_url: Option, /// The mapping from old pages to new pages/URLs to use when generating /// redirects. - pub redirect: HashMap, + pub redirect: HashMap, } impl Default for HtmlConfig { @@ -741,9 +741,9 @@ mod tests { git_repository_url: Some(String::from("https://foo.com/")), git_repository_icon: Some(String::from("fa-code-fork")), redirect: vec![ - (PathBuf::from("index.html"), String::from("overview.html")), + (String::from("index.html"), String::from("overview.html")), ( - PathBuf::from("nexted/page.md"), + String::from("nexted/page.md"), String::from("https://rust-lang.org/"), ), ] diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 2953f098..ecb8359e 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -284,7 +284,7 @@ impl HtmlHandlebars { &self, root: &Path, handlebars: &Handlebars<'_>, - redirects: &HashMap, + redirects: &HashMap, ) -> Result<()> { if redirects.is_empty() { return Ok(()); @@ -293,7 +293,11 @@ impl HtmlHandlebars { log::debug!("Emitting redirects"); for (original, new) in redirects { - log::debug!("Redirecting \"{}\" → \"{}\"", original.display(), new); + log::debug!("Redirecting \"{}\" → \"{}\"", original, new); + // Note: all paths are relative to the build directory, so the + // leading slash in an absolute path means nothing (and would mess + // up `root.join(original)`). + let original = original.trim_start_matches("/"); let filename = root.join(original); self.emit_redirect(handlebars, &filename, new)?; } @@ -307,15 +311,33 @@ impl HtmlHandlebars { original: &Path, destination: &str, ) -> Result<()> { + if original.exists() { + // sanity check to avoid accidentally overwriting a real file. + let msg = format!( + "Not redirecting \"{}\" to \"{}\" because it already exists. Are you sure it needs to be redirected?", + original.display(), + destination, + ); + return Err(Error::msg(msg)); + } + if let Some(parent) = original.parent() { - std::fs::create_dir_all(parent)?; + std::fs::create_dir_all(parent) + .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)?; + handlebars + .render_to_write("redirect", &ctx, f) + .with_context(|| { + format!( + "Unable to create a redirect file at \"{}\"", + original.display() + ) + })?; Ok(()) } @@ -445,7 +467,8 @@ impl Renderer for HtmlHandlebars { } } - self.emit_redirects(&ctx.destination, &handlebars, &html_config.redirect)?; + self.emit_redirects(&ctx.destination, &handlebars, &html_config.redirect) + .context("Unable to emit redirects")?; // Copy all remaining files, avoid a recursive copy from/to the book build dir utils::fs::copy_files_except_ext(&src_dir, &destination, true, Some(&build_dir), &["md"])?; From 5907caa732eb7810d66485d33ace5aff67ab3fa4 Mon Sep 17 00:00:00 2001 From: Michael-F-Bryan Date: Wed, 27 May 2020 03:19:24 +0800 Subject: [PATCH 06/10] I should have used a more realistic example --- tests/rendered_output.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rendered_output.rs b/tests/rendered_output.rs index 3b76cbd2..c21d6276 100644 --- a/tests/rendered_output.rs +++ b/tests/rendered_output.rs @@ -519,7 +519,7 @@ fn redirects_are_emitted_correctly() { // override the "outputs.html.redirect" table let redirects: HashMap = vec![ - (PathBuf::from("index.html"), String::from("overview.html")), + (PathBuf::from("overview.html"), String::from("index.html")), ( PathBuf::from("nexted/page.md"), String::from("https://rust-lang.org/"), From 964a10ff29b1b9de3bf516768ec586097216fdd8 Mon Sep 17 00:00:00 2001 From: Michael-F-Bryan Date: Sat, 30 May 2020 04:09:43 +0800 Subject: [PATCH 07/10] Redid the wording in the docs because it was around the wrong way --- book-example/src/format/config.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/book-example/src/format/config.md b/book-example/src/format/config.md index e8b7e9aa..b3afc2d4 100644 --- a/book-example/src/format/config.md +++ b/book-example/src/format/config.md @@ -199,9 +199,11 @@ The following configuration options are available: - **git-repository-icon:** The FontAwesome icon class to use for the git repository link. Defaults to `fa-github`. - **redirect:** A subtable used for generating redirects when a page is moved. - The table contains key-value pairs where the key is the path to the moved - page, relative to the build directory and the value can be an ordinary URL - or the *absolute* path to another page in the book + The table contains key-value pairs where the key is where the redirect file + needs to be created, as an absolute path from the build directory, (e.g. + `/appendices/bibliography.html`). The value can be any valid URI the + browser should navigate to (e.g. `https://rust-lang.org/`, + `/overview.html`, or `../bibliography.html`). Available configuration options for the `[output.html.fold]` table: @@ -287,8 +289,8 @@ heading-split-level = 3 copy-js = true [output.html.redirect] -"other-installation-methods.html" = "/infra/other-installation-methods.html" -"bibliography.html" = "https://rustc-dev-guide.rust-lang.org/appendix/bibliography.html" +/appendices/"bibliography.html" = "https://rustc-dev-guide.rust-lang.org/appendix/bibliography.html" +"/other-installation-methods.html" = "../infra/other-installation-methods.html" ``` ### Markdown Renderer From 7b3e945a27f330365386dc865df238eb93ababf9 Mon Sep 17 00:00:00 2001 From: Michael-F-Bryan Date: Sat, 30 May 2020 04:11:11 +0800 Subject: [PATCH 08/10] Updated the test to reflect how redirect keys should be absolute --- tests/rendered_output.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/rendered_output.rs b/tests/rendered_output.rs index c21d6276..56615703 100644 --- a/tests/rendered_output.rs +++ b/tests/rendered_output.rs @@ -519,9 +519,9 @@ fn redirects_are_emitted_correctly() { // override the "outputs.html.redirect" table let redirects: HashMap = vec![ - (PathBuf::from("overview.html"), String::from("index.html")), + (PathBuf::from("/overview.html"), String::from("index.html")), ( - PathBuf::from("nexted/page.md"), + PathBuf::from("/nexted/page.md"), String::from("https://rust-lang.org/"), ), ] From ebc01dbb714e76e5fe8be09cb162d0ec92d23577 Mon Sep 17 00:00:00 2001 From: Michael-F-Bryan Date: Sat, 30 May 2020 04:15:24 +0800 Subject: [PATCH 09/10] Updated the test to follow our docs --- tests/rendered_output.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/rendered_output.rs b/tests/rendered_output.rs index 56615703..db0badd7 100644 --- a/tests/rendered_output.rs +++ b/tests/rendered_output.rs @@ -16,7 +16,7 @@ use std::collections::HashMap; use std::ffi::OsStr; use std::fs; use std::io::Write; -use std::path::{Path, PathBuf}; +use std::path::{Component, Path, PathBuf}; use tempfile::Builder as TempFileBuilder; use walkdir::{DirEntry, WalkDir}; @@ -532,12 +532,22 @@ fn redirects_are_emitted_correctly() { md.build().unwrap(); for (original, redirect) in &redirects { - let redirect_file = md.build_dir_for("html").join(original); + let mut redirect_file = md.build_dir_for("html"); + // append everything except the bits that make it absolute + // (e.g. "/" or "C:\") + redirect_file.extend(remove_absolute_components(&original)); let contents = fs::read_to_string(&redirect_file).unwrap(); assert!(contents.contains(redirect)); } } +fn remove_absolute_components(path: &Path) -> impl Iterator + '_ { + path.components().skip_while(|c| match c { + Component::Prefix(_) | Component::RootDir => true, + _ => false, + }) +} + #[cfg(feature = "search")] mod search { use crate::dummy_book::DummyBook; From 25ec7ace1a0d93762d8e0519deb7123eefc7f926 Mon Sep 17 00:00:00 2001 From: Eric Huss Date: Sat, 30 May 2020 10:27:38 -0700 Subject: [PATCH 10/10] Fix typo for redirect TOML. --- book-example/src/format/config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book-example/src/format/config.md b/book-example/src/format/config.md index b3afc2d4..cc5c6845 100644 --- a/book-example/src/format/config.md +++ b/book-example/src/format/config.md @@ -289,7 +289,7 @@ heading-split-level = 3 copy-js = true [output.html.redirect] -/appendices/"bibliography.html" = "https://rustc-dev-guide.rust-lang.org/appendix/bibliography.html" +"/appendices/bibliography.html" = "https://rustc-dev-guide.rust-lang.org/appendix/bibliography.html" "/other-installation-methods.html" = "../infra/other-installation-methods.html" ```