From 5e6bacf5457a2fa111b1c78038b08041ece57eb9 Mon Sep 17 00:00:00 2001 From: Andrey Voronkov <andrey.voronkov@sbermarket.ru> Date: Wed, 28 Feb 2024 21:16:31 +0300 Subject: [PATCH 1/4] Drinks - DRY Links Proof of Concept Uses links dict in drinks.txt for general links storage Replaces `{{#drink somedrink}}` placeholders with value from the dict --- src/book/init.rs | 14 ++++++++ src/book/mod.rs | 5 +-- src/preprocess/drinks.rs | 77 ++++++++++++++++++++++++++++++++++++++++ src/preprocess/mod.rs | 2 ++ 4 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 src/preprocess/drinks.rs diff --git a/src/book/init.rs b/src/book/init.rs index faca1d09aa..9e5837f358 100644 --- a/src/book/init.rs +++ b/src/book/init.rs @@ -83,6 +83,8 @@ impl BookBuilder { self.write_book_toml()?; + self.write_drinks_txt()?; + match MDBook::load(&self.root) { Ok(book) => Ok(book), Err(e) => { @@ -108,6 +110,18 @@ impl BookBuilder { Ok(()) } + fn write_drinks_txt(&self) -> Result<()> { + debug!("Writing drinks.txt"); + let drinks_txt = self.root.join("drinks.txt"); + let entry = "hello: https://world.org"; + + File::create(drinks_txt) + .with_context(|| "Couldn't create drinks.txt")? + .write_all(&entry.as_bytes()) + .with_context(|| "Unable to write config to drinks.txt")?; + Ok(()) + } + fn copy_across_theme(&self) -> Result<()> { debug!("Copying theme"); diff --git a/src/book/mod.rs b/src/book/mod.rs index c0ab8a5461..de5d9eab5f 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -24,7 +24,7 @@ use topological_sort::TopologicalSort; use crate::errors::*; use crate::preprocess::{ - CmdPreprocessor, IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext, + CmdPreprocessor, DrinkPreprocessor, IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext, }; use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderContext, Renderer}; use crate::utils; @@ -432,7 +432,7 @@ fn determine_renderers(config: &Config) -> Vec<Box<dyn Renderer>> { renderers } -const DEFAULT_PREPROCESSORS: &[&str] = &["links", "index"]; +const DEFAULT_PREPROCESSORS: &[&str] = &["drinks", "links", "index"]; fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool { let name = pre.name(); @@ -533,6 +533,7 @@ fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>> names.sort(); for name in names { let preprocessor: Box<dyn Preprocessor> = match name.as_str() { + "drinks" => Box::new(DrinkPreprocessor::new()), "links" => Box::new(LinkPreprocessor::new()), "index" => Box::new(IndexPreprocessor::new()), _ => { diff --git a/src/preprocess/drinks.rs b/src/preprocess/drinks.rs new file mode 100644 index 0000000000..f2cd2c08de --- /dev/null +++ b/src/preprocess/drinks.rs @@ -0,0 +1,77 @@ +use crate::errors::*; + +use super::{Preprocessor, PreprocessorContext}; +use crate::book::{Book, BookItem, Chapter}; +use regex::{Captures, Regex}; +use once_cell::sync::Lazy; +use std::io::{BufRead, BufReader}; +use std::fs::File; +use std::collections::HashMap; + +const SPLITTER: char = ':'; + +type Dict=HashMap<String, String>; + +/// DRY Links - A preprocessor for using centralized links collection: +/// +/// - `{{# drink}}` - Insert link from the collection +#[derive(Default)] +pub struct DrinkPreprocessor; + +impl DrinkPreprocessor { + pub(crate) const NAME: &'static str = "drinks"; + + /// Create a new `DrinkPreprocessor`. + pub fn new() -> Self { + DrinkPreprocessor + } + + fn replace_drinks(&self, chapter: &mut Chapter, dict: &Dict) -> Result<String, Error> { + static RE: Lazy<Regex> = Lazy::new(|| { + Regex::new( + r"(?x) # insignificant whitespace mode + \{\{\s* # link opening parens and whitespace + \#(drink) # drink marker + \s+ # separating whitespace + (?<drink>[A-z0-9_-]+) # drink name + \}\} # link closing parens", + ).unwrap() + }); + + static NODRINK: Lazy<String> = Lazy::new(|| { + "deadbeef".to_string() + }); + + let res = RE.replace_all(&chapter.content, |caps: &Captures<'_>| { + dict.get(&caps["drink"]).unwrap_or(&NODRINK) + }); + Ok(res.to_string()) + } +} + +impl Preprocessor for DrinkPreprocessor { + fn name(&self) -> &str { + Self::NAME + } + + fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book> { + let path = ctx.root.join("drinks.txt"); + + let drinks: Dict = { + let reader = BufReader::new(File::open(path).expect("Cannot open drinks dictionary")); + reader.lines().filter_map(|l| { + l.expect("Cannot read line in drinks dictionary").split_once(SPLITTER).map(|(name, value)| (name.trim().to_owned(), value.trim().to_owned())) + }).collect::<HashMap<_, _>>() + }; + + book.for_each_mut(|section: &mut BookItem| { + if let BookItem::Chapter(ref mut ch) = *section { + ch.content = self + .replace_drinks(ch, &drinks) + .expect("Error converting drinks into links for chapter"); + } + }); + + Ok(book) + } +} diff --git a/src/preprocess/mod.rs b/src/preprocess/mod.rs index df01a3dbfb..7a6d47a6fd 100644 --- a/src/preprocess/mod.rs +++ b/src/preprocess/mod.rs @@ -1,10 +1,12 @@ //! Book preprocessing. pub use self::cmd::CmdPreprocessor; +pub use self::drinks::DrinkPreprocessor; pub use self::index::IndexPreprocessor; pub use self::links::LinkPreprocessor; mod cmd; +mod drinks; mod index; mod links; From dacd1d16bc4eed9d1aad8c80e3d18745cd7e42f8 Mon Sep 17 00:00:00 2001 From: Andrey Voronkov <andrey.voronkov@sbermarket.ru> Date: Wed, 28 Feb 2024 21:23:39 +0300 Subject: [PATCH 2/4] Fix spelling --- src/book/init.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/book/init.rs b/src/book/init.rs index 9e5837f358..05f6c36824 100644 --- a/src/book/init.rs +++ b/src/book/init.rs @@ -118,7 +118,7 @@ impl BookBuilder { File::create(drinks_txt) .with_context(|| "Couldn't create drinks.txt")? .write_all(&entry.as_bytes()) - .with_context(|| "Unable to write config to drinks.txt")?; + .with_context(|| "Unable to write entry to drinks.txt")?; Ok(()) } From 6f832ed434f4b691e47f32dd58851f0abad38332 Mon Sep 17 00:00:00 2001 From: Andrey Voronkov <andrey.voronkov@sbermarket.ru> Date: Wed, 28 Feb 2024 21:26:13 +0300 Subject: [PATCH 3/4] Fix formatting --- src/book/mod.rs | 3 ++- src/preprocess/drinks.rs | 28 ++++++++++++++++------------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/book/mod.rs b/src/book/mod.rs index de5d9eab5f..de24e4c92d 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -24,7 +24,8 @@ use topological_sort::TopologicalSort; use crate::errors::*; use crate::preprocess::{ - CmdPreprocessor, DrinkPreprocessor, IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext, + CmdPreprocessor, DrinkPreprocessor, IndexPreprocessor, LinkPreprocessor, Preprocessor, + PreprocessorContext, }; use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderContext, Renderer}; use crate::utils; diff --git a/src/preprocess/drinks.rs b/src/preprocess/drinks.rs index f2cd2c08de..7e7567381c 100644 --- a/src/preprocess/drinks.rs +++ b/src/preprocess/drinks.rs @@ -2,15 +2,15 @@ use crate::errors::*; use super::{Preprocessor, PreprocessorContext}; use crate::book::{Book, BookItem, Chapter}; -use regex::{Captures, Regex}; use once_cell::sync::Lazy; -use std::io::{BufRead, BufReader}; -use std::fs::File; +use regex::{Captures, Regex}; use std::collections::HashMap; +use std::fs::File; +use std::io::{BufRead, BufReader}; const SPLITTER: char = ':'; -type Dict=HashMap<String, String>; +type Dict = HashMap<String, String>; /// DRY Links - A preprocessor for using centralized links collection: /// @@ -29,18 +29,17 @@ impl DrinkPreprocessor { fn replace_drinks(&self, chapter: &mut Chapter, dict: &Dict) -> Result<String, Error> { static RE: Lazy<Regex> = Lazy::new(|| { Regex::new( - r"(?x) # insignificant whitespace mode + r"(?x) # insignificant whitespace mode \{\{\s* # link opening parens and whitespace \#(drink) # drink marker \s+ # separating whitespace (?<drink>[A-z0-9_-]+) # drink name \}\} # link closing parens", - ).unwrap() + ) + .unwrap() }); - static NODRINK: Lazy<String> = Lazy::new(|| { - "deadbeef".to_string() - }); + static NODRINK: Lazy<String> = Lazy::new(|| "deadbeef".to_string()); let res = RE.replace_all(&chapter.content, |caps: &Captures<'_>| { dict.get(&caps["drink"]).unwrap_or(&NODRINK) @@ -59,9 +58,14 @@ impl Preprocessor for DrinkPreprocessor { let drinks: Dict = { let reader = BufReader::new(File::open(path).expect("Cannot open drinks dictionary")); - reader.lines().filter_map(|l| { - l.expect("Cannot read line in drinks dictionary").split_once(SPLITTER).map(|(name, value)| (name.trim().to_owned(), value.trim().to_owned())) - }).collect::<HashMap<_, _>>() + reader + .lines() + .filter_map(|l| { + l.expect("Cannot read line in drinks dictionary") + .split_once(SPLITTER) + .map(|(name, value)| (name.trim().to_owned(), value.trim().to_owned())) + }) + .collect::<HashMap<_, _>>() }; book.for_each_mut(|section: &mut BookItem| { From 7fd73f6ff7974d77596ab4ddcb00df401905dae0 Mon Sep 17 00:00:00 2001 From: Andrey Voronkov <andrey.voronkov@sbermarket.ru> Date: Thu, 29 Feb 2024 08:28:30 +0300 Subject: [PATCH 4/4] Fix tests --- src/book/mod.rs | 7 ++++--- src/preprocess/drinks.rs | 24 +++++++++++++----------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/book/mod.rs b/src/book/mod.rs index de24e4c92d..645d23c9ba 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -662,9 +662,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()[0].name(), "index"); - assert_eq!(got.as_ref().unwrap()[1].name(), "links"); + assert_eq!(got.as_ref().unwrap().len(), 3); + assert_eq!(got.as_ref().unwrap()[0].name(), "drinks"); + assert_eq!(got.as_ref().unwrap()[1].name(), "index"); + assert_eq!(got.as_ref().unwrap()[2].name(), "links"); } #[test] diff --git a/src/preprocess/drinks.rs b/src/preprocess/drinks.rs index 7e7567381c..99255b1736 100644 --- a/src/preprocess/drinks.rs +++ b/src/preprocess/drinks.rs @@ -56,17 +56,19 @@ impl Preprocessor for DrinkPreprocessor { fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book> { let path = ctx.root.join("drinks.txt"); - let drinks: Dict = { - let reader = BufReader::new(File::open(path).expect("Cannot open drinks dictionary")); - reader - .lines() - .filter_map(|l| { - l.expect("Cannot read line in drinks dictionary") - .split_once(SPLITTER) - .map(|(name, value)| (name.trim().to_owned(), value.trim().to_owned())) - }) - .collect::<HashMap<_, _>>() - }; + if !path.exists() { + return Ok(book); + } + + let reader = BufReader::new(File::open(path).expect("Cannot open drinks dictionary")); + let drinks: Dict = reader + .lines() + .filter_map(|l| { + l.expect("Cannot read line in drinks dictionary") + .split_once(SPLITTER) + .map(|(name, value)| (name.trim().to_owned(), value.trim().to_owned())) + }) + .collect::<HashMap<_, _>>(); book.for_each_mut(|section: &mut BookItem| { if let BookItem::Chapter(ref mut ch) = *section {