fn heading_level(line: &str) -> Option<usize> {
let trimmed = line.trim_start_matches('#');
let hashes = line.len() - trimmed.len();
if hashes == 0 || hashes > 6 {
return None;
}
match trimmed.chars().next() {
None | Some(' ') => Some(hashes),
_ => None,
}
}
fn heading_text(line: &str) -> &str {
let without_hashes = line.trim_start_matches('#');
without_hashes.trim()
}
pub fn extract_section(source: &str, name: &str) -> Option<String> {
let name_lower = name.to_lowercase();
let lines: Vec<&str> = source.lines().collect();
let n = lines.len();
let mut start_idx: Option<usize> = None;
let mut section_level: usize = 0;
for (i, &line) in lines.iter().enumerate() {
if let Some(level) = heading_level(line) {
let text_lower = heading_text(line).to_lowercase();
if text_lower.contains(&name_lower) {
start_idx = Some(i);
section_level = level;
break;
}
}
}
let start = start_idx?;
let end = lines[start + 1..]
.iter()
.position(|&line| heading_level(line).is_some_and(|lvl| lvl <= section_level))
.map(|rel| start + 1 + rel)
.unwrap_or(n);
let section_lines = &lines[start..end];
let mut out = section_lines.join("\n");
let source_has_trailing_newline = source.ends_with('\n');
if source_has_trailing_newline || end < n {
out.push('\n');
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn two_section_doc() -> &'static str {
"# Title\n\n## Foo\nbody1\n\n## Bar\nbody2\n"
}
#[test]
fn heading_level_detects_h1_through_h6() {
assert_eq!(heading_level("# H1"), Some(1));
assert_eq!(heading_level("## H2"), Some(2));
assert_eq!(heading_level("###### H6"), Some(6));
}
#[test]
fn heading_level_rejects_non_headings() {
assert_eq!(heading_level("regular text"), None);
assert_eq!(heading_level("##no-space"), None); assert_eq!(heading_level("####### too many"), None); assert_eq!(heading_level(""), None);
}
#[test]
fn extracts_first_matching_section() {
let doc = two_section_doc();
let foo = extract_section(doc, "Foo").expect("should find Foo");
assert_eq!(foo, "## Foo\nbody1\n\n");
let bar = extract_section(doc, "Bar").expect("should find Bar");
assert_eq!(bar, "## Bar\nbody2\n");
}
#[test]
fn case_insensitive_substring_match() {
let doc = "# Features Overview\n\ncontent\n";
let result = extract_section(doc, "features").expect("should match");
assert_eq!(result, "# Features Overview\n\ncontent\n");
let doc2 = "## Foobar\nbody\n";
let result2 = extract_section(doc2, "foo").expect("should match");
assert_eq!(result2, "## Foobar\nbody\n");
}
#[test]
fn stops_at_same_or_higher_level_heading() {
let doc = "# Top\n\n## Sub\nbody\n# Other\n";
let result = extract_section(doc, "Sub").expect("should find Sub");
assert_eq!(result, "## Sub\nbody\n");
}
#[test]
fn last_section_extends_to_eof() {
let doc = "# Introduction\n\nbody\n\n# Appendix\n\nappendix body\n";
let result = extract_section(doc, "Appendix").expect("should find Appendix");
assert_eq!(result, "# Appendix\n\nappendix body\n");
}
#[test]
fn no_match_returns_none() {
let doc = two_section_doc();
assert!(extract_section(doc, "DoesNotExist").is_none());
}
#[test]
fn first_match_wins_when_multiple_headings_match() {
let doc = "## FooA\nfirst\n\n## FooB\nsecond\n";
let result = extract_section(doc, "foo").expect("should match");
assert_eq!(result, "## FooA\nfirst\n\n");
}
#[test]
fn section_body_may_contain_lower_level_headings() {
let doc = "## Section\n\n### Sub-sub\n\nbody\n\n## Next\n";
let result = extract_section(doc, "Section").expect("should find Section");
assert_eq!(result, "## Section\n\n### Sub-sub\n\nbody\n\n");
}
}