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