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(¬edir)
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(¬edir)
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(¬efile, 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(¬efile)
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(¬edir)
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}