Skip to content

Commit d89491e

Browse files
jforbergJohan Förberg
authored and
Johan Förberg
committed
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.
1 parent 081eb6c commit d89491e

File tree

7 files changed

+76
-0
lines changed

7 files changed

+76
-0
lines changed

src/lib.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ pub struct Exporter<'a> {
230230
vault_contents: Option<Vec<PathBuf>>,
231231
walk_options: WalkOptions<'a>,
232232
process_embeds_recursively: bool,
233+
add_titles: bool,
233234
postprocessors: Vec<&'a Postprocessor>,
234235
embed_postprocessors: Vec<&'a Postprocessor>,
235236
}
@@ -246,6 +247,7 @@ impl<'a> fmt::Debug for Exporter<'a> {
246247
"process_embeds_recursively",
247248
&self.process_embeds_recursively,
248249
)
250+
.field("add_titles", &self.add_titles)
249251
.field(
250252
"postprocessors",
251253
&format!("<{} postprocessors active>", self.postprocessors.len()),
@@ -272,6 +274,7 @@ impl<'a> Exporter<'a> {
272274
frontmatter_strategy: FrontmatterStrategy::Auto,
273275
walk_options: WalkOptions::default(),
274276
process_embeds_recursively: true,
277+
add_titles: false,
275278
vault_contents: None,
276279
postprocessors: vec![],
277280
embed_postprocessors: vec![],
@@ -312,6 +315,23 @@ impl<'a> Exporter<'a> {
312315
self
313316
}
314317

318+
/// Enable or disable addition of title headings to the top of each parsed note
319+
///
320+
/// When this option is enabled, each parsed note will be considered to start with a heading:
321+
///
322+
/// # Title-of-note
323+
///
324+
/// even when no such line is present in the source file. The title is inferred based on the
325+
/// filename of the note.
326+
///
327+
/// This option makes heavily nested note embeds make more sense in the exported document,
328+
/// since it shows which note the embedded content comes from. It loosely matches the behaviour
329+
/// of the mainline Obsidian UI when viewing notes in preview mode.
330+
pub fn add_titles(&mut self, add_titles: bool) -> &mut Exporter<'a> {
331+
self.add_titles = add_titles;
332+
self
333+
}
334+
315335
/// Append a function to the chain of [postprocessors][Postprocessor] to run on exported Obsidian Markdown notes.
316336
pub fn add_postprocessor(&mut self, processor: &'a Postprocessor) -> &mut Exporter<'a> {
317337
self.postprocessors.push(processor);
@@ -455,6 +475,16 @@ impl<'a> Exporter<'a> {
455475
// Most of the time, a reference triggers 5 events: [ or ![, [, <text>, ], ]
456476
let mut buffer = Vec::with_capacity(5);
457477

478+
if self.add_titles {
479+
// Ensure that each (possibly embedded) note starts with a reasonable top-level heading
480+
let note_name = infer_note_title_from_path(path);
481+
let h1_tag = Tag::Heading(HeadingLevel::H1, None, vec![]);
482+
483+
events.push(Event::Start(h1_tag.clone()));
484+
events.push(Event::Text(note_name));
485+
events.push(Event::End(h1_tag.clone()));
486+
}
487+
458488
for event in Parser::new_ext(&content, parser_options) {
459489
if ref_parser.state == RefParserState::Resetting {
460490
events.append(&mut buffer);
@@ -725,6 +755,15 @@ fn lookup_filename_in_vault<'a>(
725755
})
726756
}
727757

758+
fn infer_note_title_from_path<'a>(path: &'a Path) -> CowStr<'a> {
759+
const PLACEHOLDER_TITLE: &str = "invalid-note-title";
760+
761+
match path.file_stem() {
762+
None => CowStr::from(PLACEHOLDER_TITLE),
763+
Some(s) => CowStr::from(s.to_string_lossy().into_owned()),
764+
}
765+
}
766+
728767
fn render_mdevents_to_mdtext(markdown: MarkdownEvents) -> String {
729768
let mut buffer = String::new();
730769
cmark_with_options(

src/main.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ struct Opts {
5454
default = "false"
5555
)]
5656
hard_linebreaks: bool,
57+
58+
#[options(
59+
no_short,
60+
help = "Add a heading to the beginning of each note based on its filename",
61+
default = "false"
62+
)]
63+
add_titles: bool,
5764
}
5865

5966
fn frontmatter_strategy_from_str(input: &str) -> Result<FrontmatterStrategy> {
@@ -88,6 +95,7 @@ fn main() {
8895
let mut exporter = Exporter::new(root, destination);
8996
exporter.frontmatter_strategy(args.frontmatter_strategy);
9097
exporter.process_embeds_recursively(!args.no_recursive_embeds);
98+
exporter.add_titles(args.add_titles);
9199
exporter.walk_options(walk_options);
92100

93101
if args.hard_linebreaks {

tests/export_test.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,25 @@ fn test_no_recursive_embeds() {
363363
);
364364
}
365365

366+
#[test]
367+
fn test_add_titles() {
368+
let tmp_dir = TempDir::new().expect("failed to make tempdir");
369+
370+
let mut exporter = Exporter::new(
371+
// Github bug #26 causes embeds not to work with single-file exports. As a workaround, we
372+
// export a whole directory in this test.
373+
PathBuf::from("tests/testdata/input/add-titles/"),
374+
tmp_dir.path().to_path_buf(),
375+
);
376+
exporter.add_titles(true);
377+
exporter.run().expect("exporter returned error");
378+
379+
assert_eq!(
380+
read_to_string("tests/testdata/expected/add-titles/Main note.md").unwrap(),
381+
read_to_string(tmp_dir.path().clone().join(PathBuf::from("Main note.md"))).unwrap(),
382+
);
383+
}
384+
366385
#[test]
367386
fn test_non_ascii_filenames() {
368387
let tmp_dir = TempDir::new().expect("failed to make tempdir");
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Main note
2+
3+
# Sub note
4+
5+
# Sub sub note
6+
7+
No more notes
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
![[Sub note]]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
![[Sub sub note]]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
No more notes

0 commit comments

Comments
 (0)