Skip to content

Commit 7988b21

Browse files
committed
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 #175
1 parent a791273 commit 7988b21

File tree

2 files changed

+123
-12
lines changed

2 files changed

+123
-12
lines changed

src/lib.rs

+64-11
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ pub type MarkdownEvents<'a> = Vec<Event<'a>>;
3939
/// converted to regular markdown syntax.
4040
///
4141
/// Postprocessors are called in the order they've been added through [Exporter::add_postprocessor]
42-
/// just before notes are written out to their final destination.
43-
/// They may be used to achieve the following:
42+
/// or [Exporter::add_postprocessor_impl] just before notes are written out to their final
43+
/// destination. They may be used to achieve the following:
4444
///
4545
/// 1. Modify a note's [Context], for example to change the destination filename or update its [Frontmatter] (see [Context::frontmatter]).
4646
/// 2. Change a note's contents by altering [MarkdownEvents].
@@ -54,7 +54,7 @@ pub type MarkdownEvents<'a> = Vec<Event<'a>>;
5454
///
5555
/// In some cases it may be desirable to change the contents of these embedded notes *before* they
5656
/// are inserted into the final document. This is possible through the use of
57-
/// [Exporter::add_embed_postprocessor].
57+
/// [Exporter::add_embed_postprocessor] or [Exporter::add_embed_postprocessor_impl].
5858
/// These "embed postprocessors" run much the same way as regular postprocessors, but they're run on
5959
/// the note that is about to be embedded in another note. In addition:
6060
///
@@ -137,6 +137,22 @@ pub type Postprocessor<'f> =
137137
dyn Fn(&mut Context, &mut MarkdownEvents) -> PostprocessorResult + Send + Sync + 'f;
138138
type Result<T, E = ExportError> = std::result::Result<T, E>;
139139

140+
/// Postprocess is a trait form of the [Postprocessor] callback that can be passed to
141+
/// [Exporter::add_postprocessor_impl].
142+
pub trait Postprocess: Send + Sync {
143+
fn postprocess(&self, ctx: &mut Context, events: &mut MarkdownEvents) -> PostprocessorResult;
144+
}
145+
146+
/// EmbedPostprocess is a trait form of the [Postprocessor] callback that can be
147+
/// passed to [Exporter::add_embed_postprocessor_impl].
148+
pub trait EmbedPostprocess: Send + Sync {
149+
fn embed_postprocess(
150+
&self,
151+
ctx: &mut Context,
152+
events: &mut MarkdownEvents,
153+
) -> PostprocessorResult;
154+
}
155+
140156
const PERCENTENCODE_CHARS: &AsciiSet = &CONTROLS.add(b' ').add(b'(').add(b')').add(b'%').add(b'?');
141157
const NOTE_RECURSION_LIMIT: usize = 10;
142158

@@ -216,6 +232,23 @@ pub enum PostprocessorResult {
216232
StopAndSkipNote,
217233
}
218234

235+
#[derive(Clone)]
236+
enum PostprocessorRef<'p> {
237+
Function(&'p Postprocessor<'p>),
238+
Trait(&'p dyn Postprocess),
239+
EmbedTrait(&'p dyn EmbedPostprocess),
240+
}
241+
242+
impl<'p> PostprocessorRef<'p> {
243+
fn call(&'p self, ctx: &mut Context, events: &mut MarkdownEvents) -> PostprocessorResult {
244+
match self {
245+
PostprocessorRef::Function(f) => f(ctx, events),
246+
PostprocessorRef::Trait(t) => t.postprocess(ctx, events),
247+
PostprocessorRef::EmbedTrait(t) => t.embed_postprocess(ctx, events),
248+
}
249+
}
250+
}
251+
219252
#[derive(Clone)]
220253
/// Exporter provides the main interface to this library.
221254
///
@@ -231,8 +264,8 @@ pub struct Exporter<'a> {
231264
vault_contents: Option<Vec<PathBuf>>,
232265
walk_options: WalkOptions<'a>,
233266
process_embeds_recursively: bool,
234-
postprocessors: Vec<&'a Postprocessor<'a>>,
235-
embed_postprocessors: Vec<&'a Postprocessor<'a>>,
267+
postprocessors: Vec<PostprocessorRef<'a>>,
268+
embed_postprocessors: Vec<PostprocessorRef<'a>>,
236269
}
237270

