Oregami
Repositories/oxedyne/fe2o3

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

8.1 KiB, 7 runs

created by r1870400018:22581, 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//! The two supporting parts a created `.docx` carries: what its styles are, and what its lists look
2//! like.
3//!
4//! Both are generated rather than held as a blob of literal XML, because both are almost entirely
5//! repetition -- six headings that differ by a size and an outline level, nine list levels that
6//! differ by an indent -- and a literal blob is where a typo in level seven waits.
7//!
8//! # Styles are named, not drawn
9//!
10//! A heading is `<w:pStyle w:val="Heading1"/>` and never a bold run at 20 point. The difference shows
11//! the first time somebody opens the document and uses the navigation pane, or generates a table of
12//! contents, or applies their organisation's template: a named heading becomes their heading, and a
13//! bold run stays a bold run forever.
14//!
15//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
16//! Anthropic Claude
17
18use crate::office::docx::NS_W;
19use oxedyne_fe2o3_text::xml::write::Out;
20
21use oxedyne_fe2o3_core::prelude::*;
22
23/// The point size, doubled as OOXML counts it, of each heading level.
24const HEADING_SIZE: [&str; 6] = ["40", "32", "28", "24", "22", "22"];
25
26// How many levels of list nesting are defined. Word's own lists define nine and so does this: a
27// document that nested deeper than its numbering defines would lose its bullets at the bottom.
28const LEVELS: usize = 9;
29
30/// The bullet character and the font that draws it, at each of three levels, repeating.
31const BULLETS: [(&str, &str); 3] = [
32 ("\u{F0B7}", "Symbol"),
33 ("o", "Courier New"),
34 ("\u{F0A7}", "Wingdings"),
35];
36
37/// The styles part: what `Heading1`, `Quote` and the rest mean.
38pub fn styles() -> Outcome<String> {
39 let mut out = Out::declared();
40 out.open("w:styles", &[("xmlns:w", NS_W)]);
41
42 // What every paragraph and every run starts from.
43 out.open("w:docDefaults", &[]);
44 out.open("w:rPrDefault", &[]);
45 out.open("w:rPr", &[]);
46 out.empty("w:rFonts", &[("w:ascii", "Calibri"), ("w:hAnsi", "Calibri"), ("w:cs", "Calibri")]);
47 out.empty("w:sz", &[("w:val", "22")]);
48 out.empty("w:szCs", &[("w:val", "22")]);
49 res!(out.close("w:rPr"));
50 res!(out.close("w:rPrDefault"));
51 out.open("w:pPrDefault", &[]);
52 out.open("w:pPr", &[]);
53 out.empty("w:spacing", &[("w:after", "160"), ("w:line", "259"), ("w:lineRule", "auto")]);
54 res!(out.close("w:pPr"));
55 res!(out.close("w:pPrDefault"));
56 res!(out.close("w:docDefaults"));
57
58 // Normal, which everything else is based on.
59 out.open("w:style", &[("w:type", "paragraph"), ("w:default", "1"), ("w:styleId", "Normal")]);
60 out.empty("w:name", &[("w:val", "Normal")]);
61 out.empty("w:qFormat", &[]);
62 res!(out.close("w:style"));
63
64 // The six headings. `w:name` is the built-in name, lower case and spaced, which is what makes
65 // Word treat these as ITS headings rather than as six styles that happen to be called that.
66 for lvl in 1..=6usize {
67 let id = fmt!("Heading{}", lvl);
68 let name = fmt!("heading {}", lvl);
69 let outline = fmt!("{}", lvl - 1);
70 out.open("w:style", &[("w:type", "paragraph"), ("w:styleId", &id)]);
71 out.empty("w:name", &[("w:val", &name)]);
72 out.empty("w:basedOn", &[("w:val", "Normal")]);
73 out.empty("w:next", &[("w:val", "Normal")]);
74 out.empty("w:qFormat", &[]);
75 out.open("w:pPr", &[]);
76 out.empty("w:keepNext", &[]);
77 out.empty("w:spacing", &[("w:before", "240"), ("w:after", "120")]);
78 out.empty("w:outlineLvl", &[("w:val", &outline)]);
79 res!(out.close("w:pPr"));
80 out.open("w:rPr", &[]);
81 out.empty("w:b", &[]);
82 out.empty("w:sz", &[("w:val", HEADING_SIZE[lvl - 1])]);
83 out.empty("w:szCs", &[("w:val", HEADING_SIZE[lvl - 1])]);
84 res!(out.close("w:rPr"));
85 res!(out.close("w:style"));
86 }
87
88 // A quotation: indented and italic, which is what a reader expects and what the tree says nothing
89 // about. The tree carries that a passage IS a quotation; this is where that acquires a look.
90 out.open("w:style", &[("w:type", "paragraph"), ("w:styleId", "Quote")]);
91 out.empty("w:name", &[("w:val", "Quote")]);
92 out.empty("w:basedOn", &[("w:val", "Normal")]);
93 out.empty("w:next", &[("w:val", "Normal")]);
94 out.empty("w:qFormat", &[]);
95 out.open("w:pPr", &[]);
96 out.empty("w:ind", &[("w:left", "720"), ("w:right", "720")]);
97 res!(out.close("w:pPr"));
98 out.open("w:rPr", &[]);
99 out.empty("w:i", &[]);
100 res!(out.close("w:rPr"));
101 res!(out.close("w:style"));
102
103 // A listing, which is scanned rather than read: monospaced, and without the spacing between
104 // paragraphs that would put a gap between two lines of the same program.
105 out.open("w:style", &[("w:type", "paragraph"), ("w:styleId", "SourceCode")]);
106 out.empty("w:name", &[("w:val", "Source Code")]);
107 out.empty("w:basedOn", &[("w:val", "Normal")]);
108 out.empty("w:qFormat", &[]);
109 out.open("w:pPr", &[]);
110 out.empty("w:spacing", &[("w:after", "0"), ("w:line", "240"), ("w:lineRule", "auto")]);
111 out.empty("w:contextualSpacing", &[]);
112 res!(out.close("w:pPr"));
113 out.open("w:rPr", &[]);
114 out.empty("w:rFonts", &[("w:ascii", "Consolas"), ("w:hAnsi", "Consolas"), ("w:cs", "Consolas")]);
115 out.empty("w:sz", &[("w:val", "20")]);
116 res!(out.close("w:rPr"));
117 res!(out.close("w:style"));
118
119 // The style Word puts on every list item, and which its own list handling looks for.
120 out.open("w:style", &[("w:type", "paragraph"), ("w:styleId", "ListParagraph")]);
121 out.empty("w:name", &[("w:val", "List Paragraph")]);
122 out.empty("w:basedOn", &[("w:val", "Normal")]);
123 out.empty("w:qFormat", &[]);
124 out.open("w:pPr", &[]);
125 out.empty("w:spacing", &[("w:after", "0")]);
126 out.empty("w:contextualSpacing", &[]);
127 res!(out.close("w:pPr"));
128 res!(out.close("w:style"));
129
130 // A link, and a span of code within a line.
131 out.open("w:style", &[("w:type", "character"), ("w:styleId", "Hyperlink")]);
132 out.empty("w:name", &[("w:val", "Hyperlink")]);
133 out.open("w:rPr", &[]);
134 out.empty("w:color", &[("w:val", "0563C1")]);
135 out.empty("w:u", &[("w:val", "single")]);
136 res!(out.close("w:rPr"));
137 res!(out.close("w:style"));
138
139 out.open("w:style", &[("w:type", "character"), ("w:styleId", "InlineCode")]);
140 out.empty("w:name", &[("w:val", "Inline Code")]);
141 out.open("w:rPr", &[]);
142 out.empty("w:rFonts", &[("w:ascii", "Consolas"), ("w:hAnsi", "Consolas"), ("w:cs", "Consolas")]);
143 res!(out.close("w:rPr"));
144 res!(out.close("w:style"));
145
146 res!(out.close("w:styles"));
147 out.finish()
148}
149
150/// The numbering part: one bulleted definition and one numbered one, nine levels each.
151pub fn numbering() -> Outcome<String> {
152 let mut out = Out::declared();
153 out.open("w:numbering", &[("xmlns:w", NS_W)]);
154
155 // Abstract zero: bullets.
156 out.open("w:abstractNum", &[("w:abstractNumId", "0")]);
157 out.empty("w:multiLevelType", &[("w:val", "hybridMultilevel")]);
158 for lvl in 0..LEVELS {
159 let (text, font) = BULLETS[lvl % BULLETS.len()];
160 res!(level(&mut out, lvl, "bullet", text, Some(font)));
161 }
162 res!(out.close("w:abstractNum"));
163
164 // Abstract one: numbers, each level counting on its own.
165 out.open("w:abstractNum", &[("w:abstractNumId", "1")]);
166 out.empty("w:multiLevelType", &[("w:val", "hybridMultilevel")]);
167 for lvl in 0..LEVELS {
168 let text = fmt!("%{}.", lvl + 1);
169 res!(level(&mut out, lvl, "decimal", &text, None));
170 }
171 res!(out.close("w:abstractNum"));
172
173 // The two the document refers to. A `w:numId` is what a paragraph names; the abstract definition
174 // behind it is shared, which is how two lists can be the same shape and count separately.
175 for (num, abstract_id) in [("1", "0"), ("2", "1")] {
176 out.open("w:num", &[("w:numId", num)]);
177 out.empty("w:abstractNumId", &[("w:val", abstract_id)]);
178 res!(out.close("w:num"));
179 }
180
181 res!(out.close("w:numbering"));
182 out.finish()
183}
184
185/// One level of a numbering definition.
186fn level(
187 out: &mut Out,
188 lvl: usize,
189 format: &str,
190 text: &str,
191 font: Option<&str>,
192)
193 -> Outcome<()>
194{
195 let n = fmt!("{}", lvl);
196 let left = fmt!("{}", 720 * (lvl + 1));
197 out.open("w:lvl", &[("w:ilvl", &n)]);
198 out.empty("w:start", &[("w:val", "1")]);
199 out.empty("w:numFmt", &[("w:val", format)]);
200 out.empty("w:lvlText", &[("w:val", text)]);
201 out.empty("w:lvlJc", &[("w:val", "left")]);
202 out.open("w:pPr", &[]);
203 out.empty("w:ind", &[("w:left", &left), ("w:hanging", "360")]);
204 res!(out.close("w:pPr"));
205 if let Some(font) = font {
206 out.open("w:rPr", &[]);
207 out.empty("w:rFonts", &[("w:ascii", font), ("w:hAnsi", font), ("w:hint", "default")]);
208 res!(out.close("w:rPr"));
209 }
210 res!(out.close("w:lvl"));
211 Ok(())
212}