mdvault_core/domain/behaviors/
daily.rs1use std::path::PathBuf;
9use std::sync::Arc;
10
11use chrono::{Datelike, Local, NaiveDate};
12
13use crate::types::TypeDefinition;
14use crate::vars::datemath::try_evaluate_date_expr;
15
16use super::super::context::{CreationContext, FieldPrompt, PromptContext};
17use super::super::traits::{
18 DomainError, DomainResult, NoteBehavior, NoteIdentity, NoteLifecycle, NotePrompts,
19};
20
21pub struct DailyBehavior {
23 typedef: Option<Arc<TypeDefinition>>,
24}
25
26impl DailyBehavior {
27 pub fn new(typedef: Option<Arc<TypeDefinition>>) -> Self {
29 Self { typedef }
30 }
31}
32
33impl NoteIdentity for DailyBehavior {
34 fn generate_id(&self, _ctx: &CreationContext) -> DomainResult<Option<String>> {
35 Ok(None)
37 }
38
39 fn output_path(&self, ctx: &CreationContext) -> DomainResult<PathBuf> {
40 if let Some(ref td) = self.typedef
42 && let Some(ref output) = td.output
43 {
44 return super::render_output_template(output, ctx);
45 }
46
47 let date = ctx
49 .core_metadata
50 .date
51 .as_ref()
52 .ok_or_else(|| DomainError::PathResolution("date not set".into()))?;
53
54 Ok(ctx.config.vault_root.join(format!("Journal/Daily/{}.md", date)))
55 }
56
57 fn core_fields(&self) -> Vec<&'static str> {
58 vec!["type", "date"]
59 }
60}
61
62impl NoteLifecycle for DailyBehavior {
63 fn before_create(&self, ctx: &mut CreationContext) -> DomainResult<()> {
64 let date = if looks_like_date(&ctx.title) {
66 ctx.title.clone()
67 } else if let Some(evaluated) = try_evaluate_date_expr(&ctx.title) {
68 evaluated
69 } else {
70 Local::now().format("%Y-%m-%d").to_string()
71 };
72
73 ctx.core_metadata.date = Some(date.clone());
74 ctx.core_metadata.title = Some(date.clone());
75 ctx.set_var("date", &date);
76
77 if let Ok(target) = NaiveDate::parse_from_str(&date, "%Y-%m-%d") {
79 ctx.reference_date = Some(target);
80 let week = format!(
82 "[[{}-W{:02}]]",
83 target.iso_week().year(),
84 target.iso_week().week()
85 );
86 ctx.set_var("week", &week);
87 }
88
89 Ok(())
90 }
91
92 fn after_create(&self, _ctx: &CreationContext, _content: &str) -> DomainResult<()> {
93 Ok(())
95 }
96}
97
98impl NotePrompts for DailyBehavior {
99 fn type_prompts(&self, _ctx: &PromptContext) -> Vec<FieldPrompt> {
100 vec![] }
102
103 fn should_prompt_schema(&self) -> bool {
104 false }
106}
107
108impl NoteBehavior for DailyBehavior {
109 fn type_name(&self) -> &'static str {
110 "daily"
111 }
112}
113
114fn looks_like_date(s: &str) -> bool {
116 if s.len() != 10 {
117 return false;
118 }
119 let parts: Vec<&str> = s.split('-').collect();
120 if parts.len() != 3 {
121 return false;
122 }
123 parts[0].len() == 4
124 && parts[1].len() == 2
125 && parts[2].len() == 2
126 && parts.iter().all(|p| p.chars().all(|c| c.is_ascii_digit()))
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn test_looks_like_date() {
135 assert!(looks_like_date("2025-01-11"));
136 assert!(looks_like_date("2024-12-31"));
137 assert!(!looks_like_date("2025-1-11"));
138 assert!(!looks_like_date("not a date"));
139 assert!(!looks_like_date("01-11-2025"));
140 }
141}