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