Skip to content

Instantly share code, notes, and snippets.

@sulami
Created January 4, 2024 05:24
Show Gist options
  • Save sulami/04efb003796cb3756e998ed1edf5fe38 to your computer and use it in GitHub Desktop.
Save sulami/04efb003796cb3756e998ed1edf5fe38 to your computer and use it in GitHub Desktop.
Extract Obsidian meeting notes from daily notes
[package]
name = "extract-meetings"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1"
regex = "1"
//! 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, "![{title}](./{link}.md)")
.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