use regex::Regex;
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct Frontmatter {
pub title: Option<String>,
pub summary: Option<String>,
pub date: Option<String>,
pub time: Option<String>,
pub tags: Option<Vec<String>>,
pub note_type: Option<String>,
pub uplink: Option<String>,
}
fn find_balanced_paren(input: &str, start_pos: usize) -> Option<usize> {
let mut count = 0;
let bytes = input.as_bytes();
for i in start_pos..bytes.len() {
match bytes[i] as char {
'(' => count += 1,
')' => {
count -= 1;
if count == 0 {
return Some(i);
}
}
_ => {}
}
}
None
}
fn parse_string_array(inner: &str, key: &str) -> Option<Vec<String>> {
let pattern = format!(r#"{}\s*:\s*\("#, regex::escape(key));
let re = Regex::new(&pattern).ok()?;
if let Some(m) = re.find(inner) {
let paren_start = inner[..m.end()].rfind('(')?;
if let Some(paren_end) = find_balanced_paren(inner, paren_start) {
let array_inner = &inner[paren_start + 1..paren_end];
let re_string = Regex::new(r#""([^"]*)""#).unwrap();
let matches: Vec<_> = re_string.captures_iter(array_inner).collect();
let items: Vec<String> = matches.iter()
.map(|c| c.get(1).unwrap().as_str().to_string())
.collect();
if items.len() == 1 {
if !array_inner.contains(',') {
return None;
}
}
if !items.is_empty() {
return Some(items);
}
}
}
None
}
pub fn parse_frontmatter(content: &str, filename: &str) -> Frontmatter {
let mut fm = Frontmatter::default();
let re_let_title = Regex::new(r#"#let\s+title\s*=\s*"([^"]*)""#).unwrap();
if let Some(caps) = re_let_title.captures(content) {
fm.title = Some(caps.get(1).unwrap().as_str().to_string());
}
let re_start = Regex::new(r"(?s)#show:\s*card\.with\s*\(").unwrap();
if let Some(m) = re_start.find(content) {
let start_paren = m.end() - 1;
if let Some(end_paren) = find_balanced_paren(content, start_paren) {
let raw_inner = &content[start_paren + 1..end_paren];
let re_line = Regex::new(r"(?m)//.*").unwrap();
let partially_cleaned = re_line.replace_all(raw_inner, "");
let re_block = Regex::new(r"(?s)/\*.*?\*/").unwrap();
let inner = re_block.replace_all(&partially_cleaned, "").to_string();
fm.tags = parse_string_array(&inner, "tags");
let re_kv = Regex::new(r#"([a-zA-Z0-9_]+)\s*:\s*"([^"]*)""#).unwrap();
for caps in re_kv.captures_iter(&inner) {
let key = caps.get(1).unwrap().as_str();
let val = caps.get(2).unwrap().as_str().to_string();
match key {
"title" if fm.title.is_none() => fm.title = Some(val),
"summary" => fm.summary = Some(val),
"date" => fm.date = Some(val),
"time" => fm.time = Some(val),
"type" => fm.note_type = Some(val),
"uplink" => fm.uplink = Some(val),
_ => {} }
}
let re_ref = Regex::new(r#"([a-zA-Z0-9_]+)\s*:\s*\[\s*@([a-zA-Z0-9_-]+)\s*\]"#).unwrap();
for caps in re_ref.captures_iter(&inner) {
let key = caps.get(1).unwrap().as_str();
let val = caps.get(2).unwrap().as_str().to_string();
if key == "uplink" {
fm.uplink = Some(val);
}
}
} else {
eprintln!("Warning: Malformed frontmatter in '{}' (unbalanced parentheses)", filename);
}
} else {
eprintln!("Warning: No frontmatter card found in '{}' (expected #show: card.with(...))", filename);
}
fm
}