Add page title override: {{#title My Title}} (#1381)

* Add page title override: {{#title My Title}}

* Document {{#title}} in guide
This commit is contained in:
David Tolnay 2021-03-23 18:36:45 -07:00 committed by GitHub
parent e6568a70eb
commit 94f7578576
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 94 additions and 27 deletions

View File

@ -192,3 +192,12 @@ Here is what a rendered code snippet looks like:
{{#playground example.rs}} {{#playground example.rs}}
[Rust Playground]: https://play.rust-lang.org/ [Rust Playground]: https://play.rust-lang.org/
## Controlling page \<title\>
A chapter can set a \<title\> 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}}
```

View File

@ -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 name = renderer.name();
let build_dir = self.build_dir_for(name); let build_dir = self.build_dir_for(name);
let render_context = RenderContext::new( let mut render_context = RenderContext::new(
self.root.clone(), self.root.clone(),
preprocessed_book.clone(), preprocessed_book,
self.config.clone(), self.config.clone(),
build_dir, build_dir,
); );
render_context
.chapter_titles
.extend(preprocess_ctx.chapter_titles.borrow_mut().drain());
info!("Running the {} backend", renderer.name());
renderer renderer
.render(&render_context) .render(&render_context)
.with_context(|| "Rendering failed") .with_context(|| "Rendering failed")

View File

@ -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 /// This hides the lines from initial display but shows them when the reader expands the code
/// block and provides them to Rustdoc for testing. /// block and provides them to Rustdoc for testing.
/// - `{{# playground}}` - Insert runnable Rust files /// - `{{# playground}}` - Insert runnable Rust files
/// - `{{# title}}` - Override \<title\> of a webpage.
#[derive(Default)] #[derive(Default)]
pub struct LinkPreprocessor; pub struct LinkPreprocessor;
@ -51,8 +52,15 @@ impl Preprocessor for LinkPreprocessor {
.map(|dir| src_dir.join(dir)) .map(|dir| src_dir.join(dir))
.expect("All book items have a parent"); .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; 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<P1, P2>(s: &str, path: P1, source: P2, depth: usize) -> String fn replace_all<P1, P2>(
s: &str,
path: P1,
source: P2,
depth: usize,
chapter_title: &mut String,
) -> String
where where
P1: AsRef<Path>, P1: AsRef<Path>,
P2: AsRef<Path>, P2: AsRef<Path>,
@ -77,11 +91,17 @@ where
for link in find_links(s) { for link in find_links(s) {
replaced.push_str(&s[previous_end_index..link.start_index]); 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) => { Ok(new_content) => {
if depth < MAX_LINK_NESTED_DEPTH { if depth < MAX_LINK_NESTED_DEPTH {
if let Some(rel_path) = link.link_type.relative_path(path) { 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 { } else {
replaced.push_str(&new_content); replaced.push_str(&new_content);
} }
@ -116,6 +136,7 @@ enum LinkType<'a> {
Include(PathBuf, RangeOrAnchor), Include(PathBuf, RangeOrAnchor),
Playground(PathBuf, Vec<&'a str>), Playground(PathBuf, Vec<&'a str>),
RustdocInclude(PathBuf, RangeOrAnchor), RustdocInclude(PathBuf, RangeOrAnchor),
Title(&'a str),
} }
#[derive(PartialEq, Debug, Clone)] #[derive(PartialEq, Debug, Clone)]
@ -185,6 +206,7 @@ impl<'a> LinkType<'a> {
LinkType::Include(p, _) => Some(return_relative_path(base, &p)), LinkType::Include(p, _) => Some(return_relative_path(base, &p)),
LinkType::Playground(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::RustdocInclude(p, _) => Some(return_relative_path(base, &p)),
LinkType::Title(_) => None,
} }
} }
} }
@ -255,6 +277,9 @@ struct Link<'a> {
impl<'a> Link<'a> { impl<'a> Link<'a> {
fn from_capture(cap: Captures<'a>) -> Option<Link<'a>> { fn from_capture(cap: Captures<'a>) -> Option<Link<'a>> {
let link_type = match (cap.get(0), cap.get(1), cap.get(2)) { 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)) => { (_, Some(typ), Some(rest)) => {
let mut path_props = rest.as_str().split_whitespace(); let mut path_props = rest.as_str().split_whitespace();
let file_arg = path_props.next(); let file_arg = path_props.next();
@ -291,7 +316,11 @@ impl<'a> Link<'a> {
}) })
} }
fn render_with_path<P: AsRef<Path>>(&self, base: P) -> Result<String> { fn render_with_path<P: AsRef<Path>>(
&self,
base: P,
chapter_title: &mut String,
) -> Result<String> {
let base = base.as_ref(); let base = base.as_ref();
match self.link_type { match self.link_type {
// omit the escape char // omit the escape char
@ -353,6 +382,10 @@ impl<'a> Link<'a> {
contents 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<'_> { fn find_links(contents: &str) -> LinkIter<'_> {
// lazily compute following regex // lazily compute following regex
// r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([a-zA-Z0-9_.\-:/\\\s]+)\}\}")?; // r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([^}]+)\}\}")?;
lazy_static! { lazy_static! {
static ref RE: Regex = Regex::new( static ref RE: Regex = Regex::new(
r"(?x) # insignificant whitespace mode r"(?x) # insignificant whitespace mode
\\\{\{\#.*\}\} # match escaped link \\\{\{\#.*\}\} # match escaped link
| # or | # or
\{\{\s* # link opening parens and whitespace \{\{\s* # link opening parens and whitespace
\#([a-zA-Z0-9_]+) # link type \#([a-zA-Z0-9_]+) # link type
\s+ # separating whitespace \s+ # separating whitespace
([a-zA-Z0-9\s_.\-:/\\\+]+) # link target path and space separated properties ([^}]+) # link target path and space separated properties
\s*\}\} # whitespace and link closing parens" \}\} # link closing parens"
) )
.unwrap(); .unwrap();
} }
@ -406,7 +439,21 @@ mod tests {
```hbs ```hbs
{{#include file.rs}} << an escaped link! {{#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] #[test]

View File

@ -12,6 +12,8 @@ use crate::book::Book;
use crate::config::Config; use crate::config::Config;
use crate::errors::*; use crate::errors::*;
use std::cell::RefCell;
use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
/// Extra information for a `Preprocessor` to give them more context when /// Extra information for a `Preprocessor` to give them more context when
@ -27,6 +29,8 @@ pub struct PreprocessorContext {
/// The calling `mdbook` version. /// The calling `mdbook` version.
pub mdbook_version: String, pub mdbook_version: String,
#[serde(skip)] #[serde(skip)]
pub(crate) chapter_titles: RefCell<HashMap<PathBuf, String>>,
#[serde(skip)]
__non_exhaustive: (), __non_exhaustive: (),
} }
@ -38,6 +42,7 @@ impl PreprocessorContext {
config, config,
renderer, renderer,
mdbook_version: crate::MDBOOK_VERSION.to_string(), mdbook_version: crate::MDBOOK_VERSION.to_string(),
chapter_titles: RefCell::new(HashMap::new()),
__non_exhaustive: (), __non_exhaustive: (),
} }
} }

View File

@ -70,9 +70,12 @@ impl HtmlHandlebars {
.and_then(serde_json::Value::as_str) .and_then(serde_json::Value::as_str)
.unwrap_or(""); .unwrap_or("");
let title = match book_title { let title = if let Some(title) = ctx.chapter_titles.get(path) {
"" => ch.name.clone(), title.clone()
_ => ch.name.clone() + " - " + book_title, } else 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("path".to_owned(), json!(path));
@ -507,6 +510,7 @@ impl Renderer for HtmlHandlebars {
is_index, is_index,
html_config: html_config.clone(), html_config: html_config.clone(),
edition: ctx.config.rust.edition, edition: ctx.config.rust.edition,
chapter_titles: &ctx.chapter_titles,
}; };
self.render_item(item, ctx, &mut print_content)?; self.render_item(item, ctx, &mut print_content)?;
is_index = false; is_index = false;
@ -922,6 +926,7 @@ struct RenderItemContext<'a> {
is_index: bool, is_index: bool,
html_config: HtmlConfig, html_config: HtmlConfig,
edition: Option<RustEdition>, edition: Option<RustEdition>,
chapter_titles: &'a HashMap<PathBuf, String>,
} }
#[cfg(test)] #[cfg(test)]

View File

@ -18,6 +18,7 @@ mod html_handlebars;
mod markdown_renderer; mod markdown_renderer;
use shlex::Shlex; use shlex::Shlex;
use std::collections::HashMap;
use std::fs; use std::fs;
use std::io::{self, ErrorKind, Read}; use std::io::{self, ErrorKind, Read};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -64,6 +65,8 @@ pub struct RenderContext {
/// guaranteed to be empty or even exist. /// guaranteed to be empty or even exist.
pub destination: PathBuf, pub destination: PathBuf,
#[serde(skip)] #[serde(skip)]
pub(crate) chapter_titles: HashMap<PathBuf, String>,
#[serde(skip)]
__non_exhaustive: (), __non_exhaustive: (),
} }
@ -80,6 +83,7 @@ impl RenderContext {
version: crate::MDBOOK_VERSION.to_string(), version: crate::MDBOOK_VERSION.to_string(),
root: root.into(), root: root.into(),
destination: destination.into(), destination: destination.into(),
chapter_titles: HashMap::new(),
__non_exhaustive: (), __non_exhaustive: (),
} }
} }