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