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:
parent
cdb2517365
commit
a0cef3d9c8
10
README.md
10
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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
31
src/lib.rs
31
src/lib.rs
@ -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)
|
||||
}
|
||||
|
@ -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)),
|
||||
},
|
||||
|
@ -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");
|
||||
|
9
tests/testdata/expected/infinite-recursion/Note A.md
vendored
Normal file
9
tests/testdata/expected/infinite-recursion/Note A.md
vendored
Normal 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.
|
3
tests/testdata/input/infinite-recursion/Note A.md
vendored
Normal file
3
tests/testdata/input/infinite-recursion/Note A.md
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
This note (A) embeds note B:
|
||||
|
||||
![[Note B]]
|
3
tests/testdata/input/infinite-recursion/Note B.md
vendored
Normal file
3
tests/testdata/input/infinite-recursion/Note B.md
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
This note (B) embeds note C:
|
||||
|
||||
![[Note C]]
|
5
tests/testdata/input/infinite-recursion/Note C.md
vendored
Normal file
5
tests/testdata/input/infinite-recursion/Note C.md
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
This note (C) embeds note A:
|
||||
|
||||
![[Note A]]
|
||||
|
||||
Note C ends here.
|
@ -1 +0,0 @@
|
||||
![[note]]
|
Loading…
Reference in New Issue
Block a user