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(¬edir)
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(¬edir)
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(¬efile, 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(¬efile)
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(¬edir)
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(¬e.context.path.clone())?;
571 // Silently fails is source and target are identical.
572 note.rename_file_from(¬e.context.path)?;
573
574 Ok(())
575}