Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_file/src/office/docx/read.rs

31.3 KiB, 75 runs

created by r1870400018:22703, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Reading a `.docx` into the neutral document tree, and saying what did not come with it.
2//!
3//! # The coverage trap, and the way round it
4//!
5//! Counting element names gets this wrong. Across a real corpus the thirty commonest names cover 92%
6//! of the *content* and **zero of the documents**, because what breaks a reader is not the long tail
7//! -- it is the structural singletons, `w:document`, `w:body`, `w:sectPr`, `w:tbl`, which appear once
8//! each in every file. A reader that handled the top thirty would fail on all of them.
9//!
10//! So the names are not enumerated. Elements fall into four sets and the fourth is the important one:
11//!
12//! 1. **Handled** -- the ones that mean something to the tree: paragraphs, runs, text, tables, links.
13//! 2. **Dropped** -- the ones whose content is not prose in reading order: properties, field
14//! instructions, deleted text, section breaks.
15//! 3. **Counted and not drawn** -- pictures, charts, text boxes, embedded objects. These are real
16//! content and this cannot render them, so they are counted BY KIND and the count is handed back.
17//! 4. **Descended through, transparently** -- everything else, whatever it is called. A content
18//! control, a smart tag, a bidirectional override, a custom XML block and every element invented
19//! since this was written contribute nothing themselves and their content is read where they
20//! stood.
21//!
22//! The fourth set is what makes the reader work on documents nobody had when it was written. It is
23//! the same move [`oxedyne_fe2o3_text::doc::html::read`] makes with an unknown tag, for the same
24//! reason.
25//!
26//! # It reads the document's own vocabulary rather than assuming Word's
27//!
28//! A paragraph is a heading because *its style resolves to a built-in heading name*, not because its
29//! style id happens to be `Heading1`. A list is numbered rather than bulleted because
30//! `word/numbering.xml` says its level's format is not `bullet`. A link's target comes from the
31//! relationships part. A document written by LibreOffice, by Pages, or by a generator agrees with
32//! Word on none of the ids and on all of the URIs and built-in names.
33//!
34//! # Tracked changes are read as the document stands
35//!
36//! An insertion's text is in the document, so it is read. A deletion's text is not, so it is not.
37//! That is display, and display only -- nothing here authors `w:ins` or `w:del`, because getting
38//! those subtly wrong corrupts a legal review and the person who finds out is a lawyer.
39//!
40//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
41//! Anthropic Claude
42
43use crate::office::opc::{
44 REL_DOC,
45 REL_NUMBERING,
46 REL_STYLES,
47};
48use crate::zip::Zip;
49
50use oxedyne_fe2o3_core::prelude::*;
51use oxedyne_fe2o3_text::doc::{
52 Align,
53 Block,
54 Cell,
55 Doc,
56 Inline,
57 Row,
58};
59use oxedyne_fe2o3_text::xml::{
60 Elem,
61 Xml,
62};
63
64use std::collections::BTreeMap;
65
66// The most a single part is inflated to. A `word/document.xml` is XML, which compresses about ten to
67// one, so a part this size came from an archive member of tens of megabytes. Well past any document
68// and well short of trouble.
69pub const MAX_PART: u64 = 64 * 1024 * 1024;
70
71/// The leading bytes of an OLE compound file, which is what an encrypted Office document is.
72const OLE_MAGIC: [u8; 8] = [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1];
73
74/// Something a reading view cannot draw.
75///
76/// Named by kind rather than counted as a lump, because "4 things are not drawn" tells a reader
77/// nothing and "3 text boxes and 1 chart" tells them whether to go and open the file properly.
78#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
79pub enum Undrawable {
80 Image,
81 Chart, // data plus a drawing of it, not re-rendered here
82 Diagram, // SmartArt or another diagram
83 TextBox, // prose sitting outside the flow of the document
84 Object, // a spreadsheet, a slide, another program's document
85 Equation,
86 Footnote, // whose text is in another part
87 Endnote,
88 Comment, // whose text is in another part
89}
90
91impl Undrawable {
92
93 /// What to call some number of these, in English, singular or plural.
94 ///
95 /// The number is the information. A band that said "some content is not shown" would be saying
96 /// only that the reader cannot be trusted.
97 pub fn say(&self, n: usize) -> String {
98 let (one, many) = match self {
99 Self::Image => ("image", "images"),
100 Self::Chart => ("chart", "charts"),
101 Self::Diagram => ("diagram", "diagrams"),
102 Self::TextBox => ("text box", "text boxes"),
103 Self::Object => ("embedded object", "embedded objects"),
104 Self::Equation => ("equation", "equations"),
105 Self::Footnote => ("footnote", "footnotes"),
106 Self::Endnote => ("endnote", "endnotes"),
107 Self::Comment => ("comment", "comments"),
108 };
109 match n {
110 1 => fmt!("{} {}", n, one),
111 _ => fmt!("{} {}", n, many),
112 }
113 }
114}
115
116/// A document read for reading: the prose, and an honest account of what is missing from it.
117#[derive(Clone, Debug, Default)]
118pub struct Reading {
119 pub doc: Doc,
120 pub undrawn: Vec<(Undrawable, usize)>, // by kind and count, in a fixed order
121 // Said, never run. A `.docm` is a `.docx` with `word/vbaProject.bin` in it, and a reader who is
122 // not told is a reader who does not know what they have been sent.
123 pub macros: bool,
124 // Insertions read as part of the text, which is what tracked changes look like when they are
125 // displayed as accepted.
126 pub tracked: usize,
127}
128
129impl Reading {
130
131 /// The whole account of what is not drawn, as one phrase, or nothing where everything is.
132 ///
133 /// `"4 things are not drawn: 3 text boxes, 1 chart"`.
134 pub fn say_undrawn(&self) -> Option<String> {
135 if self.undrawn.is_empty() {
136 return None;
137 }
138 let total: usize = self.undrawn.iter().map(|(_, n)| n).sum();
139 let parts: Vec<String> = self.undrawn.iter().map(|(k, n)| k.say(*n)).collect();
140 let what = match total {
141 1 => "thing is",
142 _ => "things are",
143 };
144 Some(fmt!("{} {} not drawn: {}", total, what, parts.join(", ")))
145 }
146}
147
148pub fn read(bytes: &[u8]) -> Outcome<Reading> {
149 if bytes.len() >= OLE_MAGIC.len() && bytes[..OLE_MAGIC.len()] == OLE_MAGIC {
150 return Err(err!(
151 "This document is encrypted. Office writes an encrypted file as an OLE compound file \
152 with the real document inside it, and there is no password here to open it with. \
153 Nothing is guessed at and nothing is shown."; Invalid, Input, Unimplemented));
154 }
155 let zip = res!(Zip::read(bytes.to_vec()));
156 let mut out = Reading::default();
157 out.macros = zip.names().iter().any(|n| n.ends_with("vbaProject.bin"));
158
159 // The main part is whatever the package's own relationships point at. Every writer in practice
160 // calls it `word/document.xml`, and a reader that assumed so would be right until it was not.
161 let root_rels = res!(rels_of(&zip, ""));
162 let main = res!(root_rels.values()
163 .find(|(kind, _)| kind == REL_DOC)
164 .map(|(_, target)| target.clone())
165 .ok_or_else(|| err!(
166 "The package names no main document part, so this is not a Word document. It holds: \
167 {}.", zip.names().join(", "); Invalid, Input, Missing)));
168 let dir = dir_of(&main);
169 let xml = res!(Xml::parse(&res!(part_text(&zip, &main))));
170
171 let rels = res!(rels_of(&zip, &main));
172 let styles = res!(styles_of(&zip, &dir, &rels));
173 let lists = res!(lists_of(&zip, &dir, &rels));
174
175 let mut r = Read {
176 xml: &xml,
177 styles: &styles,
178 lists: &lists,
179 rels: &rels,
180 undrawn: BTreeMap::new(),
181 tracked: 0,
182 };
183 let body = match res!(xml.root()).child("w:body") {
184 Some(body) => body,
185 None => return Err(err!(
186 "'{}' has no <w:body>, so it is not a word-processing document.", main;
187 Invalid, Input, Missing)),
188 };
189 out.doc = Doc { blocks: r.body(body) };
190 out.undrawn = r.undrawn.into_iter().collect();
191 out.tracked = r.tracked;
192 Ok(out)
193}
194
195/// What one style says about the paragraphs that wear it.
196#[derive(Clone, Debug, Default)]
197struct Style {
198 name: String, // the built-in name, lowered: `heading 1`, `quote`
199 outline: Option<u8>, // the outline level the style itself sets
200 // A writer that marks code with a CHARACTER STYLE rather than a font -- which is the tidier way
201 // to write one, and what this crate's own writer does -- says nothing about the face on the run
202 // itself. A reader that looked only at the run would read every code span as ordinary prose.
203 mono: bool,
204 based_on: Option<String>,
205}
206
207/// What one numbering definition says at each of its levels.
208type Lists = BTreeMap<String, BTreeMap<usize, bool>>;
209
210/// The state of one document being read.
211struct Read<'a> {
212 xml: &'a Xml, // the document part
213 styles: &'a BTreeMap<String, Style>, // style id to what the style says
214 lists: &'a Lists, // numbering id to level to whether that level is ordered
215 rels: &'a BTreeMap<String, (String, String)>, // relationship id to its type and target
216 undrawn: BTreeMap<Undrawable, usize>,
217 tracked: usize, // how many insertions were read
218}
219
220/// One paragraph that belongs to a list, on its way to being nested.
221struct Item {
222 lvl: usize, // how deep it sits
223 ordered: bool, // numbered rather than bulleted
224 blocks: Vec<Block>,
225}
226
227impl<'a> Read<'a> {
228
229 /// Reads the body into blocks, gathering runs of list paragraphs as it goes.
230 fn body(&mut self, body: &Elem) -> Vec<Block> {
231 let mut out = Vec::new();
232 let mut items: Vec<Item> = Vec::new();
233 for kid in body.elems() {
234 match kid.name.qname.as_str() {
235 // The section properties say what the page is, which is layout and not prose.
236 "w:sectPr" => {}
237 "w:tbl" => {
238 flush(&mut out, &mut items);
239 if let Some(table) = self.table(kid) {
240 out.push(table);
241 }
242 }
243 "w:p" => {
244 match self.list_of(kid) {
245 Some((lvl, ordered)) => {
246 let blocks = self.para(kid, true);
247 if !blocks.is_empty() {
248 items.push(Item { lvl, ordered, blocks });
249 }
250 }
251 None => {
252 flush(&mut out, &mut items);
253 out.extend(self.para(kid, false));
254 }
255 }
256 }
257 // Anything else that stands at body level -- a content control, a custom XML block --
258 // holds body-level content, and is read where it stood.
259 _ => {
260 flush(&mut out, &mut items);
261 out.extend(self.body(kid));
262 }
263 }
264 }
265 flush(&mut out, &mut items);
266 out
267 }
268
269 fn list_of(&self, p: &Elem) -> Option<(usize, bool)> {
270 let num = p.find(&["w:pPr", "w:numPr"])?;
271 let id = num.child("w:numId")?.attr("w:val")?;
272 // `w:numId` of zero means "no numbering", and is how Word turns a list item back into a
273 // paragraph without taking the property off it.
274 if id == "0" {
275 return None;
276 }
277 let lvl = num.child("w:ilvl")
278 .and_then(|e| e.attr("w:val"))
279 .and_then(|v| v.parse::<usize>().ok())
280 .unwrap_or(0);
281 // A numbering the document refers to and does not define is still a list; the safe reading of
282 // an unknown format is a bullet, which says less than a wrong number would.
283 let ordered = self.lists.get(id)
284 .and_then(|levels| levels.get(&lvl))
285 .copied()
286 .unwrap_or(false);
287 Some((lvl, ordered))
288 }
289
290 /// A list item is never a heading and never a rule.
291 fn para(&mut self, p: &Elem, in_list: bool) -> Vec<Block> {
292 let style = p.find(&["w:pPr", "w:pStyle"]).and_then(|e| e.attr("w:val")).unwrap_or("");
293 let mut content = Vec::new();
294 self.inlines(p, &mut content, Fmt::default());
295 let content = coalesce(content);
296 // An empty paragraph is spacing, not prose. A document that used them for spacing -- and many
297 // do -- would otherwise read as a column of blank lines.
298 if content.is_empty() {
299 return Vec::new();
300 }
301 if !in_list {
302 if let Some(level) = self.heading_of(p, style) {
303 return vec![Block::Heading { level, content }];
304 }
305 let kind = self.kind_of(style);
306 match kind {
307 Kind::Quote => return vec![Block::Quote(vec![Block::Para(content)])],
308 Kind::Code => {
309 let text = oxedyne_fe2o3_text::doc::text_of(&content);
310 return vec![Block::Code { lang: None, text }];
311 }
312 Kind::Plain => {}
313 }
314 }
315 vec![Block::Para(content)]
316 }
317
318 /// The outline level a paragraph sets itself wins, then the one its style sets, then the style's
319 /// built-in name. Asking the id would be asking Word's spelling of a question the document
320 /// answers for itself.
321 fn heading_of(&self, p: &Elem, style: &str) -> Option<u8> {
322 if let Some(lvl) = p.find(&["w:pPr", "w:outlineLvl"])
323 .and_then(|e| e.attr("w:val"))
324 .and_then(|v| v.parse::<u8>().ok())
325 {
326 // A body-level paragraph carries outline level 9, which is "not in the outline".
327 if lvl < 9 {
328 return Some(lvl + 1);
329 }
330 }
331 let mut at = style;
332 for _ in 0..8 {
333 let s = self.styles.get(at)?;
334 if let Some(lvl) = s.outline {
335 if lvl < 9 {
336 return Some(lvl + 1);
337 }
338 }
339 if let Some(rest) = s.name.strip_prefix("heading ") {
340 if let Ok(n) = rest.trim().parse::<u8>() {
341 return Some(n.clamp(1, 6));
342 }
343 }
344 if s.name == "title" {
345 return Some(1);
346 }
347 if s.name == "subtitle" {
348 return Some(2);
349 }
350 at = s.based_on.as_deref()?;
351 }
352 None
353 }
354
355 /// What a style makes of the paragraphs that wear it, beyond being a heading.
356 fn kind_of(&self, style: &str) -> Kind {
357 let mut at = style;
358 for _ in 0..8 {
359 let s = match self.styles.get(at) {
360 Some(s) => s,
361 None => {
362 // A style the document does not define is still worth reading by its id, since
363 // an id is what a generator that wrote no styles part will have used.
364 let low = at.to_ascii_lowercase();
365 return Kind::of(&low);
366 }
367 };
368 let kind = Kind::of(&s.name);
369 if kind != Kind::Plain {
370 return kind;
371 }
372 let kind = Kind::of(&at.to_ascii_lowercase());
373 if kind != Kind::Plain {
374 return kind;
375 }
376 at = match s.based_on.as_deref() {
377 Some(b) => b,
378 None => return Kind::Plain,
379 };
380 }
381 Kind::Plain
382 }
383
384 /// Reads the inline content of an element, descending through whatever it does not know.
385 fn inlines(&mut self, at: &Elem, out: &mut Vec<Inline>, fmt: Fmt) {
386 for kid in at.elems() {
387 match kid.name.qname.as_str() {
388 // Properties, not content.
389 "w:pPr" | "w:rPr" | "w:tblPr" | "w:trPr" | "w:tcPr" | "w:sectPr" => {}
390 // Marks that carry no words.
391 "w:bookmarkStart" | "w:bookmarkEnd" | "w:proofErr" | "w:permStart"
392 | "w:permEnd" | "w:lastRenderedPageBreak" | "w:commentRangeStart"
393 | "w:commentRangeEnd" | "w:tblGrid" => {}
394 // Removed text is not what the document says.
395 "w:del" | "w:moveFrom" | "w:delText" | "w:delInstrText" => {}
396 // A field's instruction is a program, not prose. Its cached result is read as the
397 // surrounding runs.
398 "w:instrText" => {}
399 "w:r" => self.run(kid, out, fmt),
400 "w:ins" | "w:moveTo" => {
401 self.tracked += 1;
402 self.inlines(kid, out, fmt);
403 }
404 "w:hyperlink" => {
405 let to = self.target_of(kid);
406 let mut inner = Vec::new();
407 self.inlines(kid, &mut inner, fmt);
408 let inner = coalesce(inner);
409 match (to, inner.is_empty()) {
410 (_, true) => {}
411 (Some(to), _) => out.push(Inline::Link { to, content: inner }),
412 (None, _) => out.extend(inner),
413 }
414 }
415 // Two renderings of the same content, one for readers that understand a newer
416 // vocabulary and one for those that do not. Reading both would say everything twice.
417 "mc:AlternateContent" => {
418 let pick = kid.child("mc:Choice").or_else(|| kid.child("mc:Fallback"));
419 if let Some(pick) = pick {
420 self.inlines(pick, out, fmt);
421 }
422 }
423 // Everything else contributes nothing itself: a content control, a smart tag, a
424 // bidirectional override, and every element invented since this was written.
425 _ => self.inlines(kid, out, fmt),
426 }
427 }
428 }
429
430 fn run(&mut self, r: &Elem, out: &mut Vec<Inline>, fmt: Fmt) {
431 let fmt = match r.child("w:rPr") {
432 Some(pr) => Fmt {
433 // `w:b` with `w:val="0"` or `"false"` turns bold OFF, which is how a run inside a
434 // bold heading says "not this bit".
435 bold: on(pr.child("w:b"), fmt.bold),
436 italic: on(pr.child("w:i"), fmt.italic),
437 code: fmt.code
438 || mono(pr.child("w:rFonts"))
439 || self.style_mono(pr.child("w:rStyle").and_then(|e| e.attr("w:val"))),
440 },
441 None => fmt,
442 };
443 for kid in r.elems() {
444 match kid.name.qname.as_str() {
445 "w:rPr" | "w:instrText" | "w:delText" | "w:lastRenderedPageBreak"
446 | "w:fldChar" | "w:footnoteRef" | "w:endnoteRef" | "w:annotationRef" => {}
447 "w:t" => push_text(out, &self.xml.text_of(kid), fmt),
448 "w:tab" => push_text(out, "\t", fmt),
449 "w:br" | "w:cr" => out.push(Inline::Break),
450 "w:noBreakHyphen" => push_text(out, "\u{2011}", fmt),
451 // A soft hyphen is a place a word MAY break, and says nothing when it does not.
452 "w:softHyphen" => {}
453 "w:sym" => {
454 // A symbol names a character by its code point in a symbol font.
455 if let Some(c) = kid.attr("w:char")
456 .and_then(|h| u32::from_str_radix(h, 16).ok())
457 .and_then(char::from_u32)
458 {
459 push_text(out, &c.to_string(), fmt);
460 }
461 }
462 "w:drawing" | "w:pict" | "w:object" => self.count_drawing(kid),
463 "w:footnoteReference" => self.count(Undrawable::Footnote),
464 "w:endnoteReference" => self.count(Undrawable::Endnote),
465 "w:commentReference" => self.count(Undrawable::Comment),
466 _ => self.inlines(kid, out, fmt),
467 }
468 }
469 }
470
471 /// Whether a character style resolves to a monospaced face, following what it is based on.
472 fn style_mono(&self, id: Option<&str>) -> bool {
473 let mut at = match id {
474 Some(id) => id,
475 None => return false,
476 };
477 for _ in 0..8 {
478 let s = match self.styles.get(at) {
479 Some(s) => s,
480 None => return false,
481 };
482 if s.mono {
483 return true;
484 }
485 at = match s.based_on.as_deref() {
486 Some(b) => b,
487 None => return false,
488 };
489 }
490 false
491 }
492
493 /// Counts a drawing as what it actually is, which its own subtree says.
494 ///
495 /// A `w:drawing` is a picture, a chart, a diagram or a text box, and calling all four "an image"
496 /// would tell a reader looking for the missing chart that there isn't one.
497 fn count_drawing(&mut self, at: &Elem) {
498 let kind = if !at.all("w:txbxContent").is_empty() {
499 Undrawable::TextBox
500 } else if holds_local(at, "chart") {
501 Undrawable::Chart
502 } else if holds_local(at, "relIds") || holds_local(at, "dgm") {
503 Undrawable::Diagram
504 } else if at.name.qname == "w:object" {
505 Undrawable::Object
506 } else if holds_local(at, "oMath") || holds_local(at, "oMathPara") {
507 Undrawable::Equation
508 } else {
509 Undrawable::Image
510 };
511 self.count(kind);
512 }
513
514 fn count(&mut self, what: Undrawable) {
515 *self.undrawn.entry(what).or_insert(0) += 1;
516 }
517
518 /// Where a link points: a relationship for a link out, an anchor for one within.
519 fn target_of(&self, link: &Elem) -> Option<String> {
520 if let Some(id) = link.attr("r:id") {
521 if let Some((_, target)) = self.rels.get(id) {
522 return Some(target.clone());
523 }
524 }
525 link.attr("w:anchor").map(|a| fmt!("#{}", a))
526 }
527
528 fn table(&mut self, tbl: &Elem) -> Option<Block> {
529 let mut rows = Vec::new();
530 let mut head = None;
531 for (i, tr) in tbl.children("w:tr").into_iter().enumerate() {
532 let mut cells = Vec::new();
533 for tc in tr.children("w:tc") {
534 let mut content = Vec::new();
535 for (k, p) in tc.children("w:p").into_iter().enumerate() {
536 if k > 0 {
537 content.push(Inline::Break);
538 }
539 self.inlines(p, &mut content, Fmt::default());
540 }
541 // A cell holds a phrase, not a document: see `Cell`'s own note on why.
542 cells.push(Cell(coalesce(content)));
543 }
544 // A header row is one the table SAYS repeats, or a first row whose every cell is bold.
545 // Both are signals a writer actually emits; guessing from anything less would put a row of
546 // data where a reader expects column names.
547 let is_head = i == 0 && (tr.find(&["w:trPr", "w:tblHeader"]).is_some() || all_bold(tr));
548 match is_head {
549 true => head = Some(Row(cells)),
550 false => rows.push(Row(cells)),
551 }
552 }
553 if head.is_none() && rows.is_empty() {
554 return None;
555 }
556 let n = head.iter().chain(rows.iter()).map(|r| r.0.len()).max().unwrap_or(0);
557 // The alignment of a column is a property of each cell in OOXML rather than of the column, so
558 // the first row that says anything decides. A table whose cells disagree has no column
559 // alignment to report.
560 let cols = (0..n).map(|i| self.align_of(tbl, i)).collect();
561 Some(Block::Table { head, rows, cols })
562 }
563
564 /// The alignment of a column, from the first cell in it that names one.
565 fn align_of(&self, tbl: &Elem, col: usize) -> Align {
566 for tr in tbl.children("w:tr") {
567 if let Some(tc) = tr.children("w:tc").get(col) {
568 if let Some(p) = tc.child("w:p") {
569 if let Some(jc) = p.find(&["w:pPr", "w:jc"]).and_then(|e| e.attr("w:val")) {
570 return match jc {
571 "start" | "left" => Align::Start,
572 "center" | "centre" => Align::Centre,
573 "end" | "right" => Align::End,
574 _ => Align::None,
575 };
576 }
577 }
578 }
579 }
580 Align::None
581 }
582}
583
584/// What a style makes of a paragraph, beyond a heading.
585#[derive(Clone, Copy, Debug, PartialEq)]
586enum Kind {
587 Plain,
588 Quote,
589 Code, // a listing
590}
591
592impl Kind {
593
594 /// What a lowered style name or id says the paragraph is.
595 fn of(low: &str) -> Self {
596 match low {
597 // The spellings are the ones writers actually emit. `block quotation` is
598 // LibreOffice's; `intense quote` is Word's; `quotations` is OpenDocument's. Reading
599 // only Word's would call two of the three ordinary paragraphs.
600 "quote" | "blockquote" | "block text" | "intense quote" | "quotations"
601 | "blocktext" | "intensequote" | "block quotation" | "blockquotation" => Self::Quote,
602 "source code" | "sourcecode" | "html preformatted" | "htmlpreformatted"
603 | "preformatted text" | "preformattedtext" | "code" | "plain text"
604 | "plaintext" | "macro text" => Self::Code,
605 _ => Self::Plain,
606 }
607 }
608}
609
610/// How a run of text is marked.
611#[derive(Clone, Copy, Debug, Default, PartialEq)]
612struct Fmt {
613 bold: bool,
614 italic: bool,
615 code: bool, // monospaced, which the tree carries as a code span
616}
617
618/// Whether a toggle property is on.
619///
620/// An element that is simply there is on; one carrying `w:val="0"` or `"false"` is off, which is how a
621/// run inside a bold heading says "not this bit". Absent, it inherits.
622fn on(e: Option<&Elem>, inherited: bool) -> bool {
623 match e {
624 None => inherited,
625 Some(e) => match e.attr("w:val") {
626 Some("0") | Some("false") | Some("off") => false,
627 _ => true,
628 },
629 }
630}
631
632/// Whether a font specification names a monospaced face.
633///
634/// A short list of the faces a writer actually reaches for. It is a guess and it is a cheap one: at
635/// worst a passage arrives as a code span rather than as prose, which loses nothing a reader needs.
636fn mono(e: Option<&Elem>) -> bool {
637 let name = match e.and_then(|e| e.attr("w:ascii")) {
638 Some(n) => n.to_ascii_lowercase(),
639 None => return false,
640 };
641 matches!(name.as_str(),
642 "consolas" | "courier" | "courier new" | "monaco" | "menlo" | "liberation mono"
643 | "dejavu sans mono" | "lucida console" | "andale mono" | "cascadia code"
644 | "cascadia mono" | "sf mono" | "jetbrains mono" | "fira code" | "source code pro")
645}
646
647/// Whether every run in a row is bold, which is one of the two signals that says a header row.
648fn all_bold(tr: &Elem) -> bool {
649 let runs = tr.all("w:r");
650 !runs.is_empty() && runs.iter().all(|r| {
651 // A run holding no text says nothing either way, so it does not veto.
652 r.all("w:t").is_empty() || r.find(&["w:rPr", "w:b"]).is_some()
653 })
654}
655
656/// Whether an element or any of its descendants has that local name, whatever prefix it wears.
657///
658/// By local name because the prefix is the document's choice: a chart is `c:chart` in one writer's
659/// output and `chart` under a default namespace in another's.
660fn holds_local(at: &Elem, local: &str) -> bool {
661 if at.name.local() == local {
662 return true;
663 }
664 at.elems().any(|k| holds_local(k, local))
665}
666
667fn push_text(out: &mut Vec<Inline>, text: &str, fmt: Fmt) {
668 if text.is_empty() {
669 return;
670 }
671 let mut item = match fmt.code {
672 true => Inline::Code(text.to_string()),
673 false => Inline::Text(text.to_string()),
674 };
675 // Strong outside emphasis, so `**a *b* **` nests the way a writer would have written it.
676 if fmt.italic {
677 item = Inline::Emph { strong: false, content: vec![item] };
678 }
679 if fmt.bold {
680 item = Inline::Emph { strong: true, content: vec![item] };
681 }
682 out.push(item);
683}
684
685/// Joins adjacent inlines that are marked alike.
686///
687/// A run is the unit formatting applies to in OOXML, and a writer splits one wherever it likes -- a
688/// spell-check mark, a bookmark, a language change. Left alone, a bold phrase arrives as six bold
689/// inlines and renders as `**a****b**`, which is not what it says.
690fn coalesce(items: Vec<Inline>) -> Vec<Inline> {
691 let mut out: Vec<Inline> = Vec::with_capacity(items.len());
692 for item in items {
693 match (out.last_mut(), item) {
694 (Some(Inline::Text(a)), Inline::Text(b)) => a.push_str(&b),
695 (Some(Inline::Code(a)), Inline::Code(b)) => a.push_str(&b),
696 (
697 Some(Inline::Emph { strong: sa, content: ca }),
698 Inline::Emph { strong: sb, content: cb },
699 ) if *sa == sb => {
700 let mut joined = std::mem::take(ca);
701 joined.extend(cb);
702 *ca = coalesce(joined);
703 }
704 (_, item) => out.push(item),
705 }
706 }
707 // Trailing whitespace on a paragraph is the writer's, not the author's.
708 if let Some(Inline::Text(last)) = out.last_mut() {
709 let trimmed = last.trim_end_matches(['\n', '\r']);
710 if trimmed.len() != last.len() {
711 *last = trimmed.to_string();
712 }
713 }
714 out.retain(|i| !matches!(i, Inline::Text(t) if t.is_empty()));
715 out
716}
717
718/// Turns a gathered run of list paragraphs into the nested lists they make, and adds them.
719fn flush(out: &mut Vec<Block>, items: &mut Vec<Item>) {
720 if items.is_empty() {
721 return;
722 }
723 let taken = std::mem::take(items);
724 out.extend(nest(&taken, 0));
725}
726
727/// Nests a run of list items by their levels.
728///
729/// A document says only what level each item is at; the nesting is this. An item deeper than the one
730/// before it belongs inside it, which is what makes a flat run of paragraphs a tree.
731fn nest(items: &[Item], depth: usize) -> Vec<Block> {
732 let mut out: Vec<Block> = Vec::new();
733 let mut i = 0;
734 while i < items.len() {
735 let ordered = items[i].ordered;
736 let mut list: Vec<Vec<Block>> = Vec::new();
737 // One list runs while the items stay at this level and keep the same kind.
738 while i < items.len() && items[i].lvl <= depth {
739 if items[i].ordered != ordered && items[i].lvl == depth {
740 break;
741 }
742 let mut blocks = items[i].blocks.clone();
743 i += 1;
744 // Everything deeper that follows belongs to the item just taken.
745 let from = i;
746 while i < items.len() && items[i].lvl > depth {
747 i += 1;
748 }
749 if i > from {
750 blocks.extend(nest(&items[from..i], depth + 1));
751 }
752 list.push(blocks);
753 }
754 if list.is_empty() {
755 // An item deeper than its depth with nothing above it: take it at this level rather than
756 // spin.
757 list.push(items[i].blocks.clone());
758 i += 1;
759 }
760 out.push(Block::List { ordered, items: list });
761 }
762 out
763}
764
765fn part_text(zip: &Zip, name: &str) -> Outcome<String> {
766 let bytes = res!(zip.content_capped(name, MAX_PART));
767 Ok(res!(String::from_utf8(bytes), Decode, String))
768}
769
770/// The directory a part sits in, with its trailing slash, so a relative target resolves against it.
771fn dir_of(part: &str) -> String {
772 match part.rfind('/') {
773 Some(k) => part[..k + 1].to_string(),
774 None => String::new(),
775 }
776}
777
778/// Where a relationship target actually is within the package.
779fn resolve(dir: &str, target: &str) -> String {
780 match target.starts_with('/') {
781 true => target[1..].to_string(),
782 false => fmt!("{}{}", dir, target),
783 }
784}
785
786/// The relationships a part owns, by id.
787///
788/// A part's relationships live beside it, in a `_rels` directory, in a file named after it. The
789/// package's own are in `_rels/.rels`, which is the same rule with an empty name.
790fn rels_of(zip: &Zip, part: &str) -> Outcome<BTreeMap<String, (String, String)>> {
791 let dir = dir_of(part);
792 let name = &part[dir.len()..];
793 let path = fmt!("{}_rels/{}.rels", dir, name);
794 let mut out = BTreeMap::new();
795 if !zip.has(&path) {
796 return Ok(out);
797 }
798 let src = res!(part_text(zip, &path));
799 let xml = res!(Xml::parse(&src));
800 for rel in res!(xml.root()).children("Relationship") {
801 let id = match rel.attr("Id") {
802 Some(id) => id.to_string(),
803 None => continue,
804 };
805 let kind = rel.attr("Type").unwrap_or("").to_string();
806 let target = rel.attr("Target").unwrap_or("").to_string();
807 // An external target is a URL and stays as written; an internal one is a path within the
808 // package and is resolved against the part that names it.
809 let target = match rel.attr("TargetMode") {
810 Some("External") => target,
811 _ => resolve(&dir, &target),
812 };
813 out.insert(id, (kind, target));
814 }
815 Ok(out)
816}
817
818/// The styles the document defines, by id.
819fn styles_of(
820 zip: &Zip,
821 dir: &str,
822 rels: &BTreeMap<String, (String, String)>,
823)
824 -> Outcome<BTreeMap<String, Style>>
825{
826 let mut out = BTreeMap::new();
827 let part = match part_of(rels, REL_STYLES, dir, "styles.xml", zip) {
828 Some(p) => p,
829 None => return Ok(out),
830 };
831 let src = res!(part_text(zip, &part));
832 let xml = res!(Xml::parse(&src));
833 for s in res!(xml.root()).children("w:style") {
834 let id = match s.attr("w:styleId") {
835 Some(id) => id.to_string(),
836 None => continue,
837 };
838 out.insert(id, Style {
839 name: s.child("w:name")
840 .and_then(|e| e.attr("w:val"))
841 .unwrap_or("")
842 .to_ascii_lowercase(),
843 outline: s.find(&["w:pPr", "w:outlineLvl"])
844 .and_then(|e| e.attr("w:val"))
845 .and_then(|v| v.parse::<u8>().ok()),
846 mono: mono(s.find(&["w:rPr", "w:rFonts"])),
847 based_on: s.child("w:basedOn")
848 .and_then(|e| e.attr("w:val"))
849 .map(|v| v.to_string()),
850 });
851 }
852 Ok(out)
853}
854
855/// What each numbering definition's levels are, by the id a paragraph names.
856///
857/// Two hops: a paragraph names a `w:num`, a `w:num` names an abstract definition, and the abstract
858/// definition is where the levels live. A reader that skipped the indirection would read the abstract
859/// id as the paragraph's, and every list in a document with more than one would be the wrong kind.
860fn lists_of(
861 zip: &Zip,
862 dir: &str,
863 rels: &BTreeMap<String, (String, String)>,
864)
865 -> Outcome<Lists>
866{
867 let mut out = Lists::new();
868 let part = match part_of(rels, REL_NUMBERING, dir, "numbering.xml", zip) {
869 Some(p) => p,
870 None => return Ok(out),
871 };
872 let src = res!(part_text(zip, &part));
873 let xml = res!(Xml::parse(&src));
874 let root = res!(xml.root());
875 let mut abstracts: BTreeMap<String, BTreeMap<usize, bool>> = BTreeMap::new();
876 for a in root.children("w:abstractNum") {
877 let id = match a.attr("w:abstractNumId") {
878 Some(id) => id.to_string(),
879 None => continue,
880 };
881 let mut levels = BTreeMap::new();
882 for lvl in a.children("w:lvl") {
883 let n = lvl.attr("w:ilvl").and_then(|v| v.parse::<usize>().ok()).unwrap_or(0);
884 let fmt = lvl.child("w:numFmt").and_then(|e| e.attr("w:val")).unwrap_or("bullet");
885 levels.insert(n, fmt != "bullet" && fmt != "none");
886 }
887 abstracts.insert(id, levels);
888 }
889 for num in root.children("w:num") {
890 let id = match num.attr("w:numId") {
891 Some(id) => id.to_string(),
892 None => continue,
893 };
894 let at = num.child("w:abstractNumId").and_then(|e| e.attr("w:val")).unwrap_or("");
895 if let Some(levels) = abstracts.get(at) {
896 out.insert(id, levels.clone());
897 }
898 }
899 Ok(out)
900}
901
902/// Where a supporting part is: what the relationships say, or the conventional name where they say
903/// nothing.
904///
905/// The relationship is the authority. The fallback is for a document whose rels part is missing or
906/// which never declared one, which is common in generator output and which a reader can still make
907/// sense of rather than refuse.
908fn part_of(
909 rels: &BTreeMap<String, (String, String)>,
910 kind: &str,
911 dir: &str,
912 usual: &str,
913 zip: &Zip,
914)
915 -> Option<String>
916{
917 if let Some((_, target)) = rels.values().find(|(k, _)| k == kind) {
918 if zip.has(target) {
919 return Some(target.clone());
920 }
921 }
922 let guess = fmt!("{}{}", dir, usual);
923 match zip.has(&guess) {
924 true => Some(guess),
925 false => None,
926 }
927}