use super::*;
fn table_doc(rows: Vec<ir::TableRow>) -> Document {
let col_count = rows.iter().map(|r| r.cells.len()).max().unwrap_or(0);
Document {
metadata: Metadata::default(),
sections: vec![Section {
blocks: vec![Block::Table { rows, col_count, inner_margin: None }],
page_layout: None,
..Default::default()
}],
assets: Vec::new(),
}
}
fn text_cell(text: &str) -> ir::TableCell {
ir::TableCell {
blocks: vec![Block::Paragraph {
inlines: vec![Inline::plain(text)],
}],
colspan: 1,
rowspan: 1,
}
}
fn text_row(texts: &[&str], is_header: bool) -> ir::TableRow {
ir::TableRow {
cells: texts.iter().map(|t| text_cell(t)).collect(),
is_header,
}
}
#[test]
fn write_table_2x3_has_required_elements() {
let rows = vec![
text_row(&["A1", "A2"], true),
text_row(&["B1", "B2"], false),
text_row(&["C1", "C2"], false),
];
let doc = table_doc(rows);
let tables = RefTables::build(&doc, None);
let sec = &doc.sections[0];
let asset_map = ImageAssetMap::new();
let xml =
generate_section_xml(sec, 0, &tables, &asset_map).expect("generate_section_xml failed");
assert!(xml.contains("<hp:tbl "), "must contain <hp:tbl>: {xml}");
assert!(
xml.contains("<hp:tblPr>"),
"must contain <hp:tblPr>: {xml}"
);
assert!(
xml.contains("<hp:inMargin "),
"must contain <hp:inMargin>: {xml}"
);
assert!(
xml.contains(r#"borderFillIDRef="2""#),
"tbl must reference borderFill id=2: {xml}"
);
assert!(
xml.contains(r#"noAdjust="0""#),
"tbl must have noAdjust=\"0\": {xml}"
);
assert!(xml.contains("<hp:sz "), "must contain <hp:sz>: {xml}");
assert!(xml.contains("<hp:pos "), "must contain <hp:pos>: {xml}");
assert!(
xml.contains("<hp:trHeight "),
"must contain <hp:trHeight>: {xml}"
);
assert!(
xml.contains("<hp:cellAddr "),
"must contain <hp:cellAddr>: {xml}"
);
assert!(
xml.contains("<hp:cellSpan "),
"must contain <hp:cellSpan>: {xml}"
);
assert!(
xml.contains("<hp:cellSz "),
"must contain <hp:cellSz>: {xml}"
);
assert!(
xml.contains("<hp:cellMargin "),
"must contain <hp:cellMargin>: {xml}"
);
let cm_start = xml.find("<hp:cellMargin ").expect("<hp:cellMargin> missing");
let cm_end = xml[cm_start..].find("/>").expect("/>") + cm_start;
let cm_tag = &xml[cm_start..=cm_end + 1];
assert!(cm_tag.contains(r#"left="510""#), "cellMargin left must be 510: {cm_tag}");
assert!(cm_tag.contains(r#"right="510""#), "cellMargin right must be 510: {cm_tag}");
assert!(cm_tag.contains(r#"top="141""#), "cellMargin top must be 141: {cm_tag}");
assert!(cm_tag.contains(r#"bottom="141""#), "cellMargin bottom must be 141: {cm_tag}");
assert!(
xml.contains("<hp:subList>"),
"must contain <hp:subList>: {xml}"
);
assert!(
xml.contains("</hp:subList>"),
"must close </hp:subList>: {xml}"
);
}
#[test]
fn write_table_row_col_count_attributes() {
let rows = vec![
text_row(&["A1", "A2", "A3"], true),
text_row(&["B1", "B2", "B3"], false),
];
let doc = table_doc(rows);
let tables = RefTables::build(&doc, None);
let sec = &doc.sections[0];
let asset_map = ImageAssetMap::new();
let xml =
generate_section_xml(sec, 0, &tables, &asset_map).expect("generate_section_xml failed");
assert!(
xml.contains(r#"rowCnt="2""#),
"rowCnt must be 2: {xml}"
);
assert!(
xml.contains(r#"colCnt="3""#),
"colCnt must be 3: {xml}"
);
}
#[test]
fn write_table_cell_addr_indices() {
let rows = vec![
text_row(&["R0C0", "R0C1"], true),
text_row(&["R1C0", "R1C1"], false),
];
let doc = table_doc(rows);
let tables = RefTables::build(&doc, None);
let sec = &doc.sections[0];
let asset_map = ImageAssetMap::new();
let xml =
generate_section_xml(sec, 0, &tables, &asset_map).expect("generate_section_xml failed");
assert!(
xml.contains(r#"colAddr="0" rowAddr="0""#),
"first cell must have colAddr=0 rowAddr=0: {xml}"
);
assert!(
xml.contains(r#"colAddr="1" rowAddr="0""#),
"second cell of first row must have colAddr=1 rowAddr=0: {xml}"
);
assert!(
xml.contains(r#"colAddr="0" rowAddr="1""#),
"first cell of second row must have colAddr=0 rowAddr=1: {xml}"
);
}
#[test]
fn header_xml_contains_border_fill_id_2() {
let rows = vec![text_row(&["cell"], false)];
let doc = table_doc(rows);
let tables = RefTables::build(&doc, None);
let header =
super::header::generate_header_xml(&doc, &tables).expect("generate_header_xml failed");
assert!(
header.contains(r#"id="2""#),
"header must contain borderFill id=\"2\": {header}"
);
assert!(
header.contains(r#"itemCnt="2""#),
"borderFills must declare itemCnt=\"2\": {header}"
);
}
#[test]
fn write_table_roundtrip_cell_text() {
let rows = vec![
text_row(&["Alpha", "Beta"], true),
text_row(&["Gamma", "Delta"], false),
];
let doc = table_doc(rows);
let tmp = tempfile::NamedTempFile::new().expect("tmp file");
write_hwpx(&doc, tmp.path(), None).expect("write_hwpx");
let read_back = read_hwpx(tmp.path()).expect("read_hwpx");
let table_block = read_back
.sections
.into_iter()
.flat_map(|s| s.blocks)
.find_map(|b| match b {
Block::Table { rows, .. } => Some(rows),
_ => None,
})
.expect("no Table block found after roundtrip");
assert_eq!(table_block.len(), 2, "expected 2 rows: {table_block:?}");
assert_eq!(
table_block[0].cells.len(),
2,
"first row must have 2 cells: {table_block:?}"
);
let cell_text = |cell: &ir::TableCell| -> String {
cell.blocks
.iter()
.flat_map(|b| match b {
Block::Paragraph { inlines } => inlines.iter().map(|i| i.text.as_str()).collect::<Vec<_>>(),
_ => vec![],
})
.collect::<Vec<_>>()
.join("")
};
assert_eq!(
cell_text(&table_block[0].cells[0]),
"Alpha",
"cell [0][0] text mismatch"
);
assert_eq!(
cell_text(&table_block[0].cells[1]),
"Beta",
"cell [0][1] text mismatch"
);
assert_eq!(
cell_text(&table_block[1].cells[0]),
"Gamma",
"cell [1][0] text mismatch"
);
assert_eq!(
cell_text(&table_block[1].cells[1]),
"Delta",
"cell [1][1] text mismatch"
);
}
#[test]
fn write_table_colspan_cellsz_scaled() {
let merged_cell = ir::TableCell {
blocks: vec![Block::Paragraph {
inlines: vec![Inline::plain("merged")],
}],
colspan: 2,
rowspan: 1,
};
let normal_cell = ir::TableCell {
blocks: vec![Block::Paragraph {
inlines: vec![Inline::plain("normal")],
}],
colspan: 1,
rowspan: 1,
};
let rows = vec![ir::TableRow {
cells: vec![merged_cell, normal_cell],
is_header: false,
}];
let doc = Document {
metadata: Metadata::default(),
sections: vec![Section {
blocks: vec![Block::Table { rows, col_count: 3, inner_margin: None }],
page_layout: None,
..Default::default()
}],
assets: Vec::new(),
};
let tables = RefTables::build(&doc, None);
let sec = &doc.sections[0];
let asset_map = ImageAssetMap::new();
let xml =
generate_section_xml(sec, 0, &tables, &asset_map).expect("generate_section_xml failed");
let count_16000 = xml.matches(r#"width="16000""#).count();
assert_eq!(
count_16000, 1,
"expected exactly 1 occurrence of width=\"16000\" (merged cell): {xml}"
);
let count_8000 = xml.matches(r#"width="8000""#).count();
assert_eq!(
count_8000, 1,
"expected exactly 1 occurrence of width=\"8000\" (normal cell): {xml}"
);
}
#[test]
fn write_table_colspan_emitted_in_cell_span() {
let rows = vec![ir::TableRow {
cells: vec![ir::TableCell {
blocks: vec![Block::Paragraph {
inlines: vec![Inline::plain("merged")],
}],
colspan: 2,
rowspan: 1,
}],
is_header: false,
}];
let doc = table_doc(rows);
let tables = RefTables::build(&doc, None);
let sec = &doc.sections[0];
let asset_map = ImageAssetMap::new();
let xml =
generate_section_xml(sec, 0, &tables, &asset_map).expect("generate_section_xml failed");
assert!(
xml.contains(r#"<hp:cellSpan colSpan="2" rowSpan="1""#),
"cellSpan must reflect colspan=2: {xml}"
);
}
#[test]
fn write_table_has_tblpr() {
let rows = vec![text_row(&["A", "B"], false)];
let doc = table_doc(rows);
let tables = RefTables::build(&doc, None);
let sec = &doc.sections[0];
let asset_map = ImageAssetMap::new();
let xml =
generate_section_xml(sec, 0, &tables, &asset_map).expect("generate_section_xml failed");
assert!(
xml.contains("<hp:tblPr>"),
"must contain <hp:tblPr>: {xml}"
);
assert!(
xml.contains("</hp:tblPr>"),
"must close </hp:tblPr>: {xml}"
);
assert!(
xml.contains("<hp:inMargin "),
"must contain <hp:inMargin>: {xml}"
);
let im_start = xml.find("<hp:inMargin ").expect("<hp:inMargin> missing");
let im_end = xml[im_start..].find("/>").expect("/>") + im_start;
let im_tag = &xml[im_start..=im_end + 1];
assert!(im_tag.contains(r#"left="141""#), "inMargin left must be 141: {im_tag}");
assert!(im_tag.contains(r#"right="141""#), "inMargin right must be 141: {im_tag}");
assert!(im_tag.contains(r#"top="141""#), "inMargin top must be 141: {im_tag}");
assert!(im_tag.contains(r#"bottom="141""#), "inMargin bottom must be 141: {im_tag}");
let tblpr_pos = xml.find("<hp:tblPr>").expect("<hp:tblPr> missing");
let sz_pos = xml.find("<hp:sz ").expect("<hp:sz> missing");
assert!(
tblpr_pos < sz_pos,
"<hp:tblPr> must appear before <hp:sz>: tblPr@{tblpr_pos}, sz@{sz_pos}"
);
}
#[test]
fn write_table_custom_inner_margin_emitted() {
use ir::TableInnerMargin;
let rows = vec![text_row(&["A", "B"], false)];
let col_count = 2;
let doc = Document {
metadata: Metadata::default(),
sections: vec![Section {
blocks: vec![Block::Table {
rows,
col_count,
inner_margin: Some(TableInnerMargin { left: 200, right: 200, top: 100, bottom: 100 }),
}],
page_layout: None,
..Default::default()
}],
assets: Vec::new(),
};
let tables = RefTables::build(&doc, None);
let sec = &doc.sections[0];
let asset_map = ImageAssetMap::new();
let xml =
generate_section_xml(sec, 0, &tables, &asset_map).expect("generate_section_xml failed");
let im_start = xml.find("<hp:inMargin ").expect("<hp:inMargin> missing");
let im_end = xml[im_start..].find("/>").expect("/>") + im_start;
let im_tag = &xml[im_start..=im_end + 1];
assert!(im_tag.contains(r#"left="200""#), "inMargin left must be 200: {im_tag}");
assert!(im_tag.contains(r#"right="200""#), "inMargin right must be 200: {im_tag}");
assert!(im_tag.contains(r#"top="100""#), "inMargin top must be 100: {im_tag}");
assert!(im_tag.contains(r#"bottom="100""#), "inMargin bottom must be 100: {im_tag}");
}
#[test]
fn write_table_default_inner_margin_when_none() {
let rows = vec![text_row(&["X"], false)];
let doc = table_doc(rows);
let tables = RefTables::build(&doc, None);
let sec = &doc.sections[0];
let asset_map = ImageAssetMap::new();
let xml =
generate_section_xml(sec, 0, &tables, &asset_map).expect("generate_section_xml failed");
let im_start = xml.find("<hp:inMargin ").expect("<hp:inMargin> missing");
let im_end = xml[im_start..].find("/>").expect("/>") + im_start;
let im_tag = &xml[im_start..=im_end + 1];
assert!(im_tag.contains(r#"left="141""#), "default left must be 141: {im_tag}");
assert!(im_tag.contains(r#"right="141""#), "default right must be 141: {im_tag}");
assert!(im_tag.contains(r#"top="141""#), "default top must be 141: {im_tag}");
assert!(im_tag.contains(r#"bottom="141""#), "default bottom must be 141: {im_tag}");
}
#[test]
fn write_table_inner_margin_roundtrip() {
use ir::TableInnerMargin;
let rows = vec![text_row(&["Hello"], false)];
let doc = Document {
metadata: Metadata::default(),
sections: vec![Section {
blocks: vec![Block::Table {
rows,
col_count: 1,
inner_margin: Some(TableInnerMargin { left: 300, right: 300, top: 50, bottom: 50 }),
}],
page_layout: None,
..Default::default()
}],
assets: Vec::new(),
};
let tmp = tempfile::NamedTempFile::new().expect("tmp file");
write_hwpx(&doc, tmp.path(), None).expect("write_hwpx");
let read_back = read_hwpx(tmp.path()).expect("read_hwpx");
let table_margin = read_back
.sections
.into_iter()
.flat_map(|s| s.blocks)
.find_map(|b| match b {
Block::Table { inner_margin, .. } => Some(inner_margin),
_ => None,
})
.expect("no Table block found after roundtrip");
let m = table_margin.expect("inner_margin must be Some after roundtrip");
assert_eq!(m.left, 300, "roundtrip left");
assert_eq!(m.right, 300, "roundtrip right");
assert_eq!(m.top, 50, "roundtrip top");
assert_eq!(m.bottom, 50, "roundtrip bottom");
}
#[test]
fn write_table_asymmetric_inner_margin_roundtrip() {
use ir::TableInnerMargin;
let rows = vec![text_row(&["X"], false)];
let doc = Document {
metadata: Metadata::default(),
sections: vec![Section {
blocks: vec![Block::Table {
rows,
col_count: 1,
inner_margin: Some(TableInnerMargin { left: 11, right: 22, top: 33, bottom: 44 }),
}],
page_layout: None,
..Default::default()
}],
assets: Vec::new(),
};
let tmp = tempfile::NamedTempFile::new().expect("tmp file");
write_hwpx(&doc, tmp.path(), None).expect("write_hwpx");
let read_back = read_hwpx(tmp.path()).expect("read_hwpx");
let m = read_back
.sections
.into_iter()
.flat_map(|s| s.blocks)
.find_map(|b| match b {
Block::Table { inner_margin, .. } => inner_margin,
_ => None,
})
.expect("inner_margin must be Some after roundtrip");
assert_eq!(m.left, 11, "roundtrip left");
assert_eq!(m.right, 22, "roundtrip right");
assert_eq!(m.top, 33, "roundtrip top");
assert_eq!(m.bottom, 44, "roundtrip bottom");
}