//! Section text direction (`w:textDirection` in `w:sectPr`, ECMA-376 Part 1 §17.6.20). Fixtures //! are hand-written from the spec. use std::io::{Read, Write}; use craft_wordmodel::props::TextDirection; fn docx(body: &str) -> Vec { let root = r#""#; let doc = format!( r#"{body}"# ); let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new())); for (name, bytes) in [("_rels/.rels", root.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 docx_with_styles(body: &str, styles: &str) -> Vec { let mut bytes = zip::ZipWriter::new(std::io::Cursor::new(Vec::new())); let root = r#""#; let document_rels = r#""#; let doc = format!( r#"{body}"# ); for (name, value) in [ ("_rels/.rels", root.as_bytes()), ("word/_rels/document.xml.rels", document_rels.as_bytes()), ("word/document.xml", doc.as_bytes()), ("word/styles.xml", styles.as_bytes()), ] { bytes.start_file(name, zip::write::SimpleFileOptions::default()).unwrap(); bytes.write_all(value).unwrap(); } bytes.finish().unwrap().into_inner() } fn document_xml(bytes: &[u8]) -> String { let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap(); let mut s = String::new(); z.by_name("word/document.xml").unwrap().read_to_string(&mut s).unwrap(); s } #[test] fn section_text_direction_round_trips_in_schema_order() { // Three sections: vertical East Asian (tbRl), bottom-to-top (btLr) and a value no one knows. let sect = |dir: &str| format!(r#""#); let body = format!( r#"{}縦書き{}UpOdd{}"#, sect("tbRl"), sect("btLr"), sect("diagonal") ); let d = craft_docx::read(&docx(&body)).unwrap(); let dirs: Vec = d.sections().iter().map(|(_, s)| s.text_direction).collect(); assert_eq!(dirs, vec![TextDirection::Down, TextDirection::Up, TextDirection::Horizontal]); let out = craft_docx::write(&d).unwrap(); let xml = document_xml(&out); // After w:titlePg and before w:bidi (CT_SectPr's sequence); a horizontal section writes none. assert!(xml.contains(r#""#), "{xml}"); assert!(xml.contains(r#""#)); assert_eq!(xml.matches("w:textDirection").count(), 2); let back = craft_docx::read(&out).unwrap(); let again: Vec = back.sections().iter().map(|(_, s)| s.text_direction).collect(); assert_eq!(again, dirs); } #[test] fn explicit_horizontal_direction_is_kept_in_sections_and_cells() { let body = r#"CellBody"#; let d = craft_docx::read(&docx(body)).unwrap(); let xml = document_xml(&craft_docx::write(&d).unwrap()); assert_eq!(xml.matches(r#""#).count(), 2, "{xml}"); // A horizontal section or cell that never said so still writes nothing. let plain = craft_docx::read(&docx(r#"Body"#)).unwrap(); assert!(!document_xml(&craft_docx::write(&plain).unwrap()).contains("textDirection")); } #[test] fn paragraph_text_direction_round_trips_through_two_saves_and_json() { use craft_wordmodel::props::ParaProps; use craft_wordmodel::props::ParagraphTextDirection; let values = ["lrTb", "lrTbV", "tb", "tbRl", "tbRlV", "tbLr", "tbLrV", "btLr", "tbV", "rl", "rlV", "lr", "lrV"]; let mut body: String = values.iter().map(|value| format!(r#"{value}"#)).collect(); body.push_str(r#"Inherited"#); let d = craft_docx::read(&docx(&body)).unwrap(); let paras: Vec<_> = d.body.iter().filter_map(|b| b.as_para()).collect(); for (para, value) in paras.iter().zip(values) { assert_eq!(para.props.text_direction, ParagraphTextDirection::from_ooxml(value), "{value}"); } assert_eq!(paras[values.len()].props.text_direction, None); let json = serde_json::to_value(¶s[0].props).unwrap(); assert_eq!(json["textDirection"], "lrTb"); let decoded: ParaProps = serde_json::from_value(json).unwrap(); assert_eq!(decoded.text_direction, Some(ParagraphTextDirection::LrTb)); let first = craft_docx::write(&d).unwrap(); let xml = document_xml(&first); for value in values { assert!(xml.contains(&format!(r#""#)), "{xml}"); } assert_eq!(xml.matches("w:textDirection").count(), values.len(), "inherited direction stays absent: {xml}"); let second = craft_docx::write(&craft_docx::read(&first).unwrap()).unwrap(); let twice = document_xml(&second); assert_eq!(twice.matches("w:textDirection").count(), values.len(), "{twice}"); for value in values { assert!(twice.contains(&format!(r#""#)), "{twice}"); } } #[test] fn paragraph_style_text_direction_is_preserved_and_direct_value_overrides_it() { let styles = r#""#; let body = r#"StyleOverride"#; let d = craft_docx::read(&docx_with_styles(body, styles)).unwrap(); let xml = document_xml(&craft_docx::write(&d).unwrap()); assert_eq!(xml.matches(r#""#).count(), 2, "both paragraphs keep their style: {xml}"); assert!(xml.contains(r#""#), "direct horizontal is explicit: {xml}"); assert_eq!(xml.matches("w:textDirection").count(), 1, "only the direct paragraph writes a direction: {xml}"); let mut z = zip::ZipArchive::new(std::io::Cursor::new(craft_docx::write(&d).unwrap())).unwrap(); let mut styles_xml = String::new(); z.by_name("word/styles.xml").unwrap().read_to_string(&mut styles_xml).unwrap(); assert!(styles_xml.contains(r#""#), "style value survives: {styles_xml}"); }