Feat: Implement frontmatter based filtering

should close #66
This commit is contained in:
Martin Heuschober 2023-09-16 23:22:57 +01:00
parent 8ace49ded3
commit f47e86b859
17 changed files with 178 additions and 28 deletions

View File

@ -138,9 +138,12 @@ To completely remove any frontmatter from exported notes, use `--frontmatter=nev
## Ignoring files
By default, hidden files, patterns listed in `.export-ignore` as well as any files ignored by git (if your vault is part of a git repository) will be excluded from exports.
The following files are not exported by default:
- hidden files (can be adjusted with `--hidden`)
- files mattching a pattern listed in `.export-ignore` (can be adjusted with `--ignore-file`)
- any files that are ignored by git (can be adjusted with `--no-git`)
- any files having `private: true` in their frontmatter (the keyword `private` can be changed with `--ignore-frontmatter-keyword`)
These options may be adjusted with `--hidden`, `--ignore-file` and `--no-git` if desired.
(See `--help` for more information).
Notes linking to ignored notes will be unlinked (they'll only include the link text).

View File

@ -228,6 +228,7 @@ pub struct Exporter<'a> {
destination: PathBuf,
start_at: PathBuf,
frontmatter_strategy: FrontmatterStrategy,
ignore_frontmatter_keyword: &'a str,
vault_contents: Option<Vec<PathBuf>>,
walk_options: WalkOptions<'a>,
process_embeds_recursively: bool,
@ -241,6 +242,10 @@ impl<'a> fmt::Debug for Exporter<'a> {
.field("root", &self.root)
.field("destination", &self.destination)
.field("frontmatter_strategy", &self.frontmatter_strategy)
.field(
"ignore_frontmatter_keyword",
&self.ignore_frontmatter_keyword,
)
.field("vault_contents", &self.vault_contents)
.field("walk_options", &self.walk_options)
.field(
@ -271,6 +276,7 @@ impl<'a> Exporter<'a> {
root,
destination,
frontmatter_strategy: FrontmatterStrategy::Auto,
ignore_frontmatter_keyword: "private",
walk_options: WalkOptions::default(),
process_embeds_recursively: true,
vault_contents: None,
@ -299,6 +305,11 @@ impl<'a> Exporter<'a> {
self.frontmatter_strategy = strategy;
self
}
/// Set the frontmatter keyword that excludes files from being exported.
pub fn ignore_frontmatter_keyword(&mut self, keyword: &'a str) -> &mut Exporter<'a> {
self.ignore_frontmatter_keyword = keyword;
self
}
/// Set the behavior when recursive embeds are encountered.
///
@ -399,34 +410,39 @@ impl<'a> Exporter<'a> {
let mut context = Context::new(src.to_path_buf(), dest.to_path_buf());
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) {
PostprocessorResult::StopHere => break,
PostprocessorResult::StopAndSkipNote => return Ok(()),
PostprocessorResult::Continue => (),
match frontmatter.get(self.ignore_frontmatter_keyword) {
Some(serde_yaml::Value::Bool(true)) => Ok(()),
_ => {
context.frontmatter = frontmatter;
for func in &self.postprocessors {
match func(&mut context, &mut markdown_events) {
PostprocessorResult::StopHere => break,
PostprocessorResult::StopAndSkipNote => return Ok(()),
PostprocessorResult::Continue => (),
}
}
let dest = context.destination;
let mut outfile = create_file(&dest)?;
let write_frontmatter = match self.frontmatter_strategy {
FrontmatterStrategy::Always => true,
FrontmatterStrategy::Never => false,
FrontmatterStrategy::Auto => !context.frontmatter.is_empty(),
};
if write_frontmatter {
let mut frontmatter_str = frontmatter_to_str(context.frontmatter)
.context(FrontMatterEncodeSnafu { path: src })?;
frontmatter_str.push('\n');
outfile
.write_all(frontmatter_str.as_bytes())
.context(WriteSnafu { path: &dest })?;
}
outfile
.write_all(render_mdevents_to_mdtext(markdown_events).as_bytes())
.context(WriteSnafu { path: &dest })?;
Ok(())
}
}
let dest = context.destination;
let mut outfile = create_file(&dest)?;
let write_frontmatter = match self.frontmatter_strategy {
FrontmatterStrategy::Always => true,
FrontmatterStrategy::Never => false,
FrontmatterStrategy::Auto => !context.frontmatter.is_empty(),
};
if write_frontmatter {
let mut frontmatter_str = frontmatter_to_str(context.frontmatter)
.context(FrontMatterEncodeSnafu { path: src })?;
frontmatter_str.push('\n');
outfile
.write_all(frontmatter_str.as_bytes())
.context(WriteSnafu { path: &dest })?;
}
outfile
.write_all(render_mdevents_to_mdtext(markdown_events).as_bytes())
.context(WriteSnafu { path: &dest })?;
Ok(())
}
fn parse_obsidian_note<'b>(

View File

@ -39,6 +39,13 @@ struct Opts {
)]
ignore_file: String,
#[options(
no_short,
help = "Exclude files with this keyword in the frontmatter from export",
default = "private"
)]
ignore_frontmatter_keyword: String,
#[options(no_short, help = "Export hidden files", default = "false")]
hidden: bool,

View File

@ -425,3 +425,78 @@ fn test_same_filename_different_directories() {
let actual = read_to_string(tmp_dir.path().clone().join(PathBuf::from("Note.md"))).unwrap();
assert_eq!(expected, actual);
}
#[test]
fn test_ignore_frontmatter_default_keyword() {
let tmp_dir = TempDir::new().expect("failed to make tempdir");
let mut exporter = Exporter::new(
PathBuf::from("tests/testdata/input/ignore-keyword/"),
tmp_dir.path().to_path_buf(),
);
exporter.run().expect("exporter returned error");
let walker = WalkDir::new("tests/testdata/expected/ignore-default-keyword/")
// Without sorting here, different test runs may trigger the first assertion failure in
// unpredictable order.
.sort_by(|a, b| a.file_name().cmp(b.file_name()))
.into_iter();
for entry in walker {
let entry = entry.unwrap();
if entry.metadata().unwrap().is_dir() {
continue;
};
let filename = entry.file_name().to_string_lossy().into_owned();
let expected = read_to_string(entry.path()).unwrap_or_else(|_| {
panic!(
"failed to read {} from testdata/expected/ignore-default-keyword/",
entry.path().display()
)
});
let actual = read_to_string(tmp_dir.path().clone().join(PathBuf::from(&filename)))
.unwrap_or_else(|_| panic!("failed to read {} from temporary exportdir", filename));
assert_eq!(
expected, actual,
"{} does not have expected content",
filename
);
}
}
#[test]
fn test_ignore_frontmatter_specific_keyword() {
let tmp_dir = TempDir::new().expect("failed to make tempdir");
let mut exporter = Exporter::new(
PathBuf::from("tests/testdata/input/ignore-keyword/"),
tmp_dir.path().to_path_buf(),
);
exporter.ignore_frontmatter_keyword("no-expört");
exporter.run().expect("exporter returned error");
let walker = WalkDir::new("tests/testdata/expected/ignore-specific-keyword/")
// Without sorting here, different test runs may trigger the first assertion failure in
// unpredictable order.
.sort_by(|a, b| a.file_name().cmp(b.file_name()))
.into_iter();
for entry in walker {
let entry = entry.unwrap();
if entry.metadata().unwrap().is_dir() {
continue;
};
let filename = entry.file_name().to_string_lossy().into_owned();
let expected = read_to_string(entry.path()).unwrap_or_else(|_| {
panic!(
"failed to read {} from testdata/expected/ignore-specific-keyword/",
entry.path().display()
)
});
let actual = read_to_string(tmp_dir.path().clone().join(PathBuf::from(&filename)))
.unwrap_or_else(|_| panic!("failed to read {} from temporary exportdir", filename));
assert_eq!(
expected, actual,
"{} does not have expected content",
filename
);
}
}

View File

@ -0,0 +1,5 @@
---
no-expört: false
---
A note with negated special ignore keyword

View File

@ -0,0 +1,5 @@
---
no-expört: true
---
A note with a special ignore keyword

View File

@ -0,0 +1 @@
A note without frontmatter should be exported.

View File

@ -0,0 +1,5 @@
---
private: false
---
A public note.

View File

@ -0,0 +1,5 @@
---
no-expört: false
---
A note with negated special ignore keyword

View File

@ -0,0 +1 @@
A note without frontmatter should be exported.

View File

@ -0,0 +1,5 @@
---
private: true
---
A private note.

View File

@ -0,0 +1,5 @@
---
private: false
---
A public note.

View File

@ -0,0 +1,4 @@
---
no-expört: false
---
A note with negated special ignore keyword

View File

@ -0,0 +1,4 @@
---
no-expört: true
---
A note with a special ignore keyword

View File

@ -0,0 +1 @@
A note without frontmatter should be exported.

View File

@ -0,0 +1,4 @@
---
private: true
---
A private note.

View File

@ -0,0 +1,4 @@
---
private: false
---
A public note.