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