fix(docx): mark declared Normal as the default

This commit is contained in:
Dylan M. Taylor committed 2026-11-14 13:25:13 +00:00
1 parent d15bf1f35b
commit 15740f3083
2 files changed
+27 -1

No files matched your search

+14
View File
@@ -93,6 +93,7 @@ impl Reader<'_> {
}
}
let mut sheet = StyleSheet::empty();
let mut normal_is_default = None;
sheet.default_para_present = root.child("w:docDefaults").and_then(|dd| dd.child("w:pPrDefault")).is_some();
if let Some(dd) = root.child("w:docDefaults") {
if let Some(r) = dd.child("w:rPrDefault").and_then(|d| d.child("w:rPr")) {
@@ -120,6 +121,13 @@ impl Reader<'_> {
"numbering" => StyleKind::Numbering,
_ => StyleKind::Paragraph,
};
if kind == StyleKind::Paragraph && id == "Normal" {
// Word treats the built-in Normal style as the paragraph default when the file
// doesn't nominate another style. Keep that fact for an explicitly declared
// Normal; the gallery later synthesizes a visually identical Normal when the
// file did not declare one, and that synthetic style is not source provenance.
normal_is_default = Some(s.attr("w:default").is_none_or(|value| !matches!(value.trim(), "0" | "false" | "off" | "False" | "FALSE")));
}
let name = s.child_val("w:name").map(ui_style_name).unwrap_or_else(|| id.clone());
let r = |n: &str| s.child_val(n).filter(|v| !v.is_empty()).map(|v| self.pc.style_id(v));
let mut st = Style {
@@ -213,6 +221,12 @@ impl Reader<'_> {
for (s, n) in sheet.styles.iter_mut().zip(fixed) {
s.para.numbering = n;
}
if !sheet.styles.iter().any(|style| style.kind == StyleKind::Paragraph && style.default)
&& normal_is_default == Some(true)
&& let Some(normal) = sheet.styles.iter_mut().find(|style| style.kind == StyleKind::Paragraph && style.id == "Normal")
{
normal.default = true;
}
let old_fixed: Vec<Option<Option<craft_wordmodel::props::NumRef>>> = sheet
.styles
.iter()
+13 -1
View File
@@ -9,6 +9,10 @@ const W: &str = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
const R: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
fn package(normal_id: &str) -> Vec<u8> {
package_with_default(normal_id, true)
}
fn package_with_default(normal_id: &str, is_default: bool) -> Vec<u8> {
let root = r#"<?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>"#;
let doc = format!(
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="{W}" xmlns:r="{R}"><w:body><w:p><w:pPr><w:pStyle w:val="Footer"/></w:pPr><w:r><w:t>body</w:t></w:r></w:p><w:p><w:r><w:t>normal</w:t></w:r></w:p><w:sectPr><w:footerReference w:type="default" r:id="rId2"/></w:sectPr></w:body></w:document>"#
@@ -16,8 +20,9 @@ fn package(normal_id: &str) -> Vec<u8> {
let footer = format!(
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:ftr xmlns:w="{W}"><w:p><w:pPr><w:pStyle w:val="Footer"/><w:jc w:val="center"/></w:pPr><w:r><w:t>1</w:t></w:r></w:p></w:ftr>"#
);
let default_attr = if is_default { r#" w:default="1""# } else { "" };
let styles = format!(
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:styles xmlns:w="{W}"><w:docDefaults><w:rPrDefault><w:rPr><w:sz w:val="24"/></w:rPr></w:rPrDefault><w:pPrDefault><w:pPr><w:spacing w:after="200"/></w:pPr></w:pPrDefault></w:docDefaults><w:style w:type="paragraph" w:default="1" w:styleId="{normal_id}"><w:name w:val="Normal"/><w:pPr><w:spacing w:before="20" w:after="40" w:line="259" w:lineRule="auto"/></w:pPr><w:rPr><w:sz w:val="26"/></w:rPr></w:style></w:styles>"#
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:styles xmlns:w="{W}"><w:docDefaults><w:rPrDefault><w:rPr><w:sz w:val="24"/></w:rPr></w:rPrDefault><w:pPrDefault><w:pPr><w:spacing w:after="200"/></w:pPr></w:pPrDefault></w:docDefaults><w:style w:type="paragraph"{default_attr} w:styleId="{normal_id}"><w:name w:val="Normal"/><w:pPr><w:spacing w:before="20" w:after="40" w:line="259" w:lineRule="auto"/></w:pPr><w:rPr><w:sz w:val="26"/></w:rPr></w:style></w:styles>"#
);
let rels = r#"<?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/styles" Target="styles.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" Target="footer1.xml"/></Relationships>"#;
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
@@ -34,6 +39,13 @@ fn package(normal_id: &str) -> Vec<u8> {
zw.finish().unwrap().into_inner()
}
#[test]
fn an_explicit_normal_style_is_the_default_even_without_w_default() {
let doc = craft_docx::read(&package_with_default("Normal", false)).unwrap();
let normal = doc.styles.get("Normal").expect("the explicit Normal style");
assert!(normal.default, "Word treats the explicitly declared built-in Normal style as the 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) {