Created
January 4, 2024 05:24
-
-
Save sulami/04efb003796cb3756e998ed1edf5fe38 to your computer and use it in GitHub Desktop.
Extract Obsidian meeting notes from daily notes
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[package] | |
name = "extract-meetings" | |
version = "0.1.0" | |
edition = "2021" | |
[dependencies] | |
anyhow = "1" | |
regex = "1" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//! This is a very specialized piece of code, but maybe it's useful to someone. | |
//! | |
//! This takes notes from Obsidian, and extracts all meetings from daily notes | |
//! into their own files, and then embeds those in the daily note. | |
//! | |
//! For this to work, the meeting needs to have a heading of some sort, as well | |
//! as the `#meeting` tag on the following line. | |
//! | |
//! Meeting titles are slightly munged to avoid problems with the file system | |
//! (e.g. / becomes -), and the date is prefixed to the title to avoid naming | |
//! conflicts. | |
use std::{ | |
fmt::Write, | |
fs::{read_dir, read_to_string, write}, | |
path::PathBuf, | |
}; | |
use anyhow::{Context, Result}; | |
use regex::Regex; | |
const DIR: &str = "/Users/sulami/Library/Mobile Documents/iCloud~md~obsidian/Documents/default"; | |
fn main() -> Result<()> { | |
let re = | |
Regex::new(r"^\d{4}-\d{2}-\d{2}\.md$").context("failed to compile daily file regex")?; | |
for file in read_dir(DIR).context("failed to read directory")? { | |
let path = file.as_ref().unwrap().path(); | |
if path.is_dir() { | |
continue; | |
} | |
if !re.is_match(path.file_name().unwrap().to_str().unwrap()) { | |
continue; | |
} | |
extract_meetings(&path).context("failed to extract meetings")?; | |
} | |
Ok(()) | |
} | |
fn extract_meetings(path: &PathBuf) -> Result<()> { | |
let date = path | |
.file_name() | |
.unwrap() | |
.to_str() | |
.unwrap() | |
.split('.') | |
.next() | |
.unwrap(); | |
let contents = read_to_string(path).context("error reading daily note")?; | |
let header = Regex::new(r"^#+ ").context("error compiling header regex")?; | |
let mut meetings: Vec<(usize, usize)> = vec![]; | |
#[derive(Copy, Clone, Debug)] | |
enum State { | |
Searching, | |
FoundHeader(usize, usize), | |
InMeeting(usize, usize), | |
} | |
let mut state = State::Searching; | |
let mut lines = contents.lines().enumerate(); | |
let mut current_line = lines.next(); | |
loop { | |
match (state, current_line) { | |
(State::Searching, Some((num, line))) => { | |
if header.is_match(line) { | |
let level = line.chars().take_while(|c| *c == '#').count(); | |
state = State::FoundHeader(num, level); | |
} | |
current_line = lines.next(); | |
} | |
(State::FoundHeader(start, level), Some((_, line))) => { | |
if line.contains("#meeting") { | |
state = State::InMeeting(start, level); | |
} else { | |
state = State::Searching; | |
} | |
current_line = lines.next(); | |
} | |
(State::InMeeting(start, level), Some((num, line))) => { | |
if header.is_match(line) && line.chars().take_while(|c| *c == '#').count() <= level | |
{ | |
meetings.push((start, num - 1)); | |
state = State::Searching; | |
} else { | |
current_line = lines.next(); | |
} | |
} | |
(State::InMeeting(start, _), None) => { | |
meetings.push((start, contents.lines().count() - 2)); | |
break; | |
} | |
_ => break, | |
} | |
} | |
let md_link = Regex::new(r"\[(.+?)\]\(.+?\)").context("error compiling md_link regex")?; | |
for (start, end) in meetings.iter() { | |
let mut title = contents.lines().nth(*start).unwrap(); | |
while title.starts_with('#') { | |
title = &title[1..]; | |
} | |
let title = md_link.replace_all(title, "$1").replace('/', "-"); | |
let mut body = String::new(); | |
contents | |
.lines() | |
.skip(start + 1) | |
.take(end - start) | |
.for_each(|l| writeln!(&mut body, "{l}").unwrap()); | |
let file_name = format!("{date}{title}.md"); | |
write(PathBuf::from(DIR).join(&file_name), body).context("error writing meeting")?; | |
} | |
let mut new_contents = String::new(); | |
let mut lines = contents.lines().enumerate(); | |
let mut meetings = meetings.iter().peekable(); | |
while let Some((num, line)) = lines.next() { | |
if let Some((start, end)) = meetings.peek() { | |
if num == *start { | |
let mut title = line; | |
while title.starts_with('#') { | |
title = &title[1..]; | |
} | |
let title = md_link.replace_all(title, "$1"); | |
let title = format!("{}{}", date, title); | |
let link = title.replace(' ', "%20").replace('/', "-"); | |
writeln!(&mut new_contents, "") | |
.context("error writing link to new contents")?; | |
meetings.next(); | |
for _ in *start..*end - 1 { | |
lines.next(); | |
} | |
} else { | |
writeln!(new_contents, "{}", line) | |
.context("error writing old line to new contents")?; | |
} | |
} | |
} | |
if !new_contents.is_empty() { | |
write(path, new_contents).context("error writing new contents")?; | |
} | |
Ok(()) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment