From 7988b21f5d576f2d5b73d33fd9cf4d086c0ed0a3 Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Mon, 18 Sep 2023 00:06:51 -0400 Subject: [PATCH 1/2] 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). From 1fcdd9db856120fc5ec5919837d6372355a5ea4d Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Sun, 8 Oct 2023 13:59:54 -0400 Subject: [PATCH 2/2] Change the Postprocessor type from an alias to a trait This creates a parallel EmbedPostprocessor trait, and it adds new methods for registering trait objects into the postprocessor chains. The trait is implemented for the callback Fn type that was previously declared as the Postprocessor type. --- src/lib.rs | 96 +++++++++++++++++++----------------- tests/postprocessors_test.rs | 6 +-- 2 files changed, 54 insertions(+), 48 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 626d303..dbd94ab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -133,19 +133,26 @@ 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; -/// Postprocess is a trait form of the [Postprocessor] callback that can be passed to -/// [Exporter::add_postprocessor_impl]. -pub trait Postprocess: Send + Sync { +/// 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; } -/// EmbedPostprocess is a trait form of the [Postprocessor] callback that can be -/// passed to [Exporter::add_embed_postprocessor_impl]. -pub trait EmbedPostprocess: Send + Sync { +/// 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, @@ -153,6 +160,18 @@ pub trait EmbedPostprocess: Send + Sync { ) -> 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; @@ -232,23 +251,6 @@ 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. /// @@ -264,8 +266,8 @@ pub struct Exporter<'a> { vault_contents: Option>, walk_options: WalkOptions<'a>, process_embeds_recursively: bool, - postprocessors: Vec>, - embed_postprocessors: Vec>, + postprocessors: Vec<&'a dyn Postprocessor>, + embed_postprocessors: Vec<&'a dyn EmbedPostprocessor>, } impl<'a> fmt::Debug for Exporter<'a> { @@ -347,34 +349,38 @@ 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(PostprocessorRef::Function(processor)); + 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 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)); + /// 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][Postprocessor] for embeds. - pub fn add_embed_postprocessor(&mut self, processor: &'a Postprocessor) -> &mut Exporter<'a> { - self.embed_postprocessors - .push(PostprocessorRef::Function(processor)); + /// 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 implementation of [EmbedPostprocess] to the chain of [postprocessors] for - /// embeds. + /// Append a trait object to the chain of [postprocessors] for embeds. pub fn add_embed_postprocessor_impl( &mut self, - processor: &'a dyn EmbedPostprocess, + processor: &'a dyn EmbedPostprocessor, ) -> &mut Exporter<'a> { - self.embed_postprocessors - .push(PostprocessorRef::EmbedTrait(processor)); + self.embed_postprocessors.push(processor); self } @@ -454,7 +460,7 @@ impl<'a> Exporter<'a> { let (frontmatter, mut markdown_events) = self.parse_obsidian_note(src, &context)?; context.frontmatter = frontmatter; for processor in &self.postprocessors { - match processor.call(&mut context, &mut markdown_events) { + match processor.postprocess(&mut context, &mut markdown_events) { PostprocessorResult::StopHere => break, PostprocessorResult::StopAndSkipNote => return Ok(()), PostprocessorResult::Continue => (), @@ -659,7 +665,7 @@ impl<'a> Exporter<'a> { 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 processor.call(&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 fcb5e61..b6fe5ca 100644 --- a/tests/postprocessors_test.rs +++ b/tests/postprocessors_test.rs @@ -1,6 +1,6 @@ use obsidian_export::postprocessors::softbreaks_to_hardbreaks; use obsidian_export::{ - Context, EmbedPostprocess, Exporter, MarkdownEvents, Postprocess, PostprocessorResult, + Context, EmbedPostprocessor, Exporter, MarkdownEvents, Postprocessor, PostprocessorResult, }; use pretty_assertions::assert_eq; use pulldown_cmark::{CowStr, Event}; @@ -148,7 +148,7 @@ fn test_postprocessor_impl() { parents: Mutex>, embeds: Mutex, } - impl Postprocess for Impl { + impl Postprocessor for Impl { fn postprocess( &self, ctx: &mut Context, @@ -161,7 +161,7 @@ fn test_postprocessor_impl() { PostprocessorResult::Continue } } - impl EmbedPostprocess for Impl { + impl EmbedPostprocessor for Impl { fn embed_postprocess( &self, _ctx: &mut Context,