use std::io::Write;
use dxpdf::render::layout::draw_command::{DrawCommand, LayoutedPage};
fn make_docx(document_xml: &str) -> Vec<u8> {
let mut buf = Vec::new();
{
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut buf));
let o = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated);
zip.start_file("[Content_Types].xml", o).unwrap();
zip.write_all(
br#"<?xml version="1.0" encoding="UTF-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
</Types>"#,
)
.unwrap();
zip.start_file("_rels/.rels", o).unwrap();
zip.write_all(
br#"<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
</Relationships>"#,
)
.unwrap();
zip.start_file("word/document.xml", o).unwrap();
zip.write_all(document_xml.as_bytes()).unwrap();
zip.finish().unwrap();
}
buf
}
const TBL_BORDERS: &str = r#"<w:tblBorders>
<w:top w:val="single" w:sz="4" w:space="0" w:color="auto"/>
<w:left w:val="single" w:sz="4" w:space="0" w:color="auto"/>
<w:bottom w:val="single" w:sz="4" w:space="0" w:color="auto"/>
<w:right w:val="single" w:sz="4" w:space="0" w:color="auto"/>
<w:insideH w:val="single" w:sz="4" w:space="0" w:color="auto"/>
<w:insideV w:val="single" w:sz="4" w:space="0" w:color="auto"/>
</w:tblBorders>"#;
fn tc(text: &str, edge: &str, val: Option<&str>) -> String {
let borders = match val {
None => String::new(),
Some("single") => format!(
r#"<w:tcBorders><w:{edge} w:val="single" w:sz="4" w:space="0" w:color="auto"/></w:tcBorders>"#
),
Some(v) => format!(r#"<w:tcBorders><w:{edge} w:val="{v}"/></w:tcBorders>"#),
};
format!(
r#"<w:tc><w:tcPr><w:tcW w:w="3000" w:type="dxa"/>{borders}</w:tcPr>
<w:p><w:r><w:t>{text}</w:t></w:r></w:p></w:tc>"#
)
}
fn document(grid_cols: usize, rows: &str) -> String {
let grid: String = (0..grid_cols)
.map(|_| r#"<w:gridCol w:w="3000"/>"#)
.collect();
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:tbl>
<w:tblPr><w:tblW w:w="0" w:type="auto"/>{TBL_BORDERS}<w:tblLayout w:type="fixed"/></w:tblPr>
<w:tblGrid>{grid}</w:tblGrid>
{rows}
</w:tbl>
<w:sectPr><w:pgSz w:w="11906" w:h="16838"/></w:sectPr>
</w:body>
</w:document>"#
)
}
fn layout(document_xml: &str) -> Vec<LayoutedPage> {
let doc = dxpdf::docx::parse(&make_docx(document_xml)).expect("parse");
dxpdf::render::resolve_and_layout(doc).1
}
fn border_positions(pages: &[LayoutedPage], horizontal: bool) -> Vec<i64> {
let mut v: Vec<i64> = pages
.iter()
.flat_map(|p| &p.commands)
.filter_map(|c| match c {
DrawCommand::Rect { rect, .. } => {
let (w, h) = (rect.size.width.raw(), rect.size.height.raw());
let thin = if horizontal {
h < 2.0 && w > 20.0
} else {
w < 2.0 && h > 5.0
};
if !thin {
return None;
}
let pos = if horizontal {
rect.origin.y.raw()
} else {
rect.origin.x.raw()
};
Some((pos * 100.0).round() as i64)
}
_ => None,
})
.collect();
v.sort_unstable();
v.dedup();
v
}
fn stacked(upper_bottom: Option<&str>, lower_top: Option<&str>) -> Vec<i64> {
let rows = format!(
"<w:tr>{}</w:tr><w:tr>{}</w:tr>",
tc("upper", "bottom", upper_bottom),
tc("lower", "top", lower_top),
);
border_positions(&layout(&document(1, &rows)), true)
}
fn abutting(left_right: Option<&str>, right_left: Option<&str>) -> Vec<i64> {
let rows = format!(
"<w:tr>{}{}</w:tr>",
tc("L", "right", left_right),
tc("R", "left", right_left),
);
border_positions(&layout(&document(2, &rows)), false)
}
#[test]
fn a_declared_bottom_survives_a_nil_top_below() {
assert_eq!(
stacked(Some("single"), Some("nil")).len(),
3,
"the declared bottom must still be drawn under the nil top"
);
}
#[test]
fn a_declared_top_survives_a_nil_bottom_above() {
assert_eq!(
stacked(Some("nil"), Some("single")).len(),
3,
"resolution is symmetric — which side wrote nil is not which side wins"
);
}
#[test]
fn an_inherited_border_survives_a_nil_on_the_facing_cell() {
assert_eq!(
stacked(None, Some("nil")).len(),
3,
"the upper cell still draws the insideH it inherited"
);
}
#[test]
fn a_declared_right_survives_a_nil_left_beside() {
assert_eq!(
abutting(Some("single"), Some("nil")).len(),
3,
"the declared right must still be drawn against the nil left"
);
}
#[test]
fn a_declared_left_survives_a_nil_right_beside() {
assert_eq!(abutting(Some("nil"), Some("single")).len(), 3);
}
#[test]
fn an_inherited_border_survives_a_nil_beside_it() {
assert_eq!(abutting(None, Some("nil")).len(), 3);
}
#[test]
fn nil_on_both_sides_suppresses() {
assert_eq!(stacked(Some("nil"), Some("nil")).len(), 2);
}
#[test]
fn nil_on_both_sides_suppresses_vertically() {
assert_eq!(abutting(Some("nil"), Some("nil")).len(), 2);
}
#[test]
fn nil_declines_the_table_border_at_an_outer_edge() {
let one = |val: Option<&str>| {
let rows = format!("<w:tr>{}</w:tr>", tc("only", "top", val));
border_positions(&layout(&document(1, &rows)), true).len()
};
assert_eq!(one(None), 2, "baseline: the table's own top and bottom");
assert_eq!(one(Some("none")), 2, "`none` inherits the table's top");
assert_eq!(
one(Some("nil")),
1,
"`nil` declines it — only the bottom left"
);
}
#[test]
fn none_yields_to_the_declared_border() {
assert_eq!(stacked(Some("single"), Some("none")).len(), 3);
}
#[test]
fn none_inherits_the_table_border() {
assert_eq!(stacked(None, Some("none")).len(), 3);
}