manx_cli/
export.rs

1use crate::client::{Documentation, SearchResult};
2use anyhow::{Context, Result};
3use std::fs;
4use std::path::Path;
5
6pub enum ExportFormat {
7    Markdown,
8    Json,
9}
10
11impl ExportFormat {
12    pub fn from_path(path: &Path) -> Self {
13        match path.extension().and_then(|s| s.to_str()) {
14            Some("json") => ExportFormat::Json,
15            _ => ExportFormat::Markdown,
16        }
17    }
18}
19
20pub struct Exporter;
21
22impl Exporter {
23    pub fn export_search_results(results: &[SearchResult], path: &Path) -> Result<()> {
24        let format = ExportFormat::from_path(path);
25
26        let content = match format {
27            ExportFormat::Json => serde_json::to_string_pretty(results)?,
28            ExportFormat::Markdown => Self::search_results_to_markdown(results),
29        };
30
31        fs::write(path, content).with_context(|| format!("Failed to write to {:?}", path))?;
32
33        Ok(())
34    }
35
36    pub fn export_documentation(doc: &Documentation, path: &Path) -> Result<()> {
37        let format = ExportFormat::from_path(path);
38
39        let content = match format {
40            ExportFormat::Json => serde_json::to_string_pretty(doc)?,
41            ExportFormat::Markdown => Self::documentation_to_markdown(doc),
42        };
43
44        fs::write(path, content).with_context(|| format!("Failed to write to {:?}", path))?;
45
46        Ok(())
47    }
48
49    fn search_results_to_markdown(results: &[SearchResult]) -> String {
50        let mut md = String::new();
51
52        md.push_str("# Search Results\n\n");
53        md.push_str(&format!("Found {} results\n\n", results.len()));
54
55        for (idx, result) in results.iter().enumerate() {
56            md.push_str(&format!("## {}. {}\n\n", idx + 1, result.title));
57            md.push_str(&format!("- **Library**: {}\n", result.library));
58            md.push_str(&format!("- **ID**: `{}`\n", result.id));
59
60            if let Some(url) = &result.url {
61                md.push_str(&format!("- **URL**: [{}]({})\n", url, url));
62            }
63
64            md.push_str(&format!(
65                "- **Relevance**: {:.2}\n\n",
66                result.relevance_score
67            ));
68
69            md.push_str("### Excerpt\n\n");
70            md.push_str(&result.excerpt);
71            md.push_str("\n\n---\n\n");
72        }
73
74        md
75    }
76
77    fn documentation_to_markdown(doc: &Documentation) -> String {
78        let mut md = String::new();
79
80        // Header
81        md.push_str(&format!("# {} Documentation\n\n", doc.library.name));
82
83        if let Some(version) = &doc.library.version {
84            md.push_str(&format!("**Version**: {}\n\n", version));
85        }
86
87        if let Some(desc) = &doc.library.description {
88            md.push_str(&format!("> {}\n\n", desc));
89        }
90
91        // Table of Contents
92        if doc.sections.len() > 1 {
93            md.push_str("## Table of Contents\n\n");
94            for section in &doc.sections {
95                let anchor = section.title.to_lowercase().replace(' ', "-");
96                md.push_str(&format!("- [{}](#{})\n", section.title, anchor));
97            }
98            md.push('\n');
99        }
100
101        // Sections
102        for section in &doc.sections {
103            md.push_str(&format!("## {}\n\n", section.title));
104
105            if let Some(url) = &section.url {
106                md.push_str(&format!("*Source: [{}]({})*\n\n", url, url));
107            }
108
109            md.push_str(&section.content);
110            md.push_str("\n\n");
111
112            // Code examples
113            for example in &section.code_examples {
114                if let Some(desc) = &example.description {
115                    md.push_str(&format!("### {}\n\n", desc));
116                }
117
118                md.push_str(&format!("```{}\n", example.language));
119                md.push_str(&example.code);
120                if !example.code.ends_with('\n') {
121                    md.push('\n');
122                }
123                md.push_str("```\n\n");
124            }
125        }
126
127        // Footer
128        md.push_str("---\n\n");
129        md.push_str("*Generated by [Manx](https://github.com/neur0map/manx) - ");
130        md.push_str("Powered by Context7 MCP*\n");
131
132        md
133    }
134
135    pub async fn export_batch_snippets(
136        results: &[crate::client::SearchResult],
137        path: &Path,
138        json_format: bool,
139        library: &str,
140        cache_manager: &crate::cache::CacheManager,
141    ) -> Result<()> {
142        let _format = if json_format {
143            ExportFormat::Json
144        } else {
145            ExportFormat::Markdown
146        };
147
148        let content = if json_format {
149            Self::batch_snippets_to_json(results, library, cache_manager).await?
150        } else {
151            Self::batch_snippets_to_markdown(results, library, cache_manager).await?
152        };
153
154        fs::write(path, content).with_context(|| format!("Failed to write to {:?}", path))?;
155
156        Ok(())
157    }
158
159    async fn batch_snippets_to_markdown(
160        results: &[crate::client::SearchResult],
161        library: &str,
162        cache_manager: &crate::cache::CacheManager,
163    ) -> Result<String> {
164        let mut md = String::new();
165
166        md.push_str(&format!(
167            "# {} Documentation Snippets\n\n",
168            library.to_uppercase()
169        ));
170        md.push_str(&format!(
171            "Generated on: {}\n\n",
172            chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
173        ));
174        md.push_str("---\n\n");
175
176        for result in results {
177            // Get the full snippet content from cache
178            let snippet_key = format!("{}_{}", library, result.id);
179            if let Ok(Some(content)) = cache_manager.get::<String>("snippets", &snippet_key).await {
180                md.push_str(&format!("## {}\n", result.title));
181                md.push_str(&format!("**Library:** {}  \n", result.library));
182                md.push_str(&format!("**ID:** {}  \n\n", result.id));
183
184                // Parse and format the Context7 content
185                md.push_str(&Self::format_context7_content_for_markdown(&content));
186                md.push_str("\n\n---\n\n");
187            }
188        }
189
190        md.push_str("\n*Generated by manx - A blazing-fast CLI documentation finder*  \n");
191        md.push_str(&format!("*Total snippets: {}*\n", results.len()));
192
193        Ok(md)
194    }
195
196    async fn batch_snippets_to_json(
197        results: &[crate::client::SearchResult],
198        library: &str,
199        cache_manager: &crate::cache::CacheManager,
200    ) -> Result<String> {
201        use serde_json::json;
202
203        let mut snippets = Vec::new();
204
205        for result in results {
206            let snippet_key = format!("{}_{}", library, result.id);
207            if let Ok(Some(content)) = cache_manager.get::<String>("snippets", &snippet_key).await {
208                snippets.push(json!({
209                    "id": result.id,
210                    "title": result.title,
211                    "library": result.library,
212                    "content": content,
213                    "relevance_score": result.relevance_score
214                }));
215            }
216        }
217
218        let output = json!({
219            "library": library,
220            "total_snippets": results.len(),
221            "snippets": snippets,
222            "generated": chrono::Utc::now().to_rfc3339()
223        });
224
225        Ok(serde_json::to_string_pretty(&output)?)
226    }
227
228    fn format_context7_content_for_markdown(content: &str) -> String {
229        let mut formatted = String::new();
230        let lines: Vec<&str> = content.lines().collect();
231        let mut i = 0;
232
233        while i < lines.len() {
234            let line = lines[i];
235
236            if line.starts_with("TITLE: ") {
237                // Skip title as it's already in the header
238                i += 1;
239                continue;
240            }
241
242            if let Some(stripped) = line.strip_prefix("DESCRIPTION: ") {
243                formatted.push_str(stripped);
244                formatted.push_str("\n\n");
245                i += 1;
246                continue;
247            }
248
249            if let Some(stripped) = line.strip_prefix("SOURCE: ") {
250                formatted.push_str(&format!("**Source:** {}\n\n", stripped));
251                i += 1;
252                continue;
253            }
254
255            if let Some(language) = line.strip_prefix("LANGUAGE: ") {
256                formatted.push_str(&format!("**{}:**\n", language));
257                i += 1;
258
259                // Skip "CODE:" line if present
260                if i < lines.len() && lines[i].starts_with("CODE:") {
261                    i += 1;
262                }
263
264                // Look for code block
265                if i < lines.len() && lines[i].starts_with("```") {
266                    // Start code block with language
267                    formatted.push_str("```");
268                    formatted.push_str(&language.to_lowercase());
269                    formatted.push('\n');
270                    i += 1;
271
272                    // Copy all code content until closing ```
273                    while i < lines.len() && !lines[i].starts_with("```") {
274                        formatted.push_str(lines[i]);
275                        formatted.push('\n');
276                        i += 1;
277                    }
278
279                    // Close code block
280                    if i < lines.len() && lines[i].starts_with("```") {
281                        formatted.push_str("```\n\n");
282                        i += 1;
283                    }
284                } else {
285                    // If no code block found, just add a newline
286                    formatted.push('\n');
287                }
288                continue;
289            }
290
291            // Skip separators but preserve other content
292            if line.starts_with("---") {
293                i += 1;
294                continue;
295            }
296
297            // Skip excessive empty lines but preserve structure
298            if line.trim().is_empty() {
299                formatted.push('\n');
300                i += 1;
301                continue;
302            }
303
304            // Regular content - preserve as-is
305            formatted.push_str(line);
306            formatted.push('\n');
307            i += 1;
308        }
309
310        // Clean up excessive newlines
311        while formatted.ends_with("\n\n\n") {
312            formatted.pop();
313        }
314
315        formatted
316    }
317}