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