tpnote_lib/
context.rs

1//! Extends the built-in Tera filters.
2use tera::Value;
3
4use crate::config::Assertion;
5use crate::config::FILENAME_ROOT_PATH_MARKER;
6use crate::config::LIB_CFG;
7#[cfg(feature = "viewer")]
8use crate::config::TMPL_HTML_VAR_DOC_ERROR;
9#[cfg(feature = "viewer")]
10use crate::config::TMPL_HTML_VAR_DOC_TEXT;
11use crate::config::TMPL_HTML_VAR_EXPORTER_DOC_CSS;
12use crate::config::TMPL_HTML_VAR_EXPORTER_HIGHLIGHTING_CSS;
13use crate::config::TMPL_HTML_VAR_VIEWER_DOC_CSS_PATH;
14use crate::config::TMPL_HTML_VAR_VIEWER_DOC_CSS_PATH_VALUE;
15use crate::config::TMPL_HTML_VAR_VIEWER_DOC_JS;
16use crate::config::TMPL_HTML_VAR_VIEWER_HIGHLIGHTING_CSS_PATH;
17use crate::config::TMPL_HTML_VAR_VIEWER_HIGHLIGHTING_CSS_PATH_VALUE;
18use crate::config::TMPL_VAR_BODY;
19use crate::config::TMPL_VAR_CURRENT_SCHEME;
20use crate::config::TMPL_VAR_DIR_PATH;
21use crate::config::TMPL_VAR_DOC_FILE_DATE;
22use crate::config::TMPL_VAR_EXTENSION_DEFAULT;
23use crate::config::TMPL_VAR_FM_;
24use crate::config::TMPL_VAR_FM_ALL;
25use crate::config::TMPL_VAR_FM_SCHEME;
26use crate::config::TMPL_VAR_HEADER;
27use crate::config::TMPL_VAR_LANG;
28use crate::config::TMPL_VAR_PATH;
29use crate::config::TMPL_VAR_ROOT_PATH;
30use crate::config::TMPL_VAR_SCHEME_SYNC_DEFAULT;
31use crate::config::TMPL_VAR_USERNAME;
32use crate::content::Content;
33use crate::error::FileError;
34use crate::error::LibCfgError;
35use crate::error::NoteError;
36use crate::filename::Extension;
37use crate::filename::NotePath;
38use crate::filename::NotePathStr;
39use crate::filter::name;
40use crate::front_matter::FrontMatter;
41use crate::front_matter::all_leaves;
42use crate::settings::SETTINGS;
43use std::borrow::Cow;
44use std::fs::File;
45use std::marker::PhantomData;
46use std::matches;
47use std::ops::Deref;
48use std::path::Path;
49use std::path::PathBuf;
50use std::time::SystemTime;
51
52/// At trait setting up a state machine as described below.
53/// Its implementors represent one specific state defining the amount and the
54/// type of data the `Context` type holds at that moment.
55pub trait ContextState {}
56
57#[derive(Debug, PartialEq, Clone)]
58/// See description in the `ContextState` implementor list.
59pub struct Invalid;
60
61#[derive(Debug, PartialEq, Clone)]
62/// See description in the `ContextState` implementor list.
63pub struct HasSettings;
64
65#[derive(Debug, PartialEq, Clone)]
66/// See description in the `ContextState` implementor list.
67pub(crate) struct ReadyForFilenameTemplate;
68
69#[derive(Debug, PartialEq, Clone)]
70/// See description in the `ContextState` implementor list.
71pub(crate) struct HasExistingContent;
72
73#[derive(Debug, PartialEq, Clone)]
74/// See description in the `ContextState` implementor list.
75pub(crate) struct ReadyForContentTemplate;
76
77#[derive(Debug, PartialEq, Clone)]
78/// See description in the `ContextState` implementor list.
79pub(crate) struct ReadyForHtmlTemplate;
80
81#[cfg(feature = "viewer")]
82#[derive(Debug, PartialEq, Clone)]
83/// See description in the `ContextState` implementor list.
84pub(crate) struct ReadyForHtmlErrorTemplate;
85
86/// The `Context` object is in an invalid state. Either it was not initialized
87/// or its data does not correspond any more to the `Content` it represents.
88///
89/// |  State order   |                                       |
90/// |----------------|---------------------------------------|
91/// | Previous state | none                                  |
92/// | Current state  | `Invalid`                             |
93/// | Next state     | `HasSettings`                         |
94///
95impl ContextState for Invalid {}
96
97/// The `Context` has the following initialized and valid fields: `path`,
98/// `dir_path`, `root_path` and `ct`. The context `ct` contains data from
99/// `insert_config_vars()` and `insert_settings()`.
100/// `Context<HasSettings>` has the following variables set:
101///
102/// * `TMPL_VAR_CURRENT_SCHEME`
103/// * `TMPL_VAR_DIR_PATH` in sync with `self.dir_path` and
104/// * `TMPL_VAR_DOC_FILE_DATE` in sync with `self.doc_file_date` (only if
105///   available).
106/// * `TMPL_VAR_EXTENSION_DEFAULT`
107/// * `TMPL_VAR_LANG`
108/// * `TMPL_VAR_PATH` in sync with `self.path`,
109/// * `TMPL_VAR_ROOT_PATH` in sync with `self.root_path`.
110/// * `TMPL_VAR_SCHEME_SYNC_DEFAULT`.
111/// * `TMPL_VAR_USERNAME`
112///
113/// The variables are inserted by the following methods: `self.from()`,
114/// `self.insert_config_vars()` and `self.insert_settings()`.
115/// Once this state is achieved, `Context` is constant and write protected until
116/// the next state transition.
117///
118/// |  State order   |                                       |
119/// |----------------|---------------------------------------|
120/// | Previous state | `Invalid`                             |
121/// | Current state  | `HasSettings`                         |
122/// | Next state     | `ReadyForFilenameTemplate` or `HasExistingContent` |
123///
124impl ContextState for HasSettings {}
125
126/// In addition to `HasSettings`, the `context.ct` contains template variables
127/// deserialized from some note's front matter. E.g. a field named `title:`
128/// appears in the context as `fm.fm_title` template variable.
129/// In `Note` objects the `Content` is always associated with a
130/// `Context<ReadyForFilenameTemplate>`.
131/// Once this state is achieved, `Context` is constant and write protected until
132/// the next state transition.
133///
134/// |  State order   |                                       |
135/// |----------------|---------------------------------------|
136/// | Previous state | `HasSettings`                         |
137/// | Current state  | `ReadyForFilenameTemplate `           |
138/// | Next state     | none or `ReadyForHtmlTemplate`        |
139///
140impl ContextState for ReadyForFilenameTemplate {}
141
142/// In addition to the `HasSettings` the YAML headers of all clipboard
143/// `Content` objects are registered as front matter variables `fm.fm*` in the
144/// `Context`.
145/// This stage is also used for the `TemplateKind::FromTextFile` template.
146/// In this case the last inserted `Content` comes from the text file
147/// the command line parameter `<path>` points to. This adds the following key:
148///
149/// * `TMPL_VAR_DOC`
150///
151/// This state can evolve as the
152/// `insert_front_matter_and_raw_text_from_existing_content()` function can be
153/// called several times.
154///
155/// |  State order   |                                       |
156/// |----------------|---------------------------------------|
157/// | Previous state | `HasSettings` or `HasExistingContent` |
158/// | Current state  | `HasExistingContent`                  |
159/// | Next state     | `ReadyForContentTemplate`             |
160///
161impl ContextState for HasExistingContent {}
162
163/// This marker state means that enough information have been collected
164/// in the `HasExistingContent` state to be passed to a
165/// content template renderer.
166/// Once this state is achieved, `Context` is constant and write protected until
167/// the next state transition.
168///
169/// |  State order   |                                       |
170/// |----------------|---------------------------------------|
171/// | Previous state | `HasExistingContent`                  |
172/// | Current state  | `ReadyForContentTemplate`             |
173/// | Next state     | none                                  |
174///
175impl ContextState for ReadyForContentTemplate {}
176
177/// In addition to the `ReadyForFilenameTemplate` state this state has the
178/// following variables set:
179///
180/// * `TMPL_HTML_VAR_EXPORTER_DOC_CSS`
181/// * `TMPL_HTML_VAR_EXPORTER_HIGHLIGHTING_CSS`
182/// * `TMPL_HTML_VAR_EXPORTER_HIGHLIGHTING_CSS`
183/// * `TMPL_HTML_VAR_VIEWER_DOC_CSS_PATH`
184/// * `TMPL_HTML_VAR_VIEWER_DOC_CSS_PATH_VALUE`
185/// * `TMPL_HTML_VAR_VIEWER_DOC_JS` from `viewer_doc_js`
186/// * `TMPL_HTML_VAR_VIEWER_HIGHLIGHTING_CSS_PATH`
187/// * `TMPL_HTML_VAR_VIEWER_HIGHLIGHTING_CSS_PATH_VALUE`
188/// * `TMPL_VAR_DOC`
189///
190/// Once this state is achieved, `Context` is constant and write protected until
191/// the next state transition.
192///
193/// |  State order   |                                       |
194/// |----------------|---------------------------------------|
195/// | Previous state | `ReadyForFilenameTemplate`            |
196/// | Current state  | `ReadyForHtmlTemplate`                |
197/// | Next state     | none                                  |
198///
199impl ContextState for ReadyForHtmlTemplate {}
200
201/// The `Context` has all data for the intended template.
202///
203/// * `TMPL_HTML_VAR_DOC_ERROR` from `error_message`
204/// * `TMPL_HTML_VAR_DOC_TEXT` from `note_erroneous_content`
205/// * `TMPL_HTML_VAR_VIEWER_DOC_JS` from `viewer_doc_js`
206///
207/// Once this state is achieved, `Context` is constant and write protected until
208/// the next state transition.
209///
210/// |  State order   |                                       |
211/// |----------------|---------------------------------------|
212/// | Previous state | `HasSettings`                         |
213/// | Current state  | `ReadyForHtmlErrorTemplate`           |
214/// | Next state     | none                                  |
215///
216#[cfg(feature = "viewer")]
217impl ContextState for ReadyForHtmlErrorTemplate {}
218
219/// Tiny wrapper around "Tera context" with some additional information.
220#[derive(Clone, Debug, PartialEq)]
221pub struct Context<S: ContextState + ?Sized> {
222    /// Collection of substitution variables.
223    ct: tera::Context,
224    /// First positional command line argument.
225    path: PathBuf,
226    /// The directory (only) path corresponding to the first positional
227    /// command line argument. The is our working directory and
228    /// the directory where the note file is (will be) located.
229    dir_path: PathBuf,
230    /// `dir_path` is a subdirectory of `root_path`. `root_path` is the
231    /// first directory, that upwards from `dir_path`, contains a file named
232    /// `FILENAME_ROOT_PATH_MARKER` (or `/` if no marker file can be found).
233    /// The root directory is interpreted by Tp-Note's viewer as its base
234    /// directory: only files within this directory are served.
235    root_path: PathBuf,
236    /// If `path` points to a file, we store its creation date here.
237    doc_file_date: Option<SystemTime>,
238    /// Rust requires usage of generic parameters, here `S`.
239    _marker: PhantomData<S>,
240}
241
242/// The methods below are available in all `ContentState` states.
243impl<S: ContextState> Context<S> {
244    /// Getter for `self.path`.
245    /// See `from()` method for details.
246    pub fn get_path(&self) -> &Path {
247        self.path.as_path()
248    }
249
250    /// Getter for `self.dir_path`.
251    /// See `from()` method for details.
252    pub fn get_dir_path(&self) -> &Path {
253        self.dir_path.as_path()
254    }
255
256    /// Getter for `self.root_path`.
257    /// See `from()` method for details.
258    pub fn get_root_path(&self) -> &Path {
259        self.root_path.as_path()
260    }
261
262    /// Getter for `self.doc_file_date`.
263    /// See `from()` method for details.
264    pub fn get_doc_file_date(&self) -> Option<SystemTime> {
265        self.doc_file_date
266    }
267
268    /// Constructor. Unlike `from()` this constructor does not access
269    /// the file system in order to detect `dir_path`, `root_path` and
270    /// `doc_file_date`. It copies these values from the passed `context`.
271    /// Use this constructor when you are sure that the above date has
272    /// not changed since you instantiated `context`. In this case you
273    /// can avoid repeated file access.
274    pub fn from_context_path(context: &Context<S>) -> Context<HasSettings> {
275        let mut new_context = Context {
276            ct: tera::Context::new(),
277            path: context.path.clone(),
278            dir_path: context.dir_path.clone(),
279            root_path: context.root_path.clone(),
280            doc_file_date: context.doc_file_date,
281            _marker: PhantomData,
282        };
283
284        new_context.sync_paths_to_map();
285        new_context.insert_config_vars();
286        new_context.insert_settings();
287        new_context
288    }
289
290    /// Helper function that keeps the values with the `self.ct` key
291    ///
292    /// * `TMPL_VAR_PATH` in sync with `self.path`,
293    /// * `TMPL_VAR_DIR_PATH` in sync with `self.dir_path` and
294    /// * `TMPL_VAR_ROOT_PATH` in sync with `self.root_path`.
295    /// * `TMPL_VAR_DOC_FILE_DATE` in sync with `self.doc_file_date` (only if
296    ///
297    /// available).
298    /// Synchronization is performed by copying the latter to the former.
299    fn sync_paths_to_map(&mut self) {
300        self.ct.insert(TMPL_VAR_PATH, &self.path);
301        self.ct.insert(TMPL_VAR_DIR_PATH, &self.dir_path);
302        self.ct.insert(TMPL_VAR_ROOT_PATH, &self.root_path);
303        if let Some(time) = self.doc_file_date {
304            self.ct.insert(
305                TMPL_VAR_DOC_FILE_DATE,
306                &time
307                    .duration_since(SystemTime::UNIX_EPOCH)
308                    .unwrap_or_default()
309                    .as_secs(),
310            )
311        } else {
312            self.ct.remove(TMPL_VAR_DOC_FILE_DATE);
313        };
314    }
315
316    /// Insert some configuration variables into the context so that they
317    /// can be used in the templates.
318    ///
319    /// This function adds the key:
320    ///
321    /// * `TMPL_VAR_SCHEME_SYNC_DEFAULT`.
322    ///
323    /// ```
324    /// use std::path::Path;
325    /// use tpnote_lib::config::TMPL_VAR_SCHEME_SYNC_DEFAULT;
326    /// use tpnote_lib::settings::set_test_default_settings;
327    /// use tpnote_lib::context::Context;
328    /// set_test_default_settings().unwrap();
329    ///
330    /// // The constructor calls `context.insert_settings()` before returning.
331    /// let mut context = Context::from(&Path::new("/path/to/mynote.md")).unwrap();
332    ///
333    /// // When the note's YAML header does not contain a `scheme:` field,
334    /// // the `default` scheme is used.
335    /// assert_eq!(&context.get(TMPL_VAR_SCHEME_SYNC_DEFAULT).unwrap().to_string(),
336    ///     &format!("\"default\""));
337    /// ```
338    fn insert_config_vars(&mut self) {
339        let lib_cfg = LIB_CFG.read_recursive();
340
341        // Default extension for new notes as defined in the configuration file.
342        self.ct.insert(
343            TMPL_VAR_SCHEME_SYNC_DEFAULT,
344            lib_cfg.scheme_sync_default.as_str(),
345        );
346    }
347
348    /// Captures Tp-Note's environment and stores it as variables in a
349    /// `context` collection. The variables are needed later to populate
350    /// a context template and a filename template.
351    ///
352    /// This function adds the keys:
353    ///
354    /// * `TMPL_VAR_EXTENSION_DEFAULT`
355    /// * `TMPL_VAR_USERNAME`
356    /// * `TMPL_VAR_LANG`
357    /// * `TMPL_VAR_CURRENT_SCHEME`
358    ///
359    /// ```
360    /// use std::path::Path;
361    /// use tpnote_lib::config::TMPL_VAR_EXTENSION_DEFAULT;
362    /// use tpnote_lib::config::TMPL_VAR_CURRENT_SCHEME;
363    /// use tpnote_lib::settings::set_test_default_settings;
364    /// use tpnote_lib::context::Context;
365    /// set_test_default_settings().unwrap();
366    ///
367    /// // The constructor calls `context.insert_settings()` before returning.
368    /// let mut context = Context::from(&Path::new("/path/to/mynote.md")).unwrap();
369    ///
370    /// // For most platforms `context.get("extension_default")` is `md`
371    /// assert_eq!(&context.get(TMPL_VAR_EXTENSION_DEFAULT).unwrap().to_string(),
372    ///     &format!("\"md\""));
373    /// // `Settings.current_scheme` is by default the `default` scheme.
374    /// assert_eq!(&context.get(TMPL_VAR_CURRENT_SCHEME).unwrap().to_string(),
375    ///     &format!("\"default\""));
376    /// ```
377    fn insert_settings(&mut self) {
378        let settings = SETTINGS.read_recursive();
379
380        // Default extension for new notes as defined in the configuration file.
381        self.ct.insert(
382            TMPL_VAR_EXTENSION_DEFAULT,
383            settings.extension_default.as_str(),
384        );
385
386        {
387            let lib_cfg = LIB_CFG.read_recursive();
388            self.ct.insert(
389                TMPL_VAR_CURRENT_SCHEME,
390                &lib_cfg.scheme[settings.current_scheme].name,
391            );
392        } // Release `lib_cfg` here.
393
394        // Search for UNIX, Windows and MacOS user-names.
395        self.ct.insert(TMPL_VAR_USERNAME, &settings.author);
396
397        // Get the user's language tag.
398        self.ct.insert(TMPL_VAR_LANG, &settings.lang);
399    }
400
401    /// Inserts the YAML front header variables into the context for later use
402    /// with templates.
403    ///
404    fn insert_front_matter2(&mut self, fm: &FrontMatter) {
405        let mut fm_all_map = self
406            .ct
407            .remove(TMPL_VAR_FM_ALL)
408            .and_then(|v| {
409                if let tera::Value::Object(map) = v {
410                    Some(map)
411                } else {
412                    None
413                }
414            })
415            .unwrap_or_default();
416
417        // Collect all localized scheme field names.
418        // Example: `["scheme", "scheme", "Schema"]`
419        let localized_scheme_names: Vec<String> = LIB_CFG
420            .read_recursive()
421            .scheme
422            .iter()
423            .map(|s| {
424                s.tmpl
425                    .fm_var
426                    .localization
427                    .iter()
428                    .find_map(|(k, v)| (k == TMPL_VAR_FM_SCHEME).then_some(v.to_owned()))
429            })
430            .collect::<Option<Vec<String>>>()
431            .unwrap_or_default();
432
433        // Search for localized scheme names in front matter.
434        // `(scheme_idx, field_value)`. Example: `(2, "Deutsch")`
435        let localized_scheme: Option<(usize, &str)> = localized_scheme_names
436            .iter()
437            .enumerate()
438            .find_map(|(i, k)| fm.0.get(k).and_then(|s| s.as_str()).map(|s| (i, s)));
439
440        let scheme = if let Some((scheme_idx, scheme_name)) = localized_scheme {
441            {
442                log::trace!(
443                    "Found `scheme: {}` with index=={} in front matter",
444                    scheme_name,
445                    scheme_idx,
446                );
447                scheme_idx
448            }
449        } else {
450            SETTINGS.read_recursive().current_scheme
451        };
452        let scheme = &LIB_CFG.read_recursive().scheme[scheme];
453
454        let vars = &scheme.tmpl.fm_var.localization;
455        for (key, value) in fm.iter() {
456            // This delocalizes the variable name and prepends `fm_` to its name.
457            // NB: We also insert `Value::Array` and `Value::Object`
458            // variants, No flattening occurs here.
459            let fm_key = vars.iter().find(|&l| &l.1 == key).map_or_else(
460                || {
461                    let mut s = TMPL_VAR_FM_.to_string();
462                    s.push_str(key);
463                    Cow::Owned(s)
464                },
465                |l| Cow::Borrowed(&l.0),
466            );
467
468            // Store a copy in `fm`.
469            fm_all_map.insert(fm_key.to_string(), value.clone());
470        }
471        // Register the collection as `Object(Map<String, Value>)`.
472        self.ct.insert(TMPL_VAR_FM_ALL, &fm_all_map);
473    }
474
475    /// Insert a key/val pair directly. Only available in tests.
476    #[cfg(test)]
477    pub(crate) fn insert(&mut self, key: &str, val: &tera::Value) {
478        self.ct.insert(key, val);
479    }
480
481    /// Inserts a `Content` in `Context`. The content appears as key in
482    /// `context.ct` with its name taken from `content.name()`.
483    /// Its value is a `tera::Map` with two keys `TMPL_VAR_HEADER` and
484    /// `TMPL_VAR_BODY`. The corresponding values are copied from
485    /// `conten.header()` and `content.body()`.
486    fn insert_raw_text_from_existing_content(&mut self, content: &impl Content) {
487        //
488        // Register input.
489        let mut map = tera::Map::new();
490        map.insert(TMPL_VAR_HEADER.to_string(), content.header().into());
491        map.insert(TMPL_VAR_BODY.to_string(), content.body().into());
492
493        self.ct.insert(content.name(), &tera::Value::from(map));
494    }
495
496    /// See function of the same name in `impl Context<HasSettings>`.
497    fn insert_front_matter_and_raw_text_from_existing_content2(
498        &mut self,
499        clipboards: &Vec<&impl Content>,
500    ) -> Result<(), NoteError> {
501        //
502        for &clip in clipboards {
503            // Register input.
504            self.insert_raw_text_from_existing_content(clip);
505
506            // Can we find a front matter in the input stream? If yes, the
507            // unmodified input stream is our new note content.
508            if !clip.header().is_empty() {
509                let input_fm = FrontMatter::try_from(clip.header());
510                match input_fm {
511                    Ok(ref fm) => {
512                        log::trace!(
513                            "Input stream \"{}\" generates the front matter variables:\n{:#?}",
514                            clip.name(),
515                            &fm
516                        )
517                    }
518                    Err(ref e) => {
519                        if !clip.header().is_empty() {
520                            return Err(NoteError::InvalidInputYaml {
521                                tmpl_var: clip.name().to_string(),
522                                source_str: e.to_string(),
523                            });
524                        }
525                    }
526                };
527
528                // Register front matter.
529                // The variables registered here can be overwrite the ones from the clipboard.
530                if let Ok(fm) = input_fm {
531                    self.insert_front_matter2(&fm);
532                }
533            }
534        }
535        Ok(())
536    }
537}
538
539/// The start state of all `Context` objects.
540///
541impl Context<Invalid> {
542    /// Constructor: `path` is Tp-Notes first positional command line parameter
543    /// `<path>` (see man page). `path` must point to a directory or
544    /// a file.
545    ///
546    /// A copy of `path` is stored in `self.ct` as key `TMPL_VAR_PATH`. It
547    /// directory path as key `TMPL_VAR_DIR_PATH`. The root directory, where
548    /// the marker file `tpnote.toml` was found, is stored with the key
549    /// `TMPL_VAR_ROOT_PATH`. If `path` points to a file, its file creation
550    /// date is stored with the key `TMPL_VAR_DOC_FILE_DATE`.
551    ///
552    /// ```rust
553    /// use std::path::Path;
554    /// use tpnote_lib::settings::set_test_default_settings;
555    /// use tpnote_lib::config::TMPL_VAR_DIR_PATH;
556    /// use tpnote_lib::config::TMPL_VAR_PATH;
557    /// use tpnote_lib::context::Context;
558    /// set_test_default_settings().unwrap();
559    ///
560    /// let mut context = Context::from(&Path::new("/path/to/mynote.md")).unwrap();
561    ///
562    /// assert_eq!(context.get_path(), Path::new("/path/to/mynote.md"));
563    /// assert_eq!(context.get_dir_path(), Path::new("/path/to/"));
564    /// assert_eq!(&context.get(TMPL_VAR_PATH).unwrap().to_string(),
565    ///             r#""/path/to/mynote.md""#);
566    /// assert_eq!(&context.get(TMPL_VAR_DIR_PATH).unwrap().to_string(),
567    ///             r#""/path/to""#);
568    /// ```
569    pub fn from(path: &Path) -> Result<Context<HasSettings>, FileError> {
570        let path = path.to_path_buf();
571
572        // `dir_path` is a directory as fully qualified path, ending
573        // by a separator.
574        let dir_path = if path.is_dir() {
575            path.clone()
576        } else {
577            path.parent()
578                .unwrap_or_else(|| Path::new("./"))
579                .to_path_buf()
580        };
581
582        // Get the root directory.
583        let mut root_path = Path::new("");
584
585        for anc in dir_path.ancestors() {
586            root_path = anc;
587            let mut p = anc.to_owned();
588            p.push(Path::new(FILENAME_ROOT_PATH_MARKER));
589            if p.is_file() {
590                break;
591            }
592        }
593        let root_path = root_path.to_owned();
594        debug_assert!(dir_path.starts_with(&root_path));
595
596        // Get the file's creation date. Fail silently.
597        let file_creation_date = if let Ok(file) = File::open(&path) {
598            let metadata = file.metadata()?;
599            metadata.created().or_else(|_| metadata.modified()).ok()
600        } else {
601            None
602        };
603
604        // Insert environment.
605        let mut context = Context {
606            ct: tera::Context::new(),
607            path,
608            dir_path,
609            root_path,
610            doc_file_date: file_creation_date,
611            _marker: PhantomData,
612        };
613
614        context.sync_paths_to_map();
615        context.insert_config_vars();
616        context.insert_settings();
617        Ok(context)
618    }
619}
620
621impl Context<HasSettings> {
622    /// Merges `fm` into `self.ct`.
623    pub(crate) fn insert_front_matter(
624        mut self,
625        fm: &FrontMatter,
626    ) -> Context<ReadyForFilenameTemplate> {
627        Context::insert_front_matter2(&mut self, fm);
628        Context {
629            ct: self.ct,
630            path: self.path,
631            dir_path: self.dir_path,
632            root_path: self.root_path,
633            doc_file_date: self.doc_file_date,
634            _marker: PhantomData,
635        }
636    }
637
638    /// Inserts clipboard data, stdin data and/or existing note file content
639    /// into the context. The data may contain some copied text with or without
640    /// a YAML header. The latter usually carries front matter variables.
641    /// The `input` data below is registered with the key name given by
642    /// `tmpl_var_body_name`. Typical names are `"clipboard"` or `"stdin"`. If
643    /// the below `input` contains a valid YAML header, it will be registered
644    /// in the context with the key name given by `tmpl_var_header_name`. The
645    /// templates expect the key names `clipboard_header` or `std_header`. The
646    /// raw header text will be inserted with this key name.
647    ///
648    pub(crate) fn insert_front_matter_and_raw_text_from_existing_content(
649        mut self,
650        clipboards: &Vec<&impl Content>,
651    ) -> Result<Context<HasExistingContent>, NoteError> {
652        //
653        self.insert_front_matter_and_raw_text_from_existing_content2(clipboards)?;
654
655        Ok(Context {
656            ct: self.ct,
657            path: self.path,
658            dir_path: self.dir_path,
659            root_path: self.root_path,
660            doc_file_date: self.doc_file_date,
661            _marker: PhantomData,
662        })
663    }
664
665    /// This adds the following variables to `self`:
666    ///
667    /// * `TMPL_HTML_VAR_VIEWER_DOC_JS` from `viewer_doc_js`
668    /// * `TMPL_HTML_VAR_DOC_ERROR` from `error_message`
669    /// * `TMPL_HTML_VAR_DOC_TEXT` from `note_erroneous_content`
670    ///
671    #[cfg(feature = "viewer")]
672    pub(crate) fn insert_error_content(
673        mut self,
674        note_erroneous_content: &impl Content,
675        error_message: &str,
676        // Java Script live updater inject code. Will be inserted into
677        // `tmpl_html.viewer`.
678        viewer_doc_js: &str,
679    ) -> Context<ReadyForHtmlErrorTemplate> {
680        //
681        self.ct.insert(TMPL_HTML_VAR_VIEWER_DOC_JS, viewer_doc_js);
682
683        self.ct.insert(TMPL_HTML_VAR_DOC_ERROR, error_message);
684        self.ct
685            .insert(TMPL_HTML_VAR_DOC_TEXT, &note_erroneous_content.as_str());
686
687        Context {
688            ct: self.ct,
689            path: self.path,
690            dir_path: self.dir_path,
691            root_path: self.root_path,
692            doc_file_date: self.doc_file_date,
693            _marker: PhantomData,
694        }
695    }
696}
697
698impl Context<HasExistingContent> {
699    /// See same method in `Context<HasSettings>`.
700    pub(crate) fn insert_front_matter_and_raw_text_from_existing_content(
701        mut self,
702        clipboards: &Vec<&impl Content>,
703    ) -> Result<Context<HasExistingContent>, NoteError> {
704        //
705        self.insert_front_matter_and_raw_text_from_existing_content2(clipboards)?;
706
707        Ok(Context {
708            ct: self.ct,
709            path: self.path,
710            dir_path: self.dir_path,
711            root_path: self.root_path,
712            doc_file_date: self.doc_file_date,
713            _marker: PhantomData,
714        })
715    }
716
717    /// Mark this as ready for a content template.
718    pub(crate) fn set_state_ready_for_content_template(self) -> Context<ReadyForContentTemplate> {
719        Context {
720            ct: self.ct,
721            path: self.path,
722            dir_path: self.dir_path,
723            root_path: self.root_path,
724            doc_file_date: self.doc_file_date,
725            _marker: PhantomData,
726        }
727    }
728}
729
730impl Context<ReadyForFilenameTemplate> {
731    /// Checks if the front matter variables satisfy preconditions.
732    /// `self.path` is the path to the current document.
733    #[inline]
734    pub(crate) fn assert_precoditions(&self) -> Result<(), NoteError> {
735        let path = &self.path;
736        let lib_cfg = &LIB_CFG.read_recursive();
737
738        // Get front matter scheme if there is any.
739        let fm_all = self.get(TMPL_VAR_FM_ALL);
740        if fm_all.is_none() {
741            return Ok(());
742        }
743        let fm_all = fm_all.unwrap();
744        let fm_scheme = fm_all.get(TMPL_VAR_FM_SCHEME).and_then(|v| v.as_str());
745        let scheme_idx = fm_scheme.and_then(|scheme_name| {
746            lib_cfg
747                .scheme
748                .iter()
749                .enumerate()
750                .find_map(|(i, s)| (s.name == scheme_name).then_some(i))
751        });
752        // If not use `current_scheme` from `SETTINGS`
753        let scheme_idx = scheme_idx.unwrap_or_else(|| SETTINGS.read_recursive().current_scheme);
754        let scheme = &lib_cfg.scheme[scheme_idx];
755
756        for (key, conditions) in scheme.tmpl.fm_var.assertions.iter() {
757            if let Some(value) = fm_all.get(key) {
758                for cond in conditions {
759                    match cond {
760                        Assertion::IsDefined => {}
761
762                        Assertion::IsString => {
763                            if !all_leaves(value, &|v| matches!(v, Value::String(..))) {
764                                return Err(NoteError::FrontMatterFieldIsNotString {
765                                    field_name: name(scheme, key).to_string(),
766                                });
767                            }
768                        }
769
770                        Assertion::IsNotEmptyString => {
771                            if !all_leaves(value, &|v| {
772                                matches!(v, Value::String(..)) && v.as_str() != Some("")
773                            }) {
774                                return Err(NoteError::FrontMatterFieldIsEmptyString {
775                                    field_name: name(scheme, key).to_string(),
776                                });
777                            }
778                        }
779
780                        Assertion::IsNumber => {
781                            if !all_leaves(value, &|v| matches!(v, Value::Number(..))) {
782                                return Err(NoteError::FrontMatterFieldIsNotNumber {
783                                    field_name: name(scheme, key).to_string(),
784                                });
785                            }
786                        }
787
788                        Assertion::IsBool => {
789                            if !all_leaves(value, &|v| matches!(v, Value::Bool(..))) {
790                                return Err(NoteError::FrontMatterFieldIsNotBool {
791                                    field_name: name(scheme, key).to_string(),
792                                });
793                            }
794                        }
795
796                        Assertion::IsNotCompound => {
797                            if matches!(value, Value::Array(..))
798                                || matches!(value, Value::Object(..))
799                            {
800                                return Err(NoteError::FrontMatterFieldIsCompound {
801                                    field_name: name(scheme, key).to_string(),
802                                });
803                            }
804                        }
805
806                        Assertion::IsValidSortTag => {
807                            let fm_sort_tag = value.as_str().unwrap_or_default();
808                            if !fm_sort_tag.is_empty() {
809                                // Check for forbidden characters.
810                                let (_, rest, is_sequential) = fm_sort_tag.split_sort_tag(true);
811                                if !rest.is_empty() {
812                                    return Err(NoteError::FrontMatterFieldIsInvalidSortTag {
813                                        sort_tag: fm_sort_tag.to_owned(),
814                                        sort_tag_extra_chars: scheme
815                                            .filename
816                                            .sort_tag
817                                            .extra_chars
818                                            .escape_default()
819                                            .to_string(),
820                                        filename_sort_tag_letters_in_succession_max: scheme
821                                            .filename
822                                            .sort_tag
823                                            .letters_in_succession_max,
824                                    });
825                                }
826
827                                // Check for duplicate sequential sort-tags.
828                                if !is_sequential {
829                                    // No further checks.
830                                    return Ok(());
831                                }
832                                let docpath = path.to_str().unwrap_or_default();
833
834                                let (dirpath, filename) =
835                                    docpath.rsplit_once(['/', '\\']).unwrap_or(("", docpath));
836                                let sort_tag = filename.split_sort_tag(false).0;
837                                // No further check if filename(path) has no sort-tag
838                                // or if sort-tags are identical.
839                                if sort_tag.is_empty() || sort_tag == fm_sort_tag {
840                                    return Ok(());
841                                }
842                                let dirpath = Path::new(dirpath);
843
844                                if let Some(other_file) =
845                                    dirpath.has_file_with_sort_tag(fm_sort_tag)
846                                {
847                                    return Err(NoteError::FrontMatterFieldIsDuplicateSortTag {
848                                        sort_tag: fm_sort_tag.to_string(),
849                                        existing_file: other_file,
850                                    });
851                                }
852                            }
853                        }
854
855                        Assertion::IsTpnoteExtension => {
856                            let file_ext = value.as_str().unwrap_or_default();
857
858                            if !file_ext.is_empty() && !(*file_ext).is_tpnote_ext() {
859                                return Err(NoteError::FrontMatterFieldIsNotTpnoteExtension {
860                                    extension: file_ext.to_string(),
861                                    extensions: {
862                                        use std::fmt::Write;
863                                        let mut errstr = scheme.filename.extensions.iter().fold(
864                                            String::new(),
865                                            |mut output, (k, _v1, _v2)| {
866                                                let _ = write!(output, "{k}, ");
867                                                output
868                                            },
869                                        );
870                                        errstr.truncate(errstr.len().saturating_sub(2));
871                                        errstr
872                                    },
873                                });
874                            }
875                        }
876
877                        Assertion::IsConfiguredScheme => {
878                            let fm_scheme = value.as_str().unwrap_or_default();
879                            match lib_cfg.scheme_idx(fm_scheme) {
880                                Ok(_) => {}
881                                Err(LibCfgError::SchemeNotFound {
882                                    scheme_name,
883                                    schemes,
884                                }) => {
885                                    return Err(NoteError::SchemeNotFound {
886                                        scheme_val: scheme_name,
887                                        scheme_key: key.to_string(),
888                                        schemes,
889                                    });
890                                }
891                                Err(e) => return Err(e.into()),
892                            };
893                        }
894
895                        Assertion::NoOperation => {}
896                    } //
897                }
898                //
899            } else if conditions.contains(&Assertion::IsDefined) {
900                return Err(NoteError::FrontMatterFieldMissing {
901                    field_name: name(scheme, key).to_string(),
902                });
903            }
904        }
905        Ok(())
906    }
907
908    /// Indicates that this context contains all we need for the content
909    /// template.
910    #[cfg(test)]
911    pub(crate) fn set_state_ready_for_content_template(self) -> Context<ReadyForContentTemplate> {
912        //
913        Context {
914            ct: self.ct,
915            path: self.path,
916            dir_path: self.dir_path,
917            root_path: self.root_path,
918            doc_file_date: self.doc_file_date,
919            _marker: PhantomData,
920        }
921    }
922
923    /// Inserts the following variables into `self`:
924    ///
925    /// * `TMPL_HTML_VAR_VIEWER_DOC_JS` from `viewer_doc_js`
926    /// * `TMPL_VAR_DOC` from `content.header()` and `content.body()`
927    /// * `TMPL_HTML_VAR_EXPORTER_DOC_CSS`
928    /// * `TMPL_HTML_VAR_EXPORTER_HIGHLIGHTING_CSS`
929    /// * `TMPL_HTML_VAR_EXPORTER_HIGHLIGHTING_CSS`
930    /// * `TMPL_HTML_VAR_VIEWER_DOC_CSS_PATH`
931    /// * `TMPL_HTML_VAR_VIEWER_DOC_CSS_PATH_VALUE`
932    /// * `TMPL_HTML_VAR_VIEWER_HIGHLIGHTING_CSS_PATH`
933    /// * `TMPL_HTML_VAR_VIEWER_HIGHLIGHTING_CSS_PATH_VALUE`
934    ///
935    pub(crate) fn insert_raw_content_and_css(
936        mut self,
937        content: &impl Content,
938        viewer_doc_js: &str,
939    ) -> Context<ReadyForHtmlTemplate> {
940        //
941        self.ct.insert(TMPL_HTML_VAR_VIEWER_DOC_JS, viewer_doc_js);
942
943        self.insert_raw_text_from_existing_content(content);
944
945        {
946            let lib_cfg = &LIB_CFG.read_recursive();
947
948            // Insert the raw CSS
949            self.ct.insert(
950                TMPL_HTML_VAR_EXPORTER_DOC_CSS,
951                &(lib_cfg.tmpl_html.exporter_doc_css),
952            );
953
954            // Insert the raw CSS
955            #[cfg(feature = "renderer")]
956            self.ct.insert(
957                TMPL_HTML_VAR_EXPORTER_HIGHLIGHTING_CSS,
958                &(lib_cfg.tmpl_html.exporter_highlighting_css),
959            );
960        } // Drop `lib_cfg`.
961
962        // Insert the raw CSS
963        #[cfg(not(feature = "renderer"))]
964        self.ct.insert(TMPL_HTML_VAR_EXPORTER_HIGHLIGHTING_CSS, "");
965
966        // Insert the web server path to get the Tp-Note's CSS loaded.
967        self.ct.insert(
968            TMPL_HTML_VAR_VIEWER_DOC_CSS_PATH,
969            TMPL_HTML_VAR_VIEWER_DOC_CSS_PATH_VALUE,
970        );
971
972        // Insert the web server path to get the highlighting CSS loaded.
973        self.ct.insert(
974            TMPL_HTML_VAR_VIEWER_HIGHLIGHTING_CSS_PATH,
975            TMPL_HTML_VAR_VIEWER_HIGHLIGHTING_CSS_PATH_VALUE,
976        );
977        Context {
978            ct: self.ct,
979            path: self.path,
980            dir_path: self.dir_path,
981            root_path: self.root_path,
982            doc_file_date: self.doc_file_date,
983            _marker: PhantomData,
984        }
985    }
986}
987
988/// Auto dereferences for convenient access to `tera::Context`.
989impl<S: ContextState> Deref for Context<S> {
990    type Target = tera::Context;
991
992    fn deref(&self) -> &Self::Target {
993        &self.ct
994    }
995}
996
997#[cfg(test)]
998mod tests {
999
1000    use crate::{config::TMPL_VAR_FM_ALL, error::NoteError};
1001    use std::path::Path;
1002
1003    #[test]
1004    fn test_insert_front_matter() {
1005        use crate::context::Context;
1006        use crate::front_matter::FrontMatter;
1007        use std::path::Path;
1008        let context = Context::from(Path::new("/path/to/mynote.md")).unwrap();
1009        let context = context
1010            .insert_front_matter(&FrontMatter::try_from("title: My Stdin.\nsome: text").unwrap());
1011
1012        assert_eq!(
1013            &context
1014                .get(TMPL_VAR_FM_ALL)
1015                .unwrap()
1016                .get("fm_title")
1017                .unwrap()
1018                .to_string(),
1019            r#""My Stdin.""#
1020        );
1021        assert_eq!(
1022            &context
1023                .get(TMPL_VAR_FM_ALL)
1024                .unwrap()
1025                .get("fm_some")
1026                .unwrap()
1027                .to_string(),
1028            r#""text""#
1029        );
1030        assert_eq!(
1031            &context
1032                .get(TMPL_VAR_FM_ALL)
1033                .unwrap()
1034                .get("fm_title")
1035                .unwrap()
1036                .to_string(),
1037            r#""My Stdin.""#
1038        );
1039        assert_eq!(
1040            &context
1041                .get(TMPL_VAR_FM_ALL)
1042                .unwrap()
1043                .get("fm_some")
1044                .unwrap()
1045                .to_string(),
1046            r#""text""#
1047        );
1048    }
1049
1050    #[test]
1051    fn test_insert_front_matter2() {
1052        use crate::context::Context;
1053        use crate::front_matter::FrontMatter;
1054        use std::path::Path;
1055        let context = Context::from(Path::new("/path/to/mynote.md")).unwrap();
1056        let context = context
1057            .insert_front_matter(&FrontMatter::try_from("title: My Stdin.\nsome: text").unwrap());
1058        let context = context.set_state_ready_for_content_template();
1059
1060        assert_eq!(
1061            &context
1062                .get(TMPL_VAR_FM_ALL)
1063                .unwrap()
1064                .get("fm_title")
1065                .unwrap()
1066                .to_string(),
1067            r#""My Stdin.""#
1068        );
1069        assert_eq!(
1070            &context
1071                .get(TMPL_VAR_FM_ALL)
1072                .unwrap()
1073                .get("fm_some")
1074                .unwrap()
1075                .to_string(),
1076            r#""text""#
1077        );
1078        assert_eq!(
1079            &context
1080                .get(TMPL_VAR_FM_ALL)
1081                .unwrap()
1082                .get("fm_title")
1083                .unwrap()
1084                .to_string(),
1085            r#""My Stdin.""#
1086        );
1087        assert_eq!(
1088            &context
1089                .get(TMPL_VAR_FM_ALL)
1090                .unwrap()
1091                .get("fm_some")
1092                .unwrap()
1093                .to_string(),
1094            r#""text""#
1095        );
1096    }
1097
1098    #[test]
1099    fn test_insert_front_matter_and_raw_text_from_existing_content() {
1100        use crate::content::Content;
1101        use crate::content::ContentString;
1102        use crate::context::Context;
1103        use crate::settings::set_test_default_settings;
1104        use std::path::Path;
1105        set_test_default_settings().unwrap();
1106        let context = Context::from(Path::new("/path/to/mynote.md")).unwrap();
1107        let c1 = ContentString::from_string(
1108            String::from("Data from clipboard."),
1109            "txt_clipboard".to_string(),
1110        );
1111        let c2 = ContentString::from_string(
1112            "---\ntitle: My Stdin.\n---\nbody".to_string(),
1113            "stdin".to_string(),
1114        );
1115        let c = vec![&c1, &c2];
1116        let context = context
1117            .insert_front_matter_and_raw_text_from_existing_content(&c)
1118            .unwrap();
1119        assert_eq!(
1120            &context
1121                .get("txt_clipboard")
1122                .unwrap()
1123                .get("body")
1124                .unwrap()
1125                .to_string(),
1126            "\"Data from clipboard.\""
1127        );
1128        assert_eq!(
1129            &context
1130                .get("stdin")
1131                .unwrap()
1132                .get("body")
1133                .unwrap()
1134                .to_string(),
1135            "\"body\""
1136        );
1137        assert_eq!(
1138            &context
1139                .get("stdin")
1140                .unwrap()
1141                .get("header")
1142                .unwrap()
1143                .to_string(),
1144            "\"title: My Stdin.\""
1145        );
1146        // "fm_title" is dynamically generated from the header variable "title".
1147        assert_eq!(
1148            &context
1149                .get("fm")
1150                .unwrap()
1151                .get("fm_title")
1152                .unwrap()
1153                .to_string(),
1154            "\"My Stdin.\""
1155        );
1156    }
1157
1158    #[test]
1159    fn test_assert_preconditions() {
1160        // Check `tmpl.filter.assert_preconditions` in
1161        // `tpnote_lib/src/config_default.toml` to understand these tests.
1162        use crate::context::Context;
1163        use crate::front_matter::FrontMatter;
1164        use serde_json::json;
1165        //
1166        // Is empty.
1167        let input = "";
1168        let fm = FrontMatter::try_from(input).unwrap();
1169        let cx = Context::from(Path::new("does not matter")).unwrap();
1170        let cx = cx.insert_front_matter(&fm);
1171
1172        assert!(matches!(
1173            cx.assert_precoditions().unwrap_err(),
1174            NoteError::FrontMatterFieldMissing { .. }
1175        ));
1176
1177        //
1178        // Ok as long as no other file with that sort-tag exists.
1179        let input = "# document start
1180        title: The book
1181        sort_tag:    123b";
1182        let fm = FrontMatter::try_from(input).unwrap();
1183        let cx = Context::from(Path::new("./03b-test.md")).unwrap();
1184        let cx = cx.insert_front_matter(&fm);
1185
1186        assert!(matches!(cx.assert_precoditions(), Ok(())));
1187
1188        //
1189        // Should not be a compound type.
1190        let input = "# document start
1191        title: The book
1192        sort_tag:
1193        -    1234
1194        -    456";
1195        let fm = FrontMatter::try_from(input).unwrap();
1196        let cx = Context::from(Path::new("does not matter")).unwrap();
1197        let cx = cx.insert_front_matter(&fm);
1198
1199        assert!(matches!(
1200            cx.assert_precoditions().unwrap_err(),
1201            NoteError::FrontMatterFieldIsCompound { .. }
1202        ));
1203
1204        //
1205        // Should not be a compound type.
1206        let input = "# document start
1207        title: The book
1208        sort_tag:
1209          first:  1234
1210          second: 456";
1211        let fm = FrontMatter::try_from(input).unwrap();
1212        let cx = Context::from(Path::new("does not matter")).unwrap();
1213        let cx = cx.insert_front_matter(&fm);
1214
1215        assert!(matches!(
1216            cx.assert_precoditions().unwrap_err(),
1217            NoteError::FrontMatterFieldIsCompound { .. }
1218        ));
1219
1220        //
1221        // Not registered file extension.
1222        let input = "# document start
1223        title: The book
1224        file_ext:    xyz";
1225        let fm = FrontMatter::try_from(input).unwrap();
1226        let cx = Context::from(Path::new("does not matter")).unwrap();
1227        let cx = cx.insert_front_matter(&fm);
1228
1229        assert!(matches!(
1230            cx.assert_precoditions().unwrap_err(),
1231            NoteError::FrontMatterFieldIsNotTpnoteExtension { .. }
1232        ));
1233
1234        //
1235        // Check `bool`
1236        let input = "# document start
1237        title: The book
1238        filename_sync: error, here should be a bool";
1239        let fm = FrontMatter::try_from(input).unwrap();
1240        let cx = Context::from(Path::new("does not matter")).unwrap();
1241        let cx = cx.insert_front_matter(&fm);
1242
1243        assert!(matches!(
1244            cx.assert_precoditions().unwrap_err(),
1245            NoteError::FrontMatterFieldIsNotBool { .. }
1246        ));
1247
1248        //
1249        let input = "# document start
1250        title: my title
1251        subtitle: my subtitle
1252        ";
1253        let expected = json!({"fm_title": "my title", "fm_subtitle": "my subtitle"});
1254
1255        let fm = FrontMatter::try_from(input).unwrap();
1256        let cx = Context::from(Path::new("does not matter")).unwrap();
1257        let cx = cx.insert_front_matter(&fm);
1258        assert_eq!(cx.get(TMPL_VAR_FM_ALL).unwrap(), &expected);
1259
1260        //
1261        let input = "# document start
1262        title: my title
1263        file_ext: ''
1264        ";
1265        let expected = json!({"fm_title": "my title", "fm_file_ext": ""});
1266
1267        let fm = FrontMatter::try_from(input).unwrap();
1268        let cx = Context::from(Path::new("does not matter")).unwrap();
1269        let cx = cx.insert_front_matter(&fm);
1270        assert_eq!(cx.get(TMPL_VAR_FM_ALL).unwrap(), &expected);
1271
1272        //
1273        let input = "# document start
1274        title: ''
1275        subtitle: my subtitle
1276        ";
1277        let fm = FrontMatter::try_from(input).unwrap();
1278        let cx = Context::from(Path::new("does not matter")).unwrap();
1279        let cx = cx.insert_front_matter(&fm);
1280
1281        assert!(matches!(
1282            cx.assert_precoditions().unwrap_err(),
1283            NoteError::FrontMatterFieldIsEmptyString { .. }
1284        ));
1285
1286        //
1287        let input = "# document start
1288        title: My doc
1289        author: 
1290        - First author
1291        - Second author
1292        ";
1293        let fm = FrontMatter::try_from(input).unwrap();
1294        let cx = Context::from(Path::new("does not matter")).unwrap();
1295        let cx = cx.insert_front_matter(&fm);
1296
1297        assert!(cx.assert_precoditions().is_ok());
1298
1299        //
1300        let input = "# document start
1301        title: My doc
1302        subtitle: my subtitle
1303        author:
1304        - First title
1305        - 1234
1306        ";
1307        let fm = FrontMatter::try_from(input).unwrap();
1308        let cx = Context::from(Path::new("does not matter")).unwrap();
1309        let cx = cx.insert_front_matter(&fm);
1310
1311        assert!(matches!(
1312            cx.assert_precoditions().unwrap_err(),
1313            NoteError::FrontMatterFieldIsNotString { .. }
1314        ));
1315    }
1316}