diff --git a/crates/docx/src/read/mod.rs b/crates/docx/src/read/mod.rs
index 252804342..d82e6d33e 100644
--- a/crates/docx/src/read/mod.rs
+++ b/crates/docx/src/read/mod.rs
@@ -818,9 +818,6 @@ impl Reader<'_> {
for s in root.children("w:style").take(10_000) {
let Some(raw_id) = s.attr("w:styleId").filter(|i| !i.is_empty()) else { continue };
let id = self.pc.style_id(raw_id);
- if sheet.get(&id).is_some() {
- continue;
- }
let kind = match s.attr("w:type").unwrap_or("paragraph") {
"character" => StyleKind::Character,
"table" => StyleKind::Table,
@@ -862,7 +859,13 @@ impl Reader<'_> {
let parts = self.table_style_parts(s, &mut st);
st.table = (parts != TableStyleParts::default()).then_some(parts);
}
- sheet.styles.push(st);
+ // A style defined twice (some generators write their own Heading 1 after a
+ // placeholder one) is the last definition, whole: Word drops the earlier one rather
+ // than merging the two (observed with synthetic documents).
+ match sheet.styles.iter_mut().find(|s| s.id == id) {
+ Some(earlier) => *earlier = st,
+ None => sheet.styles.push(st),
+ }
}
// Styles that give only a list level take the list from the style they are based on
// (Heading 3: level 3 of Heading 2's outline list).
diff --git a/crates/docx/tests/fixtures.rs b/crates/docx/tests/fixtures.rs
index 20600aa6d..6fa6431bf 100644
--- a/crates/docx/tests/fixtures.rs
+++ b/crates/docx/tests/fixtures.rs
@@ -595,6 +595,26 @@ fn empty_tbl_borders_elements_leave_sides_unset() {
assert!(b.top.is_some() && b.between.is_some() && b.inside_v.is_some());
}
+/// A style ID defined twice is its last definition, whole (Word drops the earlier one rather
+/// than merging: observed with synthetic documents). Generators that write a placeholder
+/// Heading 1 and then their own made headings come out in the placeholder's font and size.
+#[test]
+fn a_style_defined_twice_is_its_last_definition() {
+ let styles = format!(
+ r#"
+
+
+"#
+ );
+ let body = r#"Probe"#;
+ let d = wordcraft_docx::read(&docx(body, &[("rId1", "styles", "styles.xml")], &[("word/styles.xml", &styles)])).unwrap();
+ assert_eq!(d.styles.styles.iter().filter(|s| s.id == "Heading1").count(), 1);
+ let h = d.styles.get("Heading1").unwrap();
+ assert_eq!((h.chr.bold, h.chr.italic, h.chr.size), (Some(true), None, Some(12.0)));
+ assert_eq!(h.chr.color, Some(TextColor::Rgb(Rgb(0, 0, 0xFF))));
+ assert_eq!(h.para.align, None, "nothing kept from the first definition");
+}
+
#[test]
fn vml_image_and_inline_drawing() {
let png: &[u8] = b"\x89PNG\r\n\x1a\nnot really a png";