Preserve conventional Word part fallback

This commit is contained in:
Dylan M. Taylor committed 2026-11-14 02:28:04 +00:00
1 parent de7640a006
commit fbbebecf27
2 files changed
+42 -1

No files matched your search

+18 -1
View File
@@ -197,7 +197,24 @@ fn read_opened_in(pkg: &Package, chunks: ChunkState) -> Result<(Document, usize,
r.doc.missing_pictures.main = main.clone();
r.pc.major_font = r.doc.settings.major_font.clone();
r.pc.minor_font = r.doc.settings.minor_font.clone();
let part = |t: &str| rels.by_type(t).filter(|x| !x.external).map(|x| x.target.clone());
let part = |t: &str| {
let related = rels.by_type(t).filter(|x| !x.external).map(|x| x.target.clone()).filter(|p| pkg.get(p).is_some());
related.or_else(|| {
// Older DesignCraft imports read these two conventional parts directly. Keep opening
// those files when a producer omitted the document relationship; the bounded package
// reader still applies the same entry and XML limits. An explicit usable relationship
// always wins, including when it points outside the conventional `word/` paths.
if main != "word/document.xml" {
return None;
}
let fallback = match t {
rt::STYLES => "word/styles.xml",
rt::NUMBERING => "word/numbering.xml",
_ => return None,
};
pkg.get(fallback).map(|_| fallback.to_string())
})
};
// The font table and the fonts embedded through it ride along as opaque parts.
if let Some(p) = part(rt::FONT_TABLE) {
+24
View File
@@ -69,3 +69,27 @@ fn an_undefined_style_is_written_back_as_it_was_named() {
assert_eq!(footer_para(&again).props.style.as_deref(), Some("Footer"));
assert_eq!(again.styles.resolve_para(&footer_para(&again).props).space_after, 2.0);
}
#[test]
fn conventional_styles_and_numbering_parts_without_relationships_are_read() {
// The old DesignCraft reader opened these conventional paths directly. Keep that tolerated
// input behavior while preferring OPC relationships whenever they resolve successfully.
let doc = format!(
r#"<w:document xmlns:w="{W}"><w:body><w:p><w:pPr><w:pStyle w:val="Body"/><w:numPr><w:ilvl w:val="0"/><w:numId w:val="7"/></w:numPr></w:pPr><w:r><w:t>text</w:t></w:r></w:p></w:body></w:document>"#
);
let styles = format!(r#"<w:styles xmlns:w="{W}"><w:style w:type="paragraph" w:styleId="Body"><w:name w:val="Body"/></w:style></w:styles>"#);
let numbering = format!(
r#"<w:numbering xmlns:w="{W}"><w:abstractNum w:abstractNumId="0"><w:lvl w:ilvl="0"><w:numFmt w:val="decimal"/><w:lvlText w:val="%1."/></w:lvl></w:abstractNum><w:num w:numId="7"><w:abstractNumId w:val="0"/></w:num></w:numbering>"#
);
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
for (name, body) in [("word/document.xml", doc.as_str()), ("word/styles.xml", styles.as_str()), ("word/numbering.xml", numbering.as_str())] {
zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
zw.write_all(body.as_bytes()).unwrap();
}
let d = craft_docx::read(&zw.finish().unwrap().into_inner()).unwrap();
assert!(d.styles.styles.iter().any(|style| style.id == "Body"));
let Some(Block::Para(p)) = d.body.first().map(|b| &**b) else { panic!("a body paragraph") };
assert_eq!(p.props.style.as_deref(), Some("Body"));
assert_eq!(p.props.numbering.map(|n| (n.num, n.level)), Some((7, 0)));
assert!(d.numbering.level(7, 0).is_some());
}