Skip to content

Create a chapter_config for chapter specific properties to be exposed to the handlebars template. #1465

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
28 changes: 24 additions & 4 deletions guide/src/format/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,14 @@ This controls the build process of your book.
will be created when the book is built (i.e. `create-missing = true`). If this
is `false` then the build process will instead exit with an error if any files
do not exist.
- **use-default-preprocessors:** Disable the default preprocessors of (`links` &
`index`) by setting this option to `false`.
- **use-default-preprocessors:** Disable the default preprocessors of (`links`,
`index` & `metadata`) by setting this option to `false`.

If you have the same, and/or other preprocessors declared via their table
of configuration, they will run instead.

- For clarity, with no preprocessor configuration, the default `links` and
`index` will run.
- For clarity, with no preprocessor configuration, the default `links`,
`index` and `metadata` will run.
- Setting `use-default-preprocessors = false` will disable these
default preprocessors from running.
- Adding `[preprocessor.links]`, for example, will ensure, regardless of
Expand All @@ -105,6 +105,24 @@ The following preprocessors are available and included by default:
- `index`: Convert all chapter files named `README.md` into `index.md`. That is
to say, all `README.md` would be rendered to an index file `index.html` in the
rendered book.
- `metadata`: Strips an optional TOML header from the markdown chapter sources
to provide chapter specific information. This data is then made available to
handlebars.js as a collection of properties.

**Sample Chapter With Default "index.hbs"**
```toml
---
author = "Jane Doe" # this is written to the author meta tag
title = "Blog Post #1" # this overwrites the default title handlebar
keywords = [
"Rust",
"Blog",
] # this sets the keywords meta tag
description = "A blog about rust-lang" # this sets the description meta tag
date = "2021/02/14" # this exposes date as a property for use in the handlebars template
---
This is my blog about rust. # only from this point on remains after preprocessing
```


**book.toml**
Expand All @@ -116,6 +134,8 @@ create-missing = false
[preprocessor.links]

[preprocessor.index]

