1use serde::{Deserialize, Serialize};
12use std::collections::{HashMap, HashSet};
13use std::path::PathBuf;
14
15#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
17pub struct SourcePosition {
18 pub line: usize,
19 pub column: usize,
20 pub offset: usize,
21 pub length: usize,
22}
23
24impl SourcePosition {
25 pub fn new(line: usize, column: usize, offset: usize, length: usize) -> Self {
27 Self {
28 line,
29 column,
30 offset,
31 length,
32 }
33 }
34
35 pub fn start() -> Self {
37 Self {
38 line: 0,
39 column: 0,
40 offset: 0,
41 length: 0,
42 }
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
48pub enum LinkType {
49 WikiLink,
51 Embed,
53 BlockRef,
55 HeadingRef,
57 MarkdownLink,
59 ExternalLink,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
65pub struct Link {
66 pub type_: LinkType,
67 pub source_file: PathBuf,
68 pub target: String,
69 pub display_text: Option<String>,
70 pub position: SourcePosition,
71 pub resolved_target: Option<PathBuf>,
72 pub is_valid: bool,
73}
74
75impl Link {
76 pub fn new(
78 type_: LinkType,
79 source_file: PathBuf,
80 target: String,
81 position: SourcePosition,
82 ) -> Self {
83 Self {
84 type_,
85 source_file,
86 target,
87 display_text: None,
88 position,
89 resolved_target: None,
90 is_valid: true,
91 }
92 }
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct Heading {
98 pub text: String,
99 pub level: u8, pub position: SourcePosition,
101 pub anchor: Option<String>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct Tag {
107 pub name: String,
108 pub position: SourcePosition,
109 pub is_nested: bool, }
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct TaskItem {
115 pub content: String,
116 pub is_completed: bool,
117 pub position: SourcePosition,
118 pub due_date: Option<String>,
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
123pub enum CalloutType {
124 Note,
125 Tip,
126 Info,
127 Todo,
128 Important,
129 Success,
130 Question,
131 Warning,
132 Failure,
133 Danger,
134 Bug,
135 Example,
136 Quote,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct Callout {
142 pub type_: CalloutType,
143 pub title: Option<String>,
144 pub content: String,
145 pub position: SourcePosition,
146 pub is_foldable: bool,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct Block {
152 pub content: String,
153 pub block_id: Option<String>,
154 pub position: SourcePosition,
155 pub type_: String, }
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct Frontmatter {
161 pub data: HashMap<String, serde_json::Value>,
162 pub position: SourcePosition,
163}
164
165impl Frontmatter {
166 pub fn tags(&self) -> Vec<String> {
168 match self.data.get("tags") {
169 Some(serde_json::Value::String(s)) => vec![s.clone()],
170 Some(serde_json::Value::Array(arr)) => arr
171 .iter()
172 .filter_map(|v| v.as_str().map(|s| s.to_string()))
173 .collect(),
174 _ => vec![],
175 }
176 }
177
178 pub fn aliases(&self) -> Vec<String> {
180 match self.data.get("aliases") {
181 Some(serde_json::Value::String(s)) => vec![s.clone()],
182 Some(serde_json::Value::Array(arr)) => arr
183 .iter()
184 .filter_map(|v| v.as_str().map(|s| s.to_string()))
185 .collect(),
186 _ => vec![],
187 }
188 }
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct FileMetadata {
194 pub path: PathBuf,
195 pub size: u64,
196 pub created_at: f64,
197 pub modified_at: f64,
198 pub checksum: String,
199 pub is_attachment: bool,
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct VaultFile {
205 pub path: PathBuf,
206 pub content: String,
207 pub metadata: FileMetadata,
208
209 pub frontmatter: Option<Frontmatter>,
211 pub headings: Vec<Heading>,
212 pub links: Vec<Link>,
213 pub backlinks: HashSet<Link>,
214 pub blocks: Vec<Block>,
215 pub tags: Vec<Tag>,
216 pub callouts: Vec<Callout>,
217 pub tasks: Vec<TaskItem>,
218
219 pub is_parsed: bool,
221 pub parse_error: Option<String>,
222 pub last_parsed: Option<f64>,
223}
224
225impl VaultFile {
226 pub fn new(path: PathBuf, content: String, metadata: FileMetadata) -> Self {
228 Self {
229 path,
230 content,
231 metadata,
232 frontmatter: None,
233 headings: vec![],
234 links: vec![],
235 backlinks: HashSet::new(),
236 blocks: vec![],
237 tags: vec![],
238 callouts: vec![],
239 tasks: vec![],
240 is_parsed: false,
241 parse_error: None,
242 last_parsed: None,
243 }
244 }
245
246 pub fn outgoing_links(&self) -> HashSet<&str> {
248 self.links
249 .iter()
250 .filter(|link| matches!(link.type_, LinkType::WikiLink | LinkType::Embed))
251 .map(|link| link.target.as_str())
252 .collect()
253 }
254
255 pub fn headings_by_text(&self) -> HashMap<&str, &Heading> {
257 self.headings.iter().map(|h| (h.text.as_str(), h)).collect()
258 }
259
260 pub fn blocks_with_ids(&self) -> HashMap<&str, &Block> {
262 self.blocks
263 .iter()
264 .filter_map(|b| b.block_id.as_deref().map(|id| (id, b)))
265 .collect()
266 }
267
268 pub fn has_tag(&self, tag: &str) -> bool {
270 if let Some(fm) = &self.frontmatter
271 && fm.tags().contains(&tag.to_string())
272 {
273 return true;
274 }
275
276 self.tags.iter().any(|t| t.name == tag)
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 #[test]
285 fn test_source_position() {
286 let pos = SourcePosition::new(5, 10, 100, 20);
287 assert_eq!(pos.line, 5);
288 assert_eq!(pos.column, 10);
289 assert_eq!(pos.offset, 100);
290 assert_eq!(pos.length, 20);
291 }
292
293 #[test]
294 fn test_frontmatter_tags() {
295 let mut data = HashMap::new();
296 data.insert(
297 "tags".to_string(),
298 serde_json::Value::Array(vec![
299 serde_json::Value::String("rust".to_string()),
300 serde_json::Value::String("mcp".to_string()),
301 ]),
302 );
303
304 let fm = Frontmatter {
305 data,
306 position: SourcePosition::start(),
307 };
308
309 let tags = fm.tags();
310 assert_eq!(tags.len(), 2);
311 assert!(tags.contains(&"rust".to_string()));
312 }
313}