Skip to main content

setzer_import/docx/
mod.rs

1//! .docx import.
2//!
3//! Layers, bottom up: `package` (zip, relationships, size limits), `xml`
4//! (attribute helpers), `styles` (style chains, numbering), `story`
5//! (streaming parse into paragraphs with resolved formatting), `media`
6//! (images), `inline` (segments → model inlines), `structure` (chapters,
7//! front matter, scene breaks, metadata).
8
9mod inline;
10mod media;
11mod package;
12mod pictures;
13mod story;
14mod structure;
15mod styles;
16mod table;
17mod xml;
18
19use crate::{ImportError, ImportWarning, Imported};
20use media::Media;
21use package::{Package, RelKind};
22use quick_xml::Reader;
23use quick_xml::events::Event;
24use setzer_model::{Block, Book};
25use std::collections::HashMap;
26use story::{Env, NoteKind, Para};
27use structure::Core;
28use styles::{Numbering, Styles};
29
30/// The relationship target of a kind, for internal targets only.
31fn internal(rels: &HashMap<String, package::Rel>, kind: RelKind) -> Option<String> {
32    rels.values()
33        .find(|r| r.kind == kind && !r.external)
34        .map(|r| r.target.clone())
35}
36
37/// The main document part: the `officeDocument` relationship's target, or
38/// `word/document.xml` when there is none.
39fn main_part(pkg: &Package, root: &HashMap<String, package::Rel>) -> Result<String, ImportError> {
40    internal(root, RelKind::OfficeDocument)
41        .filter(|p| pkg.has(p))
42        .or_else(|| {
43            pkg.has("word/document.xml")
44                .then(|| "word/document.xml".into())
45        })
46        .ok_or_else(|| ImportError::NotDocx("it has no main document part".into()))
47}
48
49/// The parts of a package that the importer finds through relationships
50/// (in the case the relationship spells them; entry names are matched
51/// without regard to case). The scrambled copy asks the same question, so
52/// it never reads a different part than the importer does.
53pub(crate) struct Located {
54    /// The main document.
55    pub main: String,
56    /// The core properties (title, author).
57    pub core: String,
58    /// The extended properties (company, manager).
59    pub app: String,
60}
61
62/// Where the importer would look for the main part and the properties.
63pub(crate) fn locate(bytes: &[u8]) -> Result<Located, ImportError> {
64    let mut pkg = Package::open(bytes)?;
65    let root = pkg.rels("");
66    Ok(Located {
67        main: main_part(&pkg, &root)?,
68        core: internal(&root, RelKind::CoreProperties)
69            .unwrap_or_else(|| "docProps/core.xml".into()),
70        app: internal(&root, RelKind::ExtendedProperties)
71            .unwrap_or_else(|| "docProps/app.xml".into()),
72    })
73}
74
75/// Import a .docx manuscript. Fails only if the bytes are not a Word
76/// document or contain no text; everything odd inside is a warning.
77pub fn import(bytes: &[u8]) -> Result<Imported, ImportError> {
78    let mut pkg = Package::open(bytes)?;
79    let root = pkg.rels("");
80    let main = main_part(&pkg, &root)?;
81    let xml = match pkg.read_xml(&main) {
82        Some(Ok(x)) => x,
83        Some(Err(e)) => return Err(ImportError::NotDocx(e)),
84        None => return Err(ImportError::NotDocx(format!("{main} is missing"))),
85    };
86
87    let core_path =
88        internal(&root, RelKind::CoreProperties).unwrap_or_else(|| "docProps/core.xml".into());
89    let core = match pkg.read_xml(&core_path) {
90        Some(Ok(x)) => parse_core(&x),
91        _ => Core::default(),
92    };
93    let rels = pkg.rels(&main);
94    let mut read_part = |kind| {
95        internal(&rels, kind)
96            .and_then(|p| pkg.read_xml(&p))
97            .and_then(Result::ok)
98    };
99    let styles = read_part(RelKind::Styles)
100        .map(|x| Styles::parse(&x))
101        .unwrap_or_default();
102    let numbering = read_part(RelKind::Numbering)
103        .map(|x| Numbering::parse(&x))
104        .unwrap_or_default();
105
106    let mut media = Media::default();
107    let mut warnings = Vec::new();
108    let no_notes = HashMap::new();
109    let mut notes: HashMap<(NoteKind, i64), Vec<Block>> = HashMap::new();
110    for kind in [RelKind::Footnotes, RelKind::Endnotes] {
111        let Some(path) = internal(&rels, kind) else {
112            continue;
113        };
114        let Some(Ok(x)) = pkg.read_xml(&path) else {
115            continue;
116        };
117        let note_rels = pkg.rels(&path);
118        let before = warnings.len();
119        let paras = story::parse(
120            &x,
121            &mut Env {
122                pkg: &mut pkg,
123                styles: &styles,
124                numbering: &numbering,
125                rels: &note_rels,
126                notes: &no_notes,
127                media: &mut media,
128                warnings: &mut warnings,
129            },
130        );
131        // Paragraph numbers of the notes story mean nothing to the author.
132        let label = if kind == RelKind::Footnotes {
133            "In a footnote"
134        } else {
135            "In an endnote"
136        };
137        for w in &mut warnings[before..] {
138            w.location = 0;
139            if let Some((_, rest)) = w.message.split_once(": ")
140                && w.message.starts_with("Paragraph ")
141            {
142                w.message = format!("{label}: {rest}");
143            }
144        }
145        for mut p in paras {
146            let Some(key) = p.note else { continue };
147            inline::normalize(&mut p.segs);
148            notes
149                .entry(key)
150                .or_default()
151                .extend(structure::note_blocks(&p));
152        }
153    }
154
155    let body_start = warnings.len();
156    let paras: Vec<Para> = story::parse(
157        &xml,
158        &mut Env {
159            pkg: &mut pkg,
160            styles: &styles,
161            numbering: &numbering,
162            rels: &rels,
163            notes: &notes,
164            media: &mut media,
165            warnings: &mut warnings,
166        },
167    );
168    if !paras.iter().any(Para::is_visible) {
169        return Err(ImportError::Empty);
170    }
171    let s = structure::build(paras, styles.lang.as_deref(), &core, &mut warnings);
172    attach_sections(&mut warnings[body_start..], &s.starts);
173
174    let mut book = Book::new(s.metadata);
175    book.sections = s.sections;
176    Ok(Imported {
177        book,
178        assets: media.assets,
179        warnings,
180    })
181}
182
183/// Fill `section` for warnings about a body paragraph from the paragraph
184/// where each section starts.
185fn attach_sections(warnings: &mut [ImportWarning], starts: &[usize]) {
186    for w in warnings {
187        if w.section.is_none() && w.location > 0 {
188            w.section = starts.partition_point(|&s| s <= w.location).checked_sub(1);
189        }
190    }
191}
192
193/// `docProps/core.xml`: title, author, language.
194fn parse_core(xml: &str) -> Core {
195    let mut core = Core::default();
196    let mut reader = Reader::from_str(xml);
197    let mut field: Option<&mut Option<String>> = None;
198    let mut buf = String::new();
199    loop {
200        match reader.read_event() {
201            Ok(Event::Start(e)) => {
202                buf.clear();
203                field = match e.local_name().into_inner() {
204                    "title" => Some(&mut core.title),
205                    "creator" => Some(&mut core.creator),
206                    "language" => Some(&mut core.language),
207                    _ => None,
208                };
209            }
210            Ok(Event::Text(t)) if field.is_some() => buf.push_str(&t.xml10_content()),
211            Ok(Event::GeneralRef(r)) if field.is_some() => {
212                if let Some(c) = xml::resolve_ref(&r) {
213                    buf.push(c);
214                }
215            }
216            Ok(Event::End(_)) => {
217                if let Some(f) = field.take() {
218                    let v = buf.trim();
219                    if !v.is_empty() && f.is_none() {
220                        *f = Some(v.to_string());
221                    }
222                }
223            }
224            Ok(Event::Eof) | Err(_) => break,
225            _ => {}
226        }
227    }
228    core
229}
230
231#[cfg(test)]
232pub(crate) mod tests;