use std::io::Write;
use dxpdf::render::layout::draw_command::{DrawCommand, LayoutedPage};
fn make_docx(parts: &[(&str, &str)]) -> Vec<u8> {
let mut buf = Vec::new();
{
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut buf));
let o = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated);
let overrides: String = parts
.iter()
.map(|(name, _)| {
let content_type = match *name {
"word/document.xml" => "wordprocessingml.document.main",
"word/styles.xml" => "wordprocessingml.styles",
other => panic!("no content type registered for {other}"),
};
format!(
r#"<Override PartName="/{name}" ContentType="application/vnd.openxmlformats-officedocument.{content_type}+xml"/>"#
)
})
.collect();
zip.start_file("[Content_Types].xml", o).unwrap();
zip.write_all(
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
{overrides}
</Types>"#
)
.as_bytes(),
)
.unwrap();
zip.start_file("_rels/.rels", o).unwrap();
zip.write_all(
br#"<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
</Relationships>"#,
)
.unwrap();
zip.start_file("word/_rels/document.xml.rels", o).unwrap();
zip.write_all(
br#"<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
</Relationships>"#,
)
.unwrap();
for (name, body) in parts {
zip.start_file(*name, o).unwrap();
zip.write_all(body.as_bytes()).unwrap();
}
zip.finish().unwrap();
}
buf
}
const W: &str = r#"xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main""#;
fn styles(default_lang: &str) -> String {
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<w:styles {W}>
<w:docDefaults><w:rPrDefault><w:rPr>
<w:lang w:val="{default_lang}"/>
</w:rPr></w:rPrDefault></w:docDefaults>
</w:styles>"#
)
}
fn field_document(instr: &str) -> String {
let escaped = instr.replace('"', """);
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<w:document {W}><w:body>
<w:p><w:fldSimple w:instr="{escaped}">
<w:r><w:t>CACHED</w:t></w:r>
</w:fldSimple></w:p>
</w:body></w:document>"#
)
}
fn layout(parts: &[(&str, &str)]) -> Vec<LayoutedPage> {
let doc = dxpdf::docx::parse(&make_docx(parts)).expect("fixture parses");
dxpdf::render::resolve_and_layout(doc).1
}
fn field_text(lang: &str, instr: &str) -> String {
let pages = layout(&[
("word/document.xml", &field_document(instr)),
("word/styles.xml", &styles(lang)),
]);
pages
.iter()
.flat_map(|p| p.commands.iter())
.filter_map(|c| match c {
DrawCommand::Text { text, .. } => Some(text.to_string()),
_ => None,
})
.collect::<Vec<_>>()
.join("")
.trim()
.to_string()
}
const ENGLISH_MONTHS: [&str; 12] = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const GERMAN_MONTHS: [&str; 12] = [
"Januar",
"Februar",
"März",
"April",
"Mai",
"Juni",
"Juli",
"August",
"September",
"Oktober",
"November",
"Dezember",
];
const ENGLISH_WEEKDAYS: [&str; 7] = [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
];
const GERMAN_WEEKDAYS: [&str; 7] = [
"Montag",
"Dienstag",
"Mittwoch",
"Donnerstag",
"Freitag",
"Samstag",
"Sonntag",
];
#[test]
fn a_date_field_is_evaluated_rather_than_left_as_cached_text() {
let text = field_text("en-US", r#" DATE \@ "yyyy" "#);
assert_ne!(text, "CACHED", "the field kept its cached content");
assert!(
text.len() == 4 && text.chars().all(|c| c.is_ascii_digit()),
"expected a four-digit year, got {text:?}",
);
}
#[test]
fn a_date_field_renders_a_localized_month_name() {
let german = field_text("de-DE", r#" DATE \@ "MMMM" "#);
assert!(
GERMAN_MONTHS.contains(&german.as_str()),
"not a German month name: {german:?}",
);
let english = field_text("en-US", r#" DATE \@ "MMMM" "#);
assert!(
ENGLISH_MONTHS.contains(&english.as_str()),
"not an English month name: {english:?}",
);
let same_index = GERMAN_MONTHS.iter().position(|m| *m == german)
== ENGLISH_MONTHS.iter().position(|m| *m == english);
assert!(
same_index,
"the same date named two different months: {german:?} vs {english:?}",
);
}
#[test]
fn a_date_field_renders_a_localized_weekday_name() {
let german = field_text("de-DE", r#" DATE \@ "dddd" "#);
assert!(
GERMAN_WEEKDAYS.contains(&german.as_str()),
"not a German weekday name: {german:?}",
);
let english = field_text("en-US", r#" DATE \@ "dddd" "#);
assert!(
ENGLISH_WEEKDAYS.contains(&english.as_str()),
"not an English weekday name: {english:?}",
);
let same_index = GERMAN_WEEKDAYS.iter().position(|d| *d == german)
== ENGLISH_WEEKDAYS.iter().position(|d| *d == english);
assert!(
same_index,
"the same date named two different weekdays: {german:?} vs {english:?}",
);
}
#[test]
fn a_full_date_picture_renders_through_the_pipeline() {
let text = field_text("de-DE", r#" DATE \@ "dddd, d. MMMM yyyy" "#);
let (weekday, rest) = text.split_once(", ").unwrap_or_else(|| {
panic!("expected 'weekday, d. month yyyy', got {text:?}");
});
assert!(
GERMAN_WEEKDAYS.contains(&weekday),
"leading token is not a German weekday: {text:?}",
);
assert!(
GERMAN_MONTHS.iter().any(|m| rest.contains(m)),
"no German month name in {text:?}",
);
assert!(rest.ends_with("2026") || rest.ends_with("2027"), "{text:?}");
}
#[test]
fn a_time_field_is_evaluated() {
let text = field_text("en-US", r#" TIME \@ "HH:mm" "#);
assert_ne!(text, "CACHED", "the field kept its cached content");
let (hh, mm) = text
.split_once(':')
.unwrap_or_else(|| panic!("expected HH:mm, got {text:?}"));
let (hh, mm): (u32, u32) = (
hh.parse().unwrap_or_else(|_| panic!("{text:?}")),
mm.parse().unwrap_or_else(|_| panic!("{text:?}")),
);
assert!(hh < 24 && mm < 60, "not a real time: {text:?}");
}
#[test]
fn an_unrecognised_language_falls_back_to_english_names() {
let text = field_text("zz-ZZ", r#" DATE \@ "MMMM" "#);
assert!(
ENGLISH_MONTHS.contains(&text.as_str()),
"expected an English fallback, got {text:?}",
);
}