pub struct ClauseSplitter {
min_len: usize,
max_len: usize,
buf: String,
}
impl ClauseSplitter {
pub fn new(min_len: usize, max_len: usize) -> Self {
Self {
min_len,
max_len,
buf: String::new(),
}
}
pub fn push(&mut self, text: &str) -> Vec<String> {
self.buf.push_str(text);
let mut out = Vec::new();
while let Some((emit_end, consume_end)) = self.next_cut() {
let drained: String = self.buf.drain(..consume_end).collect();
debug_assert!(emit_end <= drained.len());
let clause = &drained[..emit_end];
if !clause.is_empty() {
out.push(clause.to_string());
}
}
out
}
pub fn flush(&mut self) -> Option<String> {
if self.buf.trim().is_empty() {
self.buf.clear();
None
} else {
Some(std::mem::take(&mut self.buf))
}
}
fn next_cut(&self) -> Option<(usize, usize)> {
let mut char_count = 0usize;
let mut boundary_cut: Option<usize> = None;
let mut last_ws: Option<(usize, usize)> = None; for (byte_idx, ch) in self.buf.char_indices() {
char_count += 1;
if boundary_cut.is_none()
&& matches!(ch, '.' | '?' | '!' | ',')
&& char_count >= self.min_len
{
boundary_cut = Some(byte_idx + ch.len_utf8());
}
if char_count <= self.max_len && ch.is_whitespace() {
last_ws = Some((byte_idx, ch.len_utf8()));
}
}
if let Some(end) = boundary_cut {
return Some((end, end));
}
if char_count > self.max_len {
if let Some((ws_idx, ws_len)) = last_ws {
return Some((ws_idx, ws_idx + ws_len));
}
let hard = self
.buf
.char_indices()
.nth(self.max_len)
.map_or(self.buf.len(), |(idx, _)| idx);
return Some((hard, hard));
}
None
}
}