diff --git a/guide/src/format/config.md b/guide/src/format/config.md index bd83f62a..721c53b4 100644 --- a/guide/src/format/config.md +++ b/guide/src/format/config.md @@ -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 @@ -105,24 +105,23 @@ 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`: Reads an optional TOML header from the markdown chapter sources +- `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. The supported fields are `author`, `title`, `description`, `keywords`, - `date` and `modified`. + handlebars.js as a collection of properties. -**Sample Chapter** +**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 -date = "2021/02/14" +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 +] # 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. +This is my blog about rust. # only from this point on remains after preprocessing ``` diff --git a/guide/src/format/theme/index-hbs.md b/guide/src/format/theme/index-hbs.md index 3c27ce8b..1e18502c 100644 --- a/guide/src/format/theme/index-hbs.md +++ b/guide/src/format/theme/index-hbs.md @@ -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 \ 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` diff --git a/src/preprocess/metadata.rs b/src/preprocess/metadata.rs index ccf5d228..818cc3a0 100644 --- a/src/preprocess/metadata.rs +++ b/src/preprocess/metadata.rs @@ -1,17 +1,17 @@ use crate::errors::*; -use regex::{CaptureMatches, Captures, Regex}; +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. The supported -/// fields are: +/// 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. -/// - `date` - The date the file was created, creates a handlebar.js vairable {{date}}. -/// - `modified` - The date the file was modified, creates a handlebar.js vairable {{modified}}. #[derive(Default)] pub struct MetadataPreprocessor; @@ -32,142 +32,57 @@ impl Preprocessor for MetadataPreprocessor { fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result { book.for_each_mut(|section: &mut BookItem| { if let BookItem::Chapter(ref mut ch) = *section { - let (metadata, content) = collect(&ch.content); - ch.content = content; - ch.chapter_config.append(&mut metadata.to_map()); + 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) } } -fn collect(s: &str) -> (Metadata, String) { - let mut end_index = 0; - let mut replaced = String::new(); - - let metadata: Metadata = if let Some(metadata) = find_metadata(s).next() { - match toml::from_str(metadata.text) { - Ok(meta) => { - end_index += metadata.end_index; - meta - } - _ => Metadata::default(), - } - } else { - Metadata::default() - }; - - replaced.push_str(&s[end_index..]); - (metadata, replaced) +struct Match { + range: Range, + end: usize, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(default, rename_all = "kebab-case")] -struct Metadata { - author: Option, - title: Option, - date: Option, - keywords: Option>, - description: Option, - modified: Option, -} - -impl Metadata { - fn to_map(self) -> serde_json::Map { - let mut map = serde_json::Map::new(); - if let Some(author) = self.author { - map.insert("author".to_string(), json!(author)); - } - if let Some(title) = self.title { - map.insert("title".to_string(), json!(title)); - } - if let Some(date) = self.date { - map.insert("date".to_string(), json!(date)); - } - if let Some(keywords) = self.keywords { - map.insert("keywords".to_string(), json!(keywords)); - } - if let Some(modified) = self.modified { - map.insert("modified".to_string(), json!(modified)); - } - if let Some(description) = self.description { - map.insert("description".to_string(), json!(description)); - } - map - } -} - -impl Default for Metadata { - fn default() -> Metadata { - Metadata { - author: None, - title: None, - date: None, - keywords: None, - modified: None, - description: None, +impl Match { + fn find_metadata(contents: &str) -> Option { + // lazily compute following regex + // r"\A-{3,}\n(?P.*?)^{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.*?) # 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 } } } -#[derive(PartialEq, Debug, Clone)] -struct MetadataItem<'a> { - end_index: usize, - text: &'a str, -} - -impl<'a> MetadataItem<'a> { - fn from_capture(cap: Captures<'a>) -> Option> { - if let Some(mat) = cap.name("metadata") { - let full_match = cap.get(0).unwrap(); - if full_match.start() == 0 { - return Some(MetadataItem { - end_index: full_match.end(), - text: mat.as_str(), - }); - } - } - None - } -} - -struct MetadataIter<'a>(CaptureMatches<'a, 'a>); - -impl<'a> Iterator for MetadataIter<'a> { - type Item = MetadataItem<'a>; - fn next(&mut self) -> Option> { - for cap in &mut self.0 { - if let Some(inc) = MetadataItem::from_capture(cap) { - return Some(inc); - } - } - None - } -} - -fn find_metadata(contents: &str) -> MetadataIter<'_> { - // lazily compute following regex - // r"^-{3,}\n(?P.*?)^{3,}\n" - lazy_static! { - static ref RE: Regex = Regex::new( - r"(?xms) # insignificant whitespace mode and multiline - ^-{3,}\n # match a horizontal rule - (?P.*?) # name the match between horizontal rules metadata - ^-{3,}\n # match a horizontal rule - " - ) - .unwrap(); - } - MetadataIter(RE.captures_iter(contents)) -} - #[cfg(test)] mod tests { use super::*; #[test] - fn test_collect_not_at_start() { - let start = "\ + fn test_find_metadata_not_at_start() { + let s = "\ content\n\ --- author = \"Adam\" @@ -181,12 +96,14 @@ mod tests { --- content "; - assert_eq!(collect(start).1, start); + if let Some(_) = Match::find_metadata(s) { + panic!() + } } #[test] - fn test_collect_at_start() { - let start = "\ + fn test_find_metadata_at_start() { + let s = "\ --- author = \"Adam\" title = \"Blog Post #1\" @@ -200,60 +117,43 @@ mod tests { ---\n\ content "; - let end = "\ - content - "; - assert_eq!(collect(start).1, end); + if let None = Match::find_metadata(s) { + panic!() + } } #[test] - fn test_collect_partial_metadata() { - let start = "\ + fn test_find_metadata_partial_metadata() { + let s = "\ --- - author = \"Adam\"\n\ - ---\n\ + author = \"Adam\n\ content "; - let end = "\ - content - "; - assert_eq!(collect(start).1, end); - assert_eq!( - collect(start).0, - Metadata { - author: Some("Adam".to_string()), - ..Default::default() - } - ); + if let Some(_) = Match::find_metadata(s) { + panic!() + } } #[test] - fn test_collect_unsupported_metadata() { - let start = "\ - --- - author: \"Adam\" - unsupported_field: \"text\"\n\ - --- - followed by more content - "; - assert_eq!(collect(start).1, start); - } - - #[test] - fn test_collect_not_metadata() { - let start = "\ + fn test_find_metadata_not_metadata() { + type Map = serde_json::Map; + 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 "; - assert_eq!(collect(start).1, start); + if let Some(m) = Match::find_metadata(s) { + if let Ok(_) = toml::from_str::(&s[m.range]) { + panic!() + } + } } #[test] - fn test_metadata_to_map() { - let metadata: Metadata = toml::from_str( + fn test_parse_metadata() { + let metadata: serde_json::Map = toml::from_str( "author = \"Adam\" title = \"Blog Post #1\" keywords = [ @@ -261,17 +161,12 @@ mod tests { \"Blog\", ] date = \"2021/02/15\" - description = \"My rust blog.\" - modified = \"2021/02/16\" ", - ) - .unwrap(); - let mut map = serde_json::Map::new(); + ").unwrap(); + let mut map = serde_json::Map::::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")); - map.insert("description".to_string(), json!("My rust blog.")); - map.insert("modified".to_string(), json!("2021/02/16")); - assert_eq!(metadata.to_map(), map) + assert_eq!(metadata, map) } }