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";