New: Add --no-recursive-embeds to break infinite recursion cycles

It's possible to end up with "recursive embeds" when two notes embed
each other. This happens for example when a `Note A.md` contains
`![[Note B]]` but `Note B.md` also contains `![[Note A]]`.

By default, this will trigger an error and display the chain of notes
which caused the recursion.

Using the new `--no-recursive-embeds`, if a note is encountered for a
second time while processing the original note, rather than embedding it
again a link to the note is inserted instead to break the cycle.

See also: https://github.com/zoni/obsidian-export/issues/1
This commit is contained in:
Nick Groenen 2021-01-05 15:37:37 +01:00
parent cdb2517365
commit a0cef3d9c8
No known key found for this signature in database
GPG Key ID: 4F0AD019928AE098
12 changed files with 111 additions and 7 deletions

View File

@ -97,6 +97,16 @@ test
For more comprehensive documentation and examples, see the [gitignore](https://git-scm.com/docs/gitignore) manpage.
### Recursive embeds
It's possible to end up with "recursive embeds" when two notes embed each other.
This happens for example when a `Note A.md` contains `![[Note B]]` but `Note B.md` also contains `![[Note A]]`.
By default, this will trigger an error and display the chain of notes which caused the recursion.
This behavior may be changed by specifying `--no-recursive-embeds`.
Using this mode, if a note is encountered for a second time while processing the original note, instead of embedding it again a link to the note is inserted instead to break the cycle.
## License

View File

@ -97,6 +97,16 @@ test
For more comprehensive documentation and examples, see the [gitignore](https://git-scm.com/docs/gitignore) manpage.
### Recursive embeds
It's possible to end up with "recursive embeds" when two notes embed each other.
This happens for example when a `Note A.md` contains `![[Note B]]` but `Note B.md` also contains `![[Note A]]`.
By default, this will trigger an error and display the chain of notes which caused the recursion.
This behavior may be changed by specifying `--no-recursive-embeds`.
Using this mode, if a note is encountered for a second time while processing the original note, instead of embedding it again a link to the note is inserted instead to break the cycle.
## License

View File

@ -63,3 +63,13 @@ test
````
For more comprehensive documentation and examples, see the [gitignore](https://git-scm.com/docs/gitignore) manpage.
### Recursive embeds
It's possible to end up with "recursive embeds" when two notes embed each other.
This happens for example when a `Note A.md` contains `![[Note B]]` but `Note B.md` also contains `![[Note A]]`.
By default, this will trigger an error and display the chain of notes which caused the recursion.
This behavior may be changed by specifying `--no-recursive-embeds`.
Using this mode, if a note is encountered for a second time while processing the original note, instead of embedding it again a link to the note is inserted instead to break the cycle.

View File

@ -64,5 +64,15 @@ test
For more comprehensive documentation and examples, see the [gitignore] manpage.
### Recursive embeds
It's possible to end up with "recursive embeds" when two notes embed each other.
This happens for example when a `Note A.md` contains `![[Note B]]` but `Note B.md` also contains `![[Note A]]`.
By default, this will trigger an error and display the chain of notes which caused the recursion.
This behavior may be changed by specifying `--no-recursive-embeds`.
Using this mode, if a note is encountered for a second time while processing the original note, instead of embedding it again a link to the note is inserted instead to break the cycle.
[from_utf8_lossy]: https://doc.rust-lang.org/std/string/struct.String.html#method.from_utf8_lossy
[gitignore]: https://git-scm.com/docs/gitignore

View File

@ -107,6 +107,7 @@ pub struct Exporter<'a> {
frontmatter_strategy: FrontmatterStrategy,
vault_contents: Option<Vec<PathBuf>>,
walk_options: WalkOptions<'a>,
process_embeds_recursively: bool,
}
#[derive(Debug, Clone)]
@ -227,6 +228,7 @@ impl<'a> Exporter<'a> {
destination,
frontmatter_strategy: FrontmatterStrategy::Auto,
walk_options: WalkOptions::default(),
process_embeds_recursively: true,
vault_contents: None,
}
}
@ -243,6 +245,19 @@ impl<'a> Exporter<'a> {
self
}
/// Set the behavior when recursive embeds are encountered.
///
/// When `recursive` is true (the default), emdeds are always processed recursively. This may
/// lead to infinite recursion when note A embeds B, but B also embeds A.
/// (When this happens, [ExportError::RecursionLimitExceeded] will be returned by [Exporter::run]).
///
/// When `recursive` is false, if a note is encountered for a second time while processing the
/// original note, instead of embedding it again a link to the note is inserted instead.
pub fn process_embeds_recursively(&mut self, recursive: bool) -> &mut Exporter<'a> {
self.process_embeds_recursively = recursive;
self
}
/// Export notes using the settings configured on this exporter.
pub fn run(&mut self) -> Result<()> {
if !self.root.exists() {
@ -455,19 +470,27 @@ impl<'a> Exporter<'a> {
}
let path = path.unwrap();
let context = Context::from_parent(context, path);
let child_context = Context::from_parent(context, path);
let no_ext = OsString::new();
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),
]
.concat());
}
let tree = match path.extension().unwrap_or(&no_ext).to_str() {
Some("md") => {
let mut tree = self.parse_obsidian_note(&path, &context)?;
let mut tree = self.parse_obsidian_note(&path, &child_context)?;
if let Some(section) = note_ref.section {
tree = reduce_to_section(tree, section);
}
tree
}
Some("png") | Some("jpg") | Some("jpeg") | Some("gif") | Some("webp") => {
self.make_link_to_file(note_ref, &context)
self.make_link_to_file(note_ref, &child_context)
.into_iter()
.map(|event| match event {
// make_link_to_file returns a link to a file. With this we turn the link
@ -492,7 +515,7 @@ impl<'a> Exporter<'a> {
})
.collect()
}
_ => self.make_link_to_file(note_ref, &context),
_ => self.make_link_to_file(note_ref, &child_context),
};
Ok(tree)
}

