tpnote_lib/context.rs
1//! Extends the built-in Tera filters.
2use crate::config::FILENAME_ROOT_PATH_MARKER;
3use crate::config::LIB_CFG;
4use crate::config::TMPL_VAR_DIR_PATH;
5use crate::config::TMPL_VAR_EXTENSION_DEFAULT;
6use crate::config::TMPL_VAR_FM_;
7use crate::config::TMPL_VAR_FM_ALL;
8use crate::config::TMPL_VAR_LANG;
9use crate::config::TMPL_VAR_PATH;
10use crate::config::TMPL_VAR_ROOT_PATH;
11use crate::config::TMPL_VAR_USERNAME;
12use crate::content::Content;
13use crate::error::NoteError;
14use crate::front_matter::FrontMatter;
15use crate::settings::SETTINGS;
16use std::borrow::Cow;
17use std::ops::Deref;
18use std::ops::DerefMut;
19use std::path::Path;
20use std::path::PathBuf;
21
22/// Tiny wrapper around "Tera context" with some additional information.
23#[derive(Clone, Debug, PartialEq)]
24pub struct Context {
25 /// Collection of substitution variables.
26 ct: tera::Context,
27 /// First positional command line argument.
28 pub path: PathBuf,
29 /// The directory (only) path corresponding to the first positional
30 /// command line argument. The is our working directory and
31 /// the directory where the note file is (will be) located.
32 pub dir_path: PathBuf,
33 /// `dir_path` is a subdirectory of `root_path`. `root_path` is the
34 /// first directory, that upwards from `dir_path`, contains a file named
35 /// `FILENAME_ROOT_PATH_MARKER` (or, `/` if not marker file can be found).
36 /// The root directory is interpreted by Tp-Note's viewer as its base
37 /// directory: only files within this directory are served.
38 pub root_path: PathBuf,
39}
40
41/// A thin wrapper around `tera::Context` storing some additional
42/// information.
43///
44impl Context {
45 /// Constructor: `path` is the first positional command line parameter
46 /// `<path>` (see man page). `path` must point to a directory or
47 /// a file.
48 ///
49 /// A copy of `path` is stored in `self.ct` as key `TMPL_VAR_PATH`. It
50 /// directory path as key `TMPL_VAR_DIR_PATH`.
51 ///
52 /// ```rust
53 /// use std::path::Path;
54 /// use tpnote_lib::settings::set_test_default_settings;
55 /// use tpnote_lib::config::TMPL_VAR_DIR_PATH;
56 /// use tpnote_lib::config::TMPL_VAR_PATH;
57 /// use tpnote_lib::context::Context;
58 /// set_test_default_settings().unwrap();
59 ///
60 /// let mut context = Context::from(&Path::new("/path/to/mynote.md"));
61 ///
62 /// assert_eq!(context.path, Path::new("/path/to/mynote.md"));
63 /// assert_eq!(context.dir_path, Path::new("/path/to/"));
64 /// assert_eq!(&context.get(TMPL_VAR_PATH).unwrap().to_string(),
65 /// r#""/path/to/mynote.md""#);
66 /// assert_eq!(&context.get(TMPL_VAR_DIR_PATH).unwrap().to_string(),
67 /// r#""/path/to""#);
68 /// ```
69 ///
70 pub fn from(path: &Path) -> Self {
71 let mut ct = tera::Context::new();
72 let path = path.to_path_buf();
73
74 // `dir_path` is a directory as fully qualified path, ending
75 // by a separator.
76 let dir_path = if path.is_dir() {
77 path.clone()
78 } else {
79 path.parent()
80 .unwrap_or_else(|| Path::new("./"))
81 .to_path_buf()
82 };
83
84 // Get the root dir.
85 let mut root_path = Path::new("");
86
87 for anc in dir_path.ancestors() {
88 root_path = anc;
89 let mut p = anc.to_owned();
90 p.push(Path::new(FILENAME_ROOT_PATH_MARKER));
91 if p.is_file() {
92 break;
93 }
94 }
95 let root_path = root_path.to_owned();
96 debug_assert!(dir_path.starts_with(&root_path));
97
98 // Register the canonicalized fully qualified file name.
99 ct.insert(TMPL_VAR_PATH, &path);
100 ct.insert(TMPL_VAR_DIR_PATH, &dir_path);
101 ct.insert(TMPL_VAR_ROOT_PATH, &root_path);
102
103 // Insert environment.
104 let mut context = Self {
105 ct,
106 path,
107 dir_path,
108 root_path,
109 };
110 context.insert_settings();
111 context
112 }
113
114 /// Inserts the YAML front header variables in the context for later use
115 /// with templates.
116 ///
117 pub(crate) fn insert_front_matter(&mut self, fm: &FrontMatter) {
118 let mut fm_all_map = self
119 .ct
120 .remove(TMPL_VAR_FM_ALL)
121 .and_then(|v| {
122 if let tera::Value::Object(map) = v {
123 Some(map)
124 } else {
125 None
126 }
127 })
128 .unwrap_or_default();
129
130 let scheme = &LIB_CFG.read_recursive().scheme[SETTINGS.read_recursive().current_scheme];
131 let vars = &scheme.tmpl.fm_var.localization;
132 for (key, value) in fm.iter() {
133 // This delocalizes the variable name and prepends `fm_` to its name.
134 // NB: We also insert `Value::Array` and `Value::Object`
135 // variants, No flattening occurs here.
136 let fm_key = vars.iter().find(|&l| &l.1 == key).map_or_else(
137 || {
138 let mut s = TMPL_VAR_FM_.to_string();
139 s.push_str(key);
140 Cow::Owned(s)
141 },
142 |l| Cow::Borrowed(&l.0),
143 );
144
145 // Store a copy in `fm`.
146 fm_all_map.insert(fm_key.to_string(), value.clone());
147 }
148 // Register the collection as `Object(Map<String, Value>)`.
149 self.ct.insert(TMPL_VAR_FM_ALL, &fm_all_map);
150 }
151
152 /// Inserts clipboard or stdin data into the context. The data may
153 /// contain some copied text with or without a YAML header. The latter
154 /// usually carries front matter variable. These are added separately via
155 /// `insert_front_matter()`. The `input` data below is registered with
156 /// the key name given by `tmpl_var`. Typical names are `"clipboard"` or
157 /// `"stdin"`. If the below `input` contains a valid YAML header, it will be
158 /// registered in the context with the key name given by `tmpl_var_header`.
159 /// This string is typically one of `clipboard_header` or `std_header`. The
160 /// raw data that will be inserted into the context.
161 ///
162 /// ```rust
163 /// use std::path::Path;
164 /// use tpnote_lib::settings::set_test_default_settings;
165 /// use tpnote_lib::context::Context;
166 /// use tpnote_lib::content::Content;
167 /// use tpnote_lib::content::ContentString;
168 /// set_test_default_settings().unwrap();
169 ///
170 /// let mut context = Context::from(&Path::new("/path/to/mynote.md"));
171 ///
172 /// context.insert_content("clipboard", "clipboard_header",
173 /// &ContentString::from(String::from("Data from clipboard.")));
174 /// assert_eq!(&context.get("clipboard").unwrap().to_string(),
175 /// "\"Data from clipboard.\"");
176 ///
177 /// context.insert_content("stdin", "stdin_header",
178 /// &ContentString::from("---\ntitle: \"My Stdin.\"\n---\nbody".to_string()));
179 /// assert_eq!(&context.get("stdin").unwrap().to_string(),
180 /// r#""body""#);
181 /// assert_eq!(&context.get("stdin_header").unwrap().to_string(),
182 /// r#""title: \"My Stdin.\"""#);
183 /// // "fm_title" is dynamically generated from the header variable "title".
184 /// assert_eq!(&context
185 /// .get("fm").unwrap()
186 /// .get("fm_title").unwrap().to_string(),
187 /// r#""My Stdin.""#);
188 /// ```
189 pub fn insert_content(
190 &mut self,
191 tmpl_var: &str,
192 tmpl_var_header: &str,
193 input: &impl Content,
194 ) -> Result<(), NoteError> {
195 // Register input .
196 (*self).insert(tmpl_var_header, input.header());
197 (*self).insert(tmpl_var, input.body());
198
199 // Can we find a front matter in the input stream? If yes, the
200 // unmodified input stream is our new note content.
201 if !input.header().is_empty() {
202 let input_fm = FrontMatter::try_from(input.header());
203 match input_fm {
204 Ok(ref fm) => {
205 log::trace!(
206 "Input stream from \"{}\" results in front matter:\n{:#?}",
207 tmpl_var,
208 &fm
209 )
210 }
211 Err(ref e) => {
212 if !input.header().is_empty() {
213 return Err(NoteError::InvalidInputYaml {
214 tmpl_var: tmpl_var.to_string(),
215 source_str: e.to_string(),
216 });
217 }
218 }
219 };
220
221 // Register front matter.
222 // The variables registered here can be overwrite the ones from the clipboard.
223 if let Ok(fm) = input_fm {
224 self.insert_front_matter(&fm);
225 }
226 }
227 Ok(())
228 }
229
230 /// Captures _Tp-Note_'s environment and stores it as variables in a
231 /// `context` collection. The variables are needed later to populate
232 /// a context template and a filename template.
233 ///
234 /// This function add the keys:
235 /// TMPL_VAR_EXTENSION_DEFAULT, TMPL_VAR_USERNAME and TMPL_VAR_LANG.
236 ///
237 /// ```
238 /// use std::path::Path;
239 /// use tpnote_lib::config::TMPL_VAR_EXTENSION_DEFAULT;
240 /// use tpnote_lib::settings::set_test_default_settings;
241 /// use tpnote_lib::context::Context;
242 /// set_test_default_settings().unwrap();
243 ///
244 /// // The constructor calls `context.insert_settings()` before returning.
245 /// let mut context = Context::from(&Path::new("/path/to/mynote.md"));
246 ///
247 /// // For most platforms `context.get("extension_default")` is `md`
248 /// assert_eq!(&context.get(TMPL_VAR_EXTENSION_DEFAULT).unwrap().to_string(),
249 /// &format!("\"md\""));
250 /// ```
251 fn insert_settings(&mut self) {
252 let settings = SETTINGS.read_recursive();
253
254 // Default extension for new notes as defined in the configuration file.
255 (*self).insert(
256 TMPL_VAR_EXTENSION_DEFAULT,
257 settings.extension_default.as_str(),
258 );
259
260 // Search for UNIX, Windows and MacOS user-names.
261 (*self).insert(TMPL_VAR_USERNAME, &settings.author);
262
263 // Get the user's language tag.
264 (*self).insert(TMPL_VAR_LANG, &settings.lang);
265 }
266}
267
268/// Auto-dereference for convenient access to `tera::Context`.
269impl Deref for Context {
270 type Target = tera::Context;
271
272 fn deref(&self) -> &Self::Target {
273 &self.ct
274 }
275}
276
277/// Auto-dereference for convenient access to `tera::Context`.
278impl DerefMut for Context {
279 fn deref_mut(&mut self) -> &mut Self::Target {
280 &mut self.ct
281 }
282}
283
284#[cfg(test)]
285mod tests {
286
287 use crate::config::TMPL_VAR_FM_ALL;
288
289 #[test]
290 fn test_insert_front_matter() {
291 use crate::context::Context;
292 use crate::front_matter::FrontMatter;
293 use std::path::Path;
294 let mut context = Context::from(Path::new("/path/to/mynote.md"));
295 context
296 .insert_front_matter(&FrontMatter::try_from("title: My Stdin.\nsome: text").unwrap());
297
298 assert_eq!(
299 &context
300 .get(TMPL_VAR_FM_ALL)
301 .unwrap()
302 .get("fm_title")
303 .unwrap()
304 .to_string(),
305 r#""My Stdin.""#
306 );
307 assert_eq!(
308 &context
309 .get(TMPL_VAR_FM_ALL)
310 .unwrap()
311 .get("fm_some")
312 .unwrap()
313 .to_string(),
314 r#""text""#
315 );
316 assert_eq!(
317 &context
318 .get(TMPL_VAR_FM_ALL)
319 .unwrap()
320 .get("fm_title")
321 .unwrap()
322 .to_string(),
323 r#""My Stdin.""#
324 );
325 assert_eq!(
326 &context
327 .get(TMPL_VAR_FM_ALL)
328 .unwrap()
329 .get("fm_some")
330 .unwrap()
331 .to_string(),
332 r#""text""#
333 );
334 }
335
336 #[test]
337 fn test_insert_front_matter2() {
338 use crate::context::Context;
339 use crate::front_matter::FrontMatter;
340 use std::path::Path;
341 let mut context = Context::from(Path::new("/path/to/mynote.md"));
342 context.insert_front_matter(&FrontMatter::try_from("title: My Stdin.").unwrap());
343
344 context.insert_front_matter(&FrontMatter::try_from("some: text").unwrap());
345
346 assert_eq!(
347 &context
348 .get(TMPL_VAR_FM_ALL)
349 .unwrap()
350 .get("fm_title")
351 .unwrap()
352 .to_string(),
353 r#""My Stdin.""#
354 );
355 assert_eq!(
356 &context
357 .get(TMPL_VAR_FM_ALL)
358 .unwrap()
359 .get("fm_some")
360 .unwrap()
361 .to_string(),
362 r#""text""#
363 );
364 assert_eq!(
365 &context
366 .get(TMPL_VAR_FM_ALL)
367 .unwrap()
368 .get("fm_title")
369 .unwrap()
370 .to_string(),
371 r#""My Stdin.""#
372 );
373 assert_eq!(
374 &context
375 .get(TMPL_VAR_FM_ALL)
376 .unwrap()
377 .get("fm_some")
378 .unwrap()
379 .to_string(),
380 r#""text""#
381 );
382 }
383}