Skip to main content

specfold_core/markdown/
sections.rs

1//! Extracting one section of a document, so a reader (or an agent) pays only for what it reads.
2
3/// The heading line whose text or anchor matches `name` (case-insensitive), and everything below
4/// it up to the next heading of the same or higher level.
5pub 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
28/// Heading outline: `(level, text)` for every heading.
29pub 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}