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