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