//! Unlocked `DATE` and `TIME` fields are brought up to now when a document is opened, as Word //! does whenever it shows, prints or exports one (#1672). Packages are hand-written: the writer //! would never produce the stale cached result this is about. use std::io::Write; use wordcraft_doc::{InlineObject, StoryRef}; fn zip(entries: &[(&str, &str)]) -> Vec { let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new())); for (name, body) in entries { zw.start_file(*name, zip::write::SimpleFileOptions::default()).unwrap(); zw.write_all(body.as_bytes()).unwrap(); } zw.finish().unwrap().into_inner() } /// A field run: its code, its cached result, and whether it carries `w:fldLock`. fn field(instr: &str, cached: &str, locked: bool) -> String { let lock = if locked { r#" w:fldLock="1""# } else { "" }; format!( concat!( r#""#, r#" {} "#, r#""#, r#"{}"#, r#""#, ), lock, instr.replace('"', """), cached ) } fn open_body(body: &str) -> wordcraft_doc::Document { let rels = r#""#; let doc = format!( r#"{body}"# ); wordcraft_engine::io::open_bytes("d.docx", &zip(&[("_rels/.rels", rels), ("word/document.xml", &doc)])).expect("opens") } /// Every field of the body, as (code, result). fn fields(d: &wordcraft_doc::Document) -> Vec<(String, String)> { let mut out = Vec::new(); for path in d.para_paths(StoryRef::Body) { let Some(p) = d.para(StoryRef::Body, &path) else { continue }; for o in &p.objects { if let InlineObject::Field { instr, result, .. } = o { out.push((instr.clone(), result.clone())); } } } out } #[test] fn unlocked_date_and_time_fields_are_refreshed_on_open() { // The reported fixture: both cached in January 2020. let d = open_body(&format!( "{}{}", field(r#"DATE \@ "yyyy-MM-dd""#, "2020-01-01", false), field(r#"TIME \@ "dddd, d MMMM yyyy""#, "Wednesday, 1 January 2020", false), )); let got = fields(&d); assert_eq!(got.len(), 2); // Compared against the same clock the refresh used, so the test does not age. assert_eq!(got[0].1, wordcraft_engine::cmd::insert::format_date("yyyy-MM-dd"), "DATE is today"); assert_eq!(got[1].1, wordcraft_engine::cmd::insert::format_date("dddd, d MMMM yyyy"), "TIME is today"); assert_ne!(got[0].1, "2020-01-01", "not the stored result"); // The code itself is untouched, so a save writes the field back as it was. assert_eq!(got[0].0, r#"DATE \@ "yyyy-MM-dd""#); } #[test] fn a_date_field_with_no_picture_uses_words_default() { let d = open_body(&format!("{}{}", field("DATE", "2020-01-01", false), field("TIME", "00:00", false))); let got = fields(&d); assert_eq!(got[0].1, wordcraft_engine::cmd::insert::format_date("M/d/yyyy")); assert_eq!(got[1].1, wordcraft_engine::cmd::insert::format_date("h:mm am/pm")); } #[test] fn a_locked_date_field_keeps_its_stored_result() { // `w:fldLock` is exactly the instruction not to refresh. let d = open_body(&format!("{}", field(r#"DATE \@ "yyyy-MM-dd""#, "2020-01-01", true))); assert_eq!(fields(&d)[0].1, "2020-01-01"); } #[test] fn other_date_fields_keep_their_stored_result() { // These record when something happened; they are not "now", and Word does not refresh them // on open either. Nor is any other field touched. let d = open_body(&format!( "{}{}{}{}", field(r#"CREATEDATE \@ "yyyy-MM-dd""#, "2019-05-05", false), field(r#"SAVEDATE \@ "yyyy-MM-dd""#, "2019-06-06", false), field(r#"PRINTDATE \@ "yyyy-MM-dd""#, "2019-07-07", false), field("AUTHOR", "Ada Lovelace", false), )); let results: Vec = fields(&d).into_iter().map(|(_, r)| r).collect(); assert_eq!(results, ["2019-05-05", "2019-06-06", "2019-07-07", "Ada Lovelace"]); } #[test] fn a_date_field_in_a_header_is_refreshed_too() { // A letter template usually carries its date in the header, which is its own story. let rels = r#""#; let drels = r#""#; let header = format!( r#"{}"#, field(r#"DATE \@ "yyyy-MM-dd""#, "2020-01-01", false) ); let doc = r#"Body"#; let d = wordcraft_engine::io::open_bytes( "d.docx", &zip(&[("_rels/.rels", rels), ("word/document.xml", doc), ("word/_rels/document.xml.rels", drels), ("word/header1.xml", &header)]), ) .expect("opens"); let today = wordcraft_engine::cmd::insert::format_date("yyyy-MM-dd"); let found: Vec = d .parts .values() .flat_map(|p| p.blocks.iter().filter_map(|b| b.as_para()).flat_map(|p| p.objects.clone())) .filter_map(|o| match o { InlineObject::Field { result, .. } => Some(result), _ => None, }) .collect(); assert_eq!(found, [today], "the header's field is refreshed as well as the body's"); }