crabtalk_runtime/memory/
entry.rs1use crate::memory::storage::Storage;
4use anyhow::{Result, bail};
5use std::path::{Path, PathBuf};
6
7pub struct MemoryEntry {
9 pub name: String,
10 pub description: String,
11 pub content: String,
12 pub path: PathBuf,
13}
14
15impl MemoryEntry {
16 pub fn new(name: String, description: String, content: String, entries_dir: &Path) -> Self {
18 let slug = slugify(&name);
19 let path = entries_dir.join(format!("{slug}.md"));
20 Self {
21 name,
22 description,
23 content,
24 path,
25 }
26 }
27
28 pub fn parse(path: PathBuf, raw: &str) -> Result<Self> {
30 let raw = raw.replace("\r\n", "\n");
31 let raw = raw.trim();
32 if !raw.starts_with("---") {
33 bail!("missing frontmatter opening ---");
34 }
35
36 let after_open = &raw[3..];
37 let Some(close_pos) = after_open.find("\n---") else {
38 bail!("missing frontmatter closing ---");
39 };
40
41 let frontmatter = &after_open[..close_pos];
42 let content = after_open[close_pos + 4..].trim().to_owned();
43
44 let mut name = None;
45 let mut description = None;
46
47 for line in frontmatter.lines() {
48 let line = line.trim();
49 if let Some(val) = line.strip_prefix("name:") {
50 name = Some(val.trim().to_owned());
51 } else if let Some(val) = line.strip_prefix("description:") {
52 description = Some(val.trim().to_owned());
53 }
54 }
55
56 let Some(name) = name else {
57 bail!("missing 'name' in frontmatter");
58 };
59 let description = description.unwrap_or_default();
60
61 Ok(Self {
62 name,
63 description,
64 content,
65 path,
66 })
67 }
68
69 pub fn serialize(&self) -> String {
71 let mut out = String::new();
72 out.push_str("---\n");
73 out.push_str(&format!("name: {}\n", self.name));
74 out.push_str(&format!("description: {}\n", self.description));
75 out.push_str("---\n\n");
76 out.push_str(&self.content);
77 out.push('\n');
78 out
79 }
80
81 pub fn save(&self, storage: &dyn Storage) -> Result<()> {
83 storage.write(&self.path, &self.serialize())
84 }
85
86 pub fn delete(&self, storage: &dyn Storage) -> Result<()> {
88 storage.delete(&self.path)
89 }
90
91 pub fn search_text(&self) -> String {
93 format!("{} {}", self.description, self.content)
94 }
95}
96
97pub fn slugify(name: &str) -> String {
99 let mut slug = String::with_capacity(name.len());
100 let mut prev_dash = true;
101
102 for ch in name.chars() {
103 if ch.is_alphanumeric() {
104 for lc in ch.to_lowercase() {
105 slug.push(lc);
106 }
107 prev_dash = false;
108 } else if !prev_dash {
109 slug.push('-');
110 prev_dash = true;
111 }
112 }
113
114 if slug.ends_with('-') {
115 slug.pop();
116 }
117
118 if slug.is_empty() {
119 slug.push_str("entry");
120 }
121
122 slug
123}