Skip to main content

spool/vault/
markdown.rs

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}