mod inline;
mod media;
mod package;
mod pictures;
mod story;
mod structure;
mod styles;
mod table;
mod xml;
use crate::{ImportError, ImportWarning, Imported};
use media::Media;
use package::{Package, RelKind};
use quick_xml::Reader;
use quick_xml::events::Event;
use setzer_model::{Block, Book};
use std::collections::HashMap;
use story::{Env, NoteKind, Para};
use structure::Core;
use styles::{Numbering, Styles};
fn internal(rels: &HashMap<String, package::Rel>, kind: RelKind) -> Option<String> {
rels.values()
.find(|r| r.kind == kind && !r.external)
.map(|r| r.target.clone())
}
fn main_part(pkg: &Package, root: &HashMap<String, package::Rel>) -> Result<String, ImportError> {
internal(root, RelKind::OfficeDocument)
.filter(|p| pkg.has(p))
.or_else(|| {
pkg.has("word/document.xml")
.then(|| "word/document.xml".into())
})
.ok_or_else(|| ImportError::NotDocx("it has no main document part".into()))
}
pub(crate) struct Located {
pub main: String,
pub core: String,
pub app: String,
}
pub(crate) fn locate(bytes: &[u8]) -> Result<Located, ImportError> {
let mut pkg = Package::open(bytes)?;
let root = pkg.rels("");
Ok(Located {
main: main_part(&pkg, &root)?,
core: internal(&root, RelKind::CoreProperties)
.unwrap_or_else(|| "docProps/core.xml".into()),
app: internal(&root, RelKind::ExtendedProperties)
.unwrap_or_else(|| "docProps/app.xml".into()),
})
}
pub fn import(bytes: &[u8]) -> Result<Imported, ImportError> {
let mut pkg = Package::open(bytes)?;
let root = pkg.rels("");
let main = main_part(&pkg, &root)?;
let xml = match pkg.read_xml(&main) {
Some(Ok(x)) => x,
Some(Err(e)) => return Err(ImportError::NotDocx(e)),
None => return Err(ImportError::NotDocx(format!("{main} is missing"))),
};
let core_path =
internal(&root, RelKind::CoreProperties).unwrap_or_else(|| "docProps/core.xml".into());
let core = match pkg.read_xml(&core_path) {
Some(Ok(x)) => parse_core(&x),
_ => Core::default(),
};
let rels = pkg.rels(&main);
let mut read_part = |kind| {
internal(&rels, kind)
.and_then(|p| pkg.read_xml(&p))
.and_then(Result::ok)
};
let styles = read_part(RelKind::Styles)
.map(|x| Styles::parse(&x))
.unwrap_or_default();
let numbering = read_part(RelKind::Numbering)
.map(|x| Numbering::parse(&x))
.unwrap_or_default();
let mut media = Media::default();
let mut warnings = Vec::new();
let no_notes = HashMap::new();
let mut notes: HashMap<(NoteKind, i64), Vec<Block>> = HashMap::new();
for kind in [RelKind::Footnotes, RelKind::Endnotes] {
let Some(path) = internal(&rels, kind) else {
continue;
};
let Some(Ok(x)) = pkg.read_xml(&path) else {
continue;
};
let note_rels = pkg.rels(&path);
let before = warnings.len();
let paras = story::parse(
&x,
&mut Env {
pkg: &mut pkg,
styles: &styles,
numbering: &numbering,
rels: ¬e_rels,
notes: &no_notes,
media: &mut media,
warnings: &mut warnings,
},
);
let label = if kind == RelKind::Footnotes {
"In a footnote"
} else {
"In an endnote"
};
for w in &mut warnings[before..] {
w.location = 0;
if let Some((_, rest)) = w.message.split_once(": ")
&& w.message.starts_with("Paragraph ")
{
w.message = format!("{label}: {rest}");
}
}
for mut p in paras {
let Some(key) = p.note else { continue };
inline::normalize(&mut p.segs);
notes
.entry(key)
.or_default()
.extend(structure::note_blocks(&p));
}
}
let body_start = warnings.len();
let paras: Vec<Para> = story::parse(
&xml,
&mut Env {
pkg: &mut pkg,
styles: &styles,
numbering: &numbering,
rels: &rels,
notes: ¬es,
media: &mut media,
warnings: &mut warnings,
},
);
if !paras.iter().any(Para::is_visible) {
return Err(ImportError::Empty);
}
let s = structure::build(paras, styles.lang.as_deref(), &core, &mut warnings);
attach_sections(&mut warnings[body_start..], &s.starts);
let mut book = Book::new(s.metadata);
book.sections = s.sections;
Ok(Imported {
book,
assets: media.assets,
warnings,
})
}
fn attach_sections(warnings: &mut [ImportWarning], starts: &[usize]) {
for w in warnings {
if w.section.is_none() && w.location > 0 {
w.section = starts.partition_point(|&s| s <= w.location).checked_sub(1);
}
}
}
fn parse_core(xml: &str) -> Core {
let mut core = Core::default();
let mut reader = Reader::from_str(xml);
let mut field: Option<&mut Option<String>> = None;
let mut buf = String::new();
loop {
match reader.read_event() {
Ok(Event::Start(e)) => {
buf.clear();
field = match e.local_name().into_inner() {
"title" => Some(&mut core.title),
"creator" => Some(&mut core.creator),
"language" => Some(&mut core.language),
_ => None,
};
}
Ok(Event::Text(t)) if field.is_some() => buf.push_str(&t.xml10_content()),
Ok(Event::GeneralRef(r)) if field.is_some() => {
if let Some(c) = xml::resolve_ref(&r) {
buf.push(c);
}
}
Ok(Event::End(_)) => {
if let Some(f) = field.take() {
let v = buf.trim();
if !v.is_empty() && f.is_none() {
*f = Some(v.to_string());
}
}
}
Ok(Event::Eof) | Err(_) => break,
_ => {}
}
}
core
}
#[cfg(test)]
pub(crate) mod tests;