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