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