1use std::path::Path;
7
8#[derive(Debug, Clone)]
10pub struct ExtractedDoc {
11 pub content: String,
13 pub start_line: usize,
15}
16
17pub fn extract_module_docs(content: &str) -> Option<ExtractedDoc> {
40 let mut doc_lines = Vec::new();
41 let mut start_line = None;
42 let mut in_doc_block = false;
43
44 for (i, line) in content.lines().enumerate() {
45 let trimmed = line.trim_start();
46
47 if trimmed.starts_with("//!") {
48 if start_line.is_none() {
49 start_line = Some(i + 1); in_doc_block = true;
51 }
52
53 let doc_content = trimmed.strip_prefix("//!").unwrap_or("");
55 let doc_content = doc_content.strip_prefix(' ').unwrap_or(doc_content);
57 doc_lines.push(doc_content.to_string());
58 } else if in_doc_block {
59 if trimmed.is_empty() {
62 continue;
65 }
66 break;
68 }
69 }
70
71 if doc_lines.is_empty() {
72 return None;
73 }
74
75 Some(ExtractedDoc {
76 content: doc_lines.join("\n"),
77 start_line: start_line.unwrap_or(1),
78 })
79}
80
81pub fn find_rust_files(path: &Path) -> std::io::Result<Vec<std::path::PathBuf>> {
83 let mut files = Vec::new();
84
85 if path.is_file() {
86 if path.extension().and_then(|e| e.to_str()) == Some("rs") {
87 files.push(path.to_path_buf());
88 }
89 return Ok(files);
90 }
91
92 if path.is_dir() {
93 for entry in std::fs::read_dir(path)? {
94 let entry = entry?;
95 let entry_path = entry.path();
96
97 if let Some(name) = entry_path.file_name().and_then(|n| n.to_str())
99 && (name.starts_with('.') || name == "target")
100 {
101 continue;
102 }
103
104 if entry_path.is_dir() {
105 files.extend(find_rust_files(&entry_path)?);
106 } else if entry_path.extension().and_then(|e| e.to_str()) == Some("rs") {
107 files.push(entry_path);
108 }
109 }
110 }
111
112 Ok(files)
113}
114
115pub fn map_line_to_source(markdown_line: usize, doc_start_line: usize) -> usize {
124 doc_start_line + markdown_line - 1
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn test_extract_simple_module_docs() {
136 let content = r#"//! # My Crate
137//!
138//! This is the module documentation.
139
140fn main() {}
141"#;
142
143 let doc = extract_module_docs(content).unwrap();
144 assert_eq!(doc.start_line, 1);
145 assert!(doc.content.contains("# My Crate"));
146 assert!(doc.content.contains("This is the module documentation."));
147 }
148
149 #[test]
150 fn test_extract_docs_with_code_blocks() {
151 let content = r#"//! # Example
152//!
153//! ```rust
154//! let x = 1;
155//! ```
156
157use std::io;
158"#;
159
160 let doc = extract_module_docs(content).unwrap();
161 assert!(doc.content.contains("```rust"));
162 assert!(doc.content.contains("let x = 1;"));
163 }
164
165 #[test]
166 fn test_no_module_docs() {
167 let content = r#"/// This is an item doc, not module doc
168fn foo() {}
169"#;
170
171 assert!(extract_module_docs(content).is_none());
172 }
173
174 #[test]
175 fn test_docs_not_at_start() {
176 let content = r#"// Regular comment
177//! Module doc starts here
178//! More docs
179
180fn main() {}
181"#;
182
183 let doc = extract_module_docs(content).unwrap();
184 assert_eq!(doc.start_line, 2);
185 assert!(doc.content.contains("Module doc starts here"));
186 }
187
188 #[test]
189 fn test_line_mapping() {
190 assert_eq!(map_line_to_source(1, 5), 5);
193 assert_eq!(map_line_to_source(3, 5), 7);
194 assert_eq!(map_line_to_source(1, 1), 1);
195 }
196
197 #[test]
198 fn test_preserves_indentation() {
199 let content = r#"//! # Heading
200//!
201//! - Item 1
202//! - Nested item
203//! - Deeply nested
204"#;
205
206 let doc = extract_module_docs(content).unwrap();
207 assert!(doc.content.contains(" - Nested item"));
208 assert!(doc.content.contains(" - Deeply nested"));
209 }
210
211 #[test]
212 fn test_empty_doc_lines() {
213 let content = r#"//! # Title
214//!
215//! Paragraph after blank line.
216"#;
217
218 let doc = extract_module_docs(content).unwrap();
219 let lines: Vec<_> = doc.content.lines().collect();
220 assert_eq!(lines.len(), 3);
221 assert_eq!(lines[0], "# Title");
222 assert_eq!(lines[1], "");
223 assert_eq!(lines[2], "Paragraph after blank line.");
224 }
225}