From ad51c6ad509a767353742480d3bc46a30217a9ff Mon Sep 17 00:00:00 2001 From: James Jensen Date: Sat, 7 Dec 2024 14:48:30 +0800 Subject: [PATCH 1/3] Add `--linked-attachments-only` option --- src/lib.rs | 28 ++++++++++++++++++++++++++-- src/main.rs | 8 ++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 82e5d3fe..fa282436 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ pub mod postprocessors; mod references; mod walker; +use std::collections::HashSet; use std::ffi::OsString; use std::fs::{self, File}; use std::io::prelude::*; @@ -245,6 +246,7 @@ pub struct Exporter<'a> { preserve_mtime: bool, postprocessors: Vec<&'a Postprocessor<'a>>, embed_postprocessors: Vec<&'a Postprocessor<'a>>, + linked_attachments_only: bool, } impl<'a> fmt::Debug for Exporter<'a> { @@ -291,6 +293,7 @@ impl<'a> Exporter<'a> { vault_contents: None, postprocessors: vec![], embed_postprocessors: vec![], + linked_attachments_only: false, } } @@ -339,6 +342,12 @@ impl<'a> Exporter<'a> { self } + /// Set whether non-markdown files should only be included if linked or embedded in a note. + pub fn linked_attachments_only(&mut self, linked_only: bool) -> &mut Self { + self.linked_attachments_only = linked_only; + self + } + /// 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 Self { @@ -409,7 +418,11 @@ impl<'a> Exporter<'a> { .expect("file should always be nested under root") .to_path_buf(); let destination = &self.destination.join(relative_path); - self.export_note(&file, destination) + if !self.linked_attachments_only || is_markdown_file(&file) { + self.export_note(&file, destination) + } else { + Ok(()) + } })?; Ok(()) } @@ -583,7 +596,7 @@ impl<'a> Exporter<'a> { Some(RefType::Embed) => { let mut elements = self.embed_file( ref_parser.ref_text.clone().as_ref(), - context + context, )?; events.append(&mut elements); buffer.clear(); @@ -673,6 +686,7 @@ impl<'a> Exporter<'a> { } events } + // TODO: Include image in a list of attachments Some("png" | "jpg" | "jpeg" | "gif" | "webp" | "svg") => { self.make_link_to_file(note_ref, &child_context) .into_iter() @@ -728,6 +742,16 @@ impl<'a> Exporter<'a> { ]; } let target_file = target_file.unwrap(); + if self.linked_attachments_only && !is_markdown_file(target_file) { + let relative_path = target_file + .strip_prefix(self.start_at.clone()) + .expect("file should always be nested under root") + .to_path_buf(); + let destination = &self.destination.join(relative_path); + // We should probably do something to handle errors here, but it would require a bit of + // structural change. + let _ = self.export_note(target_file, destination); + } // We use root_file() rather than current_file() here to make sure links are always // relative to the outer-most note, which is the note which this content is inserted into // in case of embedded notes. diff --git a/src/main.rs b/src/main.rs index 1f38c604..a874ce6b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -70,6 +70,13 @@ struct Opts { default = "false" )] hard_linebreaks: bool, + + #[options( + no_short, + help = "Non-markdown files are only exported if they are linked or embedded in a note.", + default = "false" + )] + linked_attachments_only: bool, } fn frontmatter_strategy_from_str(input: &str) -> Result { @@ -105,6 +112,7 @@ fn main() { exporter.frontmatter_strategy(args.frontmatter_strategy); exporter.process_embeds_recursively(!args.no_recursive_embeds); exporter.preserve_mtime(args.preserve_mtime); + exporter.linked_attachments_only(args.linked_attachments_only); exporter.walk_options(walk_options); if args.hard_linebreaks { From 7b7b207df7224616d60c014d0c23cb93856b1448 Mon Sep 17 00:00:00 2001 From: James Jensen Date: Sat, 7 Dec 2024 17:47:59 +0800 Subject: [PATCH 2/3] Tag filter compatibility Make `linked_attachments_only` option compatible with tag filtering, i.e. only occur after post-processing. --- src/lib.rs | 49 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index fa282436..976659df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -444,7 +444,8 @@ impl<'a> Exporter<'a> { fn parse_and_export_obsidian_note(&self, src: &Path, dest: &Path) -> Result<()> { let mut context = Context::new(src.to_path_buf(), dest.to_path_buf()); - let (frontmatter, mut markdown_events) = self.parse_obsidian_note(src, &context)?; + let (frontmatter, mut markdown_events, found_attachments) = + self.parse_obsidian_note(src, &context)?; context.frontmatter = frontmatter; for func in &self.postprocessors { match func(&mut context, &mut markdown_events) { @@ -454,6 +455,17 @@ impl<'a> Exporter<'a> { } } + if self.linked_attachments_only { + for attachment in found_attachments { + let relative_path = attachment + .strip_prefix(self.start_at.clone()) + .expect("file should always be nested under root") + .to_path_buf(); + let destination = &self.destination.join(relative_path); + self.export_note(&attachment, destination)?; + } + } + let mut outfile = create_file(&context.destination)?; let write_frontmatter = match self.frontmatter_strategy { FrontmatterStrategy::Always => true, @@ -485,7 +497,7 @@ impl<'a> Exporter<'a> { &self, path: &Path, context: &Context, - ) -> Result<(Frontmatter, MarkdownEvents<'b>)> { + ) -> Result<(Frontmatter, MarkdownEvents<'b>, HashSet)> { if context.note_depth() > NOTE_RECURSION_LIMIT { return Err(ExportError::RecursionLimitExceeded { file_tree: context.file_tree(), @@ -494,6 +506,12 @@ impl<'a> Exporter<'a> { let content = fs::read_to_string(path).context(ReadSnafu { path })?; let mut frontmatter = String::new(); + // If `linked_attachments_only` is enabled, this is used to keep track of which attachments + // have been linked to in this note or any embedded notes. Note that a file is only + // considered an attachment if it is not a markdown file. These can then be exported after + // the note is fully parsed and any postprocessing has been applied. + let mut found_attachments: HashSet = HashSet::new(); + let parser_options = Options::ENABLE_TABLES | Options::ENABLE_FOOTNOTES | Options::ENABLE_STRIKETHROUGH @@ -588,6 +606,7 @@ impl<'a> Exporter<'a> { ref_parser.ref_text.clone().as_ref() ), context, + &mut found_attachments, ); events.append(&mut elements); buffer.clear(); @@ -597,6 +616,7 @@ impl<'a> Exporter<'a> { let mut elements = self.embed_file( ref_parser.ref_text.clone().as_ref(), context, + &mut found_attachments, )?; events.append(&mut elements); buffer.clear(); @@ -618,6 +638,7 @@ impl<'a> Exporter<'a> { Ok(( frontmatter_from_str(&frontmatter).context(FrontMatterDecodeSnafu { path })?, events.into_iter().map(event_to_owned).collect(), + found_attachments, )) } @@ -630,6 +651,7 @@ impl<'a> Exporter<'a> { &self, link_text: &'a str, context: &'a Context, + found_attachments: &mut HashSet, ) -> Result> { let note_ref = ObsidianNoteReference::from_str(link_text); @@ -639,7 +661,7 @@ impl<'a> Exporter<'a> { // If we have None file it is either to a section or id within the same file and thus // the current embed logic will fail, recurssing until it reaches it's limit. // For now we just bail early. - None => return Ok(self.make_link_to_file(note_ref, context)), + None => return Ok(self.make_link_to_file(note_ref, context, found_attachments)), }; if path.is_none() { @@ -661,14 +683,16 @@ impl<'a> Exporter<'a> { if !self.process_embeds_recursively && context.file_tree().contains(path) { return Ok([ vec![Event::Text(CowStr::Borrowed("→ "))], - self.make_link_to_file(note_ref, &child_context), + self.make_link_to_file(note_ref, &child_context, found_attachments), ] .concat()); } let events = match path.extension().unwrap_or(&no_ext).to_str() { Some("md") => { - let (frontmatter, mut events) = self.parse_obsidian_note(path, &child_context)?; + let (frontmatter, mut events, child_found_attachments) = + self.parse_obsidian_note(path, &child_context)?; + found_attachments.extend(child_found_attachments); child_context.frontmatter = frontmatter; if let Some(section) = note_ref.section { events = reduce_to_section(events, section); @@ -686,9 +710,8 @@ impl<'a> Exporter<'a> { } events } - // TODO: Include image in a list of attachments Some("png" | "jpg" | "jpeg" | "gif" | "webp" | "svg") => { - self.make_link_to_file(note_ref, &child_context) + self.make_link_to_file(note_ref, &child_context, found_attachments) .into_iter() .map(|event| match event { // make_link_to_file returns a link to a file. With this we turn the link @@ -711,7 +734,7 @@ impl<'a> Exporter<'a> { }) .collect() } - _ => self.make_link_to_file(note_ref, &child_context), + _ => self.make_link_to_file(note_ref, &child_context, found_attachments), }; Ok(events) } @@ -720,6 +743,7 @@ impl<'a> Exporter<'a> { &self, reference: ObsidianNoteReference<'_>, context: &Context, + found_attachments: &mut HashSet, ) -> MarkdownEvents<'c> { let target_file = reference.file.map_or_else( || Some(context.current_file()), @@ -743,14 +767,7 @@ impl<'a> Exporter<'a> { } let target_file = target_file.unwrap(); if self.linked_attachments_only && !is_markdown_file(target_file) { - let relative_path = target_file - .strip_prefix(self.start_at.clone()) - .expect("file should always be nested under root") - .to_path_buf(); - let destination = &self.destination.join(relative_path); - // We should probably do something to handle errors here, but it would require a bit of - // structural change. - let _ = self.export_note(target_file, destination); + found_attachments.insert(target_file.clone()); } // We use root_file() rather than current_file() here to make sure links are always // relative to the outer-most note, which is the note which this content is inserted into From 367a332294c593ba4263fe6e3781158e37ec741b Mon Sep 17 00:00:00 2001 From: James Jensen Date: Sat, 14 Dec 2024 15:59:16 +0800 Subject: [PATCH 3/3] Add testing for linked-attachments-only --- tests/postprocessors_test.rs | 7 ++++--- .../expected/filter-by-tags/export-me.md | 2 ++ .../testdata/expected/filter-by-tags/white.png | Bin 0 -> 90 bytes tests/testdata/input/filter-by-tags/bulb.svg | 1 + .../testdata/input/filter-by-tags/export-me.md | 2 ++ .../input/filter-by-tags/export-no-export.md | 4 ++++ .../input/filter-by-tags/no-frontmatter.md | 4 +++- .../input/filter-by-tags/no-no-export.md | 2 ++ tests/testdata/input/filter-by-tags/note.pdf | Bin 0 -> 10435 bytes tests/testdata/input/filter-by-tags/private.md | 2 ++ tests/testdata/input/filter-by-tags/white.png | Bin 0 -> 90 bytes 11 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 tests/testdata/expected/filter-by-tags/white.png create mode 100644 tests/testdata/input/filter-by-tags/bulb.svg create mode 100644 tests/testdata/input/filter-by-tags/note.pdf create mode 100644 tests/testdata/input/filter-by-tags/white.png diff --git a/tests/postprocessors_test.rs b/tests/postprocessors_test.rs index ed66e219..336db6c2 100644 --- a/tests/postprocessors_test.rs +++ b/tests/postprocessors_test.rs @@ -1,5 +1,5 @@ use std::collections::HashSet; -use std::fs::{read_to_string, remove_file}; +use std::fs::{read_to_string, remove_file, read}; use std::path::PathBuf; use std::sync::Mutex; @@ -266,6 +266,7 @@ fn test_filter_by_tags() { vec!["export".into()], ); exporter.add_postprocessor(&filter_by_tags); + exporter.linked_attachments_only(true); exporter.run().unwrap(); let walker = WalkDir::new("tests/testdata/expected/filter-by-tags/") @@ -279,13 +280,13 @@ fn test_filter_by_tags() { continue; }; let filename = entry.file_name().to_string_lossy().into_owned(); - let expected = read_to_string(entry.path()).unwrap_or_else(|_| { + let expected = read(entry.path()).unwrap_or_else(|_| { panic!( "failed to read {} from testdata/expected/filter-by-tags", entry.path().display() ) }); - let actual = read_to_string(tmp_dir.path().join(PathBuf::from(&filename))) + let actual = read(tmp_dir.path().join(PathBuf::from(&filename))) .unwrap_or_else(|_| panic!("failed to read {} from temporary exportdir", filename)); assert_eq!( diff --git a/tests/testdata/expected/filter-by-tags/export-me.md b/tests/testdata/expected/filter-by-tags/export-me.md index 2d4d4f0a..8429d7f5 100644 --- a/tests/testdata/expected/filter-by-tags/export-me.md +++ b/tests/testdata/expected/filter-by-tags/export-me.md @@ -5,3 +5,5 @@ tags: --- A public note + +![white.png](white.png) diff --git a/tests/testdata/expected/filter-by-tags/white.png b/tests/testdata/expected/filter-by-tags/white.png new file mode 100644 index 0000000000000000000000000000000000000000..0e0a663d34f4253cf9bc2106b0d67cbe302bbdf7 GIT binary patch literal 90 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1SBVv2j2ryoCO|{#S9F3${@^GvDCf{D9GdK j;uyklJ^9C%m%6ld^s^>bP0l+XkK_^1^4 literal 0 HcmV?d00001 diff --git a/tests/testdata/input/filter-by-tags/bulb.svg b/tests/testdata/input/filter-by-tags/bulb.svg new file mode 100644 index 00000000..ecbfb2ec --- /dev/null +++ b/tests/testdata/input/filter-by-tags/bulb.svg @@ -0,0 +1 @@ + diff --git a/tests/testdata/input/filter-by-tags/export-me.md b/tests/testdata/input/filter-by-tags/export-me.md index 5b36253c..71fa890b 100644 --- a/tests/testdata/input/filter-by-tags/export-me.md +++ b/tests/testdata/input/filter-by-tags/export-me.md @@ -3,3 +3,5 @@ tags: [export, me] --- A public note + +![[white.png]] diff --git a/tests/testdata/input/filter-by-tags/export-no-export.md b/tests/testdata/input/filter-by-tags/export-no-export.md index 7d604f50..742412bb 100644 --- a/tests/testdata/input/filter-by-tags/export-no-export.md +++ b/tests/testdata/input/filter-by-tags/export-no-export.md @@ -3,3 +3,7 @@ tags: [export, no-export, private] --- A private note + +![[white.png]] + +![[bulb.svg]] diff --git a/tests/testdata/input/filter-by-tags/no-frontmatter.md b/tests/testdata/input/filter-by-tags/no-frontmatter.md index 427b0ea4..0f018310 100644 --- a/tests/testdata/input/filter-by-tags/no-frontmatter.md +++ b/tests/testdata/input/filter-by-tags/no-frontmatter.md @@ -1 +1,3 @@ -A note without frontmatter should be exported. +A note without frontmatter should not be exported. + +![[note.pdf]] diff --git a/tests/testdata/input/filter-by-tags/no-no-export.md b/tests/testdata/input/filter-by-tags/no-no-export.md index cc215040..c3d762bf 100644 --- a/tests/testdata/input/filter-by-tags/no-no-export.md +++ b/tests/testdata/input/filter-by-tags/no-no-export.md @@ -3,3 +3,5 @@ tags: [no, no-export] --- A private note + +![[bulb.svg]] diff --git a/tests/testdata/input/filter-by-tags/note.pdf b/tests/testdata/input/filter-by-tags/note.pdf new file mode 100644 index 0000000000000000000000000000000000000000..574011d0c64df89b6bdf251653b3c6f588b60304 GIT binary patch literal 10435 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 literal 0 HcmV?d00001