#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Chunk {
pub text: String,
pub pos: usize,
}
fn boundaries(content: &str) -> Vec<usize> {
content
.char_indices()
.map(|(i, _)| i)
.chain(std::iter::once(content.len()))
.collect()
}
fn char_index_of(bounds: &[usize], byte: usize) -> usize {
bounds.partition_point(|&b| b < byte)
}
pub fn chunk_document(content: &str, max_chars: usize, overlap_chars: usize) -> Vec<Chunk> {
if content.is_empty() || max_chars == 0 {
return Vec::new();
}
let bounds = boundaries(content);
let total_chars = bounds.len() - 1;
if total_chars <= max_chars {
return vec![Chunk {
text: content.to_string(),
pos: 0,
}];
}
let overlap = overlap_chars.min(max_chars.saturating_sub(1));
let mut chunks = Vec::new();
let mut start = 0usize;
while start < total_chars {
let hard_end = (start + max_chars).min(total_chars);
let end = if hard_end < total_chars {
find_break(content, &bounds, start, hard_end).unwrap_or(hard_end)
} else {
hard_end
};
let end = if end <= start { hard_end } else { end };
chunks.push(Chunk {
text: content[bounds[start]..bounds[end]].to_string(),
pos: bounds[start],
});
if end >= total_chars {
break;
}
let next = end.saturating_sub(overlap);
start = if next > start { next } else { end };
}
chunks
}
fn find_break(content: &str, bounds: &[usize], start: usize, end: usize) -> Option<usize> {
let window_chars = end - start;
if window_chars == 0 {
return None;
}
let search_start_char = start + (window_chars * 7 / 10);
let search = &content[bounds[search_start_char]..bounds[end]];
let base = bounds[search_start_char];
if let Some(pos) = search.rfind("\n\n") {
return Some(char_index_of(bounds, base + pos + 2));
}
for pattern in [". ", ".\n", "? ", "?\n", "! ", "!\n"] {
if let Some(pos) = search.rfind(pattern) {
return Some(char_index_of(bounds, base + pos + 2));
}
}
if let Some(pos) = search.rfind('\n') {
return Some(char_index_of(bounds, base + pos + 1));
}
if let Some(pos) = search.rfind(' ') {
return Some(char_index_of(bounds, base + pos + 1));
}
None
}