[preprocessor.metadata]
```

### Custom Preprocessor Configuration
Expand Down
4 changes: 3 additions & 1 deletion guide/src/format/theme/index-hbs.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Here is a list of the properties that are exposed:

- ***language*** Language of the book in the form `en`, as specified in `book.toml` (if not specified, defaults to `en`). To use in <code
class="language-html">\<html lang="{{ language }}"></code> for example.
- ***title*** Title used for the current page. This is identical to `{{ chapter_title }} - {{ book_title }}` unless `book_title` is not set in which case it just defaults to the `chapter_title`.
- ***title*** Title used for the current page. This is identical to `{{ chapter_title }} - {{ book_title }}` unless `book_title` is not set in which case it just defaults to the `chapter_title`. This property can be overwritten by the TOML front matter of a chapter's source.
- ***book_title*** Title of the book, as specified in `book.toml`
- ***chapter_title*** Title of the current chapter, as listed in `SUMMARY.md`

Expand All @@ -38,6 +38,8 @@ Here is a list of the properties that are exposed:
containing all the chapters of the book. It is used for example to construct
the table of contents (sidebar).

Further properties can be exposed through the `chapter_config` field of a `Chapter` which is accessible to preprocessors.

## Handlebars Helpers

In addition to the properties you can access, there are some handlebars helpers
Expand Down
8 changes: 8 additions & 0 deletions src/book/book.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ impl From<Chapter> for BookItem {
pub struct Chapter {
/// The chapter's name.
pub name: String,
/// A collection of key, value pairs for handlebars.js templates.
pub chapter_config: serde_json::Map<String, serde_json::Value>,
/// The chapter's contents.
pub content: String,
/// The chapter's section number, if it has one.
Expand All @@ -174,6 +176,7 @@ impl Chapter {
) -> Chapter {
Chapter {
name: name.to_string(),
chapter_config: serde_json::Map::with_capacity(0),
content,
path: Some(path.into()),
parent_names,
Expand All @@ -186,6 +189,7 @@ impl Chapter {
pub fn new_draft(name: &str, parent_names: Vec<String>) -> Self {
Chapter {
name: name.to_string(),
chapter_config: serde_json::Map::with_capacity(0),
content: String::new(),
path: None,
parent_names,
Expand Down Expand Up @@ -435,6 +439,7 @@ And here is some \

let nested = Chapter {
name: String::from("Nested Chapter 1"),
chapter_config: serde_json::Map::with_capacity(0),
content: String::from("Hello World!"),
number: Some(SectionNumber(vec![1, 2])),
path: Some(PathBuf::from("second.md")),
Expand All @@ -443,6 +448,7 @@ And here is some \
};
let should_be = BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
chapter_config: serde_json::Map::with_capacity(0),
content: String::from(DUMMY_SRC),
number: None,
path: Some(PathBuf::from("chapter_1.md")),
Expand Down Expand Up @@ -507,6 +513,7 @@ And here is some \
sections: vec![
BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
chapter_config: serde_json::Map::with_capacity(0),
content: String::from(DUMMY_SRC),
number: None,
path: Some(PathBuf::from("Chapter_1/index.md")),
Expand Down Expand Up @@ -559,6 +566,7 @@ And here is some \
sections: vec![
BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
chapter_config: serde_json::Map::with_capacity(0),
content: String::from(DUMMY_SRC),
number: None,
path: Some(PathBuf::from("Chapter_1/index.md")),
Expand Down
12 changes: 9 additions & 3 deletions src/book/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ use toml::Value;

use crate::errors::*;
use crate::preprocess::{
CmdPreprocessor, IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext,
CmdPreprocessor, IndexPreprocessor, LinkPreprocessor, MetadataPreprocessor, Preprocessor,
PreprocessorContext,
};
use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderContext, Renderer};
use crate::utils;
Expand Down Expand Up @@ -375,12 +376,15 @@ fn default_preprocessors() -> Vec<Box<dyn Preprocessor>> {
vec![
Box::new(LinkPreprocessor::new()),
Box::new(IndexPreprocessor::new()),
Box::new(MetadataPreprocessor::new()),
]
}

fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
let name = pre.name();
name == LinkPreprocessor::NAME || name == IndexPreprocessor::NAME
name == LinkPreprocessor::NAME
|| name == IndexPreprocessor::NAME
|| name == MetadataPreprocessor::NAME
}

/// Look at the `MDBook` and try to figure out what preprocessors to run.
Expand All @@ -396,6 +400,7 @@ fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>
match key.as_ref() {
"links" => preprocessors.push(Box::new(LinkPreprocessor::new())),
"index" => preprocessors.push(Box::new(IndexPreprocessor::new())),
"metadata" => preprocessors.push(Box::new(MetadataPreprocessor::new())),
name => preprocessors.push(interpret_custom_preprocessor(
name,
&preprocessor_table[name],
Expand Down Expand Up @@ -513,9 +518,10 @@ mod tests {
let got = determine_preprocessors(&cfg);

assert!(got.is_ok());
assert_eq!(got.as_ref().unwrap().len(), 2);
assert_eq!(got.as_ref().unwrap().len(), 3);
assert_eq!(got.as_ref().unwrap()[0].name(), "links");
assert_eq!(got.as_ref().unwrap()[1].name(), "index");
assert_eq!(got.as_ref().unwrap()[2].name(), "metadata");
}

#[test]
Expand Down
174 changes: 174 additions & 0 deletions src/preprocess/metadata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
use crate::errors::*;
use regex::Regex;
use std::ops::Range;

use super::{Preprocessor, PreprocessorContext};
use crate::book::{Book, BookItem};

/// A preprocessor for reading TOML front matter from a markdown file. Special
/// fields are included in the `index.hbs` file for handlebars.js templating and
/// are:
/// - `author` - For setting the author meta tag.
/// - `title` - For overwritting the title tag.
/// - `description` - For setting the description meta tag.
/// - `keywords` - For setting the keywords meta tag.
#[derive(Default)]
pub struct MetadataPreprocessor;

impl MetadataPreprocessor {
pub(crate) const NAME: &'static str = "metadata";

/// Create a new `MetadataPreprocessor`.
pub fn new() -> Self {
MetadataPreprocessor
}
}

impl Preprocessor for MetadataPreprocessor {
fn name(&self) -> &str {
Self::NAME
}

fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
book.for_each_mut(|section: &mut BookItem| {
if let BookItem::Chapter(ref mut ch) = *section {
if let Some(m) = Match::find_metadata(&ch.content) {
if let Ok(mut meta) = toml::from_str(&ch.content[m.range]) {
ch.chapter_config.append(&mut meta);
ch.content = String::from(&ch.content[m.end..]);
};
}
}
});
Ok(book)
}
}

struct Match {
range: Range<usize>,
end: usize,
}

impl Match {
fn find_metadata(contents: &str) -> Option<Match> {
// lazily compute following regex
// r"\A-{3,}\n(?P<metadata>.*?)^{3,}\n"
lazy_static! {
static ref RE: Regex = Regex::new(
r"(?xms) # insignificant whitespace mode and multiline
\A-{3,}\n # match a horizontal rule at the start of the content
(?P<metadata>.*?) # name the match between horizontal rules metadata
^-{3,}\n # match a horizontal rule
"
)
.unwrap();
};
if let Some(mat) = RE.captures(contents) {
// safe to unwrap as we know there is a match
let metadata = mat.name("metadata").unwrap();
Some(Match {
range: metadata.start()..metadata.end(),
end: mat.get(0).unwrap().end(),
})
} else {
None
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_find_metadata_not_at_start() {
let s = "\
content\n\
---
author = \"Adam\"
title = \"Blog Post #1\"
keywords = [
\"rust\",
\"blog\",
]
date = \"2021/02/15\"
modified = \"2021/02/16\"\n\
---
content
";
if let Some(_) = Match::find_metadata(s) {
panic!()
}
}

#[test]
fn test_find_metadata_at_start() {
let s = "\
---
author = \"Adam\"
title = \"Blog Post #1\"
keywords = [
\"rust\",
\"blog\",
]
date = \"2021/02/15\"
description = \"My rust blog.\"
modified = \"2021/02/16\"\n\
---\n\
content
";
if let None = Match::find_metadata(s) {
panic!()
}
}

#[test]
fn test_find_metadata_partial_metadata() {
let s = "\
---
author = \"Adam\n\
content
";
if let Some(_) = Match::find_metadata(s) {
panic!()
}
}

#[test]
fn test_find_metadata_not_metadata() {
type Map = serde_json::Map<String, serde_json::Value>;
let s = "\
---
This is just standard content that happens to start with a line break
and has a second line break in the text.\n\
---
followed by more content
";
if let Some(m) = Match::find_metadata(s) {
if let Ok(_) = toml::from_str::<Map>(&s[m.range]) {
panic!()
}
}
}

#[test]
fn test_parse_metadata() {
let metadata: serde_json::Map<String, serde_json::Value> = toml::from_str(
"author = \"Adam\"
title = \"Blog Post #1\"
keywords = [
\"Rust\",
\"Blog\",
]
date = \"2021/02/15\"
",
)
.unwrap();
let mut map = serde_json::Map::<String, serde_json::Value>::new();
map.insert("author".to_string(), json!("Adam"));
map.insert("title".to_string(), json!("Blog Post #1"));
map.insert("keywords".to_string(), json!(vec!["Rust", "Blog"]));
map.insert("date".to_string(), json!("2021/02/15"));
assert_eq!(metadata, map)
}
}
2 changes: 2 additions & 0 deletions src/preprocess/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
pub use self::cmd::CmdPreprocessor;
pub use self::index::IndexPreprocessor;
pub use self::links::LinkPreprocessor;
pub use self::metadata::MetadataPreprocessor;

mod cmd;
mod index;
mod links;
mod metadata;

use crate::book::Book;
use crate::config::Config;
Expand Down
15 changes: 13 additions & 2 deletions src/renderer/html_handlebars/hbs_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ impl HtmlHandlebars {
ctx.data.insert("path".to_owned(), json!(path));
ctx.data.insert("content".to_owned(), json!(content));
ctx.data.insert("chapter_title".to_owned(), json!(ch.name));
ctx.data.insert("title".to_owned(), json!(title));
if let Some(title) = ctx.data.insert("title".to_owned(), json!(title)) {
ctx.data.insert("title".to_string(), title);
}
ctx.data.insert(
"path_to_root".to_owned(),
json!(utils::fs::path_to_root(&path)),
Expand Down Expand Up @@ -494,10 +496,19 @@ impl Renderer for HtmlHandlebars {

let mut is_index = true;
for item in book.iter() {
let item_data = if let BookItem::Chapter(ref ch) = item {
let mut chapter_data = data.clone();
for (key, value) in ch.chapter_config.iter() {
let _ = chapter_data.insert(key.to_string(), json!(value));
}
chapter_data
} else {
data.clone()
};
let ctx = RenderItemContext {
handlebars: &handlebars,
destination: destination.to_path_buf(),
data: data.clone(),
data: item_data,
is_index,
html_config: html_config.clone(),
edition: ctx.config.rust.edition,
Expand Down
Loading