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.
|
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
|
## License
|
||||||
|
|
||||||
|
@ -97,6 +97,16 @@ test
|
|||||||
|
|
||||||
For more comprehensive documentation and examples, see the [gitignore](https://git-scm.com/docs/gitignore) manpage.
|
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
|
## License
|
||||||
|
|
||||||
|
@ -63,3 +63,13 @@ test
|
|||||||
````
|
````
|
||||||
|
|
||||||
For more comprehensive documentation and examples, see the [gitignore](https://git-scm.com/docs/gitignore) manpage.
|
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.
|
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
|
[from_utf8_lossy]: https://doc.rust-lang.org/std/string/struct.String.html#method.from_utf8_lossy
|
||||||
[gitignore]: https://git-scm.com/docs/gitignore
|
[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,
|
frontmatter_strategy: FrontmatterStrategy,
|
||||||
vault_contents: Option<Vec<PathBuf>>,
|
vault_contents: Option<Vec<PathBuf>>,
|
||||||
walk_options: WalkOptions<'a>,
|
walk_options: WalkOptions<'a>,
|
||||||
|
process_embeds_recursively: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -227,6 +228,7 @@ impl<'a> Exporter<'a> {
|
|||||||
destination,
|
destination,
|
||||||
frontmatter_strategy: FrontmatterStrategy::Auto,
|
frontmatter_strategy: FrontmatterStrategy::Auto,
|
||||||
walk_options: WalkOptions::default(),
|
walk_options: WalkOptions::default(),
|
||||||
|
process_embeds_recursively: true,
|
||||||
vault_contents: None,
|
vault_contents: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -243,6 +245,19 @@ impl<'a> Exporter<'a> {
|
|||||||
self
|
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.
|
/// Export notes using the settings configured on this exporter.
|
||||||
pub fn run(&mut self) -> Result<()> {
|
pub fn run(&mut self) -> Result<()> {
|
||||||
if !self.root.exists() {
|
if !self.root.exists() {
|
||||||
@ -455,19 +470,27 @@ impl<'a> Exporter<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let path = path.unwrap();
|
let path = path.unwrap();
|
||||||
let context = Context::from_parent(context, path);
|
let child_context = Context::from_parent(context, path);
|
||||||
let no_ext = OsString::new();
|
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() {
|
let tree = match path.extension().unwrap_or(&no_ext).to_str() {
|
||||||
Some("md") => {
|
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 {
|
if let Some(section) = note_ref.section {
|
||||||
tree = reduce_to_section(tree, section);
|
tree = reduce_to_section(tree, section);
|
||||||
}
|
}
|
||||||
tree
|
tree
|
||||||
}
|
}
|
||||||
Some("png") | Some("jpg") | Some("jpeg") | Some("gif") | Some("webp") => {
|
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()
|
.into_iter()
|
||||||
.map(|event| match event {
|
.map(|event| match event {
|
||||||
// make_link_to_file returns a link to a file. With this we turn the link
|
// 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()
|
.collect()
|
||||||
}
|
}
|
||||||
_ => self.make_link_to_file(note_ref, &context),
|
_ => self.make_link_to_file(note_ref, &child_context),
|
||||||
};
|
};
|
||||||
Ok(tree)
|
Ok(tree)
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,9 @@ struct Opts {
|
|||||||
|
|
||||||
#[options(no_short, help = "Disable git integration", default = "false")]
|
#[options(no_short, help = "Disable git integration", default = "false")]
|
||||||
no_git: bool,
|
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> {
|
fn frontmatter_strategy_from_str(input: &str) -> Result<FrontmatterStrategy> {
|
||||||
@ -58,6 +61,7 @@ fn main() -> Result<()> {
|
|||||||
|
|
||||||
let mut exporter = Exporter::new(source, destination);
|
let mut exporter = Exporter::new(source, destination);
|
||||||
exporter.frontmatter_strategy(args.frontmatter_strategy);
|
exporter.frontmatter_strategy(args.frontmatter_strategy);
|
||||||
|
exporter.process_embeds_recursively(!args.no_recursive_embeds);
|
||||||
exporter.walk_options(walk_options);
|
exporter.walk_options(walk_options);
|
||||||
|
|
||||||
if let Err(err) = exporter.run() {
|
if let Err(err) = exporter.run() {
|
||||||
@ -79,8 +83,9 @@ fn main() -> Result<()> {
|
|||||||
);
|
);
|
||||||
eprintln!("\nFile tree:");
|
eprintln!("\nFile tree:");
|
||||||
for (idx, path) in file_tree.iter().enumerate() {
|
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)),
|
_ => eprintln!("Error: {:?}", eyre!(err)),
|
||||||
},
|
},
|
||||||
|
@ -258,7 +258,7 @@ fn test_infinite_recursion() {
|
|||||||
let tmp_dir = TempDir::new().expect("failed to make tempdir");
|
let tmp_dir = TempDir::new().expect("failed to make tempdir");
|
||||||
|
|
||||||
let err = Exporter::new(
|
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(),
|
tmp_dir.path().to_path_buf(),
|
||||||
)
|
)
|
||||||
.run()
|
.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]
|
#[test]
|
||||||
fn test_non_ascii_filenames() {
|
fn test_non_ascii_filenames() {
|
||||||
let tmp_dir = TempDir::new().expect("failed to make tempdir");
|
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