View File

@ -35,6 +35,9 @@ struct Opts {
#[options(no_short, help = "Disable git integration", default = "false")]
no_git: bool,
#[options(no_short, help = "Don't process embeds recursively", default = "false")]
no_recursive_embeds: bool,
}
fn frontmatter_strategy_from_str(input: &str) -> Result<FrontmatterStrategy> {
@ -58,6 +61,7 @@ fn main() -> Result<()> {
let mut exporter = Exporter::new(source, destination);
exporter.frontmatter_strategy(args.frontmatter_strategy);
exporter.process_embeds_recursively(!args.no_recursive_embeds);
exporter.walk_options(walk_options);
if let Err(err) = exporter.run() {
@ -79,8 +83,9 @@ fn main() -> Result<()> {
);
eprintln!("\nFile tree:");
for (idx, path) in file_tree.iter().enumerate() {
eprintln!("{}-> {}", " ".repeat(idx), path.display());
eprintln!(" {}-> {}", " ".repeat(idx), path.display());
}
eprintln!("\nHint: Ensure notes are non-recursive, or specify --no-recursive-embeds to break cycles")
}
_ => eprintln!("Error: {:?}", eyre!(err)),
},

View File

@ -258,7 +258,7 @@ fn test_infinite_recursion() {
let tmp_dir = TempDir::new().expect("failed to make tempdir");
let err = Exporter::new(
PathBuf::from("tests/testdata/input/infinite-recursion/note.md"),
PathBuf::from("tests/testdata/input/infinite-recursion/"),
tmp_dir.path().to_path_buf(),
)
.run()
@ -273,6 +273,23 @@ fn test_infinite_recursion() {
}
}
#[test]
fn test_no_recursive_embeds() {
let tmp_dir = TempDir::new().expect("failed to make tempdir");
let mut exporter = Exporter::new(
PathBuf::from("tests/testdata/input/infinite-recursion/"),
tmp_dir.path().to_path_buf(),
);
exporter.process_embeds_recursively(false);
exporter.run().expect("exporter returned error");
assert_eq!(
read_to_string("tests/testdata/expected/infinite-recursion/Note A.md").unwrap(),
read_to_string(tmp_dir.path().clone().join(PathBuf::from("Note A.md"))).unwrap(),
);
}
#[test]
fn test_non_ascii_filenames() {
let tmp_dir = TempDir::new().expect("failed to make tempdir");

View File

@ -0,0 +1,9 @@
This note (A) embeds note B:
This note (B) embeds note C:
This note (C) embeds note A:
→ [Note A](Note%20A.md)
Note C ends here.

View File

@ -0,0 +1,3 @@
This note (A) embeds note B:
![[Note B]]

View File

@ -0,0 +1,3 @@
This note (B) embeds note C:
![[Note C]]

View File

@ -0,0 +1,5 @@
This note (C) embeds note A:
![[Note A]]
Note C ends here.

View File

@ -1 +0,0 @@
![[note]]