use std::path::Path;
#[derive(Debug, Clone)]
pub struct ExtractedDoc {
pub content: String,
pub start_line: usize,
}
pub fn extract_module_docs(content: &str) -> Option<ExtractedDoc> {
let mut doc_lines = Vec::new();
let mut start_line = None;
let mut in_doc_block = false;
for (i, line) in content.lines().enumerate() {
let trimmed = line.trim_start();
if trimmed.starts_with("//!") {
if start_line.is_none() {
start_line = Some(i + 1); in_doc_block = true;
}
let doc_content = trimmed.strip_prefix("//!").unwrap_or("");
let doc_content = doc_content.strip_prefix(' ').unwrap_or(doc_content);
doc_lines.push(doc_content.to_string());
} else if in_doc_block {
if trimmed.is_empty() {
continue;
}
break;
}
}
if doc_lines.is_empty() {
return None;
}
Some(ExtractedDoc {
content: doc_lines.join("\n"),
start_line: start_line.unwrap_or(1),
})
}
pub fn find_rust_files(path: &Path) -> std::io::Result<Vec<std::path::PathBuf>> {
let mut files = Vec::new();
if path.is_file() {
if path.extension().and_then(|e| e.to_str()) == Some("rs") {
files.push(path.to_path_buf());
}
return Ok(files);
}
if path.is_dir() {
for entry in std::fs::read_dir(path)? {
let entry = entry?;
let entry_path = entry.path();
if let Some(name) = entry_path.file_name().and_then(|n| n.to_str())
&& (name.starts_with('.') || name == "target")
{
continue;
}
if entry_path.is_dir() {
files.extend(find_rust_files(&entry_path)?);
} else if entry_path.extension().and_then(|e| e.to_str()) == Some("rs") {
files.push(entry_path);
}
}
}
Ok(files)
}
pub fn map_line_to_source(markdown_line: usize, doc_start_line: usize) -> usize {
doc_start_line + markdown_line - 1
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_simple_module_docs() {
let content = r#"//! # My Crate
//!
//! This is the module documentation.
fn main() {}
"#;
let doc = extract_module_docs(content).unwrap();
assert_eq!(doc.start_line, 1);
assert!(doc.content.contains("# My Crate"));
assert!(doc.content.contains("This is the module documentation."));
}
#[test]
fn test_extract_docs_with_code_blocks() {
let content = r#"//! # Example
//!
//! ```rust
//! let x = 1;
//! ```
use std::io;
"#;
let doc = extract_module_docs(content).unwrap();
assert!(doc.content.contains("```rust"));
assert!(doc.content.contains("let x = 1;"));
}
#[test]
fn test_no_module_docs() {
let content = r#"/// This is an item doc, not module doc
fn foo() {}
"#;
assert!(extract_module_docs(content).is_none());
}
#[test]
fn test_docs_not_at_start() {
let content = r#"// Regular comment
//! Module doc starts here
//! More docs
fn main() {}
"#;
let doc = extract_module_docs(content).unwrap();
assert_eq!(doc.start_line, 2);
assert!(doc.content.contains("Module doc starts here"));
}
#[test]
fn test_line_mapping() {
assert_eq!(map_line_to_source(1, 5), 5);
assert_eq!(map_line_to_source(3, 5), 7);
assert_eq!(map_line_to_source(1, 1), 1);
}
#[test]
fn test_preserves_indentation() {
let content = r#"//! # Heading
//!
//! - Item 1
//! - Nested item
//! - Deeply nested
"#;
let doc = extract_module_docs(content).unwrap();
assert!(doc.content.contains(" - Nested item"));
assert!(doc.content.contains(" - Deeply nested"));
}
#[test]
fn test_empty_doc_lines() {
let content = r#"//! # Title
//!
//! Paragraph after blank line.
"#;
let doc = extract_module_docs(content).unwrap();
let lines: Vec<_> = doc.content.lines().collect();
assert_eq!(lines.len(), 3);
assert_eq!(lines[0], "# Title");
assert_eq!(lines[1], "");
assert_eq!(lines[2], "Paragraph after blank line.");
}
}