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_FORCE_LANG;
27use crate::config::TMPL_VAR_HEADER;
28use crate::config::TMPL_VAR_LANG;
29use crate::config::TMPL_VAR_PATH;
30use crate::config::TMPL_VAR_ROOT_PATH;
31use crate::config::TMPL_VAR_SCHEME_SYNC_DEFAULT;
32use crate::config::TMPL_VAR_USERNAME;
33use crate::content::Content;
34use crate::error::FileError;
35use crate::error::LibCfgError;
36use crate::error::NoteError;
37use crate::filename::Extension;
38use crate::filename::NotePath;
39use crate::filename::NotePathStr;
40use crate::filter::name;
41use crate::front_matter::FrontMatter;
42use crate::front_matter::all_leaves;
43use crate::settings::SETTINGS;
44use std::borrow::Cow;
45use std::fs::File;
46use std::marker::PhantomData;
47use std::matches;
48use std::ops::Deref;
49use std::path::Path;
50use std::path::PathBuf;
51use std::time::SystemTime;
52
53/// At trait setting up a state machine as described below.
54/// Its implementors represent one specific state defining the amount and the
55/// type of data the `Context` type holds at that moment.
56pub trait ContextState {}
57
58#[derive(Debug, PartialEq, Clone)]
59/// See description in the `ContextState` implementor list.
60pub struct Invalid;
61
62#[derive(Debug, PartialEq, Clone)]
63/// See description in the `ContextState` implementor list.
64pub struct HasSettings;
65
66#[derive(Debug, PartialEq, Clone)]
67/// See description in the `ContextState` implementor list.
68pub(crate) struct ReadyForFilenameTemplate;
69
70#[derive(Debug, PartialEq, Clone)]
71/// See description in the `ContextState` implementor list.
72pub(crate) struct HasExistingContent;
73
74#[derive(Debug, PartialEq, Clone)]
75/// See description in the `ContextState` implementor list.
76pub(crate) struct ReadyForContentTemplate;
77
78#[derive(Debug, PartialEq, Clone)]
79/// See description in the `ContextState` implementor list.
80pub(crate) struct ReadyForHtmlTemplate;
81
82#[cfg(feature = "viewer")]
83#[derive(Debug, PartialEq, Clone)]
84/// See description in the `ContextState` implementor list.
85pub(crate) struct ReadyForHtmlErrorTemplate;
86
87/// The `Context` object is in an invalid state. Either it was not initialized
88/// or its data does not correspond any more to the `Content` it represents.
89///
90/// |  State order   |                                       |
91/// |----------------|---------------------------------------|
92/// | Previous state | none                                  |
93/// | Current state  | `Invalid`                             |
94/// | Next state     | `HasSettings`                         |
95///
96impl ContextState for Invalid {}
97
98/// The `Context` has the following initialized and valid fields: `path`,
99/// `dir_path`, `root_path` and `ct`. The context `ct` contains data from
100/// `insert_config_vars()` and `insert_settings()`.
101/// `Context<HasSettings>` has the following variables set:
102///
103/// * `TMPL_VAR_CURRENT_SCHEME`
104/// * `TMPL_VAR_DIR_PATH` in sync with `self.dir_path` and
105/// * `TMPL_VAR_DOC_FILE_DATE` in sync with `self.doc_file_date` (only if
106///   available).
107/// * `TMPL_VAR_EXTENSION_DEFAULT`
108/// * `TMPL_VAR_LANG`
109/// * `TMPL_VAR_PATH` in sync with `self.path`,
110/// * `TMPL_VAR_ROOT_PATH` in sync with `self.root_path`.
111/// * `TMPL_VAR_SCHEME_SYNC_DEFAULT`.
112/// * `TMPL_VAR_USERNAME`
113///
114/// The variables are inserted by the following methods: `self.from()`,
115/// `self.insert_config_vars()` and `self.insert_settings()`.
116/// Once this state is achieved, `Context` is constant and write protected until
117/// the next state transition.
118///
119/// |  State order   |                                       |
120/// |----------------|---------------------------------------|
121/// | Previous state | `Invalid`                             |
122/// | Current state  | `HasSettings`                         |
123/// | Next state     | `ReadyForFilenameTemplate` or `HasExistingContent` |
124///
125impl ContextState for HasSettings {}
126
127/// In addition to `HasSettings`, the `context.ct` contains template variables
128/// deserialized from some note's front matter. E.g. a field named `title:`
129/// appears in the context as `fm.fm_title` template variable.
130/// In `Note` objects the `Content` is always associated with a
131/// `Context<ReadyForFilenameTemplate>`.
132/// Once this state is achieved, `Context` is constant and write protected until
133/// the next state transition.
134///
135/// |  State order   |                                       |
136/// |----------------|---------------------------------------|
137/// | Previous state | `HasSettings`                         |
138/// | Current state  | `ReadyForFilenameTemplate `           |
139/// | Next state     | none or `ReadyForHtmlTemplate`        |
140///
141impl ContextState for ReadyForFilenameTemplate {}
142
143/// In addition to the `HasSettings` the YAML headers of all clipboard
144/// `Content` objects are registered as front matter variables `fm.fm*` in the
145/// `Context`.
146/// This stage is also used for the `TemplateKind::FromTextFile` template.
147/// In this case the last inserted `Content` comes from the text file
148/// the command line parameter `<path>` points to. This adds the following key:
149///
150/// * `TMPL_VAR_DOC`
151///
152/// This state can evolve as the
153/// `insert_front_matter_and_raw_text_from_existing_content()` function can be
154/// called several times.
155///
156/// |  State order   |                                       |
157/// |----------------|---------------------------------------|
158/// | Previous state | `HasSettings` or `HasExistingContent` |
159/// | Current state  | `HasExistingContent`                  |
160/// | Next state     | `ReadyForContentTemplate`             |
161///
162impl ContextState for HasExistingContent {}
163
164/// This marker state means that enough information have been collected
165/// in the `HasExistingContent` state to be passed to a
166/// content template renderer.
167/// Once this state is achieved, `Context` is constant and write protected until
168/// the next state transition.
169///
170/// |  State order   |                                       |
171/// |----------------|---------------------------------------|
172/// | Previous state | `HasExistingContent`                  |
173/// | Current state  | `ReadyForContentTemplate`             |
174/// | Next state     | none                                  |
175///
176impl ContextState for ReadyForContentTemplate {}
177
178/// In addition to the `ReadyForFilenameTemplate` state this state has the
179/// following variables set:
180///
181/// * `TMPL_HTML_VAR_EXPORTER_DOC_CSS`
182/// * `TMPL_HTML_VAR_EXPORTER_HIGHLIGHTING_CSS`
183/// * `TMPL_HTML_VAR_EXPORTER_HIGHLIGHTING_CSS`
184/// * `TMPL_HTML_VAR_VIEWER_DOC_CSS_PATH`
185/// * `TMPL_HTML_VAR_VIEWER_DOC_CSS_PATH_VALUE`
186/// * `TMPL_HTML_VAR_VIEWER_DOC_JS` from `viewer_doc_js`
187/// * `TMPL_HTML_VAR_VIEWER_HIGHLIGHTING_CSS_PATH`
188/// * `TMPL_HTML_VAR_VIEWER_HIGHLIGHTING_CSS_PATH_VALUE`
189/// * `TMPL_VAR_DOC`
190///
191/// Once this state is achieved, `Context` is constant and write protected until
192/// the next state transition.
193///
194/// |  State order   |                                       |
195/// |----------------|---------------------------------------|
196/// | Previous state | `ReadyForFilenameTemplate`            |
197/// | Current state  | `ReadyForHtmlTemplate`                |
198/// | Next state     | none                                  |
199///
200impl ContextState for ReadyForHtmlTemplate {}
201
202/// The `Context` has all data for the intended template.
203///
204/// * `TMPL_HTML_VAR_DOC_ERROR` from `error_message`
205/// * `TMPL_HTML_VAR_DOC_TEXT` from `note_erroneous_content`
206/// * `TMPL_HTML_VAR_VIEWER_DOC_JS` from `viewer_doc_js`
207///
208/// Once this state is achieved, `Context` is constant and write protected until
209/// the next state transition.
210///
211/// |  State order   |                                       |
212/// |----------------|---------------------------------------|
213/// | Previous state | `HasSettings`                         |
214/// | Current state  | `ReadyForHtmlErrorTemplate`           |
215/// | Next state     | none                                  |
216///
217#[cfg(feature = "viewer")]
218impl ContextState for ReadyForHtmlErrorTemplate {}
219
220/// Tiny wrapper around "Tera context" with some additional information.
221#[derive(Clone, Debug, PartialEq)]
222pub struct Context<S: ContextState + ?Sized> {
223    /// Collection of substitution variables.
224    ct: tera::Context,
225    /// First positional command line argument.
226    path: PathBuf,
227    /// The directory (only) path corresponding to the first positional
228    /// command line argument. The is our working directory and
229    /// the directory where the note file is (will be) located.
230    dir_path: PathBuf,
231    /// `dir_path` is a subdirectory of `root_path`. `root_path` is the
232    /// first directory, that upwards from `dir_path`, contains a file named
233    /// `FILENAME_ROOT_PATH_MARKER` (or `/` if no marker file can be found).
234    /// The root directory is interpreted by Tp-Note's viewer as its base
235    /// directory: only files within this directory are served.
236    root_path: PathBuf,
237    /// If `path` points to a file, we store its creation date here.
238    doc_file_date: Option<SystemTime>,
239    /// Rust requires usage of generic parameters, here `S`.
240    _marker: PhantomData<S>,
241}
242
243/// The methods below are available in all `ContentState` states.
244impl<S: ContextState> Context<S> {
245    /// Getter for `self.path`.
246    /// See `from()` method for details.
247    pub fn get_path(&self) -> &Path {
248        self.path.as_path()
249    }
250
251    /// Getter for `self.dir_path`.
252    /// See `from()` method for details.
253    pub fn get_dir_path(&self) -> &Path {
254        self.dir_path.as_path()
255    }
256
257    /// Getter for `self.root_path`.
258    /// See `from()` method for details.
259    pub fn get_root_path(&self) -> &Path {
260        self.root_path.as_path()
261    }
262
263    /// Getter for `self.doc_file_date`.
264    /// See `from()` method for details.
265    pub fn get_doc_file_date(&self) -> Option<SystemTime> {
266        self.doc_file_date
267    }
268
269    /// Constructor. Unlike `from()` this constructor does not access
270    /// the filesystem in order to detect `dir_path`, `root_path` and
271    /// `doc_file_date`. It copies these values from the passed `context`.
272    /// Use this constructor when you are sure that the above date has
273    /// not changed since you instantiated `context`. In this case you
274    /// can avoid repeated file access.
275    pub fn from_context_path(context: &Context<S>) -> Context<HasSettings> {
276        let mut new_context = Context {
277            ct: tera::Context::new(),
278            path: context.path.clone(),
279            dir_path: context.dir_path.clone(),
280            root_path: context.root_path.clone(),
281            doc_file_date: context.doc_file_date,
282            _marker: PhantomData,
283        };
284
285        new_context.sync_paths_to_map();
286        new_context.insert_config_vars();
287        new_context.insert_settings();
288        new_context
289    }
290
291    /// Helper function that keeps the values with the `self.ct` key
292    ///
293    /// * `TMPL_VAR_PATH` in sync with `self.path`,
294    /// * `TMPL_VAR_DIR_PATH` in sync with `self.dir_path` and
295    /// * `TMPL_VAR_ROOT_PATH` in sync with `self.root_path`.
296    /// * `TMPL_VAR_DOC_FILE_DATE` in sync with `self.doc_file_date` (only if
297    ///
298    /// available).
299    /// Synchronization is performed by copying the latter to the former.
300    fn sync_paths_to_map(&mut self) {
301        self.ct.insert(TMPL_VAR_PATH, &self.path);
302        self.ct.insert(TMPL_VAR_DIR_PATH, &self.dir_path);
303        self.ct.insert(TMPL_VAR_ROOT_PATH, &self.root_path);
304        if let Some(time) = self.doc_file_date {
305            self.ct.insert(
306                TMPL_VAR_DOC_FILE_DATE,
307                &time
308                    .duration_since(SystemTime::UNIX_EPOCH)
309                    .unwrap_or_default()
310                    .as_secs(),
311            )
312        } else {
313            self.ct.remove(TMPL_VAR_DOC_FILE_DATE);
314        };
315    }
316
317    /// Insert some configuration variables into the context so that they
318    /// can be used in the templates.
319    ///
320    /// This function adds the key:
321    ///
322    /// * `TMPL_VAR_SCHEME_SYNC_DEFAULT`.
323    ///
324    /// ```
325    /// use std::path::Path;
326    /// use tpnote_lib::config::TMPL_VAR_SCHEME_SYNC_DEFAULT;
327    /// use tpnote_lib::settings::set_test_default_settings;
328    /// use tpnote_lib::context::Context;
329    /// set_test_default_settings().unwrap();
330    ///
331    /// // The constructor calls `context.insert_settings()` before returning.
332    /// let mut context = Context::from(&Path::new("/path/to/mynote.md")).unwrap();
333    ///
334    /// // When the note's YAML header does not contain a `scheme:` field,
335    /// // the `default` scheme is used.
336    /// assert_eq!(&context.get(TMPL_VAR_SCHEME_SYNC_DEFAULT).unwrap().to_string(),
337    ///     &format!("\"default\""));
338    /// ```
339    fn insert_config_vars(&mut self) {
340        let lib_cfg = LIB_CFG.read_recursive();
341
342        // Default extension for new notes as defined in the configuration file.
343        self.ct.insert(
344            TMPL_VAR_SCHEME_SYNC_DEFAULT,
345            lib_cfg.scheme_sync_default.as_str(),
346        );
347    }
348
349    /// Captures Tp-Note's environment and stores it as variables in a
350    /// `context` collection. The variables are needed later to populate
351    /// a context template and a filename template.
352    ///
353    /// This function adds the keys:
354    ///
355    /// * `TMPL_VAR_EXTENSION_DEFAULT`
356    /// * `TMPL_VAR_USERNAME`
357    /// * `TMPL_VAR_LANG`
358    /// * `TMPL_VAR_CURRENT_SCHEME`
359    ///
360    /// ```
361    /// use std::path::Path;
362    /// use tpnote_lib::config::TMPL_VAR_EXTENSION_DEFAULT;
363    /// use tpnote_lib::config::TMPL_VAR_CURRENT_SCHEME;
364    /// use tpnote_lib::settings::set_test_default_settings;
365    /// use tpnote_lib::context::Context;
366    /// set_test_default_settings().unwrap();
367    ///
368    /// // The constructor calls `context.insert_settings()` before returning.
369    /// let mut context = Context::from(&Path::new("/path/to/mynote.md")).unwrap();
370    ///
371    /// // For most platforms `context.get("extension_default")` is `md`
372    /// assert_eq!(&context.get(TMPL_VAR_EXTENSION_DEFAULT).unwrap().to_string(),
373    ///     &format!("\"md\""));
374    /// // `Settings.current_scheme` is by default the `default` scheme.
375    /// assert_eq!(&context.get(TMPL_VAR_CURRENT_SCHEME).unwrap().to_string(),
376    ///     &format!("\"default\""));
377    /// ```
378    fn insert_settings(&mut self) {
379        let settings = SETTINGS.read_recursive();
380
381        // Default extension for new notes as defined in the configuration file.
382        self.ct.insert(
383            TMPL_VAR_EXTENSION_DEFAULT,
384            settings.extension_default.as_str(),
385        );
386
387        {
388            let lib_cfg = LIB_CFG.read_recursive();
389            self.ct.insert(
390                TMPL_VAR_CURRENT_SCHEME,
391                &lib_cfg.scheme[settings.current_scheme].name,
392            );
393        } // Release `lib_cfg` here.
394
395        // Search for UNIX, Windows, and MacOS user-names.
396        self.ct.insert(TMPL_VAR_USERNAME, &settings.author);
397
398        // Get the user's language tag.
399        self.ct.insert(TMPL_VAR_LANG, &settings.lang);
400
401        // Store `force_lang`.
402        self.ct.insert(TMPL_VAR_FORCE_LANG, &settings.force_lang);
403    }
404
405    /// Inserts the YAML front header variables into the context for later use
406    /// with templates.
407    ///
408    fn insert_front_matter2(&mut self, fm: &FrontMatter) {
409        let mut fm_all_map = self
410            .ct
411            .remove(TMPL_VAR_FM_ALL)
412            .and_then(|v| {
413                if let tera::Value::Object(map) = v {
414                    Some(map)
415                } else {
416                    None
417                }
418            })
419            .unwrap_or_default();
420
421        // Collect all localized scheme field names.
422        // Example: `["scheme", "scheme", "Schema"]`
423        let localized_scheme_names: Vec<String> = LIB_CFG
424            .read_recursive()
425            .scheme
426            .iter()
427            .map(|s| {
428                s.tmpl
429                    .fm_var
430                    .localization
431                    .iter()
432                    .find_map(|(k, v)| (k == TMPL_VAR_FM_SCHEME).then_some(v.to_owned()))
433            })
434            .collect::<Option<Vec<String>>>()
435            .unwrap_or_default();
436
437        // Search for localized scheme names in front matter.
438        // `(scheme_idx, field_value)`. Example: `(2, "Deutsch")`
439        let localized_scheme: Option<(usize, &str)> = localized_scheme_names
440            .iter()
441            .enumerate()
442            .find_map(|(i, k)| fm.0.get(k).and_then(|s| s.as_str()).map(|s| (i, s)));
443
444        let scheme = if let Some((scheme_idx, scheme_name)) = localized_scheme {
445            {
446                log::trace!(
447                    "Found `scheme: {}` with index=={} in front matter",
448                    scheme_name,
449                    scheme_idx,
450                );
451                scheme_idx
452            }
453        } else {
454            SETTINGS.read_recursive().current_scheme
455        };
456        let scheme = &LIB_CFG.read_recursive().scheme[scheme];
457
458        let vars = &scheme.tmpl.fm_var.localization;
459        for (key, value) in fm.iter() {
460            // This delocalizes the variable name and prepends `fm_` to its name.
461            // NB: We also insert `Value::Array` and `Value::Object`
462            // variants, No flattening occurs here.
463            let fm_key = vars.iter().find(|&l| &l.1 == key).map_or_else(
464                || {
465                    let mut s = TMPL_VAR_FM_.to_string();
466                    s.push_str(key);
467                    Cow::Owned(s)
468                },
469                |l| Cow::Borrowed(&l.0),
470            );
471
472            // Store a copy in `fm`.
473            fm_all_map.insert(fm_key.to_string(), value.clone());
474        }
475        // Register the collection as `Object(Map<String, Value>)`.
476        self.ct.insert(TMPL_VAR_FM_ALL, &fm_all_map);
477    }
478
479    /// Insert a key/val pair directly. Only available in tests.
480    #[cfg(test)]
481    pub(crate) fn insert(&mut self, key: &str, val: &tera::Value) {
482        self.ct.insert(key, val);
483    }
484
485    /// Inserts a `Content` in `Context`. The content appears as key in
486    /// `context.ct` with its name taken from `content.name()`.
487    /// Its value is a `tera::Map` with two keys `TMPL_VAR_HEADER` and
488    /// `TMPL_VAR_BODY`. The corresponding values are copied from
489    /// `conten.header()` and `content.body()`.
490    fn insert_raw_text_from_existing_content(&mut self, content: &impl Content) {
491        //
492        // Register input.
493        let mut map = tera::Map::new();
494        map.insert(TMPL_VAR_HEADER.to_string(), content.header().into());
495        map.insert(TMPL_VAR_BODY.to_string(), content.body().into());
496
497        self.ct.insert(content.name(), &tera::Value::from(map));
498    }
499
500    /// See function of the same name in `impl Context<HasSettings>`.
501    fn insert_front_matter_and_raw_text_from_existing_content2(
502        &mut self,
503        clipboards: &Vec<&impl Content>,
504    ) -> Result<(), NoteError> {
505        //
506        for &clip in clipboards {
507            // Register input.
508            self.insert_raw_text_from_existing_content(clip);
509
510            // Can we find a front matter in the input stream? If yes, the
511            // unmodified input stream is our new note content.
512            if !clip.header().is_empty() {
513                let input_fm = FrontMatter::try_from(clip.header());
514                match input_fm {
515                    Ok(ref fm) => {
516                        log::trace!(
517                            "Input stream \"{}\" generates the front matter variables:\n{:#?}",
518                            clip.name(),
519                            &fm
520                        )
521                    }
522                    Err(ref e) => {
523                        if !clip.header().is_empty() {
524                            return Err(NoteError::InvalidInputYaml {
525                                tmpl_var: clip.name().to_string(),
526                                source_str: e.to_string(),
527                            });
528                        }
529                    }
530                };
531
532                // Register front matter.
533                // The variables registered here can be overwrite the ones from the clipboard.
534                if let Ok(fm) = input_fm {
535                    self.insert_front_matter2(&fm);
536                }
537            }
538        }
539        Ok(())
540    }
541}
542
543/// The start state of all `Context` objects.
544///
545impl Context<Invalid> {
546    /// Constructor: `path` is Tp-Notes first positional command line parameter
547    /// `<path>` (see man page). `path` must point to a directory or
548    /// a file.
549    ///
550    /// A copy of `path` is stored in `self.ct` as key `TMPL_VAR_PATH`. It
551    /// directory path as key `TMPL_VAR_DIR_PATH`. The root directory, where
552    /// the marker file `tpnote.toml` was found, is stored with the key
553    /// `TMPL_VAR_ROOT_PATH`. If `path` points to a file, its file creation
554    /// date is stored with the key `TMPL_VAR_DOC_FILE_DATE`.
555    ///
556    /// ```rust
557    /// use std::path::Path;
558    /// use tpnote_lib::settings::set_test_default_settings;
559    /// use tpnote_lib::config::TMPL_VAR_DIR_PATH;
560    /// use tpnote_lib::config::TMPL_VAR_PATH;
561    /// use tpnote_lib::context::Context;
562    /// set_test_default_settings().unwrap();
563    ///
564    /// let mut context = Context::from(&Path::new("/path/to/mynote.md")).unwrap();
565    ///
566    /// assert_eq!(context.get_path(), Path::new("/path/to/mynote.md"));
567    /// assert_eq!(context.get_dir_path(), Path::new("/path/to/"));
568    /// assert_eq!(&context.get(TMPL_VAR_PATH).unwrap().to_string(),
569    ///             r#""/path/to/mynote.md""#);
570    /// assert_eq!(&context.get(TMPL_VAR_DIR_PATH).unwrap().to_string(),
571    ///             r#""/path/to""#);
572    /// ```
573    pub fn from(path: &Path) -> Result<Context<HasSettings>, FileError> {
574        let path = path.to_path_buf();
575
576        // `dir_path` is a directory as fully qualified path, ending
577        // by a separator.
578        let dir_path = if path.is_dir() {
579            path.clone()
580        } else {
581            path.parent()
582                .unwrap_or_else(|| Path::new("./"))
583                .to_path_buf()
584        };
585
586        // Get the root directory.
587        let mut root_path = Path::new("");
588
589        for anc in dir_path.ancestors() {
590            root_path = anc;
591            let mut p = anc.to_owned();
592            p.push(Path::new(FILENAME_ROOT_PATH_MARKER));
593            if p.is_file() {
594                break;
595            }
596        }
597        let root_path = root_path.to_owned();
598        debug_assert!(dir_path.starts_with(&root_path));
599
600        // Get the file's creation date. Fail silently.
601        let file_creation_date = if let Ok(file) = File::open(&path) {
602            let metadata = file.metadata()?;
603            metadata.created().or_else(|_| metadata.modified()).ok()
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, standard input data and/or existing note file
643    /// content into the context. The data may contain some copied text with
644    /// or without a YAML header. The latter usually carries front matter
645    /// variables. The `input` data below is registered with the key name given
646    /// by `tmpl_var_body_name`. Typical names are `"clipboard"` or `"stdin"`.
647    /// If 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}