specfold_core/markdown/
sections.rs1pub fn extract(body: &str, name: &str) -> Option<String> {
6 let lines: Vec<&str> = body.lines().collect();
7 span(body, name).map(|(start, end)| lines[start..end].join("\n"))
8}
9
10pub fn span(body: &str, name: &str) -> Option<(usize, usize)> {
13 let want = name.trim().trim_start_matches('#').to_lowercase();
14 let lines: Vec<&str> = body.lines().collect();
15 let mut fence = false;
16 let levels: Vec<Option<usize>> = lines
17 .iter()
18 .map(|l| {
19 let t = l.trim_start();
20 if t.starts_with("```") || t.starts_with("~~~") {
21 fence = !fence;
22 return None;
23 }
24 let n = t.chars().take_while(|c| *c == '#').count();
25 (!fence && n > 0 && t[n..].starts_with(' ')).then_some(n)
26 })
27 .collect();
28 let start = (0..lines.len()).find(|&i| {
29 levels[i].is_some() && {
30 let text = lines[i].trim_start().trim_start_matches('#').trim().to_lowercase();
31 text == want || text.starts_with(&format!("{want} ")) || text.contains(&format!("{{#{want}}}"))
32 }
33 })?;
34 let lv = levels[start]?;
35 let end = (start + 1..lines.len()).find(|&i| levels[i].is_some_and(|n| n <= lv)).unwrap_or(lines.len());
36 Some((start, end))
37}
38
39pub fn outline(body: &str) -> Vec<(usize, String)> {
41 let mut fence = false;
42 body.lines()
43 .filter_map(|l| {
44 let t = l.trim_start();
45 if t.starts_with("```") || t.starts_with("~~~") {
46 fence = !fence;
47 }
48 if fence {
49 return None;
50 }
51 let n = t.chars().take_while(|c| *c == '#').count();
52 (n > 0 && t[n..].starts_with(' ')).then(|| (n, t[n..].trim().to_string()))
53 })
54 .collect()
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60
61 #[test]
62 fn extracts_by_heading_or_anchor() {
63 let b = "# T\n## Rule\nr1\n### Detail {#R-1}\nd\n## Why\nw\n";
64 assert_eq!(extract(b, "rule").unwrap(), "## Rule\nr1\n### Detail {#R-1}\nd");
65 assert_eq!(extract(b, "R-1").unwrap(), "### Detail {#R-1}\nd");
66 assert!(extract(b, "nope").is_none());
67 assert_eq!(outline(b).len(), 4);
68 assert_eq!(span(b, "why"), Some((5, 7)));
69 assert_eq!(span("## A\n```\n## not\n```\n## B\n", "a"), Some((0, 4)));
70 }
71}