1use crate::domain::Section;
2
3pub fn extract_title(relative_path: &str, body: &str) -> String {
4 body.lines()
5 .find_map(|line| {
6 line.strip_prefix("# ")
7 .map(|value| value.trim().to_string())
8 })
9 .or_else(|| {
10 relative_path
11 .rsplit('/')
12 .next()
13 .map(|name| name.trim_end_matches(".md").to_string())
14 })
15 .unwrap_or_else(|| "Untitled".to_string())
16}
17
18pub fn extract_sections(body: &str) -> Vec<Section> {
19 let mut sections = Vec::new();
20 let mut current_heading: Option<String> = None;
21 let mut current_level = 0usize;
22 let mut buffer: Vec<String> = Vec::new();
23 let mut active_fence: Option<&str> = None;
24
25 for line in body.lines() {
26 let trimmed = line.trim_start();
27 let fence = if trimmed.starts_with("```") {
28 Some("```")
29 } else if trimmed.starts_with("~~~") {
30 Some("~~~")
31 } else {
32 None
33 };
34 if let Some(fence) = fence {
35 active_fence = match active_fence {
36 Some(current) if current == fence => None,
37 None => Some(fence),
38 Some(current) => Some(current),
39 };
40 buffer.push(line.to_string());
41 continue;
42 }
43 if active_fence.is_none() && trimmed.starts_with('#') {
44 let level = trimmed.chars().take_while(|ch| *ch == '#').count();
45 if level > 0 && trimmed.chars().nth(level) == Some(' ') {
46 if !buffer.is_empty() || current_heading.is_some() {
47 sections.push(Section {
48 heading: current_heading.clone(),
49 level: current_level,
50 content: buffer.join("\n").trim().to_string(),
51 });
52 }
53 current_heading = Some(trimmed[level + 1..].trim().to_string());
54 current_level = level;
55 buffer.clear();
56 continue;
57 }
58 }
59 buffer.push(line.to_string());
60 }
61
62 if !buffer.is_empty() || current_heading.is_some() {
63 sections.push(Section {
64 heading: current_heading,
65 level: current_level,
66 content: buffer.join("\n").trim().to_string(),
67 });
68 }
69
70 if sections.is_empty() {
71 sections.push(Section {
72 heading: None,
73 level: 0,
74 content: body.trim().to_string(),
75 });
76 }
77
78 sections
79}