pub mod common;
use ciclostile::{Docx, Table};
use common::{DOCUMENT, Fixture};
fn items() -> Table {
Table::new([
["Item", "Qty"],
["Analytical engine", "1"],
["Punched cards", "500"],
])
.header(true)
}
fn tables(xml: &str) -> Vec<usize> {
xml.match_indices("<w:tbl")
.filter(|(at, tag)| matches!(xml.as_bytes().get(at + tag.len()), Some(b'>' | b' ')))
.map(|(at, _)| at)
.collect()
}
fn assert_tables_apart(xml: &str) {
for (end, _) in xml.match_indices("</w:tbl>") {
let rest = &xml[end..];
if let Some(&next) = tables(rest).first() {
assert!(rest[..next].contains("<w:p"), "adjacent tables in {xml}");
}
}
}
#[test]
fn placeholder_paragraph_becomes_a_table() {
let template = Fixture::template().zip();
let baseline = common::untouched(&template);
let mut docx = Docx::from_bytes(template).unwrap();
assert_eq!(docx.replace_with_table("{{items}}", &items()).unwrap(), 1);
let output = docx.to_bytes();
let added = common::assert_kept_except(&baseline, &output, &["word/document.xml"]);
assert_eq!(added, [] as [String; 0]);
let document = common::read_text(&output, "word/document.xml");
assert!(!document.contains("items}}"), "{document}");
let tables = tables(&document);
assert_eq!(tables.len(), 2, "the new table and the template's");
assert!(tables[0] > document.find("Signed: ").unwrap());
assert!(tables[1] > tables[0]);
assert!(document[tables[0]..tables[1]].contains("Punched cards"));
assert_tables_apart(&document);
let paragraphs = common::paragraphs(&document);
for cell in [
"Item",
"Qty",
"Analytical engine",
"1",
"Punched cards",
"500",
] {
assert!(paragraphs.iter().any(|text| text == cell), "{cell}");
}
let start = DOCUMENT.find(r#"<w:p w14:paraId="6F708192""#).unwrap();
assert_eq!(document[..start], DOCUMENT[..start]);
Docx::from_bytes(output).unwrap();
}
#[test]
fn table_can_replace_a_footer_paragraph() {
let template = Fixture::template().zip();
let baseline = common::untouched(&template);
let mut docx = Docx::from_bytes(template).unwrap();
assert_eq!(
docx.replace_with_table("{{signature}}", &items()).unwrap(),
1
);
let output = docx.to_bytes();
let added = common::assert_kept_except(&baseline, &output, &["word/footer1.xml"]);
assert_eq!(added, [] as [String; 0]);
let footer = common::read_text(&output, "word/footer1.xml");
assert_eq!(tables(&footer).len(), 1);
assert!(!footer.ends_with("</w:tbl></w:ftr>"), "{footer}");
assert!(!footer.contains("{{signature}}"));
}
#[test]
fn placeholder_sharing_its_paragraph_is_refused() {
let template = Fixture::template().zip();
let baseline = common::untouched(&template);
let mut docx = Docx::from_bytes(template).unwrap();
let err = docx
.replace_with_table("{{customer}}", &items())
.unwrap_err();
assert_eq!(err.as_str(), "{{customer}}");
assert_eq!(
err.to_string(),
"table placeholder `{{customer}}` must be the only content of its paragraph"
);
assert_eq!(docx.to_bytes(), baseline);
}
#[test]
fn refusal_leaves_qualifying_paragraphs_alone() {
let fixture = Fixture::template().with(
"word/document.xml",
DOCUMENT.replace("Signed: ", "Signed by {{signature}}: "),
);
let template = fixture.zip();
let baseline = common::untouched(&template);
let mut docx = Docx::from_bytes(template).unwrap();
let err = docx
.replace_with_table("{{signature}}", &items())
.unwrap_err();
assert_eq!(err.into_inner(), "{{signature}}");
assert_eq!(docx.to_bytes(), baseline);
assert_eq!(docx.replace_text([("{{signature}}", "Ada")]), 2);
}
#[test]
fn missing_placeholder_inserts_nothing() {
let template = Fixture::template().zip();
let baseline = common::untouched(&template);
let mut docx = Docx::from_bytes(template).unwrap();
assert_eq!(docx.replace_with_table("{{nowhere}}", &items()).unwrap(), 0);
assert_eq!(docx.to_bytes(), baseline);
}
#[test]
fn empty_placeholder_matches_nothing() {
let template = Fixture::template().zip();
let baseline = common::untouched(&template);
let mut docx = Docx::from_bytes(template).unwrap();
assert_eq!(docx.replace_with_table("", &items()).unwrap(), 0);
assert_eq!(docx.to_bytes(), baseline);
}