//! 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::>()); }