use easydoc_core::DocumentBlock;
use easydoc_reader::DocxSaxReader;
fn doc(body: &str) -> Vec<u8> {
format!(
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>{body}</w:body>
</w:document>"#
)
.into_bytes()
}
fn p(text: &str) -> String {
format!("<w:p><w:r><w:t>{text}</w:t></w:r></w:p>")
}
fn heading(text: &str, level: u32) -> String {
format!(
r#"<w:p><w:pPr><w:pStyle w:val="Heading{level}"/></w:pPr><w:r><w:t>{text}</w:t></w:r></w:p>"#
)
}
fn table_2x2(a: &str, b: &str, c: &str, d: &str) -> String {
format!(
r"<w:tbl><w:tr><w:tc>{}</w:tc><w:tc>{}</w:tc></w:tr><w:tr><w:tc>{}</w:tc><w:tc>{}</w:tc></w:tr></w:tbl>",
p(a),
p(b),
p(c),
p(d)
)
}
fn list_item(text: &str, ilvl: u32) -> String {
format!(
r#"<w:p><w:pPr><w:numPr><w:numId w:val="1"/><w:ilvl w:val="{ilvl}"/></w:numPr></w:pPr><w:r><w:t>{text}</w:t></w:r></w:p>"#
)
}
fn blocks(xml: &[u8]) -> Vec<DocumentBlock> {
let mut reader = DocxSaxReader::from_reader(xml);
reader.read_blocks().expect("parse")
}
#[test]
fn mixed_heading_paragraph_table() {
let xml = doc(&format!(
"{}{}{}",
heading("Title", 1),
p("intro"),
table_2x2("a", "b", "c", "d")
));
let bs = blocks(&xml);
assert!(bs.len() >= 3, "blocks: {bs:?}");
assert!(matches!(bs[0], DocumentBlock::Heading { .. }));
assert!(matches!(bs[1], DocumentBlock::Paragraph(_)));
assert!(bs.iter().any(|b| matches!(b, DocumentBlock::Table(_))));
}
#[test]
fn mixed_list_table_list() {
let xml = doc(&format!(
"{}{}{}",
list_item("one", 0),
table_2x2("x", "y", "z", "w"),
list_item("two", 0)
));
let bs = blocks(&xml);
let lists = bs
.iter()
.filter(|b| matches!(b, DocumentBlock::List(_)))
.count();
assert!(lists >= 1, "lists: {lists}");
assert!(bs.iter().any(|b| matches!(b, DocumentBlock::Table(_))));
}
#[test]
fn paragraph_table_paragraph_preserves_order() {
let xml = doc(&format!(
"{} {} {}",
p("first"),
table_2x2("a", "b", "c", "d"),
p("last")
));
let bs = blocks(&xml);
assert!(
matches!(bs[0], DocumentBlock::Paragraph(_)),
"bs[0]: {:?}",
bs[0]
);
let table_pos = bs
.iter()
.position(|b| matches!(b, DocumentBlock::Table(_)))
.unwrap();
let last_para = bs
.iter()
.rposition(|b| matches!(b, DocumentBlock::Paragraph(_)))
.unwrap();
assert!(
table_pos < last_para,
"table should come before last paragraph"
);
}
#[test]
fn heading_levels_all_preserved() {
let xml = doc(&format!(
"{}{}{}",
heading("H1", 1),
heading("H2", 2),
heading("H3", 3)
));
let bs = blocks(&xml);
let levels: Vec<u8> = bs
.iter()
.filter_map(|b| match b {
DocumentBlock::Heading { level, .. } => Some(*level),
_ => None,
})
.collect();
assert_eq!(levels, vec![1, 2, 3], "levels: {levels:?}");
}
#[test]
fn nested_list_two_levels() {
let xml = doc(&format!(
"{}{}",
list_item("parent", 0),
list_item("child", 1)
));
let bs = blocks(&xml);
match &bs[0] {
DocumentBlock::List(l) => {
assert_eq!(l.items.len(), 1);
assert!(l.items[0].nested.is_some(), "should have nested child");
}
_ => panic!("expected List"),
}
}
#[test]
fn three_paragraphs_preserved() {
let xml = doc(&format!("{}{}{}", p("a"), p("b"), p("c")));
let bs = blocks(&xml);
assert_eq!(bs.len(), 3);
let texts: Vec<String> = bs
.iter()
.filter_map(|b| match b {
DocumentBlock::Paragraph(runs) => {
Some(runs.iter().map(|r| r.text.clone()).collect::<String>())
}
_ => None,
})
.collect();
assert_eq!(texts, vec!["a", "b", "c"]);
}
#[test]
fn table_followed_by_list() {
let xml = doc(&format!(
"{}{}",
table_2x2("1", "2", "3", "4"),
list_item("after", 0)
));
let bs = blocks(&xml);
assert!(bs.iter().any(|b| matches!(b, DocumentBlock::Table(_))));
assert!(bs.iter().any(|b| matches!(b, DocumentBlock::List(_))));
}
#[test]
fn empty_body_no_blocks() {
let bs = blocks(&doc(""));
assert!(bs.is_empty());
}
#[test]
fn only_whitespace_body() {
let bs = blocks(&doc(" "));
assert!(bs.is_empty());
}
#[test]
fn table_with_single_row_single_cell() {
let xml =
doc(r"<w:tbl><w:tr><w:tc><w:p><w:r><w:t>only</w:t></w:r></w:p></w:tc></w:tr></w:tbl>");
let bs = blocks(&xml);
match &bs[0] {
DocumentBlock::Table(t) => {
assert_eq!(t.rows.len(), 1);
assert_eq!(t.rows[0].cells.len(), 1);
}
_ => panic!("expected Table"),
}
}
#[test]
fn heading_then_list_then_heading() {
let xml = doc(&format!(
"{}{}{}",
heading("Intro", 2),
list_item("point", 0),
heading("End", 2)
));
let bs = blocks(&xml);
let headings = bs
.iter()
.filter(|b| matches!(b, DocumentBlock::Heading { .. }))
.count();
assert_eq!(headings, 2);
assert!(bs.iter().any(|b| matches!(b, DocumentBlock::List(_))));
}
#[test]
fn two_tables_separate() {
let xml = doc(&format!(
"{}{}",
table_2x2("a", "b", "c", "d"),
table_2x2("e", "f", "g", "h")
));
let bs = blocks(&xml);
let tables = bs
.iter()
.filter(|b| matches!(b, DocumentBlock::Table(_)))
.count();
assert_eq!(tables, 2, "two tables should be separate blocks");
}