use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc};
use crate::ImportedDocMeta;
#[derive(Debug, Default, Clone)]
pub(super) struct Frontmatter {
pub title: Option<String>,
create_date: Option<i64>,
updated_date: Option<i64>,
tags: Option<Vec<String>>,
favorite: Option<bool>,
trash: Option<bool>,
}
pub(super) fn strip_frontmatter(markdown: &str) -> (String, Frontmatter) {
let Some(rest) = markdown.strip_prefix("---\n") else {
return (markdown.to_string(), Frontmatter::default());
};
let Some((frontmatter, body)) = rest.split_once("\n---\n") else {
return (markdown.to_string(), Frontmatter::default());
};
(body.to_string(), parse_frontmatter(frontmatter))
}
pub(super) fn frontmatter_meta(frontmatter: &Frontmatter, title: &str) -> Option<ImportedDocMeta> {
let meta = ImportedDocMeta {
title: Some(title.to_string()),
create_date: frontmatter.create_date,
updated_date: frontmatter.updated_date,
tags: frontmatter.tags.clone(),
favorite: frontmatter.favorite,
trash: frontmatter.trash,
};
(meta.create_date.is_some()
|| meta.updated_date.is_some()
|| meta.tags.is_some()
|| meta.favorite.is_some()
|| meta.trash.is_some())
.then_some(meta)
}
pub(super) fn parse_date_millis(value: &str) -> Option<i64> {
let value = unquote(value);
DateTime::parse_from_rfc3339(value)
.map(|date| date.timestamp_millis())
.or_else(|_| {
NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S").map(|date| date.and_utc().timestamp_millis())
})
.ok()
.or_else(|| {
NaiveDate::parse_from_str(value, "%Y-%m-%d")
.ok()
.and_then(|date| date.and_hms_opt(0, 0, 0))
.map(|date| DateTime::<Utc>::from_naive_utc_and_offset(date, Utc).timestamp_millis())
})
}
fn parse_frontmatter(frontmatter: &str) -> Frontmatter {
let mut parsed = Frontmatter::default();
for line in frontmatter.lines() {
let Some((key, value)) = line.split_once(':') else {
continue;
};
let value = value.trim();
match key.trim() {
"title" if !value.is_empty() => parsed.title = Some(unquote(value).to_string()),
"createDate" | "created" => parsed.create_date = parse_date_millis(value),
"updatedDate" | "updated" => parsed.updated_date = parse_date_millis(value),
"tags" => parsed.tags = parse_tags(value),
"favorite" => parsed.favorite = parse_bool(value),
"trash" => parsed.trash = parse_bool(value),
_ => {}
}
}
parsed
}
fn parse_tags(value: &str) -> Option<Vec<String>> {
let value = value.trim();
let tags = if let Some(inner) = value.strip_prefix('[').and_then(|value| value.strip_suffix(']')) {
inner
.split(',')
.map(unquote)
.map(str::trim)
.filter(|tag| !tag.is_empty())
.map(ToString::to_string)
.collect::<Vec<_>>()
} else {
value
.split(',')
.map(unquote)
.map(str::trim)
.filter(|tag| !tag.is_empty())
.map(ToString::to_string)
.collect::<Vec<_>>()
};
(!tags.is_empty()).then_some(tags)
}
fn parse_bool(value: &str) -> Option<bool> {
match unquote(value) {
"true" => Some(true),
"false" => Some(false),
_ => None,
}
}
fn unquote(value: &str) -> &str {
value.trim().trim_matches('"').trim_matches('\'')
}