use crate::domain::Section;
pub fn extract_title(relative_path: &str, body: &str) -> String {
body.lines()
.find_map(|line| {
line.strip_prefix("# ")
.map(|value| value.trim().to_string())
})
.or_else(|| {
relative_path
.rsplit('/')
.next()
.map(|name| name.trim_end_matches(".md").to_string())
})
.unwrap_or_else(|| "Untitled".to_string())
}
pub fn extract_sections(body: &str) -> Vec<Section> {
let mut sections = Vec::new();
let mut current_heading: Option<String> = None;
let mut current_level = 0usize;
let mut buffer: Vec<String> = Vec::new();
let mut active_fence: Option<&str> = None;
for line in body.lines() {
let trimmed = line.trim_start();
let fence = if trimmed.starts_with("```") {
Some("```")
} else if trimmed.starts_with("~~~") {
Some("~~~")
} else {
None
};
if let Some(fence) = fence {
active_fence = match active_fence {
Some(current) if current == fence => None,
None => Some(fence),
Some(current) => Some(current),
};
buffer.push(line.to_string());
continue;
}
if active_fence.is_none() && trimmed.starts_with('#') {
let level = trimmed.chars().take_while(|ch| *ch == '#').count();
if level > 0 && trimmed.chars().nth(level) == Some(' ') {
if !buffer.is_empty() || current_heading.is_some() {
sections.push(Section {
heading: current_heading.clone(),
level: current_level,
content: buffer.join("\n").trim().to_string(),
});
}
current_heading = Some(trimmed[level + 1..].trim().to_string());
current_level = level;
buffer.clear();
continue;
}
}
buffer.push(line.to_string());
}
if !buffer.is_empty() || current_heading.is_some() {
sections.push(Section {
heading: current_heading,
level: current_level,
content: buffer.join("\n").trim().to_string(),
});
}
if sections.is_empty() {
sections.push(Section {
heading: None,
level: 0,
content: body.trim().to_string(),
});
}
sections
}