Add page title override: {{#title My Title}} (#1381)
* Add page title override: {{#title My Title}} * Document {{#title}} in guide
This commit is contained in:
parent
e6568a70eb
commit
94f7578576
|
@ -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}}
|
||||||
|
```
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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,7 +406,7 @@ 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
|
||||||
|
@ -382,8 +415,8 @@ fn find_links(contents: &str) -> LinkIter<'_> {
|
||||||
\{\{\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]
|
||||||
|
|
|
@ -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: (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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: (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue