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