use serde_json::json;
use super::common::{
current_section_heading, current_section_level, extract_heading_text, heading_level,
heading_path_strings, parse_markdown_blocks, split_at_paragraph_boundary, strip_block_content,
update_heading_stack, ChunkRecordInput, ContentType, MdBlockType,
};
const MAX_PAGE_AWARE_CHUNK_CHARS: usize = 2000;
struct PageAccum {
parts: Vec<String>,
section_heading: Option<String>,
heading_path: Vec<String>,
section_level: u8,
break_type: &'static str,
}
impl PageAccum {
fn new(
section_heading: Option<String>,
heading_path: Vec<String>,
section_level: u8,
break_type: &'static str,
) -> Self {
PageAccum {
parts: Vec::new(),
section_heading,
heading_path,
section_level,
break_type,
}
}
fn push(&mut self, content: String) {
self.parts.push(content);
}
fn len(&self) -> usize {
self.parts.len()
}
fn is_empty(&self) -> bool {
self.parts.is_empty()
}
fn into_content(self) -> String {
self.parts.join("\n\n")
}
}
pub fn build_page_aware_chunks(
bytes: &[u8],
paragraphs_per_page: usize,
) -> Result<Vec<ChunkRecordInput>, String> {
if paragraphs_per_page == 0 {
return Err("paragraphs_per_page must be greater than 0".to_string());
}
let text = std::str::from_utf8(bytes)
.map(|s| s.to_string())
.unwrap_or_else(|_| String::from_utf8_lossy(bytes).to_string());
if text.trim().is_empty() {
return Err("Markdown file is empty".to_string());
}
let blocks = parse_markdown_blocks(&text);
let total_input_blocks = blocks.len();
let mut heading_stack: Vec<(u8, String)> = Vec::new();
let mut result: Vec<ChunkRecordInput> = Vec::new();
let mut accum: Option<PageAccum> = None;
let mut chunk_index = 0usize;
let flush = |accum: &mut Option<PageAccum>,
result: &mut Vec<ChunkRecordInput>,
chunk_index: &mut usize,
total: usize| {
if let Some(a) = accum.take() {
if !a.is_empty() {
let paragraph_count = a.len();
let break_type = a.break_type;
let section_heading = a.section_heading.clone();
let heading_path = a.heading_path.clone();
let section_level = a.section_level;
let content = a.into_content();
for part in split_at_paragraph_boundary(&content, MAX_PAGE_AWARE_CHUNK_CHARS) {
result.push(ChunkRecordInput {
content_type: ContentType::PageAware,
content: part,
metadata: json!({
"page_break_type": break_type,
"paragraph_count": paragraph_count,
"section_heading": section_heading,
"section_level": section_level,
"heading_path": heading_path,
"chunk_index": *chunk_index,
"document_metadata": {
"source_type": "md",
"total_input_blocks": total,
}
}),
});
*chunk_index += 1;
}
}
}
};
for block in blocks {
match block.block_type {
MdBlockType::Heading => {
flush(
&mut accum,
&mut result,
&mut chunk_index,
total_input_blocks,
);
let level = heading_level(&block.content);
let text = extract_heading_text(&block.content);
update_heading_stack(&mut heading_stack, level, text.clone());
result.push(ChunkRecordInput {
content_type: ContentType::HeadingSection,
content: text.clone(),
metadata: json!({
"page_break_type": "heading_boundary",
"paragraph_count": 0,
"section_heading": text,
"section_level": level,
"heading_path": heading_path_strings(&heading_stack),
"chunk_index": chunk_index,
"document_metadata": {
"source_type": "md",
"total_input_blocks": total_input_blocks,
}
}),
});
chunk_index += 1;
accum = Some(PageAccum::new(
Some(text),
heading_path_strings(&heading_stack),
level,
"heading_boundary",
));
}
MdBlockType::Paragraph => {
let clean = strip_block_content(&block.content, false);
if clean.is_empty() {
continue;
}
let a = accum.get_or_insert_with(|| {
PageAccum::new(
current_section_heading(&heading_stack),
heading_path_strings(&heading_stack),
current_section_level(&heading_stack),
"estimated",
)
});
a.push(clean);
if a.len() >= paragraphs_per_page {
flush(
&mut accum,
&mut result,
&mut chunk_index,
total_input_blocks,
);
}
}
MdBlockType::List => {
let clean = strip_block_content(&block.content, true);
if clean.is_empty() {
continue;
}
let a = accum.get_or_insert_with(|| {
PageAccum::new(
current_section_heading(&heading_stack),
heading_path_strings(&heading_stack),
current_section_level(&heading_stack),
"estimated",
)
});
a.push(clean);
if a.len() >= paragraphs_per_page {
flush(
&mut accum,
&mut result,
&mut chunk_index,
total_input_blocks,
);
}
}
MdBlockType::Code => {
let a = accum.get_or_insert_with(|| {
PageAccum::new(
current_section_heading(&heading_stack),
heading_path_strings(&heading_stack),
current_section_level(&heading_stack),
"estimated",
)
});
a.push(block.content);
if a.len() >= paragraphs_per_page {
flush(
&mut accum,
&mut result,
&mut chunk_index,
total_input_blocks,
);
}
}
MdBlockType::Table => {
let a = accum.get_or_insert_with(|| {
PageAccum::new(
current_section_heading(&heading_stack),
heading_path_strings(&heading_stack),
current_section_level(&heading_stack),
"estimated",
)
});
a.push(block.content);
if a.len() >= paragraphs_per_page {
flush(
&mut accum,
&mut result,
&mut chunk_index,
total_input_blocks,
);
}
}
}
}
flush(
&mut accum,
&mut result,
&mut chunk_index,
total_input_blocks,
);
if result.is_empty() {
return Err("No page-aware chunks generated".to_string());
}
Ok(result)
}