mdvault_core/domain/behaviors/
custom.rs1use 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
21pub struct CustomBehavior {
23 typedef: Arc<TypeDefinition>,
24 type_name: String,
25}
26
27impl CustomBehavior {
28 pub fn new(typedef: Arc<TypeDefinition>) -> Self {
30 let type_name = typedef.name.clone();
31 Self { typedef, type_name }
32 }
33
34 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 Ok(None)
45 }
46
47 fn output_path(&self, ctx: &CreationContext) -> DomainResult<PathBuf> {
48 let mut render_ctx = ctx.vars.clone();
50
51 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 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 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 Ok(())
94 }
95
96 fn after_create(&self, _ctx: &CreationContext, _content: &str) -> DomainResult<()> {
97 Ok(())
99 }
100}
101
102impl NotePrompts for CustomBehavior {
103 fn type_prompts(&self, _ctx: &PromptContext) -> Vec<FieldPrompt> {
104 vec![] }
106}
107
108impl NoteBehavior for CustomBehavior {
109 fn type_name(&self) -> &'static str {
110 "custom"
114 }
115}
116
117fn 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}