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 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 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}