From 7988b21f5d576f2d5b73d33fd9cf4d086c0ed0a3 Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Mon, 18 Sep 2023 00:06:51 -0400 Subject: [PATCH] Define new Postprocess and EmbedPostprocess traits These are like the Postprocessor callback function type, but they can be implemented on types for more ergonomic stateful postprocessing. Fixes zoni/obsidian-export#175 --- src/lib.rs | 75 ++++++++++++++++++++++++++++++------ tests/postprocessors_test.rs | 60 ++++++++++++++++++++++++++++- 2 files changed, 123 insertions(+), 12 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index bb3f7e5..626d303 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,8 +39,8 @@ pub type MarkdownEvents<'a> = Vec>; /// converted to regular markdown syntax. /// /// Postprocessors are called in the order they've been added through [Exporter::add_postprocessor] -/// just before notes are written out to their final destination. -/// They may be used to achieve the following: +/// or [Exporter::add_postprocessor_impl] just before notes are written out to their final +/// destination. They may be used to achieve the following: /// /// 1. Modify a note's [Context], for example to change the destination filename or update its [Frontmatter] (see [Context::frontmatter]). /// 2. Change a note's contents by altering [MarkdownEvents]. @@ -54,7 +54,7 @@ pub type MarkdownEvents<'a> = Vec>; /// /// In some cases it may be desirable to change the contents of these embedded notes *before* they /// are inserted into the final document. This is possible through the use of -/// [Exporter::add_embed_postprocessor]. +/// [Exporter::add_embed_postprocessor] or [Exporter::add_embed_postprocessor_impl]. /// These "embed postprocessors" run much the same way as regular postprocessors, but they're run on /// the note that is about to be embedded in another note. In addition: /// @@ -137,6 +137,22 @@ pub type Postprocessor<'f> = dyn Fn(&mut Context, &mut MarkdownEvents) -> PostprocessorResult + Send + Sync + 'f; type Result = std::result::Result; +/// Postprocess is a trait form of the [Postprocessor] callback that can be passed to +/// [Exporter::add_postprocessor_impl]. +pub trait Postprocess: Send + Sync { + fn postprocess(&self, ctx: &mut Context, events: &mut MarkdownEvents) -> PostprocessorResult; +} + +/// EmbedPostprocess is a trait form of the [Postprocessor] callback that can be +/// passed to [Exporter::add_embed_postprocessor_impl]. +pub trait EmbedPostprocess: Send + Sync { + fn embed_postprocess( + &self, + ctx: &mut Context, + events: &mut MarkdownEvents, + ) -> PostprocessorResult; +} + const PERCENTENCODE_CHARS: &AsciiSet = &CONTROLS.add(b' ').add(b'(').add(b')').add(b'%').add(b'?'); const NOTE_RECURSION_LIMIT: usize = 10; @@ -216,6 +232,23 @@ pub enum PostprocessorResult { StopAndSkipNote, } +#[derive(Clone)] +enum PostprocessorRef<'p> { + Function(&'p Postprocessor<'p>), + Trait(&'p dyn Postprocess), + EmbedTrait(&'p dyn EmbedPostprocess), +} + +impl<'p> PostprocessorRef<'p> { + fn call(&'p self, ctx: &mut Context, events: &mut MarkdownEvents) -> PostprocessorResult { + match self { + PostprocessorRef::Function(f) => f(ctx, events), + PostprocessorRef::Trait(t) => t.postprocess(ctx, events), + PostprocessorRef::EmbedTrait(t) => t.embed_postprocess(ctx, events), + } + } +} + #[derive(Clone)] /// Exporter provides the main interface to this library. /// @@ -231,8 +264,8 @@ pub struct Exporter<'a> { vault_contents: Option>, walk_options: WalkOptions<'a>, process_embeds_recursively: bool, - postprocessors: Vec<&'a Postprocessor<'a>>, - embed_postprocessors: Vec<&'a Postprocessor<'a>>, + postprocessors: Vec>, + embed_postprocessors: Vec>, } impl<'a> fmt::Debug for Exporter<'a> { @@ -315,13 +348,33 @@ impl<'a> Exporter<'a> { /// Append a function to the chain of [postprocessors][Postprocessor] to run on exported Obsidian Markdown notes. pub fn add_postprocessor(&mut self, processor: &'a Postprocessor) -> &mut Exporter<'a> { - self.postprocessors.push(processor); + self.postprocessors + .push(PostprocessorRef::Function(processor)); + self + } + + /// Append a trait implementation of [Postprocess] to the chain of [postprocessors] to run on + /// Obsidian Markdown notes. + pub fn add_postprocessor_impl(&mut self, processor: &'a dyn Postprocess) -> &mut Exporter<'a> { + self.postprocessors.push(PostprocessorRef::Trait(processor)); self } /// Append a function to the chain of [postprocessors][Postprocessor] for embeds. pub fn add_embed_postprocessor(&mut self, processor: &'a Postprocessor) -> &mut Exporter<'a> { - self.embed_postprocessors.push(processor); + self.embed_postprocessors + .push(PostprocessorRef::Function(processor)); + self + } + + /// Append a trait implementation of [EmbedPostprocess] to the chain of [postprocessors] for + /// embeds. + pub fn add_embed_postprocessor_impl( + &mut self, + processor: &'a dyn EmbedPostprocess, + ) -> &mut Exporter<'a> { + self.embed_postprocessors + .push(PostprocessorRef::EmbedTrait(processor)); self } @@ -400,8 +453,8 @@ impl<'a> Exporter<'a> { let (frontmatter, mut markdown_events) = self.parse_obsidian_note(src, &context)?; context.frontmatter = frontmatter; - for func in &self.postprocessors { - match func(&mut context, &mut markdown_events) { + for processor in &self.postprocessors { + match processor.call(&mut context, &mut markdown_events) { PostprocessorResult::StopHere => break, PostprocessorResult::StopAndSkipNote => return Ok(()), PostprocessorResult::Continue => (), @@ -603,10 +656,10 @@ impl<'a> Exporter<'a> { if let Some(section) = note_ref.section { events = reduce_to_section(events, section); } - for func in &self.embed_postprocessors { + for processor in &self.embed_postprocessors { // Postprocessors running on embeds shouldn't be able to change frontmatter (or // any other metadata), so we give them a clone of the context. - match func(&mut child_context, &mut events) { + match processor.call(&mut child_context, &mut events) { PostprocessorResult::StopHere => break, PostprocessorResult::StopAndSkipNote => { events = vec![]; diff --git a/tests/postprocessors_test.rs b/tests/postprocessors_test.rs index 5311896..fcb5e61 100644 --- a/tests/postprocessors_test.rs +++ b/tests/postprocessors_test.rs @@ -1,5 +1,7 @@ use obsidian_export::postprocessors::softbreaks_to_hardbreaks; -use obsidian_export::{Context, Exporter, MarkdownEvents, PostprocessorResult}; +use obsidian_export::{ + Context, EmbedPostprocess, Exporter, MarkdownEvents, Postprocess, PostprocessorResult, +}; use pretty_assertions::assert_eq; use pulldown_cmark::{CowStr, Event}; use serde_yaml::Value; @@ -139,6 +141,62 @@ fn test_postprocessor_stateful_callback() { assert!(parents.contains(expected)); } +#[test] +fn test_postprocessor_impl() { + #[derive(Default)] + struct Impl { + parents: Mutex>, + embeds: Mutex, + } + impl Postprocess for Impl { + fn postprocess( + &self, + ctx: &mut Context, + _events: &mut MarkdownEvents, + ) -> PostprocessorResult { + self.parents + .lock() + .unwrap() + .insert(ctx.destination.parent().unwrap().to_path_buf()); + PostprocessorResult::Continue + } + } + impl EmbedPostprocess for Impl { + fn embed_postprocess( + &self, + _ctx: &mut Context, + _events: &mut MarkdownEvents, + ) -> PostprocessorResult { + let mut embeds = self.embeds.lock().unwrap(); + *embeds += 1; + PostprocessorResult::Continue + } + } + + let tmp_dir = TempDir::new().expect("failed to make tempdir"); + let mut exporter = Exporter::new( + PathBuf::from("tests/testdata/input/postprocessors"), + tmp_dir.path().to_path_buf(), + ); + + let postprocessor = Impl { + ..Default::default() + }; + exporter.add_postprocessor_impl(&postprocessor); + exporter.add_embed_postprocessor_impl(&postprocessor); + + exporter.run().unwrap(); + + let expected = tmp_dir.path().clone(); + + let parents = postprocessor.parents.lock().unwrap(); + println!("{:?}", parents); + assert_eq!(1, parents.len()); + assert!(parents.contains(expected)); + + assert_eq!(1, *postprocessor.embeds.lock().unwrap()); +} + // The purpose of this test to verify the `append_frontmatter` postprocessor is called to extend // the frontmatter, and the `foo_to_bar` postprocessor is called to replace instances of "foo" with // "bar" (only in the note body).