From 4fb62abe794d50fc2a414fc05f139ac57e154400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20F=C3=B6rberg?= Date: Tue, 24 Aug 2021 22:19:56 +0200 Subject: [PATCH] Add --add-titles option When this option is enabled, each parsed note will be considered to start with a heading: # Title-of-note even when no such line is present in the source file. The title is inferred based on the filename of the note. This option makes heavily nested note embeds make more sense in the exported document, since it shows which note the embedded content comes from. It loosely matches the behaviour of the mainline Obsidian UI when viewing notes in preview mode. --- src/lib.rs | 44 +++++++++++++++++++ src/main.rs | 8 ++++ tests/export_test.rs | 19 ++++++++ .../testdata/expected/add-titles/Main note.md | 7 +++ tests/testdata/input/add-titles/Main note.md | 1 + tests/testdata/input/add-titles/Sub note.md | 1 + .../testdata/input/add-titles/Sub sub note.md | 1 + 7 files changed, 81 insertions(+) create mode 100644 tests/testdata/expected/add-titles/Main note.md create mode 100644 tests/testdata/input/add-titles/Main note.md create mode 100644 tests/testdata/input/add-titles/Sub note.md create mode 100644 tests/testdata/input/add-titles/Sub sub note.md diff --git a/src/lib.rs b/src/lib.rs index 39697a4..4fe217b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -243,6 +243,7 @@ pub struct Exporter<'a> { walk_options: WalkOptions<'a>, process_embeds_recursively: bool, preserve_mtime: bool, + add_titles: bool, postprocessors: Vec<&'a Postprocessor<'a>>, embed_postprocessors: Vec<&'a Postprocessor<'a>>, } @@ -260,6 +261,7 @@ impl fmt::Debug for Exporter<'_> { &self.process_embeds_recursively, ) .field("preserve_mtime", &self.preserve_mtime) + .field("add_titles", &self.add_titles) .field( "postprocessors", &format!("<{} postprocessors active>", self.postprocessors.len()), @@ -288,6 +290,7 @@ impl<'a> Exporter<'a> { walk_options: WalkOptions::default(), process_embeds_recursively: true, preserve_mtime: false, + add_titles: false, vault_contents: None, postprocessors: vec![], embed_postprocessors: vec![], @@ -339,6 +342,23 @@ impl<'a> Exporter<'a> { self } + /// Enable or disable addition of title headings to the top of each parsed note + /// + /// When this option is enabled, each parsed note will be considered to start with a heading: + /// + /// # Title-of-note + /// + /// even when no such line is present in the source file. The title is inferred based on the + /// filename of the note. + /// + /// This option makes heavily nested note embeds make more sense in the exported document, + /// since it shows which note the embedded content comes from. It loosely matches the behaviour + /// of the mainline Obsidian UI when viewing notes in preview mode. + pub fn add_titles(&mut self, add_titles: bool) -> &mut Exporter<'a> { + self.add_titles = add_titles; + self + } + /// Append a function to the chain of [postprocessors][Postprocessor] to run on exported /// Obsidian Markdown notes. pub fn add_postprocessor(&mut self, processor: &'a Postprocessor<'_>) -> &mut Self { @@ -504,6 +524,21 @@ impl<'a> Exporter<'a> { // Most of the time, a reference triggers 5 events: [ or ![, [, , ], ] let mut buffer = Vec::with_capacity(5); + if self.add_titles { + // Ensure that each (possibly embedded) note starts with a reasonable top-level heading + let note_name = infer_note_title_from_path(path); + let h1_tag = Tag::Heading { + level: HeadingLevel::H1, + id: None, + classes: vec![], + attrs: vec![], + }; + + events.push(Event::Start(h1_tag.clone())); + events.push(Event::Text(note_name)); + events.push(Event::End(h1_tag.to_end())); + } + let mut parser = Parser::new_ext(&content, parser_options); 'outer: while let Some(event) = parser.next() { // When encountering a metadata block (frontmatter), collect all events until getting @@ -827,6 +862,15 @@ fn lookup_filename_in_vault<'a>( }) } +fn infer_note_title_from_path(path: &Path) -> CowStr<'_> { + const PLACEHOLDER_TITLE: &str = "invalid-note-title"; + + match path.file_stem() { + None => CowStr::from(PLACEHOLDER_TITLE), + Some(s) => CowStr::from(s.to_string_lossy().into_owned()), + } +} + fn render_mdevents_to_mdtext(markdown: &MarkdownEvents<'_>) -> String { let mut buffer = String::new(); cmark_with_options( diff --git a/src/main.rs b/src/main.rs index bdab2d2..c84f45e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -70,6 +70,13 @@ struct Opts { default = "false" )] hard_linebreaks: bool, + + #[options( + no_short, + help = "Add a heading to the beginning of each note based on its filename", + default = "false" + )] + add_titles: bool, } fn frontmatter_strategy_from_str(input: &str) -> Result { @@ -105,6 +112,7 @@ fn main() { exporter.frontmatter_strategy(args.frontmatter_strategy); exporter.process_embeds_recursively(!args.no_recursive_embeds); exporter.preserve_mtime(args.preserve_mtime); + exporter.add_titles(args.add_titles); exporter.walk_options(walk_options); if args.hard_linebreaks { diff --git a/tests/export_test.rs b/tests/export_test.rs index 8b81721..8e523df 100644 --- a/tests/export_test.rs +++ b/tests/export_test.rs @@ -398,6 +398,25 @@ fn test_no_preserve_mtime() { assert_ne!(src_meta.modified().unwrap(), dest_meta.modified().unwrap()); } +#[test] +fn test_add_titles() { + let tmp_dir = TempDir::new().expect("failed to make tempdir"); + + let mut exporter = Exporter::new( + // Github bug #26 causes embeds not to work with single-file exports. As a workaround, we + // export a whole directory in this test. + PathBuf::from("tests/testdata/input/add-titles/"), + tmp_dir.path().to_path_buf(), + ); + exporter.add_titles(true); + exporter.run().expect("exporter returned error"); + + assert_eq!( + read_to_string("tests/testdata/expected/add-titles/Main note.md").unwrap(), + read_to_string(tmp_dir.path().join(PathBuf::from("Main note.md"))).unwrap(), + ); +} + #[test] fn test_non_ascii_filenames() { let tmp_dir = TempDir::new().expect("failed to make tempdir"); diff --git a/tests/testdata/expected/add-titles/Main note.md b/tests/testdata/expected/add-titles/Main note.md new file mode 100644 index 0000000..98bd5dd --- /dev/null +++ b/tests/testdata/expected/add-titles/Main note.md @@ -0,0 +1,7 @@ +# Main note + +# Sub note + +# Sub sub note + +No more notes diff --git a/tests/testdata/input/add-titles/Main note.md b/tests/testdata/input/add-titles/Main note.md new file mode 100644 index 0000000..f7da68c --- /dev/null +++ b/tests/testdata/input/add-titles/Main note.md @@ -0,0 +1 @@ +![[Sub note]] diff --git a/tests/testdata/input/add-titles/Sub note.md b/tests/testdata/input/add-titles/Sub note.md new file mode 100644 index 0000000..a9a1937 --- /dev/null +++ b/tests/testdata/input/add-titles/Sub note.md @@ -0,0 +1 @@ +![[Sub sub note]] diff --git a/tests/testdata/input/add-titles/Sub sub note.md b/tests/testdata/input/add-titles/Sub sub note.md new file mode 100644 index 0000000..3a17a64 --- /dev/null +++ b/tests/testdata/input/add-titles/Sub sub note.md @@ -0,0 +1 @@ +No more notes