use anyhow::{bail, Result};
use std::fs::File;
use std::path::Path;
use zip::ZipArchive;
use super::models::DocumentElement;
pub(crate) fn validate_docx_file(file_path: &Path) -> Result<()> {
let extension = file_path
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("");
if extension != "docx" {
bail!(
"Invalid file format. Expected .docx file, got .{}\n\
Note: doxx only supports Word .docx files (not .doc, .xlsx, .zip, etc.)",
extension
);
}
let file = File::open(file_path)?;
let mut archive = ZipArchive::new(file)?;
if archive.by_name("word/document.xml").is_err() {
if archive.by_name("xl/workbook.xml").is_ok() {
bail!(
"This appears to be an Excel file (.xlsx).\n\
doxx only supports Word documents (.docx)."
);
}
bail!(
"Invalid .docx file: missing word/document.xml\n\
This file may be corrupted or is not a valid Word document."
);
}
Ok(())
}
pub(crate) fn merge_display_equations(
elements: Vec<DocumentElement>,
display_equations_by_para: std::collections::HashMap<usize, Vec<DocumentElement>>,
) -> Vec<DocumentElement> {
if display_equations_by_para.is_empty() {
return elements;
}
let mut eq_para_indices: Vec<usize> = display_equations_by_para.keys().copied().collect();
eq_para_indices.sort_unstable();
let mut result = Vec::new();
let mut element_para_index = 0;
for element in elements {
match &element {
DocumentElement::Paragraph { .. }
| DocumentElement::Heading { .. }
| DocumentElement::List { .. } => {
element_para_index += 1;
while let Some(&eq_idx) = eq_para_indices.first() {
if eq_idx < element_para_index {
if let Some(eqs) = display_equations_by_para.get(&eq_idx) {
result.extend(eqs.clone());
}
eq_para_indices.remove(0);
} else {
break;
}
}
}
_ => {}
}
result.push(element);
}
for eq_idx in eq_para_indices {
if let Some(eqs) = display_equations_by_para.get(&eq_idx) {
result.extend(eqs.clone());
}
}
result
}