diff --git a/crates/docx/src/read/styles.rs b/crates/docx/src/read/styles.rs index 267fd9f15..b1d7388e6 100644 --- a/crates/docx/src/read/styles.rs +++ b/crates/docx/src/read/styles.rs @@ -11,6 +11,9 @@ fn numbering_level_index(value: Option<&str>) -> Option { None => Some(0), Some(value) => int(value).filter(|level| (0..=8).contains(level)).map(|level| level as usize), } + +fn style_is_default(style: &El) -> bool { + style.attr("w:default").is_some_and(|value| !matches!(value.trim(), "0" | "false" | "off" | "False" | "FALSE")) } /// Built-in style names Word stores in lower case, and how the UI shows them. @@ -67,9 +70,7 @@ impl Reader<'_> { // The default paragraph style must be called `Normal` in our model. let has_normal = root.children("w:style").any(|s| s.attr("w:styleId") == Some("Normal")); if !has_normal - && let Some(def) = root - .children("w:style") - .find(|s| s.attr("w:type") == Some("paragraph") && s.attr("w:default").is_some_and(|v| v == "1" || v == "true")) + && let Some(def) = root.children("w:style").find(|s| s.attr("w:type") == Some("paragraph") && style_is_default(s)) && let Some(id) = def.attr("w:styleId") { self.pc.alias.insert(id.to_string(), "Normal".into()); @@ -143,7 +144,7 @@ impl Reader<'_> { builtin: !s.attr("w:customStyle").is_some_and(|v| matches!(v.trim(), "1" | "true" | "on")), locked: flag(s, "w:locked").unwrap_or(false), // Only where the writer couldn't tell from the id (it flags the English ids). - default: s.attr("w:default").is_some_and(|v| v == "1" || v == "true") + default: style_is_default(s) && !matches!( (kind, id.as_str()), (StyleKind::Paragraph, "Normal") diff --git a/crates/docx/tests/it/undefined_style.rs b/crates/docx/tests/it/undefined_style.rs index 48cf03410..8cb0c19be 100644 --- a/crates/docx/tests/it/undefined_style.rs +++ b/crates/docx/tests/it/undefined_style.rs @@ -13,6 +13,10 @@ fn package(normal_id: &str) -> Vec { } fn package_with_default(normal_id: &str, is_default: bool) -> Vec { + package_with_body_default(normal_id, is_default, false) +} + +fn package_with_body_default(normal_id: &str, is_default: bool, has_body_default: bool) -> Vec { let root = r#""#; let doc = format!( r#"bodynormal"# @@ -21,8 +25,10 @@ fn package_with_default(normal_id: &str, is_default: bool) -> Vec { r#"1"# ); let default_attr = if is_default { r#" w:default="1""# } else { "" }; + let body_style = + if has_body_default { r#""# } else { "" }; let styles = format!( - r#""# + r#"{body_style}"# ); let rels = r#""#; let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new())); @@ -46,6 +52,13 @@ fn an_explicit_normal_style_is_the_default_even_without_w_default() { assert!(normal.default, "Word treats the explicitly declared built-in Normal style as the default"); } +#[test] +fn an_explicit_body_default_on_prevents_normal_inference() { + let doc = craft_docx::read(&package_with_body_default("Normal", false, true)).unwrap(); + assert!(doc.styles.styles.iter().any(|style| style.id == "Body" && style.default)); + assert!(!doc.styles.styles.iter().any(|style| style.id == "Normal" && style.default)); +} + fn footer_para(d: &Document) -> &craft_wordmodel::Paragraph { let part = d.parts.values().find(|p| p.kind == PartKind::Footer).expect("a footer"); match part.blocks.first().map(|b| &**b) {