From a0cef3d9c8d632919bc3db7c8bd8be4c5981b8bf Mon Sep 17 00:00:00 2001 From: Nick Groenen Date: Tue, 5 Jan 2021 15:37:37 +0100 Subject: [PATCH] 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 --- README.md | 10 ++++++ book/book-src/README.md | 10 ++++++ book/book-src/usage.md | 10 ++++++ book/obsidian-src/usage.md | 10 ++++++ src/lib.rs | 31 ++++++++++++++++--- src/main.rs | 7 ++++- tests/export_test.rs | 19 +++++++++++- .../expected/infinite-recursion/Note A.md | 9 ++++++ .../input/infinite-recursion/Note A.md | 3 ++ .../input/infinite-recursion/Note B.md | 3 ++ .../input/infinite-recursion/Note C.md | 5 +++ .../testdata/input/infinite-recursion/note.md | 1 - 12 files changed, 111 insertions(+), 7 deletions(-) create mode 100644 tests/testdata/expected/infinite-recursion/Note A.md create mode 100644 tests/testdata/input/infinite-recursion/Note A.md create mode 100644 tests/testdata/input/infinite-recursion/Note B.md create mode 100644 tests/testdata/input/infinite-recursion/Note C.md delete mode 100644 tests/testdata/input/infinite-recursion/note.md diff --git a/README.md b/README.md index 6139cca..507248a 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/book/book-src/README.md b/book/book-src/README.md index 6139cca..507248a 100644 --- a/book/book-src/README.md +++ b/book/book-src/README.md @@ -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 diff --git a/book/book-src/usage.md b/book/book-src/usage.md index d6d6fe2..e047cf4 100644 --- a/book/book-src/usage.md +++ b/book/book-src/usage.md @@ -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. diff --git a/book/obsidian-src/usage.md b/book/obsidian-src/usage.md index 5db3fb6..d94582c 100644 --- a/book/obsidian-src/usage.md +++ b/book/obsidian-src/usage.md @@ -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 diff --git a/src/lib.rs b/src/lib.rs index 7fbd13d..991f2c4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -107,6 +107,7 @@ pub struct Exporter<'a> { frontmatter_strategy: FrontmatterStrategy, vault_contents: Option>, 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) } diff --git a/src/main.rs b/src/main.rs index caf15f9..8a1464f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 { @@ -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)), }, diff --git a/tests/export_test.rs b/tests/export_test.rs index bbe2494..a68d598 100644 --- a/tests/export_test.rs +++ b/tests/export_test.rs @@ -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"); diff --git a/tests/testdata/expected/infinite-recursion/Note A.md b/tests/testdata/expected/infinite-recursion/Note A.md new file mode 100644 index 0000000..907f74b --- /dev/null +++ b/tests/testdata/expected/infinite-recursion/Note A.md @@ -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. diff --git a/tests/testdata/input/infinite-recursion/Note A.md b/tests/testdata/input/infinite-recursion/Note A.md new file mode 100644 index 0000000..333e460 --- /dev/null +++ b/tests/testdata/input/infinite-recursion/Note A.md @@ -0,0 +1,3 @@ +This note (A) embeds note B: + +![[Note B]] diff --git a/tests/testdata/input/infinite-recursion/Note B.md b/tests/testdata/input/infinite-recursion/Note B.md new file mode 100644 index 0000000..15753a6 --- /dev/null +++ b/tests/testdata/input/infinite-recursion/Note B.md @@ -0,0 +1,3 @@ +This note (B) embeds note C: + +![[Note C]] diff --git a/tests/testdata/input/infinite-recursion/Note C.md b/tests/testdata/input/infinite-recursion/Note C.md new file mode 100644 index 0000000..45955a7 --- /dev/null +++ b/tests/testdata/input/infinite-recursion/Note C.md @@ -0,0 +1,5 @@ +This note (C) embeds note A: + +![[Note A]] + +Note C ends here. diff --git a/tests/testdata/input/infinite-recursion/note.md b/tests/testdata/input/infinite-recursion/note.md deleted file mode 100644 index 9590f22..0000000 --- a/tests/testdata/input/infinite-recursion/note.md +++ /dev/null @@ -1 +0,0 @@ -![[note]]