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