use super::text_extractor::{DocParagraph, ParagraphType};
fn render_table_row(content: &str) -> String {
let cells: Vec<String> = content.split('|').map(|c| c.trim().to_string()).collect();
if cells.is_empty() {
return String::new();
}
let row = format!("| {} |", cells.join(" | "));
let sep = format!("| {} |", vec!["---"; cells.len()].join(" | "));
format!("{row}\n{sep}")
}
pub(crate) fn render_paragraphs_markdown(paragraphs: Vec<DocParagraph>) -> String {
let mut rendered: Vec<String> = Vec::new();
for p in paragraphs {
match p.paragraph_type {
ParagraphType::PageBreak => rendered.push("\n\n---\n\n".to_string()),
ParagraphType::Heading(level) => {
let content = p.content.trim();
if content.is_empty() {
continue;
}
let line = match level {
1 => format!("# {content}"),
2 | 3 => format!("## {content}"),
_ => format!("### {content}"),
};
rendered.push(line);
}
ParagraphType::ListItem => {
let content = p.content.trim();
if content.is_empty() {
continue;
}
rendered.push(format!("- {content}"));
}
ParagraphType::Table => {
let content = p.content.trim();
if content.is_empty() {
continue;
}
let table = render_table_row(content);
if !table.trim().is_empty() {
rendered.push(table);
}
}
ParagraphType::Normal => {
let content = p.content.trim();
if content.is_empty() {
continue;
}
rendered.push(content.to_string());
}
}
}
let mut out = rendered.join("\n\n").trim().to_string();
while out.ends_with("\n\n---") {
out.truncate(out.len() - "\n\n---".len());
out = out.trim_end().to_string();
}
out.trim().to_string()
}
pub(super) fn to_markdown_with_images(file_path: &str) -> Result<(String, Vec<(String, Vec<u8>)>), String> {
super::structural::validate_doc_path(file_path)?;
let bytes = std::fs::read(file_path).map_err(|e| format!("Failed to read .doc file: {e}"))?;
to_markdown_with_images_bytes(&bytes)
}
pub(super) fn to_markdown_with_images_bytes(bytes: &[u8]) -> Result<(String, Vec<(String, Vec<u8>)>), String> {
let indexed = super::structural::load_doc_paragraphs_indexed_bytes(bytes)?;
let images = super::images::extract_doc_images_bytes(bytes).unwrap_or_default();
let mut anchored: Vec<(usize, &str)> = images
.iter()
.filter_map(|img| img.raw_para.map(|rp| (rp, img.hash_name.as_str())))
.collect();
anchored.sort_by_key(|(rp, _)| *rp);
let mut anchored_iter = anchored.into_iter().peekable();
let image_paragraph = |hash_name: &str| DocParagraph {
content: format!(""),
paragraph_type: ParagraphType::Normal,
heading_level: None,
};
let mut paragraphs: Vec<DocParagraph> = Vec::with_capacity(indexed.len() + images.len());
for (raw_idx, para) in indexed {
while let Some((rp, _)) = anchored_iter.peek() {
if *rp < raw_idx {
let (_, hash) = anchored_iter.next().unwrap();
paragraphs.push(image_paragraph(hash));
} else {
break;
}
}
paragraphs.push(para);
}
for (_, hash) in anchored_iter {
paragraphs.push(image_paragraph(hash));
}
for img in images.iter().filter(|i| i.raw_para.is_none()) {
paragraphs.push(image_paragraph(&img.hash_name));
}
let mut image_out: Vec<(String, Vec<u8>)> = Vec::new();
for img in &images {
if !image_out.iter().any(|(n, _)| n == &img.hash_name) {
image_out.push((img.hash_name.clone(), img.bytes.clone()));
}
}
Ok((render_paragraphs_markdown(paragraphs), image_out))
}