238271
impl<'a> fmt::Debug for Exporter<'a> {
@@ -315,13 +348,33 @@ impl<'a> Exporter<'a> {
315348

316349
/// Append a function to the chain of [postprocessors][Postprocessor] to run on exported Obsidian Markdown notes.
317350
pub fn add_postprocessor(&mut self, processor: &'a Postprocessor) -> &mut Exporter<'a> {
318-
self.postprocessors.push(processor);
351+
self.postprocessors
352+
.push(PostprocessorRef::Function(processor));
353+
self
354+
}
355+
356+
/// Append a trait implementation of [Postprocess] to the chain of [postprocessors] to run on
357+
/// Obsidian Markdown notes.
358+
pub fn add_postprocessor_impl(&mut self, processor: &'a dyn Postprocess) -> &mut Exporter<'a> {
359+
self.postprocessors.push(PostprocessorRef::Trait(processor));
319360
self
320361
}
321362

322363
/// Append a function to the chain of [postprocessors][Postprocessor] for embeds.
323364
pub fn add_embed_postprocessor(&mut self, processor: &'a Postprocessor) -> &mut Exporter<'a> {
324-
self.embed_postprocessors.push(processor);
365+
self.embed_postprocessors
366+
.push(PostprocessorRef::Function(processor));
367+
self
368+
}
369+
370+
/// Append a trait implementation of [EmbedPostprocess] to the chain of [postprocessors] for
371+
/// embeds.
372+
pub fn add_embed_postprocessor_impl(
373+
&mut self,
374+
processor: &'a dyn EmbedPostprocess,
375+
) -> &mut Exporter<'a> {
376+
self.embed_postprocessors
377+
.push(PostprocessorRef::EmbedTrait(processor));
325378
self
326379
}
327380

@@ -400,8 +453,8 @@ impl<'a> Exporter<'a> {
400453

401454
let (frontmatter, mut markdown_events) = self.parse_obsidian_note(src, &context)?;
402455
context.frontmatter = frontmatter;
403-
for func in &self.postprocessors {
404-
match func(&mut context, &mut markdown_events) {
456+
for processor in &self.postprocessors {
457+
match processor.call(&mut context, &mut markdown_events) {
405458
PostprocessorResult::StopHere => break,
406459
PostprocessorResult::StopAndSkipNote => return Ok(()),
407460
PostprocessorResult::Continue => (),
@@ -603,10 +656,10 @@ impl<'a> Exporter<'a> {
603656
if let Some(section) = note_ref.section {
604657
events = reduce_to_section(events, section);
605658
}
606-
for func in &self.embed_postprocessors {
659+
for processor in &self.embed_postprocessors {
607660
// Postprocessors running on embeds shouldn't be able to change frontmatter (or
608661
// any other metadata), so we give them a clone of the context.
609-
match func(&mut child_context, &mut events) {
662+
match processor.call(&mut child_context, &mut events) {
610663
PostprocessorResult::StopHere => break,
611664
PostprocessorResult::StopAndSkipNote => {
612665
events = vec![];

tests/postprocessors_test.rs

+59-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use obsidian_export::postprocessors::softbreaks_to_hardbreaks;
2-
use obsidian_export::{Context, Exporter, MarkdownEvents, PostprocessorResult};
2+
use obsidian_export::{
3+
Context, EmbedPostprocess, Exporter, MarkdownEvents, Postprocess, PostprocessorResult,
4+
};
35
use pretty_assertions::assert_eq;
46
use pulldown_cmark::{CowStr, Event};
57
use serde_yaml::Value;
@@ -139,6 +141,62 @@ fn test_postprocessor_stateful_callback() {
139141
assert!(parents.contains(expected));
140142
}
141143

144+
#[test]
145+
fn test_postprocessor_impl() {
146+
#[derive(Default)]
147+
struct Impl {
148+
parents: Mutex<HashSet<PathBuf>>,
149+
embeds: Mutex<u32>,
150+
}
151+
impl Postprocess for Impl {
152+
fn postprocess(
153+
&self,
154+
ctx: &mut Context,
155+
_events: &mut MarkdownEvents,
156+
) -> PostprocessorResult {
157+
self.parents
158+
.lock()
159+
.unwrap()
160+
.insert(ctx.destination.parent().unwrap().to_path_buf());
161+
PostprocessorResult::Continue
162+
}
163+
}
164+
impl EmbedPostprocess for Impl {
165+
fn embed_postprocess(
166+
&self,
167+
_ctx: &mut Context,
168+
_events: &mut MarkdownEvents,
169+
) -> PostprocessorResult {
170+
let mut embeds = self.embeds.lock().unwrap();
171+
*embeds += 1;
172+
PostprocessorResult::Continue
173+
}
174+
}
175+
176+
let tmp_dir = TempDir::new().expect("failed to make tempdir");
177+
let mut exporter = Exporter::new(
178+
PathBuf::from("tests/testdata/input/postprocessors"),
179+
tmp_dir.path().to_path_buf(),
180+
);
181+
182+
let postprocessor = Impl {
183+
..Default::default()
184+
};
185+
exporter.add_postprocessor_impl(&postprocessor);
186+
exporter.add_embed_postprocessor_impl(&postprocessor);
187+
188+
exporter.run().unwrap();
189+
190+
let expected = tmp_dir.path().clone();
191+
192+
let parents = postprocessor.parents.lock().unwrap();
193+
println!("{:?}", parents);
194+
assert_eq!(1, parents.len());
195+
assert!(parents.contains(expected));
196+
197+
assert_eq!(1, *postprocessor.embeds.lock().unwrap());
198+
}
199+
142200
// The purpose of this test to verify the `append_frontmatter` postprocessor is called to extend
143201
// the frontmatter, and the `foo_to_bar` postprocessor is called to replace instances of "foo" with
144202
// "bar" (only in the note body).

0 commit comments

Comments
 (0)