Merge pull request #1237 from Michael-F-Bryan/redirects
Implement URL redirecting
This commit is contained in:
commit
b375f4e3d5
|
@ -198,6 +198,12 @@ 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 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:
|
||||
|
||||
|
@ -281,6 +287,10 @@ boost-paragraph = 1
|
|||
expand = true
|
||||
heading-split-level = 3
|
||||
copy-js = true
|
||||
|
||||
[output.html.redirect]
|
||||
"/appendices/bibliography.html" = "https://rustc-dev-guide.rust-lang.org/appendix/bibliography.html"
|
||||
"/other-installation-methods.html" = "../infra/other-installation-methods.html"
|
||||
```
|
||||
|
||||
### Markdown Renderer
|
||||
|
@ -291,7 +301,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]
|
||||
|
|
|
@ -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<String>,
|
||||
/// The mapping from old pages to new pages/URLs to use when generating
|
||||
/// redirects.
|
||||
pub redirect: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Default for HtmlConfig {
|
||||
|
@ -535,6 +539,7 @@ impl Default for HtmlConfig {
|
|||
git_repository_url: None,
|
||||
git_repository_icon: None,
|
||||
livereload_url: None,
|
||||
redirect: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -693,6 +698,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 +740,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![
|
||||
(String::from("index.html"), String::from("overview.html")),
|
||||
(
|
||||
String::from("nexted/page.md"),
|
||||
String::from("https://rust-lang.org/"),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
|
|
@ -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,68 @@ impl HtmlHandlebars {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn emit_redirects(
|
||||
&self,
|
||||
root: &Path,
|
||||
handlebars: &Handlebars<'_>,
|
||||
redirects: &HashMap<String, String>,
|
||||
) -> Result<()> {
|
||||
if redirects.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::debug!("Emitting redirects");
|
||||
|
||||
for (original, new) in redirects {
|
||||
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)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn emit_redirect(
|
||||
&self,
|
||||
handlebars: &Handlebars<'_>,
|
||||
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)
|
||||
.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()
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(mattico): Remove some time after the 0.1.8 release
|
||||
|
@ -343,6 +405,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 +467,9 @@ impl Renderer for HtmlHandlebars {
|
|||
}
|
||||
}
|
||||
|
||||
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"])?;
|
||||
|
||||
|
|
|
@ -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<u8>,
|
||||
pub head: Vec<u8>,
|
||||
pub redirect: Vec<u8>,
|
||||
pub header: Vec<u8>,
|
||||
pub chrome_css: Vec<u8>,
|
||||
pub general_css: Vec<u8>,
|
||||
|
@ -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(),
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Redirecting...</title>
|
||||
<meta http-equiv="refresh" content="0;URL='{{url}}'">
|
||||
<meta rel="canonical" href="{{url}}">
|
||||
</head>
|
||||
<body>
|
||||
<p>Redirecting to... <a href="{{url}}">{{url}}</a>.</p>
|
||||
</body>
|
||||
</html>
|
|
@ -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::{Component, Path, PathBuf};
|
||||
use tempfile::Builder as TempFileBuilder;
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
|
||||
|
@ -511,6 +512,42 @@ 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<PathBuf, String> = vec![
|
||||
(PathBuf::from("/overview.html"), String::from("index.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 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<Item = Component> + '_ {
|
||||
path.components().skip_while(|c| match c {
|
||||
Component::Prefix(_) | Component::RootDir => true,
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "search")]
|
||||
mod search {
|
||||
use crate::dummy_book::DummyBook;
|
||||
|
|
Loading…
Reference in New Issue