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::all_leaves;
41use crate::front_matter::FrontMatter;
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            if let Ok(time) = metadata.created().or_else(|_| metadata.modified()) {
600                Some(time)
601            } else {
602                None
603            }
604        } else {
605            None
606        };
607
608        // Insert environment.
609        let mut context = Context {
610            ct: tera::Context::new(),
611            path,
612            dir_path,
613            root_path,
614            doc_file_date: file_creation_date,
615            _marker: PhantomData,
616        };
617
618        context.sync_paths_to_map();
619        context.insert_config_vars();
620        context.insert_settings();
621        Ok(context)
622    }
623}
624
625impl Context<HasSettings> {
626    /// Merges `fm` into `self.ct`.
627    pub(crate) fn insert_front_matter(
628        mut self,
629        fm: &FrontMatter,
630    ) -> Context<ReadyForFilenameTemplate> {
631        Context::insert_front_matter2(&mut self, fm);
632        Context {
633            ct: self.ct,
634            path: self.path,
635            dir_path: self.dir_path,
636            root_path: self.root_path,
637            doc_file_date: self.doc_file_date,
638            _marker: PhantomData,
639        }
640    }
641
642    /// Inserts clipboard data, stdin data and/or existing note file content
643    /// into the context. The data may contain some copied text with or without
644    /// a YAML header. The latter usually carries front matter variables.
645    /// The `input` data below is registered with the key name given by
646    /// `tmpl_var_body_name`. Typical names are `"clipboard"` or `"stdin"`. If
647    /// the below `input` contains a valid YAML header, it will be registered
648    /// in the context with the key name given by `tmpl_var_header_name`. The
649    /// templates expect the key names `clipboard_header` or `std_header`. The
650    /// raw header text will be inserted with this key name.
651    ///
652    pub(crate) fn insert_front_matter_and_raw_text_from_existing_content(
653        mut self,
654        clipboards: &Vec<&impl Content>,
655    ) -> Result<Context<HasExistingContent>, NoteError> {
656        //
657        self.insert_front_matter_and_raw_text_from_existing_content2(clipboards)?;
658
659        Ok(Context {
660            ct: self.ct,
661            path: self.path,
662            dir_path: self.dir_path,
663            root_path: self.root_path,
664            doc_file_date: self.doc_file_date,
665            _marker: PhantomData,
666        })
667    }
668
669    /// This adds the following variables to `self`:
670    ///
671    /// * `TMPL_HTML_VAR_VIEWER_DOC_JS` from `viewer_doc_js`
672    /// * `TMPL_HTML_VAR_DOC_ERROR` from `error_message`
673    /// * `TMPL_HTML_VAR_DOC_TEXT` from `note_erroneous_content`
674    ///
675    #[cfg(feature = "viewer")]
676    pub(crate) fn insert_error_content(
677        mut self,
678        note_erroneous_content: &impl Content,
679        error_message: &str,
680        // Java Script live updater inject code. Will be inserted into
681        // `tmpl_html.viewer`.
682        viewer_doc_js: &str,
683    ) -> Context<ReadyForHtmlErrorTemplate> {
684        //
685        self.ct.insert(TMPL_HTML_VAR_VIEWER_DOC_JS, viewer_doc_js);
686
687        self.ct.insert(TMPL_HTML_VAR_DOC_ERROR, error_message);
688        self.ct
689            .insert(TMPL_HTML_VAR_DOC_TEXT, &note_erroneous_content.as_str());
690
691        Context {
692            ct: self.ct,
693            path: self.path,
694            dir_path: self.dir_path,
695            root_path: self.root_path,
696            doc_file_date: self.doc_file_date,
697            _marker: PhantomData,
698        }
699    }
700}
701
702impl Context<HasExistingContent> {
703    /// See same method in `Context<HasSettings>`.
704    pub(crate) fn insert_front_matter_and_raw_text_from_existing_content(
705        mut self,
706        clipboards: &Vec<&impl Content>,
707    ) -> Result<Context<HasExistingContent>, NoteError> {
708        //
709        self.insert_front_matter_and_raw_text_from_existing_content2(clipboards)?;
710
711        Ok(Context {
712            ct: self.ct,
713            path: self.path,
714            dir_path: self.dir_path,
715            root_path: self.root_path,
716            doc_file_date: self.doc_file_date,
717            _marker: PhantomData,
718        })
719    }
720
721    /// Mark this as ready for a content template.
722    pub(crate) fn set_state_ready_for_content_template(self) -> Context<ReadyForContentTemplate> {
723        Context {
724            ct: self.ct,
725            path: self.path,
726            dir_path: self.dir_path,
727            root_path: self.root_path,
728            doc_file_date: self.doc_file_date,
729            _marker: PhantomData,
730        }
731    }
732}
733
734impl Context<ReadyForFilenameTemplate> {
735    /// Checks if the front matter variables satisfy preconditions.
736    /// `self.path` is the path to the current document.
737    #[inline]
738    pub(crate) fn assert_precoditions(&self) -> Result<(), NoteError> {
739        let path = &self.path;
740        let lib_cfg = &LIB_CFG.read_recursive();
741
742        // Get front matter scheme if there is any.
743        let fm_all = self.get(TMPL_VAR_FM_ALL);
744        if fm_all.is_none() {
745            return Ok(());
746        }
747        let fm_all = fm_all.unwrap();
748        let fm_scheme = fm_all.get(TMPL_VAR_FM_SCHEME).and_then(|v| v.as_str());
749        let scheme_idx = fm_scheme.and_then(|scheme_name| {
750            lib_cfg
751                .scheme
752                .iter()
753                .enumerate()
754                .find_map(|(i, s)| (s.name == scheme_name).then_some(i))
755        });
756        // If not use `current_scheme` from `SETTINGS`
757        let scheme_idx = scheme_idx.unwrap_or_else(|| SETTINGS.read_recursive().current_scheme);
758        let scheme = &lib_cfg.scheme[scheme_idx];
759
760        for (key, conditions) in scheme.tmpl.fm_var.assertions.iter() {
761            if let Some(value) = fm_all.get(key) {
762                for cond in conditions {
763                    match cond {
764                        Assertion::IsDefined => {}
765
766                        Assertion::IsString => {
767                            if !all_leaves(value, &|v| matches!(v, Value::String(..))) {
768                                return Err(NoteError::FrontMatterFieldIsNotString {
769                                    field_name: name(scheme, key).to_string(),
770                                });
771                            }
772                        }
773
774                        Assertion::IsNotEmptyString => {
775                            if !all_leaves(value, &|v| {
776                                matches!(v, Value::String(..)) && v.as_str() != Some("")
777                            }) {
778                                return Err(NoteError::FrontMatterFieldIsEmptyString {
779                                    field_name: name(scheme, key).to_string(),
780                                });
781                            }
782                        }
783
784                        Assertion::IsNumber => {
785                            if !all_leaves(value, &|v| matches!(v, Value::Number(..))) {
786                                return Err(NoteError::FrontMatterFieldIsNotNumber {
787                                    field_name: name(scheme, key).to_string(),
788                                });
789                            }
790                        }
791
792                        Assertion::IsBool => {
793                            if !all_leaves(value, &|v| matches!(v, Value::Bool(..))) {
794                                return Err(NoteError::FrontMatterFieldIsNotBool {
795                                    field_name: name(scheme, key).to_string(),
796                                });
797                            }
798                        }
799
800                        Assertion::IsNotCompound => {
801                            if matches!(value, Value::Array(..))
802                                || matches!(value, Value::Object(..))
803                            {
804                                return Err(NoteError::FrontMatterFieldIsCompound {
805                                    field_name: name(scheme, key).to_string(),
806                                });
807                            }
808                        }
809
810                        Assertion::IsValidSortTag => {
811                            let fm_sort_tag = value.as_str().unwrap_or_default();
812                            if !fm_sort_tag.is_empty() {
813                                // Check for forbidden characters.
814                                let (_, rest, is_sequential) = fm_sort_tag.split_sort_tag(true);
815                                if !rest.is_empty() {
816                                    return Err(NoteError::FrontMatterFieldIsInvalidSortTag {
817                                        sort_tag: fm_sort_tag.to_owned(),
818                                        sort_tag_extra_chars: scheme
819                                            .filename
820                                            .sort_tag
821                                            .extra_chars
822                                            .escape_default()
823                                            .to_string(),
824                                        filename_sort_tag_letters_in_succession_max: scheme
825                                            .filename
826                                            .sort_tag
827                                            .letters_in_succession_max,
828                                    });
829                                }
830
831                                // Check for duplicate sequential sort-tags.
832                                if !is_sequential {
833                                    // No further checks.
834                                    return Ok(());
835                                }
836                                let docpath = path.to_str().unwrap_or_default();
837
838                                let (dirpath, filename) =
839                                    docpath.rsplit_once(['/', '\\']).unwrap_or(("", docpath));
840                                let sort_tag = filename.split_sort_tag(false).0;
841                                // No further check if filename(path) has no sort-tag
842                                // or if sort-tags are identical.
843                                if sort_tag.is_empty() || sort_tag == fm_sort_tag {
844                                    return Ok(());
845                                }
846                                let dirpath = Path::new(dirpath);
847
848                                if let Some(other_file) =
849                                    dirpath.has_file_with_sort_tag(fm_sort_tag)
850                                {
851                                    return Err(NoteError::FrontMatterFieldIsDuplicateSortTag {
852                                        sort_tag: fm_sort_tag.to_string(),
853                                        existing_file: other_file,
854                                    });
855                                }
856                            }
857                        }
858
859                        Assertion::IsTpnoteExtension => {
860                            let file_ext = value.as_str().unwrap_or_default();
861
862                            if !file_ext.is_empty() && !(*file_ext).is_tpnote_ext() {
863                                return Err(NoteError::FrontMatterFieldIsNotTpnoteExtension {
864                                    extension: file_ext.to_string(),
865                                    extensions: {
866                                        use std::fmt::Write;
867                                        let mut errstr = scheme.filename.extensions.iter().fold(
868                                            String::new(),
869                                            |mut output, (k, _v1, _v2)| {
870                                                let _ = write!(output, "{k}, ");
871                                                output
872                                            },
873                                        );
874                                        errstr.truncate(errstr.len().saturating_sub(2));
875                                        errstr
876                                    },
877                                });
878                            }
879                        }
880
881                        Assertion::IsConfiguredScheme => {
882                            let fm_scheme = value.as_str().unwrap_or_default();
883                            match lib_cfg.scheme_idx(fm_scheme) {
884                                Ok(_) => {}
885                                Err(LibCfgError::SchemeNotFound {
886                                    scheme_name,
887                                    schemes,
888                                }) => {
889                                    return Err(NoteError::SchemeNotFound {
890                                        scheme_val: scheme_name,
891                                        scheme_key: key.to_string(),
892                                        schemes,
893                                    })
894                                }
895                                Err(e) => return Err(e.into()),
896                            };
897                        }
898
899                        Assertion::NoOperation => {}
900                    } //
901                }
902                //
903            } else if conditions.contains(&Assertion::IsDefined) {
904                return Err(NoteError::FrontMatterFieldMissing {
905                    field_name: name(scheme, key).to_string(),
906                });
907            }
908        }
909        Ok(())
910    }
911
912    /// Indicates that this context contains all we need for the content
913    /// template.
914    #[cfg(test)]
915    pub(crate) fn set_state_ready_for_content_template(self) -> Context<ReadyForContentTemplate> {
916        //
917        Context {
918            ct: self.ct,
919            path: self.path,
920            dir_path: self.dir_path,
921            root_path: self.root_path,
922            doc_file_date: self.doc_file_date,
923            _marker: PhantomData,
924        }
925    }
926
927    /// Inserts the following variables into `self`:
928    ///
929    /// * `TMPL_HTML_VAR_VIEWER_DOC_JS` from `viewer_doc_js`
930    /// * `TMPL_VAR_DOC` from `content.header()` and `content.body()`
931    /// * `TMPL_HTML_VAR_EXPORTER_DOC_CSS`
932    /// * `TMPL_HTML_VAR_EXPORTER_HIGHLIGHTING_CSS`
933    /// * `TMPL_HTML_VAR_EXPORTER_HIGHLIGHTING_CSS`
934    /// * `TMPL_HTML_VAR_VIEWER_DOC_CSS_PATH`
935    /// * `TMPL_HTML_VAR_VIEWER_DOC_CSS_PATH_VALUE`
936    /// * `TMPL_HTML_VAR_VIEWER_HIGHLIGHTING_CSS_PATH`
937    /// * `TMPL_HTML_VAR_VIEWER_HIGHLIGHTING_CSS_PATH_VALUE`
938    ///
939    pub(crate) fn insert_raw_content_and_css(
940        mut self,
941        content: &impl Content,
942        viewer_doc_js: &str,
943    ) -> Context<ReadyForHtmlTemplate> {
944        //
945        self.ct.insert(TMPL_HTML_VAR_VIEWER_DOC_JS, viewer_doc_js);
946
947        self.insert_raw_text_from_existing_content(content);
948
949        {
950            let lib_cfg = &LIB_CFG.read_recursive();
951
952            // Insert the raw CSS
953            self.ct.insert(
954                TMPL_HTML_VAR_EXPORTER_DOC_CSS,
955                &(lib_cfg.tmpl_html.exporter_doc_css),
956            );
957
958            // Insert the raw CSS
959            #[cfg(feature = "renderer")]
960            self.ct.insert(
961                TMPL_HTML_VAR_EXPORTER_HIGHLIGHTING_CSS,
962                &(lib_cfg.tmpl_html.exporter_highlighting_css),
963            );
964        } // Drop `lib_cfg`.
965
966        // Insert the raw CSS
967        #[cfg(not(feature = "renderer"))]
968        self.ct.insert(TMPL_HTML_VAR_EXPORTER_HIGHLIGHTING_CSS, "");
969
970        // Insert the web server path to get the Tp-Note's CSS loaded.
971        self.ct.insert(
972            TMPL_HTML_VAR_VIEWER_DOC_CSS_PATH,
973            TMPL_HTML_VAR_VIEWER_DOC_CSS_PATH_VALUE,
974        );
975
976        // Insert the web server path to get the highlighting CSS loaded.
977        self.ct.insert(
978            TMPL_HTML_VAR_VIEWER_HIGHLIGHTING_CSS_PATH,
979            TMPL_HTML_VAR_VIEWER_HIGHLIGHTING_CSS_PATH_VALUE,
980        );
981        Context {
982            ct: self.ct,
983            path: self.path,
984            dir_path: self.dir_path,
985            root_path: self.root_path,
986            doc_file_date: self.doc_file_date,
987            _marker: PhantomData,
988        }
989    }
990}
991
992/// Auto dereferences for convenient access to `tera::Context`.
993impl<S: ContextState> Deref for Context<S> {
994    type Target = tera::Context;
995
996    fn deref(&self) -> &Self::Target {
997        &self.ct
998    }
999}
1000
1001#[cfg(test)]
1002mod tests {
1003
1004    use crate::{config::TMPL_VAR_FM_ALL, error::NoteError};
1005    use std::path::Path;
1006
1007    #[test]
1008    fn test_insert_front_matter() {
1009        use crate::context::Context;
1010        use crate::front_matter::FrontMatter;
1011        use std::path::Path;
1012        let context = Context::from(Path::new("/path/to/mynote.md")).unwrap();
1013        let context = context
1014            .insert_front_matter(&FrontMatter::try_from("title: My Stdin.\nsome: text").unwrap());
1015
1016        assert_eq!(
1017            &context
1018                .get(TMPL_VAR_FM_ALL)
1019                .unwrap()
1020                .get("fm_title")
1021                .unwrap()
1022                .to_string(),
1023            r#""My Stdin.""#
1024        );
1025        assert_eq!(
1026            &context
1027                .get(TMPL_VAR_FM_ALL)
1028                .unwrap()
1029                .get("fm_some")
1030                .unwrap()
1031                .to_string(),
1032            r#""text""#
1033        );
1034        assert_eq!(
1035            &context
1036                .get(TMPL_VAR_FM_ALL)
1037                .unwrap()
1038                .get("fm_title")
1039                .unwrap()
1040                .to_string(),
1041            r#""My Stdin.""#
1042        );
1043        assert_eq!(
1044            &context
1045                .get(TMPL_VAR_FM_ALL)
1046                .unwrap()
1047                .get("fm_some")
1048                .unwrap()
1049                .to_string(),
1050            r#""text""#
1051        );
1052    }
1053
1054    #[test]
1055    fn test_insert_front_matter2() {
1056        use crate::context::Context;
1057        use crate::front_matter::FrontMatter;
1058        use std::path::Path;
1059        let context = Context::from(Path::new("/path/to/mynote.md")).unwrap();
1060        let context = context
1061            .insert_front_matter(&FrontMatter::try_from("title: My Stdin.\nsome: text").unwrap());
1062        let context = context.set_state_ready_for_content_template();
1063
1064        assert_eq!(
1065            &context
1066                .get(TMPL_VAR_FM_ALL)
1067                .unwrap()
1068                .get("fm_title")
1069                .unwrap()
1070                .to_string(),
1071            r#""My Stdin.""#
1072        );
1073        assert_eq!(
1074            &context
1075                .get(TMPL_VAR_FM_ALL)
1076                .unwrap()
1077                .get("fm_some")
1078                .unwrap()
1079                .to_string(),
1080            r#""text""#
1081        );
1082        assert_eq!(
1083            &context
1084                .get(TMPL_VAR_FM_ALL)
1085                .unwrap()
1086                .get("fm_title")
1087                .unwrap()
1088                .to_string(),
1089            r#""My Stdin.""#
1090        );
1091        assert_eq!(
1092            &context
1093                .get(TMPL_VAR_FM_ALL)
1094                .unwrap()
1095                .get("fm_some")
1096                .unwrap()
1097                .to_string(),
1098            r#""text""#
1099        );
1100    }
1101
1102    #[test]
1103    fn test_insert_front_matter_and_raw_text_from_existing_content() {
1104        use crate::content::Content;
1105        use crate::content::ContentString;
1106        use crate::context::Context;
1107        use crate::settings::set_test_default_settings;
1108        use std::path::Path;
1109        set_test_default_settings().unwrap();
1110        let context = Context::from(Path::new("/path/to/mynote.md")).unwrap();
1111        let c1 = ContentString::from_string(
1112            String::from("Data from clipboard."),
1113            "txt_clipboard".to_string(),
1114        );
1115        let c2 = ContentString::from_string(
1116            "---\ntitle: My Stdin.\n---\nbody".to_string(),
1117            "stdin".to_string(),
1118        );
1119        let c = vec![&c1, &c2];
1120        let context = context
1121            .insert_front_matter_and_raw_text_from_existing_content(&c)
1122            .unwrap();
1123        assert_eq!(
1124            &context
1125                .get("txt_clipboard")
1126                .unwrap()
1127                .get("body")
1128                .unwrap()
1129                .to_string(),
1130            "\"Data from clipboard.\""
1131        );
1132        assert_eq!(
1133            &context
1134                .get("stdin")
1135                .unwrap()
1136                .get("body")
1137                .unwrap()
1138                .to_string(),
1139            "\"body\""
1140        );
1141        assert_eq!(
1142            &context
1143                .get("stdin")
1144                .unwrap()
1145                .get("header")
1146                .unwrap()
1147                .to_string(),
1148            "\"title: My Stdin.\""
1149        );
1150        // "fm_title" is dynamically generated from the header variable "title".
1151        assert_eq!(
1152            &context
1153                .get("fm")
1154                .unwrap()
1155                .get("fm_title")
1156                .unwrap()
1157                .to_string(),
1158            "\"My Stdin.\""
1159        );
1160    }
1161
1162    #[test]
1163    fn test_assert_preconditions() {
1164        // Check `tmpl.filter.assert_preconditions` in
1165        // `tpnote_lib/src/config_default.toml` to understand these tests.
1166        use crate::context::Context;
1167        use crate::front_matter::FrontMatter;
1168        use serde_json::json;
1169        //
1170        // Is empty.
1171        let input = "";
1172        let fm = FrontMatter::try_from(input).unwrap();
1173        let cx = Context::from(Path::new("does not matter")).unwrap();
1174        let cx = cx.insert_front_matter(&fm);
1175
1176        assert!(matches!(
1177            cx.assert_precoditions().unwrap_err(),
1178            NoteError::FrontMatterFieldMissing { .. }
1179        ));
1180
1181        //
1182        // Ok as long as no other file with that sort-tag exists.
1183        let input = "# document start
1184        title: The book
1185        sort_tag:    123b";
1186        let fm = FrontMatter::try_from(input).unwrap();
1187        let cx = Context::from(Path::new("./03b-test.md")).unwrap();
1188        let cx = cx.insert_front_matter(&fm);
1189
1190        assert!(matches!(cx.assert_precoditions(), Ok(())));
1191
1192        //
1193        // Should not be a compound type.
1194        let input = "# document start
1195        title: The book
1196        sort_tag:
1197        -    1234
1198        -    456";
1199        let fm = FrontMatter::try_from(input).unwrap();
1200        let cx = Context::from(Path::new("does not matter")).unwrap();
1201        let cx = cx.insert_front_matter(&fm);
1202
1203        assert!(matches!(
1204            cx.assert_precoditions().unwrap_err(),
1205            NoteError::FrontMatterFieldIsCompound { .. }
1206        ));
1207
1208        //
1209        // Should not be a compound type.
1210        let input = "# document start
1211        title: The book
1212        sort_tag:
1213          first:  1234
1214          second: 456";
1215        let fm = FrontMatter::try_from(input).unwrap();
1216        let cx = Context::from(Path::new("does not matter")).unwrap();
1217        let cx = cx.insert_front_matter(&fm);
1218
1219        assert!(matches!(
1220            cx.assert_precoditions().unwrap_err(),
1221            NoteError::FrontMatterFieldIsCompound { .. }
1222        ));
1223
1224        //
1225        // Not registered file extension.
1226        let input = "# document start
1227        title: The book
1228        file_ext:    xyz";
1229        let fm = FrontMatter::try_from(input).unwrap();
1230        let cx = Context::from(Path::new("does not matter")).unwrap();
1231        let cx = cx.insert_front_matter(&fm);
1232
1233        assert!(matches!(
1234            cx.assert_precoditions().unwrap_err(),
1235            NoteError::FrontMatterFieldIsNotTpnoteExtension { .. }
1236        ));
1237
1238        //
1239        // Check `bool`
1240        let input = "# document start
1241        title: The book
1242        filename_sync: error, here should be a bool";
1243        let fm = FrontMatter::try_from(input).unwrap();
1244        let cx = Context::from(Path::new("does not matter")).unwrap();
1245        let cx = cx.insert_front_matter(&fm);
1246
1247        assert!(matches!(
1248            cx.assert_precoditions().unwrap_err(),
1249            NoteError::FrontMatterFieldIsNotBool { .. }
1250        ));
1251
1252        //
1253        let input = "# document start
1254        title: my title
1255        subtitle: my subtitle
1256        ";
1257        let expected = json!({"fm_title": "my title", "fm_subtitle": "my subtitle"});
1258
1259        let fm = FrontMatter::try_from(input).unwrap();
1260        let cx = Context::from(Path::new("does not matter")).unwrap();
1261        let cx = cx.insert_front_matter(&fm);
1262        assert_eq!(cx.get(TMPL_VAR_FM_ALL).unwrap(), &expected);
1263
1264        //
1265        let input = "# document start
1266        title: my title
1267        file_ext: ''
1268        ";
1269        let expected = json!({"fm_title": "my title", "fm_file_ext": ""});
1270
1271        let fm = FrontMatter::try_from(input).unwrap();
1272        let cx = Context::from(Path::new("does not matter")).unwrap();
1273        let cx = cx.insert_front_matter(&fm);
1274        assert_eq!(cx.get(TMPL_VAR_FM_ALL).unwrap(), &expected);
1275
1276        //
1277        let input = "# document start
1278        title: ''
1279        subtitle: my subtitle
1280        ";
1281        let fm = FrontMatter::try_from(input).unwrap();
1282        let cx = Context::from(Path::new("does not matter")).unwrap();
1283        let cx = cx.insert_front_matter(&fm);
1284
1285        assert!(matches!(
1286            cx.assert_precoditions().unwrap_err(),
1287            NoteError::FrontMatterFieldIsEmptyString { .. }
1288        ));
1289
1290        //
1291        let input = "# document start
1292        title: My doc
1293        author: 
1294        - First author
1295        - Second author
1296        ";
1297        let fm = FrontMatter::try_from(input).unwrap();
1298        let cx = Context::from(Path::new("does not matter")).unwrap();
1299        let cx = cx.insert_front_matter(&fm);
1300
1301        assert!(cx.assert_precoditions().is_ok());
1302
1303        //
1304        let input = "# document start
1305        title: My doc
1306        subtitle: my subtitle
1307        author:
1308        - First title
1309        - 1234
1310        ";
1311        let fm = FrontMatter::try_from(input).unwrap();
1312        let cx = Context::from(Path::new("does not matter")).unwrap();
1313        let cx = cx.insert_front_matter(&fm);
1314
1315        assert!(matches!(
1316            cx.assert_precoditions().unwrap_err(),
1317            NoteError::FrontMatterFieldIsNotString { .. }
1318        ));
1319    }
1320}