Skip to main content

tpnote_lib/
context.rs

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