diff --git a/guide/src/format/mdbook.md b/guide/src/format/mdbook.md
index a9221ee1..169a8baa 100644
--- a/guide/src/format/mdbook.md
+++ b/guide/src/format/mdbook.md
@@ -192,3 +192,12 @@ Here is what a rendered code snippet looks like:
{{#playground example.rs}}
[Rust Playground]: https://play.rust-lang.org/
+
+## Controlling page \
+
+A chapter can set a \ that is different from its entry in the table of
+contents (sidebar) by including a `\{{#title ...}}` near the top of the page.
+
+```hbs
+\{{#title My Title}}
+```
diff --git a/src/book/mod.rs b/src/book/mod.rs
index 161ffc01..286234bd 100644
--- a/src/book/mod.rs
+++ b/src/book/mod.rs
@@ -196,23 +196,20 @@ impl MDBook {
}
}
- info!("Running the {} backend", renderer.name());
- self.render(&preprocessed_book, renderer)?;
-
- Ok(())
- }
-
- fn render(&self, preprocessed_book: &Book, renderer: &dyn Renderer) -> Result<()> {
let name = renderer.name();
let build_dir = self.build_dir_for(name);
- let render_context = RenderContext::new(
+ let mut render_context = RenderContext::new(
self.root.clone(),
- preprocessed_book.clone(),
+ preprocessed_book,
self.config.clone(),
build_dir,
);
+ render_context
+ .chapter_titles
+ .extend(preprocess_ctx.chapter_titles.borrow_mut().drain());
+ info!("Running the {} backend", renderer.name());
renderer
.render(&render_context)
.with_context(|| "Rendering failed")
diff --git a/src/preprocess/links.rs b/src/preprocess/links.rs
index 0e2eefbc..edd97ba9 100644
--- a/src/preprocess/links.rs
+++ b/src/preprocess/links.rs
@@ -23,6 +23,7 @@ const MAX_LINK_NESTED_DEPTH: usize = 10;
/// This hides the lines from initial display but shows them when the reader expands the code
/// block and provides them to Rustdoc for testing.
/// - `{{# playground}}` - Insert runnable Rust files
+/// - `{{# title}}` - Override \ of a webpage.
#[derive(Default)]
pub struct LinkPreprocessor;
@@ -51,8 +52,15 @@ impl Preprocessor for LinkPreprocessor {
.map(|dir| src_dir.join(dir))
.expect("All book items have a parent");
- let content = replace_all(&ch.content, base, chapter_path, 0);
+ let mut chapter_title = ch.name.clone();
+ let content =
+ replace_all(&ch.content, base, chapter_path, 0, &mut chapter_title);
ch.content = content;
+ if chapter_title != ch.name {
+ ctx.chapter_titles
+ .borrow_mut()
+ .insert(chapter_path.clone(), chapter_title);
+ }
}
}
});
@@ -61,7 +69,13 @@ impl Preprocessor for LinkPreprocessor {
}
}
-fn replace_all(s: &str, path: P1, source: P2, depth: usize) -> String
+fn replace_all(
+ s: &str,
+ path: P1,
+ source: P2,
+ depth: usize,
+ chapter_title: &mut String,
+) -> String
where
P1: AsRef,
P2: AsRef,
@@ -77,11 +91,17 @@ where
for link in find_links(s) {
replaced.push_str(&s[previous_end_index..link.start_index]);
- match link.render_with_path(&path) {
+ match link.render_with_path(&path, chapter_title) {
Ok(new_content) => {
if depth < MAX_LINK_NESTED_DEPTH {
if let Some(rel_path) = link.link_type.relative_path(path) {
- replaced.push_str(&replace_all(&new_content, rel_path, source, depth + 1));
+ replaced.push_str(&replace_all(
+ &new_content,
+ rel_path,
+ source,
+ depth + 1,
+ chapter_title,
+ ));
} else {
replaced.push_str(&new_content);
}
@@ -116,6 +136,7 @@ enum LinkType<'a> {
Include(PathBuf, RangeOrAnchor),
Playground(PathBuf, Vec<&'a str>),
RustdocInclude(PathBuf, RangeOrAnchor),
+ Title(&'a str),
}
#[derive(PartialEq, Debug, Clone)]
@@ -185,6 +206,7 @@ impl<'a> LinkType<'a> {
LinkType::Include(p, _) => Some(return_relative_path(base, &p)),
LinkType::Playground(p, _) => Some(return_relative_path(base, &p)),
LinkType::RustdocInclude(p, _) => Some(return_relative_path(base, &p)),
+ LinkType::Title(_) => None,
}
}
}
@@ -255,6 +277,9 @@ struct Link<'a> {
impl<'a> Link<'a> {
fn from_capture(cap: Captures<'a>) -> Option> {
let link_type = match (cap.get(0), cap.get(1), cap.get(2)) {
+ (_, Some(typ), Some(title)) if typ.as_str() == "title" => {
+ Some(LinkType::Title(title.as_str()))
+ }
(_, Some(typ), Some(rest)) => {
let mut path_props = rest.as_str().split_whitespace();
let file_arg = path_props.next();
@@ -291,7 +316,11 @@ impl<'a> Link<'a> {
})
}
- fn render_with_path>(&self, base: P) -> Result {
+ fn render_with_path>(
+ &self,
+ base: P,
+ chapter_title: &mut String,
+ ) -> Result {
let base = base.as_ref();
match self.link_type {
// omit the escape char
@@ -353,6 +382,10 @@ impl<'a> Link<'a> {
contents
))
}
+ LinkType::Title(title) => {
+ *chapter_title = title.to_owned();
+ Ok(String::new())
+ }
}
}
}
@@ -373,17 +406,17 @@ impl<'a> Iterator for LinkIter<'a> {
fn find_links(contents: &str) -> LinkIter<'_> {
// lazily compute following regex
- // r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([a-zA-Z0-9_.\-:/\\\s]+)\}\}")?;
+ // r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([^}]+)\}\}")?;
lazy_static! {
static ref RE: Regex = Regex::new(
- r"(?x) # insignificant whitespace mode
- \\\{\{\#.*\}\} # match escaped link
- | # or
- \{\{\s* # link opening parens and whitespace
- \#([a-zA-Z0-9_]+) # link type
- \s+ # separating whitespace
- ([a-zA-Z0-9\s_.\-:/\\\+]+) # link target path and space separated properties
- \s*\}\} # whitespace and link closing parens"
+ r"(?x) # insignificant whitespace mode
+ \\\{\{\#.*\}\} # match escaped link
+ | # or
+ \{\{\s* # link opening parens and whitespace
+ \#([a-zA-Z0-9_]+) # link type
+ \s+ # separating whitespace
+ ([^}]+) # link target path and space separated properties
+ \}\} # link closing parens"
)
.unwrap();
}
@@ -406,7 +439,21 @@ mod tests {
```hbs
{{#include file.rs}} << an escaped link!
```";
- assert_eq!(replace_all(start, "", "", 0), end);
+ let mut chapter_title = "test_replace_all_escaped".to_owned();
+ assert_eq!(replace_all(start, "", "", 0, &mut chapter_title), end);
+ }
+
+ #[test]
+ fn test_set_chapter_title() {
+ let start = r"{{#title My Title}}
+ # My Chapter
+ ";
+ let end = r"
+ # My Chapter
+ ";
+ let mut chapter_title = "test_set_chapter_title".to_owned();
+ assert_eq!(replace_all(start, "", "", 0, &mut chapter_title), end);
+ assert_eq!(chapter_title, "My Title");
}
#[test]
diff --git a/src/preprocess/mod.rs b/src/preprocess/mod.rs
index ebc34311..ee660636 100644
--- a/src/preprocess/mod.rs
+++ b/src/preprocess/mod.rs
@@ -12,6 +12,8 @@ use crate::book::Book;
use crate::config::Config;
use crate::errors::*;
+use std::cell::RefCell;
+use std::collections::HashMap;
use std::path::PathBuf;
/// Extra information for a `Preprocessor` to give them more context when
@@ -27,6 +29,8 @@ pub struct PreprocessorContext {
/// The calling `mdbook` version.
pub mdbook_version: String,
#[serde(skip)]
+ pub(crate) chapter_titles: RefCell>,
+ #[serde(skip)]
__non_exhaustive: (),
}
@@ -38,6 +42,7 @@ impl PreprocessorContext {
config,
renderer,
mdbook_version: crate::MDBOOK_VERSION.to_string(),
+ chapter_titles: RefCell::new(HashMap::new()),
__non_exhaustive: (),
}
}
diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs
index f417a014..254f189f 100644
--- a/src/renderer/html_handlebars/hbs_renderer.rs
+++ b/src/renderer/html_handlebars/hbs_renderer.rs
@@ -70,9 +70,12 @@ impl HtmlHandlebars {
.and_then(serde_json::Value::as_str)
.unwrap_or("");
- let title = match book_title {
- "" => ch.name.clone(),
- _ => ch.name.clone() + " - " + book_title,
+ let title = if let Some(title) = ctx.chapter_titles.get(path) {
+ title.clone()
+ } else if book_title.is_empty() {
+ ch.name.clone()
+ } else {
+ ch.name.clone() + " - " + book_title
};
ctx.data.insert("path".to_owned(), json!(path));
@@ -507,6 +510,7 @@ impl Renderer for HtmlHandlebars {
is_index,
html_config: html_config.clone(),
edition: ctx.config.rust.edition,
+ chapter_titles: &ctx.chapter_titles,
};
self.render_item(item, ctx, &mut print_content)?;
is_index = false;
@@ -922,6 +926,7 @@ struct RenderItemContext<'a> {
is_index: bool,
html_config: HtmlConfig,
edition: Option,
+ chapter_titles: &'a HashMap,
}
#[cfg(test)]
diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs
index a3350c57..05a5ac6f 100644
--- a/src/renderer/mod.rs
+++ b/src/renderer/mod.rs
@@ -18,6 +18,7 @@ mod html_handlebars;
mod markdown_renderer;
use shlex::Shlex;
+use std::collections::HashMap;
use std::fs;
use std::io::{self, ErrorKind, Read};
use std::path::{Path, PathBuf};
@@ -64,6 +65,8 @@ pub struct RenderContext {
/// guaranteed to be empty or even exist.
pub destination: PathBuf,
#[serde(skip)]
+ pub(crate) chapter_titles: HashMap,
+ #[serde(skip)]
__non_exhaustive: (),
}
@@ -80,6 +83,7 @@ impl RenderContext {
version: crate::MDBOOK_VERSION.to_string(),
root: root.into(),
destination: destination.into(),
+ chapter_titles: HashMap::new(),
__non_exhaustive: (),
}
}