use crate::config::{SectionConfig, SectionEnd, SectionStart};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SectionState {
NotStarted,
InSection,
BetweenSections,
Done,
}
pub struct SectionSelector {
config: SectionConfig,
state: SectionState,
sections_seen: i64,
}
impl SectionSelector {
pub fn new(config: SectionConfig) -> Self {
let initial_state = if config.start.is_none() {
SectionState::InSection
} else {
SectionState::NotStarted
};
Self {
config,
state: initial_state,
sections_seen: 0,
}
}
pub fn should_include_line(&mut self, line: &str) -> bool {
match self.state {
SectionState::Done => {
false
}
SectionState::NotStarted => {
if let Some(include_line) = self.matches_start(line) {
self.sections_seen += 1;
self.state = SectionState::InSection;
include_line
} else {
false
}
}
SectionState::InSection => {
if let Some(include_line) = self.matches_end(line) {
if self.limit_reached() {
self.state = SectionState::Done;
} else if self.config.start.is_some() {
self.state = SectionState::BetweenSections;
} else {
self.state = SectionState::InSection;
}
include_line
} else if let Some(include_line) = self.matches_start(line) {
self.sections_seen += 1;
if self.limit_exceeded() {
self.state = SectionState::Done;
false
} else {
include_line
}
} else {
true }
}
SectionState::BetweenSections => {
if let Some(include_line) = self.matches_start(line) {
self.sections_seen += 1;
if self.limit_exceeded() {
self.state = SectionState::Done;
false
} else {
self.state = SectionState::InSection;
include_line
}
} else {
false
}
}
}
}
fn matches_start(&self, line: &str) -> Option<bool> {
match &self.config.start {
Some(SectionStart::From(pattern)) => {
if pattern.is_match(line) {
Some(true)
} else {
None
}
}
Some(SectionStart::After(pattern)) => {
if pattern.is_match(line) {
Some(false)
} else {
None
}
}
None => None,
}
}
fn matches_end(&self, line: &str) -> Option<bool> {
match &self.config.end {
Some(SectionEnd::Before(pattern)) => {
if pattern.is_match(line) {
Some(false)
} else {
None
}
}
Some(SectionEnd::Through(pattern)) => {
if pattern.is_match(line) {
Some(true)
} else {
None
}
}
None => None,
}
}
fn limit_reached(&self) -> bool {
self.config.max_sections > 0 && self.sections_seen >= self.config.max_sections
}
fn limit_exceeded(&self) -> bool {
self.config.max_sections > 0 && self.sections_seen > self.config.max_sections
}
}
#[cfg(test)]
mod tests {
use super::*;
use regex::Regex;
fn start_from(pattern: &str) -> SectionStart {
SectionStart::From(Regex::new(pattern).unwrap())
}
fn start_after(pattern: &str) -> SectionStart {
SectionStart::After(Regex::new(pattern).unwrap())
}
fn end_before(pattern: &str) -> SectionEnd {
SectionEnd::Before(Regex::new(pattern).unwrap())
}
fn end_through(pattern: &str) -> SectionEnd {
SectionEnd::Through(Regex::new(pattern).unwrap())
}
fn build_config(
start: Option<SectionStart>,
end: Option<SectionEnd>,
max_sections: i64,
) -> SectionConfig {
SectionConfig {
start,
end,
max_sections,
}
}
#[test]
fn start_from_end_before_matches_legacy_behavior() {
let config = build_config(Some(start_from(r"^START")), Some(end_before(r"^END")), -1);
let mut selector = SectionSelector::new(config);
assert!(!selector.should_include_line("before"));
assert!(selector.should_include_line("START line"));
assert!(selector.should_include_line("content1"));
assert!(selector.should_include_line("content2"));
assert!(!selector.should_include_line("END line"));
assert!(!selector.should_include_line("after"));
}
#[test]
fn start_after_skips_marker_line() {
let config = build_config(
Some(start_after(r"^== HEADER")),
Some(end_before(r"^== NEXT")),
-1,
);
let mut selector = SectionSelector::new(config);
assert!(!selector.should_include_line("preamble"));
assert!(!selector.should_include_line("== HEADER"));
assert!(selector.should_include_line("body line"));
assert!(!selector.should_include_line("== NEXT"));
}
#[test]
fn end_through_includes_terminator() {
let config = build_config(Some(start_from(r"^BEGIN")), Some(end_through(r"^END$")), -1);
let mut selector = SectionSelector::new(config);
assert!(selector.should_include_line("BEGIN"));
assert!(selector.should_include_line("payload"));
assert!(selector.should_include_line("END"));
assert!(!selector.should_include_line("after"));
}
#[test]
fn multiple_sections_respect_limits() {
let config = build_config(Some(start_from(r"^START")), Some(end_before(r"^END")), 2);
let mut selector = SectionSelector::new(config);
assert!(selector.should_include_line("START 1"));
assert!(selector.should_include_line("line 1"));
assert!(!selector.should_include_line("END 1"));
assert!(!selector.should_include_line("noise"));
assert!(selector.should_include_line("START 2"));
assert!(selector.should_include_line("line 2"));
assert!(!selector.should_include_line("END 2"));
assert!(!selector.should_include_line("START 3"));
assert!(!selector.should_include_line("line 3"));
assert!(!selector.should_include_line("END 3"));
}
#[test]
fn start_only_with_inclusive_marker() {
let config = build_config(Some(start_from(r"^== ")), None, -1);
let mut selector = SectionSelector::new(config);
assert!(!selector.should_include_line("header"));
assert!(selector.should_include_line("== A"));
assert!(selector.should_include_line("a1"));
assert!(selector.should_include_line("== B"));
assert!(selector.should_include_line("b1"));
}
#[test]
fn start_after_respects_limits_without_end() {
let config = build_config(Some(start_after(r"^== ")), None, 1);
let mut selector = SectionSelector::new(config);
assert!(!selector.should_include_line("== A"));
assert!(selector.should_include_line("a1"));
assert!(selector.should_include_line("a2"));
assert!(!selector.should_include_line("== B"));
assert!(!selector.should_include_line("b1"));
}
#[test]
fn no_patterns_include_everything() {
let config = build_config(None, None, -1);
let mut selector = SectionSelector::new(config);
assert!(selector.should_include_line("line 1"));
assert!(selector.should_include_line("line 2"));
assert!(selector.should_include_line("line 3"));
}
}