diff --git a/src/lib.rs b/src/lib.rs index bb3f7e5..dbd94ab 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: /// @@ -133,10 +133,45 @@ pub type MarkdownEvents<'a> = Vec>; /// # exporter.run().unwrap(); /// ``` -pub type Postprocessor<'f> = - dyn Fn(&mut Context, &mut MarkdownEvents) -> PostprocessorResult + Send + Sync + 'f; type Result = std::result::Result; +/// Postprocessor that can be that can be passed to [Exporter::add_postprocessor_impl]. +pub trait Postprocessor: Send + Sync { + fn postprocess(&self, ctx: &mut Context, events: &mut MarkdownEvents) -> PostprocessorResult; +} + +/// Postprocessor is implemented for any callback function type that matches the +/// signature. +impl PostprocessorResult + Send + Sync> Postprocessor + for F +{ + fn postprocess(&self, ctx: &mut Context, events: &mut MarkdownEvents) -> PostprocessorResult { + self(ctx, events) + } +} + +/// EmbedPostprocessor is like [Postprocessor] but for note embeds, and it is passed to +/// [Exporter::add_embed_postprocessor_impl]. +pub trait EmbedPostprocessor: Send + Sync { + fn embed_postprocess( + &self, + ctx: &mut Context, + events: &mut MarkdownEvents, + ) -> PostprocessorResult; +} + +impl PostprocessorResult + Send + Sync> + EmbedPostprocessor for F +{ + fn embed_postprocess( + &self, + ctx: &mut Context, + events: &mut MarkdownEvents, + ) -> PostprocessorResult { + self(ctx, events) + } +} + const PERCENTENCODE_CHARS: &AsciiSet = &CONTROLS.add(b' ').add(b'(').add(b')').add(b'%').add(b'?'); const NOTE_RECURSION_LIMIT: usize = 10; @@ -231,8 +266,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<&'a dyn Postprocessor>, + embed_postprocessors: Vec<&'a dyn EmbedPostprocessor>, } impl<'a> fmt::Debug for Exporter<'a> { @@ -314,13 +349,37 @@ 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> { + pub fn add_postprocessor( + &mut self, + processor: &'a (impl Fn(&mut Context, &mut MarkdownEvents) -> PostprocessorResult + Send + Sync), + ) -> &mut Exporter<'a> { self.postprocessors.push(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> { + /// Append a trait object to the chain of [postprocessors] to run on Obsidian Markdown notes. + pub fn add_postprocessor_impl( + &mut self, + processor: &'a dyn Postprocessor, + ) -> &mut Exporter<'a> { + self.postprocessors.push(processor); + self + } + + /// Append a function to the chain of [postprocessors][EmbedPostprocessor] for embeds. + pub fn add_embed_postprocessor( + &mut self, + processor: &'a (impl Fn(&mut Context, &mut MarkdownEvents) -> PostprocessorResult + Send + Sync), + ) -> &mut Exporter<'a> { + self.embed_postprocessors.push(processor); + self + } + + /// Append a trait object to the chain of [postprocessors] for embeds. + pub fn add_embed_postprocessor_impl( + &mut self, + processor: &'a dyn EmbedPostprocessor, + ) -> &mut Exporter<'a> { self.embed_postprocessors.push(processor); self } @@ -400,8 +459,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.postprocess(&mut context, &mut markdown_events) { PostprocessorResult::StopHere => break, PostprocessorResult::StopAndSkipNote => return Ok(()), PostprocessorResult::Continue => (), @@ -603,10 +662,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.embed_postprocess(&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..b6fe5ca 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, EmbedPostprocessor, Exporter, MarkdownEvents, Postprocessor, 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 Postprocessor 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 EmbedPostprocessor 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).