Oregami
Repositories/oxedyne/fe2o3

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

14.7 KiB, 40 runs

created by r1870400018:22583, 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//! Creating a `.docx` from the neutral document tree.
2//!
3//! The direction that is genuinely easy: every byte is written here, so there is nothing to preserve
4//! and nothing to guess. What arrives is [`Doc`], which knows what a passage *is* -- a heading, a
5//! quotation, a list item -- and what leaves is a document in which those are Word's own heading,
6//! quotation and list, named rather than drawn.
7//!
8//! # What is not carried, and is said rather than dropped quietly
9//!
10//! [`write`] hands back the parts it could not carry along with the bytes, so a caller can
11//! tell the user. Today that is one thing: an image, whose bytes this cannot reach -- the tree holds
12//! the *source* an image was written with, a path or a URL, and this crate has no filesystem and no
13//! network. The alt text is written in its place and the image is counted. A caller that can resolve
14//! the source is where images will be added.
15//!
16//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
17//! Anthropic Claude
18
19use oxedyne_fe2o3_text::doc::{
20 Align,
21 Block,
22 Cell,
23 Doc,
24 Inline,
25 Row,
26};
27use crate::office::docx::{
28 MARGIN,
29 NS_W,
30 NUM_BULLET,
31 NUM_ORDERED,
32 PAGE_H,
33 PAGE_W,
34 TEXT_W,
35 parts,
36};
37use crate::office::opc::{
38 CT_DOCUMENT,
39 CT_NUMBERING,
40 CT_STYLES,
41 NS_R,
42 REL_DOC,
43 REL_HYPERLINK,
44 REL_NUMBERING,
45 REL_STYLES,
46 Rels,
47 Types,
48};
49use oxedyne_fe2o3_text::xml::write::Out;
50
51use oxedyne_fe2o3_core::prelude::*;
52use crate::zip::{
53 Method,
54 Zip,
55};
56
57// The deepest a list may nest before its items stop being indented further. `parts::numbering`
58// defines nine levels, so a tenth would name a level the document does not define and lose its
59// bullet. Deeper items sit at the ninth rather than vanish.
60const MAX_LEVEL: usize = 8;
61
62/// What a created document could not carry.
63///
64/// Counted and named rather than dropped in silence. A reader who is told "one image is not drawn"
65/// knows what they are looking at; a reader who is told nothing thinks the document is complete.
66#[derive(Clone, Debug, Default, PartialEq)]
67pub struct Left {
68 pub images: Vec<String>, // by the source each was written with
69}
70
71impl Left {
72
73 /// Whether everything in the tree reached the document.
74 pub fn is_empty(&self) -> bool {
75 self.images.is_empty()
76 }
77}
78
79/// Writes a document tree as the bytes of a `.docx`, and says what did not fit.
80pub fn write(doc: &Doc) -> Outcome<(Vec<u8>, Left)> {
81 let mut b = Build {
82 out: Out::declared(),
83 rels: Rels::new(),
84 left: Left::default(),
85 };
86 // The two parts the document always refers to. They are added before the body is written so a
87 // hyperlink's id, handed out during the body, never collides with them.
88 let _ = b.rels.add(REL_STYLES, "styles.xml");
89 let _ = b.rels.add(REL_NUMBERING, "numbering.xml");
90
91 b.out.open("w:document", &[("xmlns:w", NS_W), ("xmlns:r", NS_R)]);
92 b.out.open("w:body", &[]);
93 res!(b.blocks(&doc.blocks, Ctx::default()));
94 res!(b.section());
95 res!(b.out.close("w:body"));
96 res!(b.out.close("w:document"));
97 let document = res!(b.out.finish());
98
99 let mut types = Types::new();
100 types.over("/word/document.xml", CT_DOCUMENT);
101 types.over("/word/styles.xml", CT_STYLES);
102 types.over("/word/numbering.xml", CT_NUMBERING);
103
104 let mut root = Rels::new();
105 let _ = root.add(REL_DOC, "word/document.xml");
106
107 let mut zip = Zip::new();
108 // The order is the order Word writes them in. Nothing depends on it, and matching it means a
109 // document made here and one made there differ in their content rather than in their shape.
110 zip.set("[Content_Types].xml", res!(types.write()).into_bytes(), Method::Deflate);
111 zip.set("_rels/.rels", res!(root.write()).into_bytes(), Method::Deflate);
112 zip.set("word/document.xml", document.into_bytes(), Method::Deflate);
113 zip.set("word/_rels/document.xml.rels", res!(b.rels.write()).into_bytes(), Method::Deflate);
114 zip.set("word/styles.xml", res!(parts::styles()).into_bytes(), Method::Deflate);
115 zip.set("word/numbering.xml", res!(parts::numbering()).into_bytes(), Method::Deflate);
116 Ok((res!(zip.write()), b.left))
117}
118
119/// What a block is being written inside, which decides what its paragraphs look like.
120///
121/// Carried down rather than looked up, because the same paragraph is a quotation inside a quotation
122/// and a list item inside a list, and nothing about the paragraph itself says which.
123#[derive(Clone, Copy, Debug, Default)]
124struct Ctx {
125 style: Option<&'static str>, // where the block does not name one of its own
126 num: Option<(&'static str, usize)>, // the `w:numId`, and how deep it sits
127}
128
129/// How a run of text is marked.
130#[derive(Clone, Copy, Debug, Default, PartialEq)]
131struct Fmt {
132 bold: bool,
133 italic: bool,
134 code: bool, // a span of code within a line
135 link: bool, // part of a link, which is what the `Hyperlink` character style is for
136}
137
138/// The state of one document being written.
139struct Build {
140 out: Out, // the document part, under construction
141 rels: Rels, // what the document part refers to
142 left: Left, // what could not be carried
143}
144
145impl Build {
146
147 fn blocks(&mut self, blocks: &[Block], ctx: Ctx) -> Outcome<()> {
148 for block in blocks {
149 res!(self.block(block, ctx));
150 }
151 Ok(())
152 }
153
154 fn block(&mut self, block: &Block, ctx: Ctx) -> Outcome<()> {
155 match block {
156 Block::Heading { level, content } => {
157 // A heading is Word's OWN heading, by name, so the navigation pane and a generated
158 // contents page both find it. Six levels, and anything past six is a level six.
159 let style = match level.clamp(&1, &6) {
160 1 => "Heading1",
161 2 => "Heading2",
162 3 => "Heading3",
163 4 => "Heading4",
164 5 => "Heading5",
165 _ => "Heading6",
166 };
167 res!(self.para(content, Ctx { style: Some(style), num: None }))
168 }
169 Block::Para(content) => res!(self.para(content, ctx)),
170 Block::Quote(inner) => {
171 res!(self.blocks(inner, Ctx { style: Some("Quote"), ..ctx }))
172 }
173 Block::List { ordered, items } => {
174 let num = match ordered {
175 true => NUM_ORDERED,
176 false => NUM_BULLET,
177 };
178 let lvl = match ctx.num {
179 Some((_, l)) => (l + 1).min(MAX_LEVEL),
180 None => 0,
181 };
182 let inner = Ctx { style: Some("ListParagraph"), num: Some((num, lvl)) };
183 for item in items {
184 res!(self.blocks(item, inner));
185 }
186 }
187 Block::Code { text, .. } => {
188 // One paragraph to a line: a listing's line structure is what it says, and a single
189 // paragraph holding newlines would be reflowed by Word into one long line.
190 for line in text.lines() {
191 let content = [Inline::Text(line.to_string())];
192 res!(self.para(&content, Ctx { style: Some("SourceCode"), num: None }));
193 }
194 }
195 Block::Rule => res!(self.rule()),
196 Block::Table { head, rows, cols } => res!(self.table(head, rows, cols)),
197 // A division names a region and says nothing about how it looks, which is the tree's whole
198 // design. There is nothing to draw, so its content is written where it stands.
199 Block::Div { content, .. } => res!(self.blocks(content, ctx)),
200 }
201 Ok(())
202 }
203
204 fn para(&mut self, content: &[Inline], ctx: Ctx) -> Outcome<()> {
205 self.out.open("w:p", &[]);
206 if ctx.style.is_some() || ctx.num.is_some() {
207 self.out.open("w:pPr", &[]);
208 if let Some(style) = ctx.style {
209 self.out.empty("w:pStyle", &[("w:val", style)]);
210 }
211 if let Some((num, lvl)) = ctx.num {
212 let ilvl = fmt!("{}", lvl);
213 self.out.open("w:numPr", &[]);
214 self.out.empty("w:ilvl", &[("w:val", &ilvl)]);
215 self.out.empty("w:numId", &[("w:val", num)]);
216 res!(self.out.close("w:numPr"));
217 }
218 res!(self.out.close("w:pPr"));
219 }
220 res!(self.inlines(content, Fmt::default()));
221 res!(self.out.close("w:p"));
222 Ok(())
223 }
224
225 fn inlines(&mut self, content: &[Inline], fmt: Fmt) -> Outcome<()> {
226 for item in content {
227 match item {
228 Inline::Text(text) => res!(self.run(text, fmt)),
229 Inline::Code(text) => {
230 res!(self.run(text, Fmt { code: true, ..fmt }))
231 }
232 Inline::Emph { strong, content } => {
233 let fmt = match strong {
234 true => Fmt { bold: true, ..fmt },
235 false => Fmt { italic: true, ..fmt },
236 };
237 res!(self.inlines(content, fmt))
238 }
239 Inline::Link { to, content } => {
240 // A link out of the document is a relationship, not an attribute: the URL lives
241 // in the rels part and the body names it by id.
242 let id = self.rels.add_external(REL_HYPERLINK, to);
243 self.out.open("w:hyperlink", &[("r:id", &id)]);
244 res!(self.inlines(content, Fmt { link: true, ..fmt }));
245 res!(self.out.close("w:hyperlink"));
246 }
247 Inline::Image { src, alt } => {
248 // The tree holds where an image is, not what it holds, and nothing here can
249 // fetch it. The alt text stands in its place and the omission is counted, so the
250 // caller can say so rather than the reader having to notice.
251 self.left.images.push(src.clone());
252 res!(self.run(alt, Fmt { italic: true, ..fmt }));
253 }
254 // A span names a region and says nothing about how it looks. Its content stands.
255 Inline::Span { content, .. } => res!(self.inlines(content, fmt)),
256 Inline::Break => {
257 self.out.open("w:r", &[]);
258 self.out.empty("w:br", &[]);
259 res!(self.out.close("w:r"));
260 }
261 }
262 }
263 Ok(())
264 }
265
266 /// Writes one run of text, split at any newline it holds.
267 ///
268 /// A newline inside a `w:t` is whitespace to Word, and would join two lines into one. It is a
269 /// break, so it is written as one.
270 fn run(&mut self, text: &str, fmt: Fmt) -> Outcome<()> {
271 for (i, line) in text.split('\n').enumerate() {
272 if i > 0 {
273 self.out.open("w:r", &[]);
274 self.out.empty("w:br", &[]);
275 res!(self.out.close("w:r"));
276 }
277 if line.is_empty() {
278 continue;
279 }
280 self.out.open("w:r", &[]);
281 if fmt != Fmt::default() {
282 self.out.open("w:rPr", &[]);
283 // Only one character style may apply, and a link that happens to be in code is a link
284 // first: what the reader does with it is follow it.
285 match (fmt.link, fmt.code) {
286 (true, _) => self.out.empty("w:rStyle", &[("w:val", "Hyperlink")]),
287 (false, true) => self.out.empty("w:rStyle", &[("w:val", "InlineCode")]),
288 (false, false) => {}
289 }
290 if fmt.bold {
291 self.out.empty("w:b", &[]);
292 }
293 if fmt.italic {
294 self.out.empty("w:i", &[]);
295 }
296 res!(self.out.close("w:rPr"));
297 }
298 // `xml:space="preserve"` or the leading and trailing spaces of a run go, and a sentence
299 // built from three runs loses the spaces between them.
300 self.out.leaf("w:t", &[("xml:space", "preserve")], line);
301 res!(self.out.close("w:r"));
302 }
303 Ok(())
304 }
305
306 /// Writes a thematic break, which Word has no element for.
307 ///
308 /// It is an empty paragraph with a rule under it. That is what Word itself writes when a person
309 /// types three hyphens, so it is what a person opening the document will recognise.
310 fn rule(&mut self) -> Outcome<()> {
311 self.out.open("w:p", &[]);
312 self.out.open("w:pPr", &[]);
313 self.out.open("w:pBdr", &[]);
314 self.out.empty("w:bottom", &[
315 ("w:val", "single"),
316 ("w:sz", "6"),
317 ("w:space", "1"),
318 ("w:color", "auto"),
319 ]);
320 res!(self.out.close("w:pBdr"));
321 res!(self.out.close("w:pPr"));
322 res!(self.out.close("w:p"));
323 Ok(())
324 }
325
326 fn table(&mut self, head: &Option<Row>, rows: &[Row], cols: &[Align]) -> Outcome<()> {
327 // The widest row decides the grid, because a row with fewer cells than the header is a table
328 // somebody wrote by hand and Word still has to lay it out.
329 let n = head.iter().chain(rows).map(|r| r.0.len()).max().unwrap_or(0).max(cols.len());
330 if n == 0 {
331 return Ok(());
332 }
333 let width = fmt!("{}", TEXT_W as usize / n);
334 self.out.open("w:tbl", &[]);
335 self.out.open("w:tblPr", &[]);
336 self.out.empty("w:tblW", &[("w:w", "0"), ("w:type", "auto")]);
337 // The borders are written on the table rather than taken from a style, so the table has lines
338 // in a document that carries no theme and no table styles.
339 self.out.open("w:tblBorders", &[]);
340 for side in ["top", "left", "bottom", "right", "insideH", "insideV"] {
341 self.out.empty(&fmt!("w:{}", side), &[
342 ("w:val", "single"),
343 ("w:sz", "4"),
344 ("w:space", "0"),
345 ("w:color", "auto"),
346 ]);
347 }
348 res!(self.out.close("w:tblBorders"));
349 res!(self.out.close("w:tblPr"));
350 self.out.open("w:tblGrid", &[]);
351 for _ in 0..n {
352 self.out.empty("w:gridCol", &[("w:w", &width)]);
353 }
354 res!(self.out.close("w:tblGrid"));
355 if let Some(head) = head {
356 res!(self.row(head, cols, n, true));
357 }
358 for row in rows {
359 res!(self.row(row, cols, n, false));
360 }
361 res!(self.out.close("w:tbl"));
362 // A table may not be the last thing in a body, and two tables in a row would run together. An
363 // empty paragraph after one is what Word writes and what keeps both true.
364 self.out.open("w:p", &[]);
365 res!(self.out.close("w:p"));
366 Ok(())
367 }
368
369 /// Writes one row of a table, padded to the width of the grid.
370 fn row(&mut self, row: &Row, cols: &[Align], n: usize, head: bool) -> Outcome<()> {
371 self.out.open("w:tr", &[]);
372 if head {
373 // A header row repeats at the top of each page it runs onto, which is what makes a long
374 // table readable and what a reader will notice the absence of.
375 self.out.open("w:trPr", &[]);
376 self.out.empty("w:tblHeader", &[]);
377 res!(self.out.close("w:trPr"));
378 }
379 let empty = Cell::default();
380 for i in 0..n {
381 let cell = row.0.get(i).unwrap_or(&empty);
382 let align = cols.get(i).copied().unwrap_or(Align::None);
383 self.out.open("w:tc", &[]);
384 self.out.open("w:tcPr", &[]);
385 self.out.empty("w:tcW", &[("w:w", "0"), ("w:type", "auto")]);
386 res!(self.out.close("w:tcPr"));
387 self.out.open("w:p", &[]);
388 // The tree names the sides `Start` and `End` because it does not know which way its text
389 // runs, and OOXML has the same two words for the same reason. They line up exactly, so
390 // nothing here has to decide what "left" means.
391 let jc = match align {
392 Align::None => None,
393 Align::Start => Some("start"),
394 Align::Centre => Some("center"),
395 Align::End => Some("end"),
396 };
397 match (jc, head) {
398 (None, false) => {}
399 (jc, head) => {
400 self.out.open("w:pPr", &[]);
401 if let Some(jc) = jc {
402 self.out.empty("w:jc", &[("w:val", jc)]);
403 }
404 if head {
405 self.out.open("w:rPr", &[]);
406 self.out.empty("w:b", &[]);
407 res!(self.out.close("w:rPr"));
408 }
409 res!(self.out.close("w:pPr"));
410 }
411 }
412 res!(self.inlines(&cell.0, Fmt { bold: head, ..Fmt::default() }));
413 res!(self.out.close("w:p"));
414 res!(self.out.close("w:tc"));
415 }
416 res!(self.out.close("w:tr"));
417 Ok(())
418 }
419
420 /// Writes the section properties, which say what the page is. Last thing in the body, as the
421 /// schema requires.
422 fn section(&mut self) -> Outcome<()> {
423 let (w, h) = (fmt!("{}", PAGE_W), fmt!("{}", PAGE_H));
424 let m = fmt!("{}", MARGIN);
425 self.out.open("w:sectPr", &[]);
426 self.out.empty("w:pgSz", &[("w:w", &w), ("w:h", &h)]);
427 self.out.empty("w:pgMar", &[
428 ("w:top", &m),
429 ("w:right", &m),
430 ("w:bottom", &m),
431 ("w:left", &m),
432 ("w:header", "720"),
433 ("w:footer", "720"),
434 ("w:gutter", "0"),
435 ]);
436 self.out.empty("w:cols", &[("w:space", "720")]);
437 res!(self.out.close("w:sectPr"));
438 Ok(())
439 }
440}