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 {