Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --linked-attachments-only option #316

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
59 changes: 50 additions & 9 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down Expand Up @@ -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> {
Expand Down Expand Up @@ -291,6 +293,7 @@ impl<'a> Exporter<'a> {
vault_contents: None,
postprocessors: vec![],
embed_postprocessors: vec![],
linked_attachments_only: false,
}
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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(())
}
Expand All @@ -431,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) {
Expand All @@ -441,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,
Expand Down Expand Up @@ -472,7 +497,7 @@ impl<'a> Exporter<'a> {
&self,
path: &Path,
context: &Context,
) -> Result<(Frontmatter, MarkdownEvents<'b>)> {
) -> Result<(Frontmatter, MarkdownEvents<'b>, HashSet<PathBuf>)> {
if context.note_depth() > NOTE_RECURSION_LIMIT {
return Err(ExportError::RecursionLimitExceeded {
file_tree: context.file_tree(),
Expand All @@ -481,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<PathBuf> = HashSet::new();

let parser_options = Options::ENABLE_TABLES
| Options::ENABLE_FOOTNOTES
| Options::ENABLE_STRIKETHROUGH
Expand Down Expand Up @@ -575,6 +606,7 @@ impl<'a> Exporter<'a> {
ref_parser.ref_text.clone().as_ref()
),
context,
&mut found_attachments,
);
events.append(&mut elements);
buffer.clear();
Expand All @@ -583,7 +615,8 @@ impl<'a> Exporter<'a> {
Some(RefType::Embed) => {
let mut elements = self.embed_file(
ref_parser.ref_text.clone().as_ref(),
context
context,
&mut found_attachments,
)?;
events.append(&mut elements);
buffer.clear();
Expand All @@ -605,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,
))
}

Expand All @@ -617,6 +651,7 @@ impl<'a> Exporter<'a> {
&self,
link_text: &'a str,
context: &'a Context,
found_attachments: &mut HashSet<PathBuf>,
) -> Result<MarkdownEvents<'b>> {
let note_ref = ObsidianNoteReference::from_str(link_text);

Expand All @@ -626,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() {
Expand All @@ -648,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);
Expand All @@ -674,7 +711,7 @@ impl<'a> Exporter<'a> {
events
}
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
Expand All @@ -697,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)
}
Expand All @@ -706,6 +743,7 @@ impl<'a> Exporter<'a> {
&self,
reference: ObsidianNoteReference<'_>,
context: &Context,
found_attachments: &mut HashSet<PathBuf>,
) -> MarkdownEvents<'c> {
let target_file = reference.file.map_or_else(
|| Some(context.current_file()),
Expand All @@ -728,6 +766,9 @@ impl<'a> Exporter<'a> {
];
}
let target_file = target_file.unwrap();
if self.linked_attachments_only && !is_markdown_file(target_file) {
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
// in case of embedded notes.
Expand Down
8 changes: 8 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<FrontmatterStrategy> {
Expand Down Expand Up @@ -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 {
Expand Down