1#[cfg(feature = "templates")]
2use crate::errors::Result;
3#[cfg(feature = "templates")]
4use crate::models::Entry;
5#[cfg(feature = "templates")]
6use std::io::Write;
7
8#[cfg(feature = "templates")]
9pub enum TemplateFormat {
11 Markdown,
12 Html,
13}
14
15#[cfg(feature = "templates")]
16impl std::str::FromStr for TemplateFormat {
17 type Err = String;
18
19 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
20 match s.to_lowercase().as_str() {
21 "markdown" | "md" => Ok(TemplateFormat::Markdown),
22 "html" => Ok(TemplateFormat::Html),
23 _ => Err(format!("Unknown template format: {}", s)),
24 }
25 }
26}
27
28#[cfg(feature = "templates")]
29pub fn export_with_template<W: Write>(
31 writer: &mut W,
32 entries: &[Entry],
33 format: &TemplateFormat,
34 title: Option<&str>,
35) -> Result<()> {
36 match format {
37 TemplateFormat::Markdown => export_markdown(writer, entries, title),
38 TemplateFormat::Html => export_html(writer, entries, title),
39 }
40}
41
42#[cfg(feature = "templates")]
43fn export_markdown<W: Write>(writer: &mut W, entries: &[Entry], title: Option<&str>) -> Result<()> {
45 if let Some(title) = title {
47 writeln!(writer, "# {}\n", title)?;
48 }
49
50 let total_files = entries
52 .iter()
53 .filter(|e| e.kind == crate::models::EntryKind::File)
54 .count();
55 let total_size: u64 = entries
56 .iter()
57 .filter(|e| e.kind == crate::models::EntryKind::File)
58 .map(|e| e.size)
59 .sum();
60
61 writeln!(writer, "**Total Files:** {} ", total_files)?;
62 writeln!(
63 writer,
64 "**Total Size:** {} \n",
65 humansize::format_size(total_size, humansize::BINARY)
66 )?;
67
68 writeln!(writer, "| Path | Size | Modified | Type |")?;
70 writeln!(writer, "|------|------|----------|------|")?;
71
72 for entry in entries {
74 let size_str = if entry.kind == crate::models::EntryKind::File {
75 humansize::format_size(entry.size, humansize::BINARY)
76 } else {
77 "-".to_string()
78 };
79
80 let kind_str = format!("{:?}", entry.kind);
81 let mtime_str = entry.mtime.format("%Y-%m-%d %H:%M").to_string();
82
83 writeln!(
84 writer,
85 "| {} | {} | {} | {} |",
86 entry.path.display(),
87 size_str,
88 mtime_str,
89 kind_str
90 )?;
91 }
92
93 Ok(())
94}
95
96#[cfg(feature = "templates")]
97fn export_html<W: Write>(writer: &mut W, entries: &[Entry], title: Option<&str>) -> Result<()> {
99 let total_files = entries
101 .iter()
102 .filter(|e| e.kind == crate::models::EntryKind::File)
103 .count();
104 let total_size: u64 = entries
105 .iter()
106 .filter(|e| e.kind == crate::models::EntryKind::File)
107 .map(|e| e.size)
108 .sum();
109
110 let title_text = title.unwrap_or("File Explorer Results");
111
112 writeln!(writer, "<!DOCTYPE html>")?;
114 writeln!(writer, "<html lang=\"en\">")?;
115 writeln!(writer, "<head>")?;
116 writeln!(writer, " <meta charset=\"UTF-8\">")?;
117 writeln!(
118 writer,
119 " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
120 )?;
121 writeln!(writer, " <title>{}</title>", title_text)?;
122 writeln!(writer, " <style>")?;
123 writeln!(writer, " body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 40px; background: #f5f5f5; }}")?;
124 writeln!(writer, " .container {{ max-width: 1200px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }}")?;
125 writeln!(writer, " h1 {{ color: #333; margin-top: 0; }}")?;
126 writeln!(writer, " .summary {{ background: #e8f4f8; padding: 15px; border-radius: 4px; margin-bottom: 20px; }}")?;
127 writeln!(writer, " .summary strong {{ color: #0066cc; }}")?;
128 writeln!(
129 writer,
130 " table {{ width: 100%; border-collapse: collapse; margin-top: 20px; }}"
131 )?;
132 writeln!(writer, " th {{ background: #0066cc; color: white; padding: 12px; text-align: left; font-weight: 600; }}")?;
133 writeln!(
134 writer,
135 " td {{ padding: 10px 12px; border-bottom: 1px solid #e0e0e0; }}"
136 )?;
137 writeln!(writer, " tr:hover {{ background: #f8f8f8; }}")?;
138 writeln!(
139 writer,
140 " .file-path {{ font-family: 'Monaco', 'Menlo', monospace; color: #0066cc; }}"
141 )?;
142 writeln!(writer, " .dir {{ color: #666; font-weight: 600; }}")?;
143 writeln!(writer, " .file {{ color: #333; }}")?;
144 writeln!(
145 writer,
146 " .symlink {{ color: #8b4513; font-style: italic; }}"
147 )?;
148 writeln!(writer, " </style>")?;
149 writeln!(writer, "</head>")?;
150 writeln!(writer, "<body>")?;
151 writeln!(writer, " <div class=\"container\">")?;
152 writeln!(writer, " <h1>{}</h1>", title_text)?;
153
154 writeln!(writer, " <div class=\"summary\">")?;
156 writeln!(
157 writer,
158 " <strong>Total Files:</strong> {} ",
159 total_files
160 )?;
161 writeln!(
162 writer,
163 " <strong>Total Size:</strong> {}",
164 humansize::format_size(total_size, humansize::BINARY)
165 )?;
166 writeln!(writer, " </div>")?;
167
168 writeln!(writer, " <table>")?;
170 writeln!(writer, " <thead>")?;
171 writeln!(writer, " <tr>")?;
172 writeln!(writer, " <th>Path</th>")?;
173 writeln!(writer, " <th>Size</th>")?;
174 writeln!(writer, " <th>Modified</th>")?;
175 writeln!(writer, " <th>Type</th>")?;
176 writeln!(writer, " </tr>")?;
177 writeln!(writer, " </thead>")?;
178 writeln!(writer, " <tbody>")?;
179
180 for entry in entries {
181 let size_str = if entry.kind == crate::models::EntryKind::File {
182 humansize::format_size(entry.size, humansize::BINARY)
183 } else {
184 "-".to_string()
185 };
186
187 let kind_class = match entry.kind {
188 crate::models::EntryKind::Dir => "dir",
189 crate::models::EntryKind::File => "file",
190 crate::models::EntryKind::Symlink => "symlink",
191 };
192
193 let kind_str = format!("{:?}", entry.kind);
194 let mtime_str = entry.mtime.format("%Y-%m-%d %H:%M").to_string();
195
196 writeln!(writer, " <tr>")?;
197 writeln!(
198 writer,
199 " <td class=\"file-path {}\">{}</td>",
200 kind_class,
201 entry.path.display()
202 )?;
203 writeln!(writer, " <td>{}</td>", size_str)?;
204 writeln!(writer, " <td>{}</td>", mtime_str)?;
205 writeln!(writer, " <td>{}</td>", kind_str)?;
206 writeln!(writer, " </tr>")?;
207 }
208
209 writeln!(writer, " </tbody>")?;
210 writeln!(writer, " </table>")?;
211 writeln!(writer, " </div>")?;
212 writeln!(writer, "</body>")?;
213 writeln!(writer, "</html>")?;
214
215 Ok(())
216}
217
218#[cfg(test)]
219#[cfg(feature = "templates")]
220mod tests {
221 use super::*;
222 use crate::models::EntryKind;
223 use chrono::Utc;
224 use std::path::PathBuf;
225
226 fn make_test_entry(name: &str, size: u64, kind: EntryKind) -> Entry {
227 Entry {
228 path: PathBuf::from(name),
229 name: name.to_string(),
230 size,
231 kind,
232 mtime: Utc::now(),
233 perms: None,
234 owner: None,
235 depth: 0,
236 }
237 }
238
239 #[test]
240 fn test_markdown_export() {
241 let entries = vec![
242 make_test_entry("file1.txt", 100, EntryKind::File),
243 make_test_entry("file2.txt", 200, EntryKind::File),
244 ];
245
246 let mut output = Vec::new();
247 export_markdown(&mut output, &entries, Some("Test Report")).unwrap();
248 let output_str = String::from_utf8(output).unwrap();
249
250 assert!(output_str.contains("# Test Report"));
251 assert!(output_str.contains("| Path | Size | Modified | Type |"));
252 assert!(output_str.contains("file1.txt"));
253 assert!(output_str.contains("file2.txt"));
254 }
255
256 #[test]
257 fn test_html_export() {
258 let entries = vec![make_test_entry("file1.txt", 100, EntryKind::File)];
259
260 let mut output = Vec::new();
261 export_html(&mut output, &entries, Some("Test Report")).unwrap();
262 let output_str = String::from_utf8(output).unwrap();
263
264 assert!(output_str.contains("<!DOCTYPE html>"));
265 assert!(output_str.contains("<title>Test Report</title>"));
266 assert!(output_str.contains("file1.txt"));
267 }
268}