//! Content controls (`w:sdt`) keep their properties, levels and nesting through a round trip. //! All markup here is synthetic, written from ECMA-376 §17.5.2 and the [MS-DOCX] `w14`/`w15` //! extensions. use std::io::Write; use wordcraft_doc::control::{ControlKind, ControlLock, ListItem}; use wordcraft_doc::{Block, ContentControl, Document, StoryRef}; use wordcraft_docx::{read, write}; const NS: &str = r#"xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" 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 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 cell(text: &str) -> String { format!("{}", p(&t(text))) } /// Every kind of control, at every level, with nesting. fn form() -> Vec { let block = sdt( r#""#, &format!("{}{}", p(&t("Write the introduction.")), p(&t("Second paragraph"))), ); let inline = p(&format!( "{}{}{}{}{}{}", t("Name: "), sdt(r#""#, &t("Ada")), t(" Agree: "), sdt( r#""#, &t("\u{2612}") ), sdt( r#""#, &t("Beta") ), sdt( r#""#, &t("11.10.2026") ), )); // A combo box holding a nested rich text control. let nested = p(&sdt( r#""#, &format!("{}{}", t("Pick "), sdt(r#""#, &t("inner"))), )); let gallery = sdt(r#""#, &p(&t("Cover"))); // A repeating section around table rows, each row an item; a control around one cell; a // picture control holding nothing but a paragraph mark's worth of text. let row = |a: &str, b: &str| format!("{}{}", cell(a), cell(b)); let table = format!( "{}", sdt( r#""#, &format!( "{}{}", sdt(r#""#, &row("x", "y")), sdt( r#""#, &format!("{}{}", cell("z"), sdt(r#""#, &cell("pic"))) ) ) ) ); // A block control whose content is a table. let around_table = sdt( r#""#, &format!("{}", format_args!("{}", cell("t"))), ); package(&format!("{block}{inline}{nested}{gallery}{table}{around_table}{}", p(&t("End")))) } 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 controls(d: &Document) -> Vec { let mut out = Vec::new(); wordcraft_doc::control::walk_controls(&d.body, &mut |c| out.push(c.clone())); out } fn by_id(d: &Document, id: i64) -> ContentControl { controls(d).into_iter().find(|c| c.id == Some(id)).unwrap_or_else(|| panic!("control {id}")) } #[test] fn every_kind_reads_with_its_properties() { let d = read(&form()).unwrap(); let intro = controls(&d).into_iter().find(|c| c.title == "Intro").unwrap(); assert!(intro.block && intro.showing_placeholder); assert_eq!((intro.tag.as_str(), intro.id, intro.lock), ("intro", Some(-1201), ControlLock::SdtLocked)); assert_eq!(intro.placeholder, "DefaultPlaceholder_1"); assert_eq!(intro.placeholder_text, "Write the introduction."); assert!(intro.rpr_xml.contains("w:b")); assert_eq!(intro.extra, vec![r#""#.to_string()]); let ranges = d.control_ranges(StoryRef::Body); let r = ranges.iter().find(|r| r.control.title == "Intro").unwrap(); assert_eq!(d.control_text(r), "Write the introduction.\nSecond paragraph"); assert_eq!(by_id(&d, 7).kind, ControlKind::Text { multi_line: true }); assert!(!by_id(&d, 7).block); assert!( matches!(by_id(&d, 8).kind, ControlKind::CheckBox { checked: true, checked_char: '\u{2612}', ref checked_font, .. } if checked_font == "Symbola") ); let dd = by_id(&d, 9); assert_eq!(dd.lock, ControlLock::ContentLocked); assert_eq!( dd.kind, ControlKind::DropDown { items: vec![ListItem { display: "Alpha".into(), value: "a".into() }, ListItem { display: "Beta".into(), value: "b".into() }], last_value: "b".into() } ); assert!( matches!(by_id(&d, 10).kind, ControlKind::Date { ref full_date, ref format, .. } if full_date == "2026-10-11T00:00:00Z" && format == "d.M.yyyy") ); assert_eq!(by_id(&d, 10).extra.len(), 1, "data binding kept"); assert_eq!(by_id(&d, 12).kind, ControlKind::RichText); assert!(matches!(by_id(&d, 13).kind, ControlKind::Gallery { list: false, ref gallery, unique: true, .. } if gallery == "Cover Pages")); assert!(matches!(by_id(&d, 20).kind, ControlKind::RepeatingSection { ref title, .. } if title == "Items")); assert_eq!(by_id(&d, 21).kind, ControlKind::RepeatingSectionItem); assert_eq!(by_id(&d, 23).kind, ControlKind::Picture); // Row-level controls sit on the rows, the cell-level one on its cell, the table's on the table. let tables: Vec<_> = d.body.iter().filter_map(|b| b.as_table()).collect(); assert_eq!(tables.len(), 2); let rows = &tables[0].rows; assert_eq!(rows[0].controls.open.iter().map(|c| c.id).collect::>(), vec![Some(20), Some(21)]); assert_eq!((rows[0].controls.close, rows[1].controls.close), (1, 2)); assert_eq!(rows[1].cells[1].controls.open[0].id, Some(23)); assert_eq!(tables[1].controls.open[0].tag, "grid"); assert_eq!(tables[1].controls.close, 1); // The text reads as before. assert!(d.plain_text(StoryRef::Body).contains("Name: Ada Agree: \u{2612}Beta11.10.2026")); } #[test] fn controls_round_trip() { let d = read(&form()).unwrap(); let bytes = write(&d).unwrap(); let back = read(&bytes).unwrap(); assert_eq!(controls(&back), controls(&d)); assert_eq!(back.plain_text(StoryRef::Body), d.plain_text(StoryRef::Body)); assert_eq!(back.body.len(), d.body.len()); let names: Vec<_> = back.control_ranges(StoryRef::Body).iter().map(|r| (r.control.id, r.start.path.clone(), r.end.path.clone())).collect(); let before: Vec<_> = d.control_ranges(StoryRef::Body).iter().map(|r| (r.control.id, r.start.path.clone(), r.end.path.clone())).collect(); assert_eq!(names, before); // Written in schema order, with the extension elements the file had. let xml = document_xml(&bytes); assert!(xml.contains( r#""# )); assert!(xml.contains(r#""#)); // The block control is around the paragraphs, the inline ones inside theirs. assert!(xml.contains("")); assert!(xml.contains(r#""#), &s); } let mut b = p(&t("x")); for i in 0..400 { b = sdt(&format!(r#""#), &b); } for body in [p(&s), b] { // Too deep for the parser: an error or a document, never a panic or a hang. if let Ok(d) = read(&package(&body)) { let _ = write(&d).unwrap(); } } let mut s = t("core"); for i in 0..60 { s = sdt(&format!(r#""#), &s); } let d = read(&package(&p(&s))).unwrap(); let back = read(&write(&d).unwrap()).unwrap(); assert_eq!(controls(&back).len(), 60); } /// A control's `w:rPr` (ECMA-376 §17.5.2.27) is read as the formatting entered text takes, and /// still written back as it was. #[test] fn a_controls_run_properties_are_read_and_kept() { let docx = package(&p(&format!( "{}{}", t("Name: "), sdt(r#""#, &t("Eingabe")) ))); let d = read(&docx).unwrap(); let c = by_id(&d, 11); let rp = c.run_props.expect("run properties read"); assert_eq!((rp.style.as_deref(), rp.size), (Some("Eingabe"), Some(10.0))); let out = document_xml(&write(&d).unwrap()); assert!(out.contains(r#""#), "{out}"); }