turbovault_parser/
parsers.rs

1//! OFM parser implementation using pulldown-cmark + custom regex layers
2
3use turbovault_core::{FileMetadata, Frontmatter, Result, SourcePosition, VaultFile};
4use std::path::{Path, PathBuf};
5
6mod callouts;
7mod embeds;
8mod frontmatter_parser;
9mod headings;
10mod tags;
11mod tasks;
12mod wikilinks;
13
14pub use self::frontmatter_parser::extract_frontmatter;
15
16/// Main parser for OFM files
17#[allow(dead_code)]
18pub struct Parser {
19    vault_root: PathBuf,
20}
21
22impl Parser {
23    /// Create a new parser for the given vault root
24    pub fn new(vault_root: PathBuf) -> Self {
25        Self { vault_root }
26    }
27
28    /// Parse a file from path and content
29    pub fn parse_file(&self, path: &Path, content: &str) -> Result<VaultFile> {
30        let metadata = self.extract_metadata(path, content)?;
31        let mut vault_file = VaultFile::new(path.to_path_buf(), content.to_string(), metadata);
32
33        // Parse content if markdown
34        if path.extension().is_some_and(|ext| ext == "md") {
35            self.parse_content(&mut vault_file)?;
36            vault_file.is_parsed = true;
37            vault_file.last_parsed = Some(
38                std::time::SystemTime::now()
39                    .duration_since(std::time::UNIX_EPOCH)
40                    .unwrap_or_default()
41                    .as_secs_f64(),
42            );
43        }
44
45        Ok(vault_file)
46    }
47
48    fn extract_metadata(&self, path: &Path, content: &str) -> Result<FileMetadata> {
49        use std::collections::hash_map::DefaultHasher;
50        use std::hash::{Hash, Hasher};
51
52        let size = content.len() as u64;
53        let mut hasher = DefaultHasher::new();
54        content.hash(&mut hasher);
55        let checksum = format!("{:x}", hasher.finish());
56
57        Ok(FileMetadata {
58            path: path.to_path_buf(),
59            size,
60            created_at: 0.0,
61            modified_at: 0.0,
62            checksum,
63            is_attachment: !matches!(
64                path.extension().map(|e| e.to_str()),
65                Some(Some("md" | "txt"))
66            ),
67        })
68    }
69
70    /// Parse all content elements from file
71    fn parse_content(&self, vault_file: &mut VaultFile) -> Result<()> {
72        let content = &vault_file.content;
73
74        // Step 1: Extract frontmatter
75        if let Ok((fm_str, content_without_fm)) = extract_frontmatter(content) {
76            if let Some(fm_str) = fm_str {
77                vault_file.frontmatter = self.parse_frontmatter(&fm_str)?;
78            }
79            vault_file.content = content_without_fm;
80        }
81
82        let content = &vault_file.content;
83
84        // Step 2: Parse wikilinks and embeds
85        vault_file
86            .links
87            .extend(wikilinks::parse_wikilinks(content, &vault_file.path));
88        vault_file
89            .links
90            .extend(embeds::parse_embeds(content, &vault_file.path));
91
92        // Step 3: Parse tags
93        vault_file.tags.extend(tags::parse_tags(content));
94
95        // Step 4: Parse tasks
96        vault_file.tasks.extend(tasks::parse_tasks(content));
97
98        // Step 5: Parse callouts
99        vault_file
100            .callouts
101            .extend(callouts::parse_callouts(content));
102
103        // Step 6: Parse headings
104        vault_file
105            .headings
106            .extend(headings::parse_headings(content));
107
108        Ok(())
109    }
110
111    fn parse_frontmatter(&self, fm_str: &str) -> Result<Option<Frontmatter>> {
112        match serde_yaml::from_str::<serde_json::Value>(fm_str) {
113            Ok(serde_json::Value::Object(map)) => {
114                let data = map.into_iter().collect();
115                Ok(Some(Frontmatter {
116                    data,
117                    position: SourcePosition::start(),
118                }))
119            }
120            Ok(_) => Ok(None),
121            Err(_) => Ok(None),
122        }
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn test_parser_creation() {
132        let parser = Parser::new(PathBuf::from("/vault"));
133        assert_eq!(parser.vault_root, PathBuf::from("/vault"));
134    }
135}