use serde_json::json;
use super::common::{
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_SECTION_CHARS: usize = 2000;
struct SectionBody {
parts: Vec<(String, &'static str)>, section_heading: Option<String>,
section_level: u8,
heading_path: Vec<String>,
}
impl SectionBody {
fn joined(&self) -> String {
self.parts
.iter()
.map(|(c, _)| c.as_str())
.collect::<Vec<_>>()
.join("\n\n")
}
fn char_count(&self) -> usize {
self.parts.iter().map(|(c, _)| c.len()).sum::<usize>()
+ self.parts.len().saturating_sub(1) * 2
}
fn block_types(&self) -> Vec<&'static str> {
let mut seen: Vec<&'static str> = Vec::new();
for (_, t) in &self.parts {
if !seen.contains(t) {
seen.push(t);
}
}
seen
}
fn paragraph_count(&self) -> usize {
self.parts
.iter()
.filter(|(_, t)| *t == "paragraph" || *t == "list")
.count()
}
}
fn flush_section(
result: &mut Vec<ChunkRecordInput>,
body: SectionBody,
chunk_index: &mut usize,
total_input_blocks: usize,
) {
let joined = body.joined();
if joined.trim().is_empty() {
return;
}
let block_types = body.block_types();
let paragraph_count = body.paragraph_count();
let parts = split_at_paragraph_boundary(&joined, MAX_SECTION_CHARS);
let part_count = parts.len();
for (i, content) in parts.into_iter().enumerate() {
if content.is_empty() {
continue;
}
result.push(ChunkRecordInput {
content_type: ContentType::Section,
content: content.clone(),
metadata: json!({
"section_heading": body.section_heading,
"section_level": body.section_level,
"heading_path": body.heading_path,
"paragraph_count": paragraph_count,
"block_types": block_types,
"char_count": content.len(),
"split_part": if part_count > 1 { serde_json::json!(i + 1) } else { serde_json::Value::Null },
"split_total": if part_count > 1 { serde_json::json!(part_count) } else { serde_json::Value::Null },
"chunk_index": *chunk_index,
"document_metadata": {
"source_type": "md",
"total_input_blocks": total_input_blocks,
}
}),
});
*chunk_index += 1;
}
}
pub fn build_section_chunks(bytes: &[u8]) -> Result<Vec<ChunkRecordInput>, 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 result: Vec<ChunkRecordInput> = Vec::new();
let mut heading_stack: Vec<(u8, String)> = Vec::new();
let mut current: Option<SectionBody> = None;
let mut chunk_index = 0usize;
for block in blocks {
match block.block_type {
MdBlockType::Heading => {
if let Some(body) = current.take() {
flush_section(&mut result, body, &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!({
"section_heading": text,
"section_level": level,
"heading_path": heading_path_strings(&heading_stack),
"paragraph_count": 0,
"block_types": ["heading"],
"char_count": text.len(),
"chunk_index": chunk_index,
"document_metadata": {
"source_type": "md",
"total_input_blocks": total_input_blocks,
}
}),
});
chunk_index += 1;
current = Some(SectionBody {
parts: Vec::new(),
section_heading: Some(text),
section_level: level,
heading_path: heading_path_strings(&heading_stack),
});
}
MdBlockType::Paragraph => {
let clean = strip_block_content(&block.content, false);
if clean.is_empty() {
continue;
}
if let Some(ref mut body) = current {
if body.char_count() + clean.len() + 2 > MAX_SECTION_CHARS
&& !body.parts.is_empty()
{
let finished = current.take().unwrap();
let next_heading = finished.section_heading.clone();
let next_level = finished.section_level;
let next_path = finished.heading_path.clone();
flush_section(&mut result, finished, &mut chunk_index, total_input_blocks);
current = Some(SectionBody {
parts: Vec::new(),
section_heading: next_heading,
section_level: next_level,
heading_path: next_path,
});
}
current.as_mut().unwrap().parts.push((clean, "paragraph"));
} else {
current = Some(SectionBody {
parts: vec![(clean, "paragraph")],
section_heading: None,
section_level: current_section_level(&heading_stack),
heading_path: heading_path_strings(&heading_stack),
});
}
}
MdBlockType::List => {
let clean = strip_block_content(&block.content, true);
if clean.is_empty() {
continue;
}
if let Some(ref mut body) = current {
body.parts.push((clean, "list"));
} else {
current = Some(SectionBody {
parts: vec![(clean, "list")],
section_heading: None,
section_level: current_section_level(&heading_stack),
heading_path: heading_path_strings(&heading_stack),
});
}
}
MdBlockType::Code => {
if let Some(ref mut body) = current {
body.parts.push((block.content, "code_block"));
} else {
current = Some(SectionBody {
parts: vec![(block.content, "code_block")],
section_heading: None,
section_level: current_section_level(&heading_stack),
heading_path: heading_path_strings(&heading_stack),
});
}
}
MdBlockType::Table => {
if let Some(ref mut body) = current {
body.parts.push((block.content, "table"));
} else {
current = Some(SectionBody {
parts: vec![(block.content, "table")],
section_heading: None,
section_level: current_section_level(&heading_stack),
heading_path: heading_path_strings(&heading_stack),
});
}
}
}
}
if let Some(body) = current.take() {
flush_section(&mut result, body, &mut chunk_index, total_input_blocks);
}
if result.is_empty() {
return Err("No section chunks generated".to_string());
}
Ok(result)
}