#[derive(Debug, PartialEq)]
pub struct FilterResult {
pub content: String,
pub matched: bool,
pub missing: Vec<String>,
pub available: Vec<String>,
}
pub fn filter_sections(content: &str, sections: &[&str]) -> FilterResult {
debug_assert!(
!sections.is_empty(),
"precondition: sections must be non-empty"
);
let mut preamble_lines: Vec<&str> = Vec::new();
let mut blocks: Vec<(String, Vec<&str>)> = Vec::new();
let mut in_preamble = true;
for line in content.lines() {
if let Some(rest) = line.strip_prefix("### ") {
let normalized = rest.trim().to_string();
blocks.push((normalized, vec![line]));
in_preamble = false;
} else if in_preamble {
preamble_lines.push(line);
} else if let Some(block) = blocks.last_mut() {
block.1.push(line);
}
}
let available: Vec<String> = blocks.iter().map(|(h, _)| h.clone()).collect();
let missing: Vec<String> = sections
.iter()
.filter(|&&s| !blocks.iter().any(|(h, _)| h.eq_ignore_ascii_case(s)))
.map(|&s| s.to_string())
.collect();
let matched_lines: Vec<&str> = blocks
.iter()
.filter(|(h, _)| sections.iter().any(|s| s.eq_ignore_ascii_case(h)))
.flat_map(|(_, lines)| lines.iter().copied())
.collect();
let matched = !matched_lines.is_empty();
let output: Vec<&str> = preamble_lines
.iter()
.copied()
.chain(matched_lines)
.collect();
let mut result_content = output.join("\n");
if content.ends_with('\n') {
result_content.push('\n');
}
FilterResult {
content: result_content,
matched,
missing,
available,
}
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE: &str = "\
# Language Patterns
Intro line.
### Rust
Rust anti-patterns here.
#### Sub-heading
More Rust content.
### TypeScript
TypeScript patterns here.
### Python
Python patterns here.
";
#[test]
fn filter_sections_returns_matching_section() {
let r = filter_sections(SAMPLE, &["Rust"]);
assert!(r.matched);
assert!(r.content.contains("### Rust"), "should include heading");
assert!(
r.content.contains("Rust anti-patterns here."),
"should include body"
);
assert!(
r.content.contains("# Language Patterns"),
"should include preamble"
);
assert!(
!r.content.contains("### TypeScript"),
"should exclude TypeScript"
);
}
#[test]
fn filter_sections_case_insensitive() {
let r = filter_sections(SAMPLE, &["rust"]);
assert!(r.matched);
assert!(r.content.contains("### Rust"));
}
#[test]
fn filter_sections_multiple_sections() {
let r = filter_sections(SAMPLE, &["Rust", "TypeScript"]);
assert!(r.matched);
assert!(r.content.contains("### Rust"));
assert!(r.content.contains("### TypeScript"));
assert!(!r.content.contains("### Python"));
assert!(r.missing.is_empty());
}
#[test]
fn filter_sections_preserves_preamble() {
let r = filter_sections(SAMPLE, &["Rust"]);
assert!(r.content.starts_with("# Language Patterns"));
}
#[test]
fn filter_sections_no_match_returns_not_matched() {
let r = filter_sections(SAMPLE, &["Go"]);
assert!(!r.matched);
assert_eq!(r.missing, vec!["Go"]);
assert_eq!(r.available, vec!["Rust", "TypeScript", "Python"]);
}
#[test]
fn filter_sections_partial_match_returns_missing() {
let r = filter_sections(SAMPLE, &["Rust", "typescript", "Go"]);
assert!(r.matched);
assert!(r.content.contains("### Rust"));
assert!(r.content.contains("### TypeScript"));
assert_eq!(r.missing, vec!["Go"]);
assert!(
!r.content.contains("### Python"),
"unrelated section should be excluded"
);
}
#[test]
fn filter_sections_duplicate_headings_both_included() {
let content = "### Rust\n\nFirst block.\n\n### Rust\n\nSecond block.\n";
let r = filter_sections(content, &["Rust"]);
assert!(r.matched);
assert!(r.content.contains("First block."));
assert!(r.content.contains("Second block."));
assert_eq!(r.available, vec!["Rust", "Rust"]);
}
#[test]
fn filter_sections_nested_h4_included_in_body() {
let r = filter_sections(SAMPLE, &["Rust"]);
assert!(
r.content.contains("#### Sub-heading"),
"h4 should be part of section body"
);
assert!(r.content.contains("More Rust content."));
}
#[test]
fn filter_sections_heading_whitespace_normalized() {
let content = "### Rust \n\nContent.\n";
let r = filter_sections(content, &["rust"]);
assert!(r.matched, "should match despite whitespace");
assert!(
r.content.contains("Content."),
"body should be included when matched via whitespace"
);
assert_eq!(r.available, vec!["Rust"]);
}
#[test]
fn filter_sections_no_headings_in_file_returns_not_matched() {
let content = "Just a preamble\nno headings here\n";
let r = filter_sections(content, &["Rust"]);
assert!(!r.matched);
assert!(r.available.is_empty());
assert_eq!(r.missing, vec!["Rust"]);
}
#[test]
fn filter_sections_indented_heading_not_a_boundary() {
let content = "### Real\n\nBody.\n\n ### Fake\n\nNot a section.\n";
let r = filter_sections(content, &["Real"]);
assert!(r.matched);
assert_eq!(r.available, vec!["Real"]);
assert!(r.content.contains(" ### Fake"));
}
#[test]
#[should_panic(expected = "precondition")]
fn filter_sections_empty_sections_is_caller_error() {
filter_sections("### Rust\nContent\n", &[]);
}
#[test]
fn filter_sections_available_in_file_order() {
let r = filter_sections(SAMPLE, &["Python"]);
assert_eq!(r.available, vec!["Rust", "TypeScript", "Python"]);
}
}