//! Word forms keep their legacy form fields (`w:ffData`) and their rows' skipped grid columns
//! (`w:gridBefore`/`w:wBefore`, `w:gridAfter`/`w:wAfter`) through a round trip (#1174). All
//! markup here is synthetic, written from ECMA-376 §17.16.17 and §17.4.14-17.
use std::io::Write;
use wordcraft_doc::Block;
use wordcraft_docx::{read, write};
const NS: &str = r#"xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main""#;
const ROOT_RELS: &str = r#""#;
fn package(body: &str) -> Vec {
let doc = format!(r#"{body}"#);
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
for (name, bytes) in [("_rels/.rels", ROOT_RELS.as_bytes()), ("word/document.xml", doc.as_bytes())] {
zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
zw.write_all(bytes).unwrap();
}
zw.finish().unwrap().into_inner()
}
fn document_xml(docx: &[u8]) -> String {
let mut z = zip::ZipArchive::new(std::io::Cursor::new(docx)).unwrap();
let mut s = String::new();
std::io::Read::read_to_string(&mut z.by_name("word/document.xml").unwrap(), &mut s).unwrap();
s
}
fn resaved(body: &str) -> String {
document_xml(&write(&read(&package(body)).unwrap()).unwrap())
}
fn t(s: &str) -> String {
format!(r#"{s}"#)
}
fn checkbox(name: &str, checked: bool) -> String {
format!(
r#" FORMCHECKBOX "#,
u8::from(checked)
)
}
fn text_input(name: &str, text: &str) -> String {
format!(
r#" FORMTEXT {}"#,
t(text)
)
}
/// The `w:ffData` of each field begin, in document order.
fn ff_data(xml: &str) -> Vec<&str> {
xml.match_indices("")
.filter_map(|(at, _)| xml.get(at..).and_then(|s| s.find("").and_then(|end| s.get(..end + 11))))
.collect()
}
#[test]
fn form_fields_keep_their_ff_data() {
let body = format!(
"{}{}{}{}{}{}",
t("Yes "),
checkbox("Yes1", true),
t(" No "),
checkbox("No1", false),
t("Name: "),
text_input("Name1", "Example Ltd")
);
let xml = resaved(&body);
let ff = ff_data(&xml);
assert_eq!(ff.len(), 3, "{xml}");
assert!(
ff[0].contains(r#""#) && ff[0].contains(r#""#),
"{}",
ff[0]
);
assert!(ff[1].contains(r#""#) && ff[1].contains(r#""#), "{}", ff[1]);
assert!(ff[2].contains(r#""#) && ff[2].contains(r#""#), "{}", ff[2]);
// Each definition stays inside its own field's begin, ahead of its code.
let begin = xml.find(r#"w:fldCharType="begin">"#).expect("ffData inside fldChar");
assert!(xml.get(begin..).is_some_and(|s| s.find("FORMTEXT") < s.find("Example Ltd")), "{xml}");
}
#[test]
fn fields_without_ff_data_write_none() {
let body = r#" PAGE 1"#;
let xml = resaved(body);
assert!(xml.contains("PAGE") && !xml.contains("ffData"), "{xml}");
}
#[test]
fn rows_keep_their_skipped_grid_columns() {
let tc = |w: u32, span: u32, s: &str| {
let gs = if span > 1 { format!(r#""#) } else { String::new() };
format!(r#"{gs}{}"#, t(s))
};
let body = format!(
concat!(
r#""#,
"{}{}{}",
r#"{}"#,
r#"{}"#,
""
),
tc(300, 1, "a"),
tc(4200, 1, "b"),
tc(4500, 1, "c"),
tc(8700, 2, "Title"),
tc(300, 1, "x"),
);
let d = read(&package(&body)).unwrap();
let Some(Block::Table(tb)) = d.body.first().map(|b| b.as_ref()) else { panic!("table") };
let (r1, r2) = (&tb.rows[1].props, &tb.rows[2].props);
assert_eq!((r1.grid_before, r1.w_before, r1.grid_after, r1.w_after), (1, Some(15.0), 0, None));
assert_eq!((r2.grid_before, r2.w_before, r2.grid_after, r2.w_after), (0, None, 2, Some(435.0)));
assert_eq!(tb.rows[0].props, Default::default());
let xml = document_xml(&write(&d).unwrap());
assert_eq!(xml.matches(""#), "{xml}");
assert!(xml.contains(r#""#), "{xml}");
let again = read(&write(&d).unwrap()).unwrap();
let Some(Block::Table(tb2)) = again.body.first().map(|b| b.as_ref()) else { panic!("table") };
assert_eq!(tb2.rows.iter().map(|r| r.props.clone()).collect::>(), tb.rows.iter().map(|r| r.props.clone()).collect::>());
}