Skip to main content

mdvault_core/domain/behaviors/
weekly.rs

1//! Weekly note type behavior.
2//!
3//! Weekly notes have:
4//! - Week-based identity (no ID, uses week)
5//! - Output path: Journal/Weekly/{week}.md
6//! - week field in frontmatter (YYYY-WXX format)
7
8use std::path::PathBuf;
9use std::sync::Arc;
10
11use chrono::Local;
12
13use crate::types::TypeDefinition;
14use crate::vars::datemath::{is_date_expr, 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 weekly notes.
22pub struct WeeklyBehavior {
23    typedef: Option<Arc<TypeDefinition>>,
24}
25
26impl WeeklyBehavior {
27    /// Create a new WeeklyBehavior, optionally wrapping a Lua typedef override.
28    pub fn new(typedef: Option<Arc<TypeDefinition>>) -> Self {
29        Self { typedef }
30    }
31}
32
33impl NoteIdentity for WeeklyBehavior {
34    fn generate_id(&self, _ctx: &CreationContext) -> DomainResult<Option<String>> {
35        // Weekly notes don't have IDs, they use week numbers
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/Weekly/YYYY-WXX.md
48        let week = ctx
49            .core_metadata
50            .week
51            .as_ref()
52            .ok_or_else(|| DomainError::PathResolution("week not set".into()))?;
53
54        Ok(ctx.config.vault_root.join(format!("Journal/Weekly/{}.md", week)))
55    }
56
57    fn core_fields(&self) -> Vec<&'static str> {
58        vec!["type", "week"]
59    }
60}
61
62impl NoteLifecycle for WeeklyBehavior {
63    fn before_create(&self, ctx: &mut CreationContext) -> DomainResult<()> {
64        // Use title as week if it looks like a week, otherwise use current week
65        let week = if looks_like_week(&ctx.title) {
66            ctx.title.clone()
67        } else {
68            // Try to evaluate as date expr, forcing ISO week format if not specified
69            let expr_to_eval = if is_date_expr(&ctx.title) && !ctx.title.contains('|') {
70                format!("{} | %Y-W%V", ctx.title)
71            } else {
72                ctx.title.clone()
73            };
74
75            if let Some(evaluated) = try_evaluate_date_expr(&expr_to_eval) {
76                evaluated
77            } else {
78                Local::now().format("%Y-W%W").to_string()
79            }
80        };
81
82        ctx.core_metadata.week = Some(week.clone());
83        ctx.core_metadata.title = Some(week.clone());
84        ctx.set_var("week", &week);
85
86        Ok(())
87    }
88
89    fn after_create(&self, _ctx: &CreationContext, _content: &str) -> DomainResult<()> {
90        // TODO: Run Lua on_create hook if defined
91        Ok(())
92    }
93}
94
95impl NotePrompts for WeeklyBehavior {
96    fn type_prompts(&self, _ctx: &PromptContext) -> Vec<FieldPrompt> {
97        vec![] // No type-specific prompts for weekly notes
98    }
99
100    fn should_prompt_schema(&self) -> bool {
101        false // Weekly notes typically don't need prompts
102    }
103}
104
105impl NoteBehavior for WeeklyBehavior {
106    fn type_name(&self) -> &'static str {
107        "weekly"
108    }
109}
110
111/// Check if a string looks like a week (YYYY-WXX format).
112fn looks_like_week(s: &str) -> bool {
113    if s.len() < 7 || s.len() > 8 {
114        return false;
115    }
116    let parts: Vec<&str> = s.split("-W").collect();
117    if parts.len() != 2 {
118        return false;
119    }
120    parts[0].len() == 4
121        && (parts[1].len() == 1 || parts[1].len() == 2)
122        && parts[0].chars().all(|c| c.is_ascii_digit())
123        && parts[1].chars().all(|c| c.is_ascii_digit())
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn test_looks_like_week() {
132        assert!(looks_like_week("2025-W01"));
133        assert!(looks_like_week("2025-W52"));
134        assert!(looks_like_week("2024-W1"));
135        assert!(!looks_like_week("2025-01"));
136        assert!(!looks_like_week("not a week"));
137        assert!(!looks_like_week("W01-2025"));
138    }
139}