Skip to main content

mdvault_core/domain/behaviors/
daily.rs

1//! Daily note type behavior.
2//!
3//! Daily notes have:
4//! - Date-based identity (no ID, uses date)
5//! - Output path: Journal/Daily/{date}.md
6//! - date field in frontmatter
7
8use 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
21/// Behavior implementation for daily notes.
22pub struct DailyBehavior {
23    typedef: Option<Arc<TypeDefinition>>,
24}
25
26impl DailyBehavior {
27    /// Create a new DailyBehavior, optionally wrapping a Lua typedef override.
28    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        // Daily notes don't have IDs, they use dates
36        Ok(None)
37    }
38
39    fn output_path(&self, ctx: &CreationContext) -> DomainResult<PathBuf> {
40        // Check Lua typedef for output template first
41        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        // Default: Journal/Daily/YYYY-MM-DD.md
48        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        // Use title as date if it looks like a date, otherwise use today
65        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        // Parse target date and set as reference for all date expressions
78        if let Ok(target) = NaiveDate::parse_from_str(&date, "%Y-%m-%d") {
79            ctx.reference_date = Some(target);
80            // Set week var to override schema default (which was evaluated at load time)
81            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        // TODO: Run Lua on_create hook if defined
94        Ok(())
95    }
96}
97
98impl NotePrompts for DailyBehavior {
99    fn type_prompts(&self, _ctx: &PromptContext) -> Vec<FieldPrompt> {
100        vec![] // No type-specific prompts for daily notes
101    }
102
103    fn should_prompt_schema(&self) -> bool {
104        false // Daily notes typically don't need prompts
105    }
106}
107
108impl NoteBehavior for DailyBehavior {
109    fn type_name(&self) -> &'static str {
110        "daily"
111    }
112}
113
114/// Check if a string looks like a date (YYYY-MM-DD format).
115fn 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}