Skip to content
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