mdbook_variables/
lib.rs

1#[macro_use]
2extern crate lazy_static;
3use mdbook_preprocessor::{
4    book::{Book, BookItem, Chapter},
5    errors::Result,
6    Preprocessor, PreprocessorContext,
7};
8use regex::{CaptureMatches, Captures, Regex};
9use toml::value::{Table, Value};
10#[derive(Default)]
11pub struct VariablesPreprocessor;
12
13impl VariablesPreprocessor {
14    pub(crate) const NAME: &'static str = "variables";
15
16    /// Create a new `LinkPreprocessor`.
17    pub fn new() -> Self {
18        VariablesPreprocessor
19    }
20}
21
22impl Preprocessor for VariablesPreprocessor {
23    fn name(&self) -> &str {
24        Self::NAME
25    }
26
27    fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
28        let mut variables = None;
29        let mut use_env = false;
30        let mut warn_missing = true;
31        let preprocessors = ctx.config.preprocessors::<Value>()?;
32        if let Some(config) = preprocessors.get(VariablesPreprocessor::NAME) {
33            if let Some(vars) = config.get("variables") {
34                variables = Some(vars);
35            } else {
36                eprintln!(" not found variables in configuration {:?} ", config);
37            }
38            if let Some(env_config) = config.get("use_env") {
39                if let &Value::Boolean(enabled) = env_config {
40                    use_env = enabled;
41                } else {
42                    eprintln!(" variables preprocess use_env configuration must be a boolean ");
43                }
44            }
45
46            if let Some(warn_missing_config) = config.get("warn_missing") {
47                if let &Value::Boolean(enabled) = warn_missing_config {
48                    warn_missing = enabled;
49                } else {
50                    eprintln!(" variables preprocess use_env configuration must be a boolean ");
51                }
52            }
53        } else {
54            eprintln!(" not found {} configuration ", VariablesPreprocessor::NAME);
55        }
56        if let Some(&Value::Table(ref vars)) = variables {
57            book.for_each_mut(|section: &mut BookItem| {
58                if let BookItem::Chapter(ref mut ch) = *section {
59                    ch.content = replace_all(ch, vars, use_env, warn_missing);
60                }
61            });
62        }
63        Ok(book)
64    }
65}
66
67fn replace_all(ch: &Chapter, variables: &Table, use_env: bool, warn_missing: bool) -> String {
68    // When replacing one thing in a string by something with a different length,
69    // the indices after that will not correspond,
70    // we therefore have to store the difference to correct this
71    let mut previous_end_index = 0;
72    let mut replaced = String::new();
73    let start = Value::Table(variables.clone());
74    for variable in find_variables(&ch.content) {
75        replaced.push_str(&ch.content[previous_end_index..variable.start_index]);
76        let variable_path = variable.name.split('.');
77        let mut current_value = Some(&start);
78        for variable_name in variable_path {
79            current_value = if let Some(&Value::Table(ref table)) = current_value {
80                table.get(variable_name)
81            } else {
82                None
83            };
84        }
85        if let Some(value) = current_value {
86            if let Value::String(s) = value {
87                replaced.push_str(&s);
88            } else {
89                replaced.push_str(&value.to_string());
90            }
91        } else if use_env {
92            if let Ok(value) = std::env::var(&variable.name) {
93                replaced.push_str(&value);
94            } else {
95                if warn_missing {
96                    eprintln!(
97                        "Not found value for variable '{}' from chapter '{}'",
98                        variable.name,
99                        ch.path.as_ref().map(|p| p.to_str()).flatten().unwrap_or("")
100                    );
101                }
102                replaced.push_str(&ch.content[variable.start_index..variable.end_index]);
103            }
104        } else {
105            if warn_missing {
106                eprintln!(
107                    "Not found value for variable '{}' from chapter '{}'",
108                    variable.name,
109                    ch.path.as_ref().map(|p| p.to_str()).flatten().unwrap_or("")
110                );
111            }
112            replaced.push_str(&ch.content[variable.start_index..variable.end_index]);
113        }
114        previous_end_index = variable.end_index;
115    }
116
117    replaced.push_str(&ch.content[previous_end_index..]);
118    replaced
119}
120
121struct VariablesIter<'a>(CaptureMatches<'a, 'a>);
122
123struct Variable {
124    start_index: usize,
125    end_index: usize,
126    name: String,
127}
128
129impl Variable {
130    fn from_capture(cap: Captures) -> Option<Variable> {
131        let value = cap.get(1);
132        value.map(|v| {
133            cap.get(0)
134                .map(|mat| Variable {
135                    start_index: mat.start(),
136                    end_index: mat.end(),
137                    name: v.as_str().to_string(),
138                })
139                .expect("base match exists a this point ")
140        })
141    }
142}
143
144impl<'a> Iterator for VariablesIter<'a> {
145    type Item = Variable;
146    fn next(&mut self) -> Option<Variable> {
147        for cap in &mut self.0 {
148            return Variable::from_capture(cap);
149        }
150        None
151    }
152}
153
154fn find_variables(contents: &str) -> VariablesIter<'_> {
155    lazy_static! {
156        static ref RE: Regex = Regex::new(r"\{\{\s*([a-zA-Z0-9_.]+)\s*\}\}").unwrap();
157    }
158    VariablesIter(RE.captures_iter(contents))
159}
160
161#[cfg(test)]
162mod tests {
163    use super::replace_all;
164    use mdbook_preprocessor::book::Chapter;
165    use toml::value::{Table, Value};
166
167    #[test]
168    pub fn test_variable_replaced() {
169        let to_replace = r" # Text {{var1}} \
170            text \
171            text {{var2}} \
172            val  \
173            (text {{var3}})[{{var3}}/other] \
174        ";
175
176        let mut table = Table::new();
177        table.insert("var1".to_owned(), Value::String("first".to_owned()));
178        table.insert("var2".to_owned(), Value::String("second".to_owned()));
179        table.insert("var3".to_owned(), Value::String("third".to_owned()));
180
181        let result = replace_all(
182            &Chapter::new("", to_replace.to_owned(), "", vec![]),
183            &table,
184            false,
185            true,
186        );
187
188        assert_eq!(
189            result,
190            r" # Text first \
191            text \
192            text second \
193            val  \
194            (text third)[third/other] \
195        "
196        );
197    }
198    #[test]
199    pub fn test_variable_replaced_env() {
200        let to_replace = r" # Text {{var1}} \
201            text \
202            text {{var2}} \
203            val  \
204            (text {{var3}})[{{var3}}/other] \
205        ";
206
207        std::env::set_var("var1".to_owned(), "first".to_owned());
208        std::env::set_var("var2".to_owned(), "second".to_owned());
209        std::env::set_var("var3".to_owned(), "third".to_owned());
210
211        let table = Table::new();
212        let result = replace_all(
213            &Chapter::new("", to_replace.to_owned(), "", vec![]),
214            &table,
215            true,
216            true,
217        );
218
219        assert_eq!(
220            result,
221            r" # Text first \
222            text \
223            text second \
224            val  \
225            (text third)[third/other] \
226        "
227        );
228    }
229
230    #[test]
231    pub fn test_keep_variable_for_missing() {
232        let to_replace = "Text {{var1}} ";
233
234        let table = Table::new();
235
236        let result = replace_all(
237            &Chapter::new("", to_replace.to_owned(), "", vec![]),
238            &table,
239            false,
240            true,
241        );
242
243        assert_eq!(result, "Text {{var1}} ");
244    }
245
246    #[test]
247    pub fn test_keep_variable_for_missing_env() {
248        let to_replace = "Text {{var1_missing}} ";
249
250        let table = Table::new();
251
252        let result = replace_all(
253            &Chapter::new("", to_replace.to_owned(), "", vec![]),
254            &table,
255            true,
256            true,
257        );
258
259        assert_eq!(result, "Text {{var1_missing}} ");
260    }
261}