Skip to main content

tpnote_lib/
workflow.rs

1//! Tp-Note's high level API.<!-- The low level API is documented
2//! in the module `tpnote_lib::note`. -->
3//!
4//! How to integrate this in your text editor code?
5//! First, call `create_new_note_or_synchronize_filename()`
6//! with the first positional command line parameter `<path>`.
7//! Then open the new text file with the returned path in your
8//! text editor. After modifying the text, saving it and closing your
9//! text editor, call `synchronize_filename()`.
10//! The returned path points to the possibly renamed note file.
11//!
12//! Tp-Note is customizable at runtime by modifying its configuration stored in
13//! `crate::config::LIB_CFG` before executing the functions in this
14//! module (see type definition and documentation in `crate::config::LibCfg`).
15//! All functions in this API are stateless.
16//!
17//!
18//! ## Example with `TemplateKind::New`
19//!
20//! ```rust
21//! use tpnote_lib::content::Content;
22//! use tpnote_lib::content::ContentString;
23//! use tpnote_lib::workflow::WorkflowBuilder;
24//! use std::env::temp_dir;
25//! use std::fs;
26//! use std::path::Path;
27//!
28//! // Prepare test.
29//! let notedir = temp_dir().join("tpnote-lib-doctest-workflow-1");
30//! fs::create_dir_all(&notedir).unwrap();
31//!
32//! let html_clipboard = ContentString::from_string("".to_string(), "html_clipboard".to_string());
33//! let txt_clipboard = ContentString::from_string("".to_string(), "txt_clipboard".to_string());
34//! let stdin = ContentString::from_string("".to_string(), "stdin".to_string());
35//! let v = vec![&html_clipboard, &txt_clipboard, &stdin];
36//! // This is the condition to choose: `TemplateKind::New`:
37//! assert!(html_clipboard.is_empty() && txt_clipboard.is_empty() &&stdin.is_empty());
38//! // There are no inhibitor rules to change the `TemplateKind`.
39//! let template_kind_filter = |tk|tk;
40//!
41//! // Build and run workflow.
42//! let n = WorkflowBuilder::new(&notedir)
43//!       // You can plug in your own type (must impl. `Content`).
44//!      .upgrade::<ContentString, _>(
45//!          "default", v, template_kind_filter)
46//!      .build()
47//!      .run()
48//!      .unwrap();
49//!
50//! // Check result.
51//! assert!(n.as_os_str().to_str().unwrap()
52//!    .contains("--Note"));
53//! assert!(n.is_file());
54//! let raw_note = fs::read_to_string(n).unwrap();
55//! #[cfg(not(target_family = "windows"))]
56//! assert!(raw_note.starts_with("\u{feff}---\ntitle:"));
57//! #[cfg(target_family = "windows")]
58//! assert!(raw_note.starts_with("\u{feff}---\r\ntitle:"));
59//! ```
60//!
61//! The internal data storage for the note's content is `ContentString`
62//! which implements the `Content` trait. Now we modify slightly
63//! the above example to showcase, how to overwrite
64//! one of the trait's methods.
65//!
66//! ```rust
67//! use std::path::Path;
68//! use tpnote_lib::content::Content;
69//! use tpnote_lib::content::ContentString;
70//! use tpnote_lib::workflow::WorkflowBuilder;
71//! use std::env::temp_dir;
72//! use std::path::PathBuf;
73//! use std::fs;
74//! use std::fs::OpenOptions;
75//! use std::io::Write;
76//! use std::ops::Deref;
77//!
78//! #[derive(Default, Debug, Eq, PartialEq)]
79//! // We need a newtype because of the orphan rule.
80//! pub struct MyContentString(ContentString);
81//!
82//! impl AsRef<str> for MyContentString {
83//!     fn as_ref(&self) -> &str {
84//!         self.0.as_ref()
85//!     }
86//! }
87//!
88//! impl Content for MyContentString {
89//!     // Now we overwrite one method to show how to plugin custom code.
90//!     fn save_as(&self, new_file_path: &Path) -> Result<(), std::io::Error> {
91//!         let mut outfile = OpenOptions::new()
92//!             .write(true)
93//!             .create(true)
94//!             .open(&new_file_path)?;
95//!         // We do not save the content to disk, we write intstead:
96//!         write!(outfile, "Simulation")?;
97//!         Ok(())
98//!    }
99//!    // The rest we delegate.
100//!    fn from_string(input: String, name: String) -> Self {
101//!       MyContentString(
102//!           ContentString::from_string(input, name))
103//!    }
104//!    fn header(&self) -> &str {
105//!        self.0.header()
106//!    }
107//!    fn body(&self) -> &str {
108//!        self.0.header()
109//!    }
110//!    fn name(&self) -> &str {
111//!        self.0.name()
112//!    }
113//! }
114//!
115//! // Prepare test.
116//! let notedir = temp_dir().join("tpnote-lib-doctest-workflow-2");
117//! fs::create_dir_all(&notedir).unwrap();
118//!
119//! let html_clipboard = MyContentString::from_string("".to_string(), "html_clipboard".to_string());
120//! let txt_clipboard = MyContentString::from_string("".to_string(), "txt_clipboard".to_string());
121//! let stdin = MyContentString::from_string("".to_string(), "stdin".to_string());
122//! let v = vec![&html_clipboard, &txt_clipboard, &stdin];
123//! // There are no inhibitor rules to change the `TemplateKind`.
124//! let template_kind_filter = |tk|tk;
125//!
126//! // Build and run workflow.
127//! let n = WorkflowBuilder::new(&notedir)
128//!       // You can plug in your own type (must impl. `Content`).
129//!      .upgrade::<MyContentString, _>(
130//!          "default", v, template_kind_filter)
131//!      .build()
132//!      .run()
133//!      .unwrap();
134//!
135//! // Check result.
136//! assert!(n.as_os_str().to_str().unwrap()
137//!    .contains("--Note"));
138//! assert!(n.is_file());
139//! let raw_note = fs::read_to_string(n).unwrap();
140//! assert_eq!(raw_note, "Simulation");
141//! ```
142
143use crate::config::LocalLinkKind;
144use crate::config::TMPL_VAR_FM_;
145use crate::config::TMPL_VAR_FM_ALL;
146use crate::config::TMPL_VAR_FM_FILENAME_SYNC;
147use crate::config::TMPL_VAR_FM_NO_FILENAME_SYNC;
148use crate::config::TMPL_VAR_FM_SCHEME;
149use crate::content::Content;
150use crate::context::Context;
151use crate::error::NoteError;
152use crate::html_renderer::HtmlRenderer;
153use crate::note::Note;
154use crate::settings::SETTINGS;
155use crate::settings::SchemeSource;
156use crate::settings::Settings;
157use crate::template::TemplateKind;
158use parking_lot::RwLockUpgradableReadGuard;
159use std::path::Path;
160use std::path::PathBuf;
161
162/// Typestate of the `WorkflowBuilder`.
163#[derive(Debug, Clone)]
164pub struct WorkflowBuilder<W> {
165    input: W,
166}
167
168/// In this state the workflow will only synchronize the filename.
169#[derive(Debug, Clone)]
170pub struct SyncFilename<'a> {
171    path: &'a Path,
172}
173
174/// In this state the workflow will either synchronize the filename of an
175/// existing note or, -if none exists- create a new note.
176#[derive(Debug, Clone)]
177pub struct SyncFilenameOrCreateNew<'a, T, F> {
178    scheme_source: SchemeSource<'a>,
179    path: &'a Path,
180    clipboards: Vec<&'a T>,
181    tk_filter: F,
182    html_export: Option<(&'a Path, LocalLinkKind)>,
183    force_lang: Option<&'a str>,
184}
185
186impl<'a> WorkflowBuilder<SyncFilename<'a>> {
187    /// Constructor of all workflows. The `path` points
188    /// 1. to an existing note file, or
189    /// 2. to a directory where the new note should be created, or
190    /// 3. to a non-Tp-Note file that will be annotated.
191    ///
192    /// For cases 2. and 3. upgrade the `WorkflowBuilder` with
193    /// `upgrade()` to add additional input data.
194    pub fn new(path: &'a Path) -> Self {
195        Self {
196            input: SyncFilename { path },
197        }
198    }
199
200    /// Upgrade the `WorkflowBuilder` to enable also the creation of new note
201    /// files. It requires providing additional input data:
202    ///
203    /// New notes are created by inserting `Tp-Note`'s environment
204    /// in a template. The template set being used, is determined by
205    /// `scheme_new_default`. If the note to be created exists already, append
206    /// a so called `copy_counter` to the filename and try to save it again. In
207    /// case this does not succeed either, increment the `copy_counter` until a
208    /// free filename is found. The returned path points to the (new) note file
209    /// on disk. Depending on the context, Tp-Note chooses one `TemplateKind`
210    /// to operate (cf. `tpnote_lib::template::TemplateKind::from()`).
211    /// The `tk-filter` allows to overwrite this choice, e.g. you may set
212    /// `TemplateKind::None` under certain circumstances. This way the caller
213    /// can disable the filename synchronization and inject behavior like
214    /// `--no-filename-sync`.
215    ///
216    /// Some templates insert the content of the clipboard or the standard
217    /// input pipe. The input data (can be empty) is provided with a
218    /// vector of `Content` named `clipboards`. The templates expect text with
219    /// markup or HTML. In case of HTML, the `Content.body` must start with
220    /// `<!DOCTYPE html` or `<html`
221    pub fn upgrade<T: Content, F: Fn(TemplateKind) -> TemplateKind>(
222        self,
223        scheme_new_default: &'a str,
224        clipboards: Vec<&'a T>,
225        tk_filter: F,
226    ) -> WorkflowBuilder<SyncFilenameOrCreateNew<'a, T, F>> {
227        WorkflowBuilder {
228            input: SyncFilenameOrCreateNew {
229                scheme_source: SchemeSource::SchemeNewDefault(scheme_new_default),
230                path: self.input.path,
231                clipboards,
232                tk_filter,
233                html_export: None,
234                force_lang: None,
235            },
236        }
237    }
238
239    /// Finalize the build.
240    pub fn build(self) -> Workflow<SyncFilename<'a>> {
241        Workflow { input: self.input }
242    }
243}
244
245impl<'a, T: Content, F: Fn(TemplateKind) -> TemplateKind>
246    WorkflowBuilder<SyncFilenameOrCreateNew<'a, T, F>>
247{
248    /// Set a flag, that the workflow also stores an HTML-rendition of the
249    /// note file next to it.
250    /// This optional HTML rendition is performed just before returning and does
251    /// not affect any above described operation.
252    pub fn html_export(&mut self, path: &'a Path, local_link_kind: LocalLinkKind) {
253        self.input.html_export = Some((path, local_link_kind));
254    }
255
256    /// Overwrite the default scheme.
257    pub fn force_scheme(&mut self, scheme: &'a str) {
258        self.input.scheme_source = SchemeSource::Force(scheme);
259    }
260
261    /// By default, the natural language, the note is written in is guessed
262    /// from the title and subtitle. This disables the automatic guessing
263    /// and forces the language.
264    pub fn force_lang(&mut self, force_lang: &'a str) {
265        self.input.force_lang = Some(force_lang);
266    }
267
268    /// Finalize the build.
269    pub fn build(self) -> Workflow<SyncFilenameOrCreateNew<'a, T, F>> {
270        Workflow { input: self.input }
271    }
272}
273
274/// Holds the input data for the `run()` method.
275#[derive(Debug, Clone)]
276pub struct Workflow<W> {
277    input: W,
278}
279
280impl Workflow<SyncFilename<'_>> {
281    /// Starts the "synchronize filename" workflow. Errors can occur in
282    /// various ways, see `NoteError`.
283    ///
284    /// First, the workflow opens the note file `path` on disk and read its
285    /// YAML front matter. Then, it calculates from the front matter how the
286    /// filename should be to be in sync. If it is different, rename the note on
287    /// disk. Finally, it returns the note's new or existing filename. Repeated
288    /// calls, will reload the environment variables, but not the configuration
289    /// file. This function is stateless.
290    ///
291    /// Note: this method holds an (upgradeable read) lock on the `SETTINGS`
292    /// object to ensure that the `SETTINGS` content does not change. The lock
293    /// also prevents from concurrent execution.
294    ///
295    ///
296    /// ## Example with `TemplateKind::SyncFilename`
297    ///
298    /// ```rust
299    /// use tpnote_lib::content::ContentString;
300    /// use tpnote_lib::workflow::WorkflowBuilder;
301    /// use std::env::temp_dir;
302    /// use std::fs;
303    /// use std::path::Path;
304    ///
305    /// // Prepare test: create existing note.
306    /// let raw = r#"
307    ///
308    /// ---
309    /// title: "My day"
310    /// subtitle: "Note"
311    /// ---
312    /// Body text
313    /// "#;
314    /// let notefile = temp_dir().join("20221030-hello.md");
315    /// fs::write(&notefile, raw.as_bytes()).unwrap();
316    ///
317    /// let expected = temp_dir().join("20221030-My day--Note.md");
318    /// let _ = fs::remove_file(&expected);
319    ///
320    /// // Build and run workflow.
321    /// let n = WorkflowBuilder::new(&notefile)
322    ///      .build()
323    ///      // You can plug in your own type (must impl. `Content`).
324    ///      .run::<ContentString>()
325    ///      .unwrap();
326    ///
327    /// // Check result
328    /// assert_eq!(n, expected);
329    /// assert!(n.is_file());
330    /// ```
331    pub fn run<T: Content>(self) -> Result<PathBuf, NoteError> {
332        // Prevent the rest to run in parallel, other threads will block when they
333        // try to write.
334        let mut settings = SETTINGS.upgradable_read();
335
336        // Collect input data for templates.
337        let context = Context::from(self.input.path)?;
338
339        let content = <T>::open(self.input.path).unwrap_or_default();
340
341        // This does not fill any templates,
342        let mut n = Note::from_existing_content(context, content, TemplateKind::SyncFilename)?;
343
344        synchronize_filename(&mut settings, &mut n)?;
345
346        Ok(n.rendered_filename)
347    }
348}
349
350impl<T: Content, F: Fn(TemplateKind) -> TemplateKind> Workflow<SyncFilenameOrCreateNew<'_, T, F>> {
351    /// Starts the "synchronize filename or create a new note" workflow.
352    /// Returns the note's new or existing filename. Repeated calls, will
353    /// reload the environment variables, but not the configuration file. This
354    /// function is stateless.
355    /// Errors can occur in various ways, see `NoteError`.
356    ///
357    /// Note: this method holds an (upgradeable read) lock on the `SETTINGS`
358    /// object to ensure that the `SETTINGS` content does not change. The lock
359    /// also prevents from concurrent execution.
360    ///
361    ///
362    /// ## Example with `TemplateKind::FromClipboard`
363    ///
364    /// ```rust
365    /// use tpnote_lib::content::Content;
366    /// use tpnote_lib::content::ContentString;
367    /// use tpnote_lib::workflow::WorkflowBuilder;
368    /// use std::env::temp_dir;
369    /// use std::path::PathBuf;
370    /// use std::fs;
371    ///
372    /// // Prepare test.
373    /// let notedir = temp_dir().join("tpnote-lib-doctest-workflow-3");
374    /// fs::create_dir_all(&notedir).unwrap();
375    ///
376    /// let html_clipboard = ContentString::from_string(
377    ///     "my HTML clipboard\n".to_string(),
378    ///     "html_clipboard".to_string()
379    /// );
380    /// let txt_clipboard = ContentString::from_string(
381    ///     "my TXT clipboard\n".to_string(),
382    ///     "txt_clipboard".to_string()
383    /// );
384    /// let stdin = ContentString::from_string(
385    ///     "my stdin\n".to_string(),
386    ///     "stdin".to_string()
387    /// );
388    /// let v = vec![&html_clipboard, &txt_clipboard, &stdin];
389    /// // This is the condition to choose: `TemplateKind::FromClipboard`:
390    /// assert!(html_clipboard.header().is_empty()
391    ///            && txt_clipboard.header().is_empty()
392    ///            && stdin.header().is_empty());
393    /// assert!(!html_clipboard.body().is_empty() || !txt_clipboard.body().is_empty() || !stdin.body().is_empty());
394    /// let template_kind_filter = |tk|tk;
395    ///
396    /// // Build and run workflow.
397    /// let n = WorkflowBuilder::new(&notedir)
398    ///       // You can plug in your own type (must impl. `Content`).
399    ///      .upgrade::<ContentString, _>(
400    ///            "default", v, template_kind_filter)
401    ///      .build()
402    ///      .run()
403    ///      .unwrap();
404    ///
405    /// // Check result.
406    /// assert!(n.as_os_str().to_str().unwrap()
407    ///    .contains("my stdin--Note"));
408    /// assert!(n.is_file());
409    /// let raw_note = fs::read_to_string(n).unwrap();
410    ///
411    /// #[cfg(not(target_family = "windows"))]
412    /// assert!(raw_note.starts_with(
413    ///            "\u{feff}---\ntitle:        my stdin"));
414    /// #[cfg(target_family = "windows")]
415    /// assert!(raw_note.starts_with(
416    ///            "\u{feff}---\r\ntitle:"));
417    /// ```
418    pub fn run(self) -> Result<PathBuf, NoteError> {
419        // Prevent the rest to run in parallel, other threads will block when they
420        // try to write.
421        let mut settings = SETTINGS.upgradable_read();
422
423        // Initialize settings.
424        settings.with_upgraded(|settings| {
425            settings.update(self.input.scheme_source, self.input.force_lang)
426        })?;
427
428        // First, generate a new note (if it does not exist), then parse its front_matter
429        // and finally rename the file, if it is not in sync with its front matter.
430
431        // Collect input data for templates.
432        let context = Context::from(self.input.path)?;
433
434        // `template_kind` will tell us what to do.
435        let (template_kind, content) = TemplateKind::from(self.input.path);
436        let template_kind = (self.input.tk_filter)(template_kind);
437
438        let n = match template_kind {
439            TemplateKind::FromDir | TemplateKind::AnnotateFile => {
440                // CREATE A NEW NOTE WITH THE `TMPL_NEW_CONTENT` TEMPLATE
441                // All these template do not refer to existing front matter,
442                // as there is none yet.
443                let context = context
444                    .insert_front_matter_and_raw_text_from_existing_content(&self.input.clipboards)?
445                    .set_state_ready_for_content_template();
446
447                let mut n = Note::from_content_template(context, template_kind)?;
448                n.render_filename(template_kind)?;
449                // Check if the filename is not taken already
450                n.set_next_unused_rendered_filename()?;
451                n.save()?;
452                n
453            }
454
455            TemplateKind::FromTextFile => {
456                // This is part of the contract for this template:
457                let content: T = content.unwrap();
458                debug_assert!(&content.header().is_empty());
459                debug_assert!(!&content.body().is_empty());
460
461                let context = context
462                    .insert_front_matter_and_raw_text_from_existing_content(&self.input.clipboards)?
463                    .insert_front_matter_and_raw_text_from_existing_content(&vec![&content])?;
464
465                let context = context.set_state_ready_for_content_template();
466
467                let mut n = Note::from_content_template(context, TemplateKind::FromTextFile)?;
468                // Render filename.
469                n.render_filename(template_kind)?;
470
471                // Save new note.
472                let context_path = n.context.get_path().to_owned();
473                n.set_next_unused_rendered_filename_or(&context_path)?;
474                n.save_and_delete_from(&context_path)?;
475                n
476            }
477
478            TemplateKind::SyncFilename => {
479                let mut n = Note::from_existing_content(
480                    context,
481                    content.unwrap(),
482                    TemplateKind::SyncFilename,
483                )?;
484
485                synchronize_filename(&mut settings, &mut n)?;
486                n
487            }
488
489            TemplateKind::None => {
490                Note::from_existing_content(context, content.unwrap(), template_kind)?
491            }
492        };
493
494        // If no new filename was rendered, return the old one.
495        let mut n = n;
496        if n.rendered_filename == PathBuf::new() {
497            n.rendered_filename = n.context.get_path().to_owned();
498        }
499
500        // Export HTML rendition, if wanted.
501        if let Some((export_dir, local_link_kind)) = self.input.html_export {
502            HtmlRenderer::save_exporter_page(
503                &n.rendered_filename,
504                n.content,
505                export_dir,
506                local_link_kind,
507            )?;
508        }
509
510        Ok(n.rendered_filename)
511    }
512}
513
514///
515/// Helper function. We take `RwLockUpgradableReadGuard<Settings>` as parameter
516/// with a unique `mut` pointer because:
517/// 1. It serves as a lock to prevent several instances of
518///    `synchronize_filename` from running in parallel.
519/// 2. We need write access to `SETTINGS` in this function.
520fn synchronize_filename<T: Content>(
521    settings: &mut RwLockUpgradableReadGuard<Settings>,
522    note: &mut Note<T>,
523) -> Result<(), NoteError> {
524    let no_filename_sync = match (
525        note.context
526            .get(TMPL_VAR_FM_ALL)
527            .and_then(|v| v.get_from_path(TMPL_VAR_FM_FILENAME_SYNC)),
528        note.context
529            .get(TMPL_VAR_FM_ALL)
530            .and_then(|v| v.get_from_path(TMPL_VAR_FM_NO_FILENAME_SYNC)),
531    ) {
532        // By default we sync.
533        (None, None) => false,
534        (None, Some(v)) => v.as_bool().unwrap_or(true),
535        (Some(v), None) => !v.as_bool().unwrap_or(false),
536        _ => false,
537    };
538
539    if no_filename_sync {
540        log::info!(
541            "Filename synchronisation disabled with the front matter field: `{}: {}`",
542            TMPL_VAR_FM_FILENAME_SYNC.trim_start_matches(TMPL_VAR_FM_),
543            !no_filename_sync
544        );
545        return Ok(());
546    }
547
548    // Shall we switch the `settings.current_theme`?
549    // If `fm_scheme` is defined, prefer this value.
550    let fm_scheme_val = note
551        .context
552        .get(TMPL_VAR_FM_ALL)
553        .and_then(|v| v.get_from_path(TMPL_VAR_FM_SCHEME));
554    match fm_scheme_val {
555        None => {
556            settings.with_upgraded(|settings| {
557                settings.update_current_scheme(SchemeSource::SchemeSyncDefault)
558            })?;
559        }
560        Some(v) => match v.as_str() {
561            Some(s) if !s.is_empty() => {
562                settings
563                    .with_upgraded(|settings| settings.update_current_scheme(SchemeSource::Force(s)))?;
564                log::info!("Switch to scheme `{}` as indicated in front matter", s);
565            }
566            Some(_) => {
567                settings.with_upgraded(|settings| {
568                    settings.update_current_scheme(SchemeSource::SchemeSyncDefault)
569                })?;
570            }
571            None => {
572                return Err(NoteError::FrontMatterFieldIsNotString {
573                    field_name: TMPL_VAR_FM_SCHEME.to_string(),
574                });
575            }
576        },
577    };
578
579    note.render_filename(TemplateKind::SyncFilename)?;
580
581    let path = note.context.get_path().to_owned();
582    note.set_next_unused_rendered_filename_or(&path)?;
583    // Silently fails is source and target are identical.
584    note.rename_file_from(note.context.get_path())?;
585
586    Ok(())
587}