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