1use super::{Formatter, PathDisplayMode};
14use crate::scanner::{FileNode, TreeStats};
15use anyhow::Result;
16use marqant::Marqant as MarqantCore;
17use std::collections::HashMap;
18use std::io::Write;
19use std::path::Path;
20
21pub struct MarqantFormatter {
23 no_emoji: bool,
24}
25
26impl MarqantFormatter {
27 pub fn new(_path_mode: PathDisplayMode, no_emoji: bool) -> Self {
28 Self { no_emoji }
29 }
30
31 pub fn compress_markdown(content: &str) -> Result<String> {
33 MarqantCore::compress_markdown(content)
34 }
35
36 pub fn compress_markdown_with_flags(content: &str, flags: Option<&str>) -> Result<String> {
38 MarqantCore::compress_markdown_with_flags(content, flags)
39 }
40
41 #[allow(dead_code)]
43 fn add_section_tags(content: &str) -> String {
44 let mut result = String::new();
45 let mut in_code_block = false;
46
47 for line in content.lines() {
48 if line.trim_start().starts_with("```") {
50 in_code_block = !in_code_block;
51 }
52
53 if !in_code_block {
55 if let Some(stripped) = line.strip_prefix("# ") {
56 let section = stripped.trim();
57 result.push_str(&format!("::section:{}::\n", section));
58 } else if let Some(stripped) = line.strip_prefix("## ") {
59 let subsection = stripped.trim();
60 result.push_str(&format!("::section:{}::\n", subsection));
61 }
62 }
63
64 result.push_str(line);
65 result.push('\n');
66 }
67
68 result
69 }
70
71 pub fn tokenize_content(content: &str) -> (HashMap<String, String>, String) {
73 MarqantCore::tokenize_content(content)
74 }
75
76 pub fn decompress_marqant(compressed: &str) -> Result<String> {
78 MarqantCore::decompress_marqant(compressed)
79 }
80}
81
82impl Formatter for MarqantFormatter {
83 fn format(
84 &self,
85 writer: &mut dyn Write,
86 nodes: &[FileNode],
87 stats: &TreeStats,
88 root_path: &Path,
89 ) -> Result<()> {
90 let mut markdown = String::new();
92
93 let project_name = root_path
95 .file_name()
96 .and_then(|n| n.to_str())
97 .unwrap_or("Directory");
98
99 markdown.push_str(&format!("# {} Structure\n\n", project_name));
100
101 markdown.push_str("## File Tree\n\n");
103 markdown.push_str("```\n");
104
105 for node in nodes {
106 let indent = " ".repeat(node.depth);
107 let name = node.path.file_name().and_then(|n| n.to_str()).unwrap_or("");
108 let suffix = if node.is_dir { "/" } else { "" };
109 let emoji = if !self.no_emoji {
110 if node.is_dir {
111 "📁 "
112 } else {
113 "📄 "
114 }
115 } else {
116 ""
117 };
118 markdown.push_str(&format!("{}{}{}{}\n", indent, emoji, name, suffix));
119 }
120
121 markdown.push_str("```\n\n");
122
123 markdown.push_str("## Statistics\n\n");
125 markdown.push_str(&format!("- Total files: {}\n", stats.total_files));
126 markdown.push_str(&format!("- Total directories: {}\n", stats.total_dirs));
127 markdown.push_str(&format!(
128 "- Total size: {:.2} MB\n",
129 stats.total_size as f64 / 1_048_576.0
130 ));
131
132 if !stats.file_types.is_empty() {
134 markdown.push_str("\n### File Types\n\n");
135 let mut types: Vec<_> = stats.file_types.iter().collect();
136 types.sort_by(|a, b| b.1.cmp(a.1));
137
138 for (ext, count) in types.iter().take(10) {
139 markdown.push_str(&format!("- .{}: {} files\n", ext, count));
140 }
141 }
142
143 let compressed = Self::compress_markdown(&markdown)?;
145 writer.write_all(compressed.as_bytes())?;
146
147 Ok(())
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn test_markdown_compression() {
157 let markdown = r#"# Test Document
158
159## Section One
160
161This is a test document. This is a test document.
162
163### Subsection
164
165- Item one
166- Item two
167- Item three
168
169## Section Two
170
171This is a test document.
172
173```rust
174fn main() {
175 println!("Hello, world!");
176}
177```
178
179## Section Three
180
181**Bold text** and *italic text*.
182"#;
183
184 let compressed = MarqantFormatter::compress_markdown(markdown).unwrap();
185
186 assert!(
188 compressed.starts_with("MARQANT"),
189 "Compressed data should start with MARQANT header"
190 );
191
192 let decompressed = MarqantFormatter::decompress_marqant(&compressed).unwrap();
197 assert_eq!(decompressed.trim(), markdown.trim());
198
199 assert!(
201 compressed.starts_with("MARQANT"),
202 "Should have proper header"
203 );
204 assert!(compressed.len() > 20, "Should have header and content");
205 }
206
207 #[test]
208 fn test_token_assignment() {
209 let markdown_content = "## Section 1\n\n## Section 2\n\n## Section 3\n\n## Section 4\n\n## Section 5\n\nContent here.";
214 let (tokens, tokenized) = MarqantFormatter::tokenize_content(markdown_content);
215
216 assert!(
221 !tokens.is_empty() || tokenized != markdown_content,
222 "Tokenization should create tokens or modify content. Got tokens: {:?}, content modified: {}",
223 tokens.keys().collect::<Vec<_>>(),
224 tokenized != markdown_content
225 );
226 }
227}
228
229