Skip to main content

easydoc_reader/extractor/
table.rs

1//! Table extraction from DOCX/DOC files via `office_oxide` IR.
2
3use std::path::Path;
4
5use easydoc_core::{CellData, DocError, DocxRow, Result, RowData};
6use office_oxide::ir::{DocumentIR, Element, InlineContent};
7
8/// Extracts all tables from a document and deserialises each into `Vec<T>`.
9///
10/// Uses `office_oxide`'s IR (intermediate representation) to find tables,
11/// then converts each row via the [`DocxRow`] trait.
12///
13/// # Errors
14///
15/// Returns I/O, format, or conversion errors.
16pub fn extract_tables<T: DocxRow>(path: &Path) -> Result<Vec<Vec<T>>> {
17    let doc = office_oxide::Document::open(path)
18        .map_err(|e| DocError::Document(format!("failed to open document: {e}")))?;
19    let ir = doc.to_ir();
20    extract_tables_from_ir(&ir)
21}
22
23/// Extracts tables from an already-parsed IR.
24fn extract_tables_from_ir<T: DocxRow>(ir: &DocumentIR) -> Result<Vec<Vec<T>>> {
25    let mut all_tables: Vec<Vec<T>> = Vec::new();
26
27    for section in &ir.sections {
28        for element in &section.elements {
29            if let Element::Table(table) = element {
30                let mut rows: Vec<T> = Vec::new();
31                let mut header_skipped = false;
32
33                for row in &table.rows {
34                    // Skip header row (first row marked as header)
35                    if row.is_header && !header_skipped {
36                        header_skipped = true;
37                        continue;
38                    }
39
40                    // Extract cell text values
41                    let cells: Vec<CellData> = row
42                        .cells
43                        .iter()
44                        .map(|cell| {
45                            let text = cell_text(cell);
46                            CellData::new(text)
47                        })
48                        .collect();
49
50                    let row_data = RowData::new(cells);
51                    match T::from_row(&row_data) {
52                        Ok(item) => rows.push(item),
53                        Err(e) => {
54                            // Skip row on conversion error
55                            eprintln!("warning: skipping row: {e}");
56                        }
57                    }
58                }
59
60                if !rows.is_empty() {
61                    all_tables.push(rows);
62                }
63            }
64        }
65    }
66
67    Ok(all_tables)
68}
69
70/// Extracts all text from a table cell by flattening paragraph content.
71fn cell_text(cell: &office_oxide::ir::TableCell) -> String {
72    let mut text = String::new();
73    for element in &cell.content {
74        if let Element::Paragraph(para) = element {
75            if !text.is_empty() {
76                text.push('\n');
77            }
78            for inline in &para.content {
79                if let InlineContent::Text(span) = inline {
80                    text.push_str(&span.text);
81                }
82            }
83        }
84    }
85    text
86}