Skip to main content

mdvault_core/domain/behaviors/
zettel.rs

1//! Zettel (knowledge note) type behavior.
2//!
3//! Zettels have:
4//! - Minimal Rust behavior, mostly Lua-driven
5//! - Output path: zettels/{slug}.md or Lua-defined
6
7use std::path::PathBuf;
8use std::sync::Arc;
9
10use crate::types::TypeDefinition;
11
12use super::super::context::{CreationContext, FieldPrompt, PromptContext};
13use super::super::traits::{
14    DomainResult, NoteBehavior, NoteIdentity, NoteLifecycle, NotePrompts,
15};
16
17/// Behavior implementation for zettel (knowledge) notes.
18pub struct ZettelBehavior {
19    typedef: Option<Arc<TypeDefinition>>,
20}
21
22impl ZettelBehavior {
23    /// Create a new ZettelBehavior, optionally wrapping a Lua typedef override.
24    pub fn new(typedef: Option<Arc<TypeDefinition>>) -> Self {
25        Self { typedef }
26    }
27}
28
29impl NoteIdentity for ZettelBehavior {
30    fn generate_id(&self, _ctx: &CreationContext) -> DomainResult<Option<String>> {
31        // Zettels don't have special IDs
32        Ok(None)
33    }
34
35    fn output_path(&self, ctx: &CreationContext) -> DomainResult<PathBuf> {
36        // Check Lua typedef for output template first
37        if let Some(ref td) = self.typedef
38            && let Some(ref output) = td.output
39        {
40            return super::render_output_template(output, ctx);
41        }
42
43        // Default: zettels/{slug}.md
44        let slug = slugify(&ctx.title);
45        Ok(ctx.config.vault_root.join(format!("zettels/{}.md", slug)))
46    }
47
48    fn core_fields(&self) -> Vec<&'static str> {
49        vec!["type", "title"]
50    }
51}
52
53impl NoteLifecycle for ZettelBehavior {
54    fn before_create(&self, _ctx: &mut CreationContext) -> DomainResult<()> {
55        // No special before_create logic for zettels
56        Ok(())
57    }
58
59    fn after_create(&self, _ctx: &CreationContext, _content: &str) -> DomainResult<()> {
60        // TODO: Run Lua on_create hook if defined
61        Ok(())
62    }
63}
64
65impl NotePrompts for ZettelBehavior {
66    fn type_prompts(&self, _ctx: &PromptContext) -> Vec<FieldPrompt> {
67        vec![] // Zettels use schema-based prompts only
68    }
69}
70
71impl NoteBehavior for ZettelBehavior {
72    fn type_name(&self) -> &'static str {
73        "zettel"
74    }
75}
76
77/// Convert a title to a URL-friendly slug.
78fn slugify(s: &str) -> String {
79    let mut result = String::with_capacity(s.len());
80
81    for c in s.chars() {
82        if c.is_ascii_alphanumeric() {
83            result.push(c.to_ascii_lowercase());
84        } else if (c == ' ' || c == '_' || c == '-') && !result.ends_with('-') {
85            result.push('-');
86        }
87    }
88
89    result.trim_matches('-').to_string()
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn test_slugify() {
98        assert_eq!(slugify("Hello World"), "hello-world");
99        assert_eq!(slugify("My Cool Note!"), "my-cool-note");
100        assert_eq!(slugify("  spaced  out  "), "spaced-out");
101        assert_eq!(slugify("under_score"), "under-score");
102    }
103}