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 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 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 for section in &doc.sections {
103 md.push_str(&format!("## {}\n\n", section.title));
104
105 if let Some(url) = §ion.url {
106 md.push_str(&format!("*Source: [{}]({})*\n\n", url, url));
107 }
108
109 md.push_str(§ion.content);
110 md.push_str("\n\n");
111
112 for example in §ion.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 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 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 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 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 if i < lines.len() && lines[i].starts_with("CODE:") {
261 i += 1;
262 }
263
264 if i < lines.len() && lines[i].starts_with("```") {
266 formatted.push_str("```");
268 formatted.push_str(&language.to_lowercase());
269 formatted.push('\n');
270 i += 1;
271
272 while i < lines.len() && !lines[i].starts_with("```") {
274 formatted.push_str(lines[i]);
275 formatted.push('\n');
276 i += 1;
277 }
278
279 if i < lines.len() && lines[i].starts_with("```") {
281 formatted.push_str("```\n\n");
282 i += 1;
283 }
284 } else {
285 formatted.push('\n');
287 }
288 continue;
289 }
290
291 if line.starts_with("---") {
293 i += 1;
294 continue;
295 }
296
297 if line.trim().is_empty() {
299 formatted.push('\n');
300 i += 1;
301 continue;
302 }
303
304 formatted.push_str(line);
306 formatted.push('\n');
307 i += 1;
308 }
309
310 while formatted.ends_with("\n\n\n") {
312 formatted.pop();
313 }
314
315 formatted
316 }
317}