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