rust-docx 0.2.4

Rust parser, document model, and serializer for DOCX and legacy Word documents
Documentation
use std::cell::RefCell;
use std::collections::HashMap;

use rust_docx::model::TableNode;
use rust_docx::parse::{empty_style_sheet, parse_table, ParseContext};

fn cell(width: Option<i64>, span: i64, merge: Option<&str>) -> String {
    let width = width
        .map(|value| format!(r#"<w:tcW w:type="dxa" w:w="{value}"/>"#))
        .unwrap_or_default();
    let span = format!(r#"<w:gridSpan w:val="{span}"/>"#);
    let merge = merge
        .map(|value| format!(r#"<w:vMerge w:val="{value}"/>"#))
        .unwrap_or_default();
    format!("<w:tc><w:tcPr>{width}{span}{merge}</w:tcPr><w:p/></w:tc>")
}

fn row(cells: &[String]) -> String {
    format!("<w:tr>{}</w:tr>", cells.concat())
}

fn ordinary_row(widths: &[i64]) -> String {
    row(&widths
        .iter()
        .map(|&width| cell(Some(width), 1, None))
        .collect::<Vec<_>>())
}

fn parse(grid: &[i64], rows: &[String]) -> TableNode {
    let grid = grid
        .iter()
        .map(|width| format!(r#"<w:gridCol w:w="{width}"/>"#))
        .collect::<String>();
    let xml = format!(
        r#"<w:tbl><w:tblPr><w:tblLayout w:type="fixed"/></w:tblPr><w:tblGrid>{grid}</w:tblGrid>{}</w:tbl>"#,
        rows.concat()
    );
    let parts = HashMap::new();
    let binary_assets = HashMap::new();
    let context = ParseContext {
        relationships: HashMap::new(),
        content_types: Default::default(),
        parts: &parts,
        binary_assets: &binary_assets,
        style_sheet: empty_style_sheet(),
        warnings: RefCell::new(Vec::new()),
    };
    parse_table(&xml, &context)
}

fn widths(table: &TableNode) -> &[i64] {
    table
        .style
        .as_ref()
        .unwrap()
        .column_widths_twips
        .as_deref()
        .unwrap()
}

fn spans(table: &TableNode, row: usize) -> Vec<i64> {
    table.rows[row]
        .cells
        .iter()
        .map(|cell| {
            cell.style
                .as_ref()
                .and_then(|style| style.grid_span)
                .unwrap_or(1)
        })
        .collect()
}

#[test]
fn preserves_full_row_merge_over_repeated_measured_columns() {
    for columns in [3, 5, 8] {
        let measured = vec![1200; columns];
        let complete = ordinary_row(&measured);
        let table = parse(
            &vec![1400; columns],
            &[
                complete.clone(),
                complete,
                row(&[cell(Some(1200), columns as i64, None)]),
            ],
        );
        assert_eq!(widths(&table), measured);
        assert_eq!(spans(&table, 2), vec![columns as i64]);
        assert_eq!(spans(&table, 0), vec![1; columns]);
    }
}

#[test]
fn preserves_split_merged_header_with_singleton_restarts() {
    let complete = ordinary_row(&[1100; 7]);
    let table = parse(
        &[1100; 7],
        &[
            row(&[
                cell(Some(500), 2, Some("restart")),
                cell(Some(2500), 5, Some("restart")),
            ]),
            complete.clone(),
            complete,
        ],
    );
    assert_eq!(widths(&table), &[1100; 7]);
    assert_eq!(spans(&table, 0), vec![2, 5]);
    assert_eq!(spans(&table, 1), vec![1; 7]);
    assert_eq!(
        table.rows[0].cells[0].style.as_ref().unwrap().row_span,
        Some(1)
    );
}

#[test]
fn derives_nonuniform_columns_from_repeated_measurements() {
    let measured = [300, 700, 1100];
    let complete = ordinary_row(&measured);
    let table = parse(
        &[800, 1200, 1600],
        &[complete.clone(), complete, row(&[cell(Some(300), 3, None)])],
    );
    assert_eq!(widths(&table), &measured);
    assert_eq!(spans(&table, 2), vec![3]);
}

#[test]
fn does_not_infer_shared_columns_from_one_row_or_empty_rows() {
    let table = parse(
        &[1000; 3],
        &[
            ordinary_row(&[900; 3]),
            row(&[]),
            row(&[]),
            row(&[cell(Some(900), 3, None)]),
        ],
    );
    assert_eq!(widths(&table), &[900; 3]);
    assert_eq!(table.rows.len(), 2);
    assert_eq!(spans(&table, 1), vec![1]);
}

#[test]
fn does_not_infer_shared_columns_from_continuations_or_restarts() {
    for merge in ["restart", "continue"] {
        let table = parse(
            &[1000; 3],
            &[
                ordinary_row(&[900; 3]),
                row(&vec![cell(Some(900), 1, Some(merge)); 3]),
                row(&[cell(Some(900), 3, None)]),
            ],
        );
        assert_eq!(spans(&table, 2), vec![1], "{merge}");
    }
}

#[test]
fn ignores_continuation_widths_after_independent_rows_establish_columns() {
    let complete = ordinary_row(&[900; 3]);
    let table = parse(
        &[1000; 3],
        &[
            complete.clone(),
            complete,
            row(&[cell(Some(900), 3, Some("restart"))]),
            row(&[cell(Some(5000), 3, Some("continue"))]),
        ],
    );
    assert_eq!(widths(&table), &[900; 3]);
    assert_eq!(spans(&table, 2), vec![3]);
    assert_eq!(spans(&table, 3), vec![3]);
    assert_eq!(
        table.rows[2].cells[0].style.as_ref().unwrap().row_span,
        Some(2)
    );
    assert_eq!(
        table.rows[3].cells[0]
            .style
            .as_ref()
            .unwrap()
            .v_merge_continuation,
        Some(true)
    );
}

#[test]
fn retains_union_repair_when_complete_rows_disagree() {
    let complete = ordinary_row(&[900; 3]);
    let table = parse(
        &[1000; 3],
        &[
            complete.clone(),
            complete,
            ordinary_row(&[700, 1100, 900]),
            row(&[cell(Some(900), 3, None)]),
        ],
    );
    assert_eq!(widths(&table), &[700, 200, 900, 900]);
    assert_eq!(spans(&table, 3), vec![2]);
}

#[test]
fn retains_union_repair_when_a_single_column_measurement_conflicts() {
    let complete = ordinary_row(&[900; 4]);
    let table = parse(
        &[1000; 4],
        &[
            complete.clone(),
            complete,
            row(&[cell(Some(700), 1, None), cell(Some(900), 3, None)]),
        ],
    );
    assert_eq!(widths(&table), &[700, 200, 700, 200, 900, 900]);
    assert_eq!(spans(&table, 2), vec![1, 2]);
}

#[test]
fn retains_union_repair_when_a_row_does_not_cover_the_source_grid() {
    let complete = ordinary_row(&[900; 4]);
    let table = parse(
        &[1000; 4],
        &[complete.clone(), complete, row(&[cell(Some(900), 3, None)])],
    );
    assert_eq!(widths(&table), &[900; 4]);
    assert_eq!(spans(&table, 2), vec![1]);
}

#[test]
fn retains_union_repair_when_a_source_column_is_collapsed() {
    let complete = ordinary_row(&[400; 3]);
    let table = parse(
        &[1000, 0, 1000],
        &[complete.clone(), complete, row(&[cell(Some(400), 3, None)])],
    );
    assert_eq!(widths(&table), &[400; 3]);
    assert_eq!(spans(&table, 2), vec![1]);
}

#[test]
fn retains_union_repair_when_a_merged_preference_exceeds_anchored_width() {
    let complete = ordinary_row(&[900; 4]);
    let table = parse(
        &[1000; 4],
        &[
            complete.clone(),
            complete,
            row(&[cell(Some(900), 4, None)]),
            row(&[cell(Some(4000), 4, None)]),
        ],
    );
    assert_eq!(widths(&table), &[900, 900, 900, 900, 400]);
    assert_eq!(spans(&table, 2), vec![1]);
    assert_eq!(spans(&table, 3), vec![5]);
}

#[test]
fn retains_union_repair_when_noncontinuation_widths_are_missing() {
    let complete = ordinary_row(&[200; 3]);
    let table = parse(
        &[1000; 3],
        &[
            complete.clone(),
            complete,
            row(&vec![cell(None, 1, None); 3]),
            row(&[cell(Some(200), 3, None)]),
        ],
    );
    assert_eq!(widths(&table), &[200, 200, 200, 400, 1000, 1000]);
    assert_eq!(spans(&table, 3), vec![1]);
}

#[test]
fn leaves_row_skips_on_the_existing_grid_path() {
    for skip in ["gridBefore", "gridAfter"] {
        let complete = ordinary_row(&[900; 3]);
        let skipped = format!(
            r#"<w:tr><w:trPr><w:{skip} w:val="1"/></w:trPr>{}</w:tr>"#,
            vec![cell(Some(900), 1, None); 2].concat()
        );
        let table = parse(
            &[1000; 3],
            &[
                complete.clone(),
                complete,
                row(&[cell(Some(900), 3, None)]),
                skipped,
            ],
        );
        assert_eq!(widths(&table), &[1000; 3], "{skip}");
        assert_eq!(spans(&table, 2), vec![3], "{skip}");
    }
}

#[test]
fn retains_distinct_boundaries_when_rows_disagree() {
    let table = parse(
        &[1000; 2],
        &[ordinary_row(&[400, 1600]), ordinary_row(&[1200, 800])],
    );
    assert_eq!(widths(&table), &[400, 800, 800]);
    assert_eq!(spans(&table, 0), vec![1, 2]);
    assert_eq!(spans(&table, 1), vec![2, 1]);
}

#[test]
fn retains_union_repair_for_orphaned_continuations() {
    let complete = ordinary_row(&[1000; 3]);
    let table = parse(
        &[1200; 3],
        &[
            complete.clone(),
            complete,
            row(&[cell(Some(5000), 3, Some("continue"))]),
        ],
    );
    assert_eq!(widths(&table), &[1000; 3]);
    assert_eq!(spans(&table, 2), vec![1]);
}

#[test]
fn retains_union_repair_for_partially_overlapping_continuations() {
    let complete = ordinary_row(&[1000; 3]);
    let table = parse(
        &[1200; 3],
        &[
            complete.clone(),
            complete,
            row(&[
                cell(Some(500), 2, Some("restart")),
                cell(Some(1000), 1, None),
            ]),
            row(&[
                cell(Some(1000), 1, Some("continue")),
                cell(Some(2000), 2, None),
            ]),
        ],
    );
    assert_eq!(widths(&table), &[500; 6]);
    assert_eq!(spans(&table, 2), vec![1, 2]);
    assert_eq!(spans(&table, 3), vec![1, 4]);
}

#[test]
fn retains_union_repair_when_a_continuation_combines_anchors() {
    let complete = ordinary_row(&[1000; 3]);
    let table = parse(
        &[1200; 3],
        &[
            complete.clone(),
            complete,
            row(&[
                cell(Some(500), 1, Some("restart")),
                cell(Some(1500), 2, Some("restart")),
            ]),
            row(&[cell(Some(3000), 3, Some("continue"))]),
        ],
    );
    assert_eq!(widths(&table), &[500, 500, 1000, 1000]);
    assert_eq!(spans(&table, 3), vec![1]);
}