Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 73 additions & 14 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ pub type MarkdownEvents<'a> = Vec<Event<'a>>;
/// 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].
Expand All @@ -54,7 +54,7 @@ pub type MarkdownEvents<'a> = Vec<Event<'a>>;
///
/// 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:
///
Expand Down Expand Up @@ -133,10 +133,45 @@ pub type MarkdownEvents<'a> = Vec<Event<'a>>;
/// # exporter.run().unwrap();
/// ```

pub type Postprocessor<'f> =
dyn Fn(&mut Context, &mut MarkdownEvents) -> PostprocessorResult + Send + Sync + 'f;
type Result<T, E = ExportError> = std::result::Result<T, E>;

/// 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<F: Fn(&mut Context, &mut MarkdownEvents) -> 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<F: Fn(&mut Context, &mut MarkdownEvents) -> 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;

Expand Down Expand Up @@ -231,8 +266,8 @@ pub struct Exporter<'a> {
vault_contents: Option<Vec<PathBuf>>,
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> {
Expand Down Expand Up @@ -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 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> {
/// 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
}
Expand Down Expand Up @@ -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 => (),
Expand Down Expand Up @@ -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![];
Expand Down
60 changes: 59 additions & 1 deletion tests/postprocessors_test.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -139,6 +141,62 @@ fn test_postprocessor_stateful_callback() {
assert!(parents.contains(expected));
}

#[test]
fn test_postprocessor_impl() {
#[derive(Default)]
struct Impl {
parents: Mutex<HashSet<PathBuf>>,
embeds: Mutex<u32>,
}
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).
Expand Down