Skip to main content

mneme/export/
markdown.rs

1use crate::store::memory::{Importance, Memory, MemoryType, Scope};
2use chrono::Utc;
3
4/// Representa una memoria importada desde Markdown.
5#[derive(Debug)]
6pub struct ImportedMemory {
7    pub title: String,
8    pub content: String,
9    pub memory_type: MemoryType,
10    pub importance: Importance,
11    pub tags: Vec<String>,
12    pub scope: Scope,
13    pub what: Option<String>,
14    pub why: Option<String>,
15    pub context: Option<String>,
16    pub learned: Option<String>,
17}
18
19/// Exporta una lista de memorias al formato Markdown.
20pub fn export_to_markdown(memories: &[Memory], project: &str) -> String {
21    let mut out = String::new();
22    let now = Utc::now().to_rfc3339();
23
24    out.push_str(&format!("# mneme export — proyecto: {}\n", project));
25    out.push_str(&format!("Exportado el: {}\n", now));
26    out.push_str(&format!("Total: {} memorias\n", memories.len()));
27
28    for memory in memories {
29        out.push_str("\n---\n\n");
30        out.push_str(&memory_to_markdown_section(memory));
31    }
32    out
33}
34
35fn memory_to_markdown_section(memory: &Memory) -> String {
36    let mut s = String::new();
37    s.push_str(&format!("## {}\n", memory.title));
38    s.push_str(&format!("- **ID**: `{}`\n", memory.id));
39    s.push_str(&format!("- **Tipo**: {}\n", memory.memory_type));
40    s.push_str(&format!("- **Importancia**: {}\n", memory.importance));
41    s.push_str(&format!("- **Scope**: {}\n", memory.scope));
42
43    if !memory.tags.is_empty() {
44        let tags = memory
45            .tags
46            .iter()
47            .map(|t| format!("`{}`", t))
48            .collect::<Vec<_>>()
49            .join(" ");
50        s.push_str(&format!("- **Tags**: {}\n", tags));
51    }
52
53    s.push_str(&format!(
54        "- **Creado**: {}\n",
55        memory.created_at.to_rfc3339()
56    ));
57    s.push_str(&format!(
58        "- **Actualizado**: {}\n",
59        memory.updated_at.to_rfc3339()
60    ));
61
62    if memory.is_encrypted {
63        let for_whom = memory.encrypted_for.as_deref().unwrap_or("unknown");
64        s.push_str(&format!("- **🔒 Encriptado**: sí ({})\n", for_whom));
65        s.push_str("\n*[contenido encriptado — usar `mneme decrypt <id>` para ver]*\n");
66        return s;
67    }
68
69    s.push('\n');
70
71    if !memory.content.is_empty() {
72        s.push_str(&memory.content);
73        s.push('\n');
74    }
75
76    if let Some(what) = &memory.what {
77        s.push_str(&format!("\n**What**: {}\n", what));
78    }
79    if let Some(why) = &memory.why {
80        s.push_str(&format!("**Why**: {}\n", why));
81    }
82    if let Some(context) = &memory.context {
83        s.push_str(&format!("**Context**: {}\n", context));
84    }
85    if let Some(learned) = &memory.learned {
86        s.push_str(&format!("**Learned**: {}\n", learned));
87    }
88
89    s.push('\n');
90    s
91}
92
93/// Importa memorias desde Markdown.
94/// Parsea el formato generado por `export_to_markdown`.
95/// Retorna los datos parseados como `ImportedMemory` (sin ID — se genera nuevo al guardar).
96pub fn import_from_markdown(content: &str) -> crate::error::Result<Vec<ImportedMemory>> {
97    let mut memories = Vec::new();
98
99    let sections: Vec<&str> = content.split("\n---\n").collect();
100
101    for section in sections.iter().skip(1) {
102        let section = section.trim();
103        if section.is_empty() {
104            continue;
105        }
106
107        let title = section
108            .lines()
109            .find(|l| l.starts_with("## "))
110            .map(|l| l.trim_start_matches("## ").trim().to_string())
111            .unwrap_or_default();
112
113        if title.is_empty() {
114            continue;
115        }
116
117        let mut memory_type = MemoryType::Note;
118        let mut importance = Importance::Medium;
119        let mut tags = Vec::new();
120        let mut scope = Scope::Project;
121
122        for line in section.lines() {
123            if let Some(rest) = line.strip_prefix("- **Tipo**: ") {
124                let v = rest.trim();
125                memory_type = v.parse().unwrap_or(MemoryType::Note);
126            } else if let Some(rest) = line.strip_prefix("- **Importancia**: ") {
127                let v = rest.trim();
128                importance = v.parse().unwrap_or(Importance::Medium);
129            } else if let Some(rest) = line.strip_prefix("- **Tags**: ") {
130                let v = rest.trim();
131                tags = v
132                    .split_whitespace()
133                    .map(|t| t.trim_matches('`').to_string())
134                    .filter(|t| !t.is_empty())
135                    .collect();
136            } else if let Some(rest) = line.strip_prefix("- **Scope**: ") {
137                let v = rest.trim();
138                scope = v.parse().unwrap_or(Scope::Project);
139            }
140        }
141
142        let mut in_meta = true;
143        let mut content_lines = Vec::new();
144        let mut what = None;
145        let mut why = None;
146        let mut context_field = None;
147        let mut learned = None;
148
149        for line in section.lines().skip(1) {
150            // skip title line
151            if line.starts_with("- **") {
152                in_meta = true;
153                continue;
154            }
155            if in_meta && line.is_empty() {
156                in_meta = false;
157                continue;
158            }
159            if in_meta {
160                continue;
161            }
162
163            if line.starts_with("**What**: ") {
164                what = Some(line.trim_start_matches("**What**: ").to_string());
165            } else if line.starts_with("**Why**: ") {
166                why = Some(line.trim_start_matches("**Why**: ").to_string());
167            } else if line.starts_with("**Context**: ") {
168                context_field = Some(line.trim_start_matches("**Context**: ").to_string());
169            } else if line.starts_with("**Learned**: ") {
170                learned = Some(line.trim_start_matches("**Learned**: ").to_string());
171            } else if !line.starts_with("*[contenido encriptado") {
172                content_lines.push(line);
173            }
174        }
175
176        let content = content_lines.join("\n").trim().to_string();
177
178        memories.push(ImportedMemory {
179            title,
180            content,
181            memory_type,
182            importance,
183            tags,
184            scope,
185            what,
186            why,
187            context: context_field,
188            learned,
189        });
190    }
191
192    Ok(memories)
193}