//! Indents in character units (#534), common in Chinese documents: ECMA-376 Part 1 §17.3.1.12 //! `w:ind` `w:firstLineChars`, `w:hangingChars`, `w:leftChars` and `w:rightChars`, in hundredths //! of a character. Fixtures are hand-written. use std::io::{Read, Write}; use craft_wordmodel::Document; use craft_wordmodel::props::ParaProps; fn zip(entries: &[(&str, &[u8])]) -> Vec { let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new())); for (name, bytes) in entries { zw.start_file(*name, zip::write::SimpleFileOptions::default()).unwrap(); zw.write_all(bytes).unwrap(); } zw.finish().unwrap().into_inner() } /// A package whose body is `body`. fn docx(body: &str) -> Vec { let root = r#""#; let doc = format!( r#"{body}"# ); zip(&[("_rels/.rels", root.as_bytes()), ("word/document.xml", doc.as_bytes())]) } fn props(d: &Document) -> Vec { d.body.iter().filter_map(|b| b.as_para()).map(|p| p.props.clone()).collect() } #[test] fn character_indents_read_and_round_trip() { let body = r#"一 二 三 四"#; let d = craft_docx::read(&docx(body)).unwrap(); let p = props(&d); assert_eq!((p[0].indent_first_chars, p[0].indent_first), (Some(2.0), Some(21.0))); assert_eq!((p[1].indent_left_chars, p[1].indent_right_chars, p[1].indent_first_chars), (Some(1.5), Some(0.5), Some(-1.0))); // Zero cancels an inherited value; the points apply. assert_eq!((p[2].indent_first_chars, p[2].indent_first), (Some(0.0), Some(12.0))); // Hostile values are clamped or dropped. assert_eq!((p[3].indent_first_chars, p[3].indent_left_chars), (Some(1584.0), None)); let mut z = zip::ZipArchive::new(std::io::Cursor::new(craft_docx::write(&d).unwrap())).unwrap(); let mut xml = String::new(); z.by_name("word/document.xml").unwrap().read_to_string(&mut xml).unwrap(); for attr in [r#"w:firstLineChars="200""#, r#"w:leftChars="150""#, r#"w:rightChars="50""#, r#"w:hangingChars="100""#, r#"w:firstLineChars="0""#] { assert!(xml.contains(attr), "{attr} missing: {xml}"); } let again = props(&craft_docx::read(&docx(&xml[xml.find("").unwrap() + 8..xml.find("").unwrap()])).unwrap()); assert_eq!(again[..3], p[..3]); } /// HTML-style automatic paragraph spacing (`w:beforeAutospacing`, `w:afterAutospacing`, ECMA-376 /// §17.3.1.33), common in text pasted from web pages: read, "0" switching an inherited one off, /// and written back. #[test] fn automatic_spacing_reads_and_round_trips() { let body = r#"a b c"#; let d = craft_docx::read(&docx(body)).unwrap(); let p = props(&d); assert_eq!((p[0].space_before_auto, p[0].space_after_auto, p[0].space_before), (Some(true), Some(true), Some(5.0))); assert_eq!((p[1].space_before_auto, p[1].space_after_auto), (Some(false), Some(true))); assert_eq!((p[2].space_before_auto, p[2].space_after_auto), (None, None)); let rp = d.styles.resolve_para(&p[0]); assert_eq!((rp.space_before, rp.space_after, rp.before_auto), (14.0, 14.0, true), "automatic spacing is 14 pt"); assert_eq!(d.styles.resolve_para(&p[2]).space_after, 5.0); let mut z = zip::ZipArchive::new(std::io::Cursor::new(craft_docx::write(&d).unwrap())).unwrap(); let mut xml = String::new(); z.by_name("word/document.xml").unwrap().read_to_string(&mut xml).unwrap(); for attr in [r#"w:beforeAutospacing="1""#, r#"w:afterAutospacing="1""#, r#"w:beforeAutospacing="0""#] { assert!(xml.contains(attr), "{attr} missing"); } let again = craft_docx::read(&craft_docx::write(&d).unwrap()).unwrap(); assert_eq!(props(&again)[..3], p[..3]); } #[test] fn adjust_right_indent_is_preserved() { let body = r#"false true absent"#; let mut d = craft_docx::read(&docx(body)).unwrap(); assert_eq!(props(&d).iter().map(|p| p.adjust_right_indent).collect::>(), [Some(false), Some(true), None]); for _ in 0..2 { let out = craft_docx::write(&d).unwrap(); let mut z = zip::ZipArchive::new(std::io::Cursor::new(out.clone())).unwrap(); let mut xml = String::new(); z.by_name("word/document.xml").unwrap().read_to_string(&mut xml).unwrap(); assert!(xml.contains(r#""), "explicit true was lost: {xml}"); let tags = ["").skip(1).filter_map(|rest| rest.split("").next()).collect::>(); let first = pprs.first().expect("paragraph properties must be present"); for &tag in &tags { assert!(first.contains(tag), "{tag} missing: {xml}"); } for ppr in pprs { let order = tags.iter().filter_map(|tag| ppr.find(*tag)).collect::>(); assert!(order.windows(2).all(|pair| pair[0] < pair[1]), "paragraph properties are out of schema order: {xml}"); } d = craft_docx::read(&out).unwrap(); assert_eq!(props(&d).iter().map(|p| p.adjust_right_indent).collect::>(), [Some(false), Some(true), None]); } }