Skip to main content

mdvault_core/domain/behaviors/
custom.rs

1//! Custom (Lua-driven) note type behavior.
2//!
3//! Custom types delegate everything to their Lua typedef:
4//! - Output path from typedef.output template
5//! - Prompts from typedef.schema
6//! - Hooks from typedef.on_create
7
8use std::path::PathBuf;
9use std::sync::Arc;
10
11use chrono::Local;
12
13use crate::templates::engine::render_string;
14use crate::types::TypeDefinition;
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 custom (Lua-defined) note types.
22pub struct CustomBehavior {
23    typedef: Arc<TypeDefinition>,
24    type_name: String,
25}
26
27impl CustomBehavior {
28    /// Create a new CustomBehavior wrapping a Lua typedef.
29    pub fn new(typedef: Arc<TypeDefinition>) -> Self {
30        let type_name = typedef.name.clone();
31        Self { typedef, type_name }
32    }
33
34    /// Get the underlying typedef.
35    pub fn typedef(&self) -> &TypeDefinition {
36        &self.typedef
37    }
38}
39
40impl NoteIdentity for CustomBehavior {
41    fn generate_id(&self, _ctx: &CreationContext) -> DomainResult<Option<String>> {
42        // Custom types don't generate IDs in Rust
43        // Lua hooks can set them
44        Ok(None)
45    }
46
47    fn output_path(&self, ctx: &CreationContext) -> DomainResult<PathBuf> {
48        // Build render context
49        let mut render_ctx = ctx.vars.clone();
50
51        // Add standard context variables
52        let now = Local::now();
53        render_ctx.insert("date".into(), now.format("%Y-%m-%d").to_string());
54        render_ctx.insert("time".into(), now.format("%H:%M").to_string());
55        render_ctx.insert("datetime".into(), now.to_rfc3339());
56        render_ctx.insert("today".into(), now.format("%Y-%m-%d").to_string());
57        render_ctx.insert("now".into(), now.to_rfc3339());
58
59        render_ctx.insert(
60            "vault_root".into(),
61            ctx.config.vault_root.to_string_lossy().to_string(),
62        );
63        render_ctx.insert("type".into(), self.type_name.clone());
64        render_ctx.insert("title".into(), ctx.title.clone());
65
66        // Use Lua typedef output template if available
67        if let Some(ref output_template) = self.typedef.output {
68            let rendered = render_string(output_template, &render_ctx).map_err(|e| {
69                DomainError::Other(format!("Failed to render output path: {}", e))
70            })?;
71
72            let path = PathBuf::from(&rendered);
73            if path.is_absolute() {
74                Ok(path)
75            } else {
76                Ok(ctx.config.vault_root.join(path))
77            }
78        } else {
79            // Default: {type}s/{slug}.md
80            let slug = slugify(&ctx.title);
81            Ok(ctx.config.vault_root.join(format!("{}s/{}.md", self.type_name, slug)))
82        }
83    }
84
85    fn core_fields(&self) -> Vec<&'static str> {
86        vec!["type", "title"]
87    }
88}
89
90impl NoteLifecycle for CustomBehavior {
91    fn before_create(&self, _ctx: &mut CreationContext) -> DomainResult<()> {
92        // No Rust-side before_create for custom types
93        Ok(())
94    }
95
96    fn after_create(&self, _ctx: &CreationContext, _content: &str) -> DomainResult<()> {
97        // TODO: Run Lua on_create hook if defined
98        Ok(())
99    }
100}
101
102impl NotePrompts for CustomBehavior {
103    fn type_prompts(&self, _ctx: &PromptContext) -> Vec<FieldPrompt> {
104        vec![] // Custom types only use schema-based prompts
105    }
106}
107
108impl NoteBehavior for CustomBehavior {
109    fn type_name(&self) -> &'static str {
110        // This is a bit awkward because we need 'static lifetime
111        // In practice, we'll use the type_name from the typedef
112        // For now, return a placeholder - the actual type name is in self.type_name
113        "custom"
114    }
115}
116
117/// Convert a title to a URL-friendly slug.
118fn slugify(s: &str) -> String {
119    let mut result = String::with_capacity(s.len());
120
121    for c in s.chars() {
122        if c.is_ascii_alphanumeric() {
123            result.push(c.to_ascii_lowercase());
124        } else if (c == ' ' || c == '_' || c == '-') && !result.ends_with('-') {
125            result.push('-');
126        }
127    }
128
129    result.trim_matches('-').to_string()
130}