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 | |
| 18 | use crate::office::docx::NS_W; |
| 19 | use oxedyne_fe2o3_text::xml::write::Out; |
| 20 | |
| 21 | use oxedyne_fe2o3_core::prelude::*; |
| 22 | |
| 23 | /// The point size, doubled as OOXML counts it, of each heading level. |
| 24 | const 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. |
| 28 | const LEVELS: usize = 9; |
| 29 | |
| 30 | /// The bullet character and the font that draws it, at each of three levels, repeating. |
| 31 | const 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. |
| 38 | pub 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. |
| 151 | pub 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. |
| 186 | fn 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 | } |