Chg: Pass context and events as mutable references to postprocessors

Instead of passing clones of context and the markdown tree to
postprocessors, pass them a mutable reference which may be modified
in-place.

This is a breaking change to the postprocessor implementation, changing
both the input arguments as well as the return value:

```diff
-    dyn Fn(Context, MarkdownEvents) -> (Context, MarkdownEvents, PostprocessorResult) + Send + Sync;
+    dyn Fn(&mut Context, &mut MarkdownEvents) -> PostprocessorResult + Send + Sync;
```

With this change the postprocessor API becomes a little more ergonomic
to use however, especially making the intent around return statements more clear.
This commit is contained in:
Nick Groenen 2022-01-09 12:52:42 +01:00
parent 85adc314b6
commit d25c6d80c6
3 changed files with 43 additions and 70 deletions

View File

@ -74,9 +74,8 @@ pub type MarkdownEvents<'a> = Vec<Event<'a>>;
/// defined inline as a closure. /// defined inline as a closure.
/// ///
/// ``` /// ```
/// use obsidian_export::{Context, Exporter, MarkdownEvents, PostprocessorResult};
/// use obsidian_export::pulldown_cmark::{CowStr, Event};
/// use obsidian_export::serde_yaml::Value; /// use obsidian_export::serde_yaml::Value;
/// use obsidian_export::{Exporter, PostprocessorResult};
/// # use std::path::PathBuf; /// # use std::path::PathBuf;
/// # use tempfile::TempDir; /// # use tempfile::TempDir;
/// ///
@ -86,7 +85,7 @@ pub type MarkdownEvents<'a> = Vec<Event<'a>>;
/// let mut exporter = Exporter::new(source, destination); /// let mut exporter = Exporter::new(source, destination);
/// ///
/// // add_postprocessor registers a new postprocessor. In this example we use a closure. /// // add_postprocessor registers a new postprocessor. In this example we use a closure.
/// exporter.add_postprocessor(&|mut context, events| { /// exporter.add_postprocessor(&|context, _events| {
/// // This is the key we'll insert into the frontmatter. In this case, the string "foo". /// // This is the key we'll insert into the frontmatter. In this case, the string "foo".
/// let key = Value::String("foo".to_string()); /// let key = Value::String("foo".to_string());
/// // This is the value we'll insert into the frontmatter. In this case, the string "bar". /// // This is the value we'll insert into the frontmatter. In this case, the string "bar".
@ -95,9 +94,8 @@ pub type MarkdownEvents<'a> = Vec<Event<'a>>;
/// // Frontmatter can be updated in-place, so we can call insert on it directly. /// // Frontmatter can be updated in-place, so we can call insert on it directly.
/// context.frontmatter.insert(key, value); /// context.frontmatter.insert(key, value);
/// ///
/// // Postprocessors must return their (modified) context, the markdown events that make /// // This return value indicates processing should continue.
/// // up the note and a next action to take. /// PostprocessorResult::Continue
/// (context, events, PostprocessorResult::Continue)
/// }); /// });
/// ///
/// exporter.run().unwrap(); /// exporter.run().unwrap();
@ -117,18 +115,13 @@ pub type MarkdownEvents<'a> = Vec<Event<'a>>;
/// # use tempfile::TempDir; /// # use tempfile::TempDir;
/// # /// #
/// /// This postprocessor replaces any instance of "foo" with "bar" in the note body. /// /// This postprocessor replaces any instance of "foo" with "bar" in the note body.
/// fn foo_to_bar( /// fn foo_to_bar(context: &mut Context, events: &mut MarkdownEvents) -> PostprocessorResult {
/// context: Context, /// for event in events.iter_mut() {
/// events: MarkdownEvents, /// if let Event::Text(text) = event {
/// ) -> (Context, MarkdownEvents, PostprocessorResult) { /// *event = Event::Text(CowStr::from(text.replace("foo", "bar")))
/// let events = events /// }
/// .into_iter() /// }
/// .map(|event| match event { /// PostprocessorResult::Continue
/// Event::Text(text) => Event::Text(CowStr::from(text.replace("foo", "bar"))),
/// event => event,
/// })
/// .collect();
/// (context, events, PostprocessorResult::Continue)
/// } /// }
/// ///
/// # let tmp_dir = TempDir::new().expect("failed to make tempdir"); /// # let tmp_dir = TempDir::new().expect("failed to make tempdir");
@ -140,7 +133,7 @@ pub type MarkdownEvents<'a> = Vec<Event<'a>>;
/// ``` /// ```
pub type Postprocessor = pub type Postprocessor =
dyn Fn(Context, MarkdownEvents) -> (Context, MarkdownEvents, PostprocessorResult) + Send + Sync; dyn Fn(&mut Context, &mut MarkdownEvents) -> PostprocessorResult + Send + Sync;
type Result<T, E = ExportError> = std::result::Result<T, E>; type Result<T, E = ExportError> = std::result::Result<T, E>;
const PERCENTENCODE_CHARS: &AsciiSet = &CONTROLS.add(b' ').add(b'(').add(b')').add(b'%').add(b'?'); const PERCENTENCODE_CHARS: &AsciiSet = &CONTROLS.add(b' ').add(b'(').add(b')').add(b'%').add(b'?');
@ -407,10 +400,7 @@ impl<'a> Exporter<'a> {
let (frontmatter, mut markdown_events) = self.parse_obsidian_note(src, &context)?; let (frontmatter, mut markdown_events) = self.parse_obsidian_note(src, &context)?;
context.frontmatter = frontmatter; context.frontmatter = frontmatter;
for func in &self.postprocessors { for func in &self.postprocessors {
let res = func(context, markdown_events); match func(&mut context, &mut markdown_events) {
context = res.0;
markdown_events = res.1;
match res.2 {
PostprocessorResult::StopHere => break, PostprocessorResult::StopHere => break,
PostprocessorResult::StopAndSkipNote => return Ok(()), PostprocessorResult::StopAndSkipNote => return Ok(()),
PostprocessorResult::Continue => (), PostprocessorResult::Continue => (),
@ -615,10 +605,7 @@ impl<'a> Exporter<'a> {
for func in &self.embed_postprocessors { for func in &self.embed_postprocessors {
// Postprocessors running on embeds shouldn't be able to change frontmatter (or // Postprocessors running on embeds shouldn't be able to change frontmatter (or
// any other metadata), so we give them a clone of the context. // any other metadata), so we give them a clone of the context.
let res = func(child_context, events); match func(&mut child_context, &mut events) {
child_context = res.0;
events = res.1;
match res.2 {
PostprocessorResult::StopHere => break, PostprocessorResult::StopHere => break,
PostprocessorResult::StopAndSkipNote => { PostprocessorResult::StopAndSkipNote => {
events = vec![]; events = vec![];

View File

@ -6,15 +6,13 @@ use pulldown_cmark::Event;
/// This postprocessor converts all soft line breaks to hard line breaks. Enabling this mimics /// This postprocessor converts all soft line breaks to hard line breaks. Enabling this mimics
/// Obsidian's _'Strict line breaks'_ setting. /// Obsidian's _'Strict line breaks'_ setting.
pub fn softbreaks_to_hardbreaks( pub fn softbreaks_to_hardbreaks(
context: Context, _context: &mut Context,
events: MarkdownEvents, events: &mut MarkdownEvents,
) -> (Context, MarkdownEvents, PostprocessorResult) { ) -> PostprocessorResult {
let events = events for event in events.iter_mut() {
.into_iter() if event == &Event::SoftBreak {
.map(|event| match event { *event = Event::HardBreak;
Event::SoftBreak => Event::HardBreak, }
_ => event, }
}) PostprocessorResult::Continue
.collect();
(context, events, PostprocessorResult::Continue)
} }

View File

@ -8,30 +8,22 @@ use std::path::PathBuf;
use tempfile::TempDir; use tempfile::TempDir;
/// This postprocessor replaces any instance of "foo" with "bar" in the note body. /// This postprocessor replaces any instance of "foo" with "bar" in the note body.
fn foo_to_bar( fn foo_to_bar(_ctx: &mut Context, events: &mut MarkdownEvents) -> PostprocessorResult {
ctx: Context, for event in events.iter_mut() {
events: MarkdownEvents, if let Event::Text(text) = event {
) -> (Context, MarkdownEvents, PostprocessorResult) { *event = Event::Text(CowStr::from(text.replace("foo", "bar")))
let events = events }
.into_iter() }
.map(|event| match event { PostprocessorResult::Continue
Event::Text(text) => Event::Text(CowStr::from(text.replace("foo", "bar"))),
event => event,
})
.collect();
(ctx, events, PostprocessorResult::Continue)
} }
/// This postprocessor appends "bar: baz" to frontmatter. /// This postprocessor appends "bar: baz" to frontmatter.
fn append_frontmatter( fn append_frontmatter(ctx: &mut Context, _events: &mut MarkdownEvents) -> PostprocessorResult {
mut ctx: Context,
events: MarkdownEvents,
) -> (Context, MarkdownEvents, PostprocessorResult) {
ctx.frontmatter.insert( ctx.frontmatter.insert(
Value::String("bar".to_string()), Value::String("bar".to_string()),
Value::String("baz".to_string()), Value::String("baz".to_string()),
); );
(ctx, events, PostprocessorResult::Continue) PostprocessorResult::Continue
} }
// The purpose of this test to verify the `append_frontmatter` postprocessor is called to extend // The purpose of this test to verify the `append_frontmatter` postprocessor is called to extend
@ -62,9 +54,8 @@ fn test_postprocessor_stophere() {
tmp_dir.path().to_path_buf(), tmp_dir.path().to_path_buf(),
); );
exporter.add_postprocessor(&|ctx, mdevents| (ctx, mdevents, PostprocessorResult::StopHere)); exporter.add_postprocessor(&|_ctx, _mdevents| PostprocessorResult::StopHere);
exporter exporter.add_embed_postprocessor(&|_ctx, _mdevents| PostprocessorResult::StopHere);
.add_embed_postprocessor(&|ctx, mdevents| (ctx, mdevents, PostprocessorResult::StopHere));
exporter.add_postprocessor(&|_, _| panic!("should not be called due to above processor")); exporter.add_postprocessor(&|_, _| panic!("should not be called due to above processor"));
exporter.add_embed_postprocessor(&|_, _| panic!("should not be called due to above processor")); exporter.add_embed_postprocessor(&|_, _| panic!("should not be called due to above processor"));
exporter.run().unwrap(); exporter.run().unwrap();
@ -84,8 +75,7 @@ fn test_postprocessor_stop_and_skip() {
assert!(note_path.exists()); assert!(note_path.exists());
remove_file(&note_path).unwrap(); remove_file(&note_path).unwrap();
exporter exporter.add_postprocessor(&|_ctx, _mdevents| PostprocessorResult::StopAndSkipNote);
.add_postprocessor(&|ctx, mdevents| (ctx, mdevents, PostprocessorResult::StopAndSkipNote));
exporter.run().unwrap(); exporter.run().unwrap();
assert!(!note_path.exists()); assert!(!note_path.exists());
@ -104,9 +94,9 @@ fn test_postprocessor_change_destination() {
assert!(original_note_path.exists()); assert!(original_note_path.exists());
remove_file(&original_note_path).unwrap(); remove_file(&original_note_path).unwrap();
exporter.add_postprocessor(&|mut ctx, mdevents| { exporter.add_postprocessor(&|ctx, _mdevents| {
ctx.destination.set_file_name("MovedNote.md"); ctx.destination.set_file_name("MovedNote.md");
(ctx, mdevents, PostprocessorResult::Continue) PostprocessorResult::Continue
}); });
exporter.run().unwrap(); exporter.run().unwrap();
@ -147,9 +137,7 @@ fn test_embed_postprocessors_stop_and_skip() {
PathBuf::from("tests/testdata/input/postprocessors"), PathBuf::from("tests/testdata/input/postprocessors"),
tmp_dir.path().to_path_buf(), tmp_dir.path().to_path_buf(),
); );
exporter.add_embed_postprocessor(&|ctx, mdevents| { exporter.add_embed_postprocessor(&|_ctx, _mdevents| PostprocessorResult::StopAndSkipNote);
(ctx, mdevents, PostprocessorResult::StopAndSkipNote)
});
exporter.run().unwrap(); exporter.run().unwrap();
@ -171,9 +159,9 @@ fn test_embed_postprocessors_context() {
tmp_dir.path().to_path_buf(), tmp_dir.path().to_path_buf(),
); );
exporter.add_postprocessor(&|ctx, mdevents| { exporter.add_postprocessor(&|ctx, _mdevents| {
if ctx.current_file() != &PathBuf::from("Note.md") { if ctx.current_file() != &PathBuf::from("Note.md") {
return (ctx, mdevents, PostprocessorResult::Continue); return PostprocessorResult::Continue;
} }
let is_root_note = ctx let is_root_note = ctx
.frontmatter .frontmatter
@ -188,9 +176,9 @@ fn test_embed_postprocessors_context() {
&ctx.current_file().display() &ctx.current_file().display()
) )
} }
(ctx, mdevents, PostprocessorResult::Continue) PostprocessorResult::Continue
}); });
exporter.add_embed_postprocessor(&|ctx, mdevents| { exporter.add_embed_postprocessor(&|ctx, _mdevents| {
let is_root_note = ctx let is_root_note = ctx
.frontmatter .frontmatter
.get(&Value::String("is_root_note".to_string())) .get(&Value::String("is_root_note".to_string()))
@ -204,7 +192,7 @@ fn test_embed_postprocessors_context() {
&ctx.current_file().display() &ctx.current_file().display()
) )
} }
(ctx, mdevents, PostprocessorResult::Continue) PostprocessorResult::Continue
}); });
exporter.run().unwrap(); exporter.run().unwrap();