From 94f7578576b150edb8167b82ee36828e50530c12 Mon Sep 17 00:00:00 2001 From: David Tolnay Date: Tue, 23 Mar 2021 18:36:45 -0700 Subject: [PATCH] Add page title override: {{#title My Title}} (#1381) * Add page title override: {{#title My Title}} * Document {{#title}} in guide --- guide/src/format/mdbook.md | 9 +++ src/book/mod.rs | 15 ++-- src/preprocess/links.rs | 77 ++++++++++++++++---- src/preprocess/mod.rs | 5 ++ src/renderer/html_handlebars/hbs_renderer.rs | 11 ++- src/renderer/mod.rs | 4 + 6 files changed, 94 insertions(+), 27 deletions(-) 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: (), } }