//! A body-level content control nested directly in another body-level one stays body-level when //! saved, even when it holds a single paragraph (ECMA-376 ยง17.5.2.29-30; [MS-DOCX] `w15` //! repeating sections, which must wrap whole paragraphs or rows). All markup here is synthetic. use std::io::Write; use craft_docx::{read, write}; use craft_wordmodel::{ContentControl, Document, StoryRef}; const NS: &str = r#"xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml""#; 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 t(s: &str) -> String { format!(r#"{s}"#) } fn p(inner: &str) -> String { format!("{inner}") } fn sdt(pr: &str, content: &str) -> String { format!("{pr}{content}") } fn controls(d: &Document) -> Vec { let mut out = Vec::new(); craft_wordmodel::control::walk_controls(&d.body, &mut |c| out.push(c.clone())); out } /// How many `w:p` elements are open at byte `at` of `xml`. fn open_paragraphs(xml: &str, at: usize) -> usize { let head = &xml[..at]; let opened = head.matches("").count() + head.matches("").count() } /// Paragraph depth at the start of the `w:sdt` whose `w:sdtPr` holds `marker`. fn depth_of_control(xml: &str, marker: &str) -> usize { let at = xml.find(marker).unwrap_or_else(|| panic!("{marker} not written:\n{xml}")); let start = xml[..at].rfind("").expect("control start"); open_paragraphs(xml, start) } fn round_trip(body: &str) -> (Document, Document, String) { let d = read(&package(body)).unwrap(); let bytes = write(&d).unwrap(); let back = read(&bytes).unwrap(); (d, back, document_xml(&bytes)) } fn repeating_section() -> String { let field = sdt(r#""#, &t("note 1")); let item = sdt(r#""#, &p(&format!("{field}{}", t(" ")))); let section = sdt(r#""#, &item); format!("{}{section}{}", p(&t("Notes")), p(&t("End"))) } #[test] fn a_repeating_section_around_one_paragraph_stays_around_the_paragraph() { let (_, _, xml) = round_trip(&repeating_section()); assert_eq!(depth_of_control(&xml, r#""#), 0, "section written inside a paragraph:\n{xml}"); assert_eq!(depth_of_control(&xml, r#""#), 0, "item written inside a paragraph:\n{xml}"); // The text control stays inside the paragraph, and the item holds that paragraph. assert_eq!(depth_of_control(&xml, r#""#), 1); assert!(xml.contains(""#, &p(&t("Only paragraph"))); let outer = sdt(r#""#, &inner); let (d, back, xml) = round_trip(&format!("{outer}{}", p(&t("After")))); assert_eq!(depth_of_control(&xml, r#""#), 0, "{xml}"); assert_eq!(depth_of_control(&xml, r#""#), 0, "{xml}"); assert_eq!(controls(&back), controls(&d)); } #[test] fn nested_block_controls_in_a_table_cell_stay_block_level() { let inner = sdt(r#""#, &p(&t("In cell"))); let outer = sdt(r#""#, &inner); let table = format!(r#"{outer}"#); let (_, _, xml) = round_trip(&format!("{table}{}", p(&t("After")))); assert_eq!(depth_of_control(&xml, r#""#), 0, "{xml}"); assert_eq!(depth_of_control(&xml, r#""#), 0, "{xml}"); } #[test] fn a_run_level_control_stays_inside_its_paragraph() { let field = sdt(r#""#, &t("value")); let (_, _, xml) = round_trip(&p(&format!("{}{field}", t("Name: ")))); assert_eq!(depth_of_control(&xml, r#""#), 1, "{xml}"); } /// A bound repeating section as Word writes one at the end of a form, optionally followed by /// the body-level `_GoBack` bookmark Word leaves after the last control. fn bound_section_body(bound: bool, bookmark: bool) -> String { let section_binding = if bound { r#""# } else { "" }; let trailing = if bookmark { r#""# } else { "" }; let bind = |x: &str| format!(r#""#); let field = format!( r#"{}{}"#, if bound { bind("/a[1]/n[1]") } else { String::new() }, t("note 1") ); let item = format!( r#"{}"#, p(&format!("{field}{}", t(" "))) ); let section = format!( r#"{section_binding}{item}"# ); format!(r#"{}{section}{trailing}"#, p(&t("Notes"))) } fn assert_section_block_level(body: &str) { let (d, back, xml) = round_trip(body); assert_eq!(depth_of_control(&xml, r#""#), 0, "section written inside a paragraph:\n{xml}"); assert_eq!(depth_of_control(&xml, r#""#), 0, "item written inside a paragraph:\n{xml}"); assert_eq!(depth_of_control(&xml, r#""#), 1, "{xml}"); assert_eq!(controls(&back), controls(&d)); assert_eq!(back.plain_text(StoryRef::Body), d.plain_text(StoryRef::Body)); } #[test] fn a_bound_repeating_section_followed_by_a_body_bookmark_stays_block_level() { assert_section_block_level(&bound_section_body(true, true)); } #[test] fn a_bound_repeating_section_at_the_end_of_the_body_stays_block_level() { assert_section_block_level(&bound_section_body(true, false)); } #[test] fn a_repeating_section_followed_by_a_body_bookmark_stays_block_level() { assert_section_block_level(&bound_section_body(false, true)); } #[test] fn a_block_control_followed_by_a_body_bookmark_stays_around_its_paragraph() { let inner = sdt(r#""#, &p(&t("Last paragraph"))); let outer = sdt(r#""#, &inner); let body = format!(r#"{}{outer}"#, p(&t("First"))); let (d, back, xml) = round_trip(&body); assert_eq!(depth_of_control(&xml, r#""#), 0, "{xml}"); assert_eq!(depth_of_control(&xml, r#""#), 0, "{xml}"); assert_eq!(controls(&back), controls(&d)); // The bookmark is kept. assert!(xml.contains(r#"w:name="_GoBack""#), "{xml}"); }