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}