word_ooxml/model.rs
1//! The minimal in-memory model of a WordprocessingML document: a document body is a sequence of
2//! block-level content — paragraphs and tables, interleaved in reading order, matching how
3//! WordprocessingML itself structures `w:body` (`EG_BlockLevelElts`: a mix of `w:p` and `w:tbl`).
4//!
5//! Character formatting (bold/italic/underline on runs), paragraph alignment, inline images, a
6//! default header/footer, simple fields (page numbers), named styles, hyperlinks, numbered/bulleted
7//! lists, footnotes/endnotes, comments, structured document tags (content controls) and a document
8//! theme (colors/fonts) are modeled — see the project roadmap, for what comes next.
9
10/// A `.docx` document: a sequence of block-level content (paragraphs and tables), in reading order,
11/// plus an optional default header/footer and a set of named styles (`word/styles.xml`)
12/// paragraphs/runs can refer to.
13#[derive(Debug, Default, Clone, PartialEq)]
14pub struct Document {
15 /// The blocks that make up the document body, in order.
16 pub blocks: Vec<Block>,
17 /// The document's default header (`w:headerReference[@w:type='default']`), shown on every page
18 /// unless a page-specific header overrides it (`header_first`/ `header_even`, below).
19 pub header: Option<HeaderFooter>,
20 /// The document's default footer, mirroring `header`.
21 pub footer: Option<HeaderFooter>,
22 /// A header shown only on the section's first page (`w:headerReference[@w:type='first']`),
23 /// overriding `header` for that one page. Setting this (or `footer_first`) automatically makes
24 /// the writer emit `w:sectPr/w:titlePg` (`CT_OnOff`) — without it, Word ignores the
25 /// `first`-type reference entirely and just repeats the default header, per ECMA-376.
26 pub header_first: Option<HeaderFooter>,
27 /// A footer shown only on the section's first page, mirroring `header_first`.
28 pub footer_first: Option<HeaderFooter>,
29 /// A header shown only on even-numbered pages (`w:headerReference[@w:type='even']`) —
30 /// `header`/`header_first` become the odd-page headers once this is set. Setting this (or
31 /// `footer_even`) automatically makes the writer emit `w:evenAndOddHeaders` in
32 /// `word/settings.xml` (`CT_OnOff`) — without it, Word ignores the `even`-type reference and
33 /// just repeats the default/odd header on every page, per ECMA-376.
34 pub header_even: Option<HeaderFooter>,
35 /// A footer shown only on even-numbered pages, mirroring `header_even`.
36 pub footer_even: Option<HeaderFooter>,
37 /// This document's page setup — size, orientation, margins, columns (`w:sectPr`'s
38 /// `pgSz`/`pgMar`/`cols`). Unlike `header`/`footer`, this is not optional: WordprocessingML
39 /// always needs *some* page setup, so `Default` gives the exact fixed A4/standard-margins
40 /// values this crate has always written since (`PageSetup::default`), preserving old behavior
41 /// for callers who don't set this. Only a single, document-wide page setup is modeled —
42 /// WordprocessingML actually allows several independent page setups via mid-document section
43 /// breaks (a new `w:sectPr` inside a paragraph's own `w:pPr`), deliberately not modeled here:
44 /// mid-document section breaks are a rare, advanced case, and supporting only a single
45 /// document-wide page setup keeps the model considerably simpler without real demand yet for
46 /// the alternative.
47 pub page_setup: PageSetup,
48 /// Named styles declared for this document (`word/styles.xml`, `CT_Styles`'s `style` elements),
49 /// referenced from paragraphs (`Paragraph.style_id`) and runs (`Run.style_id`) by id. Only
50 /// written if non-empty — see `writer.rs`.
51 pub styles: Vec<Style>,
52 /// Numbering (list) definitions declared for this document (`word/numbering.xml`), referenced
53 /// from paragraphs (`Paragraph.numbering_id`) by id. Only written if non-empty — see
54 /// `writer.rs`.
55 pub numbering_definitions: Vec<NumberingDefinition>,
56 /// Footnotes declared for this document (`word/footnotes.xml`), referenced from the body by id
57 /// (`RunContent::NoteReference`). Only written if non-empty — see `writer.rs`.
58 pub footnotes: Vec<Note>,
59 /// Endnotes declared for this document (`word/endnotes.xml`), mirroring `footnotes`.
60 pub endnotes: Vec<Note>,
61 /// Comments declared for this document (`word/comments.xml`), anchored to a range of the body
62 /// via `Run.comment_ids` — see [`Comment`]'s doc comment for how the underlying
63 /// `w:commentRangeStart`/ `w:commentRangeEnd`/`w:commentReference` markers are derived from
64 /// that field rather than modeled by hand. Only written if non-empty — see `writer.rs`.
65 pub comments: Vec<Comment>,
66 /// This document's theme (`word/theme/theme1.xml`), or `None` to leave it unset entirely — a
67 /// document with no theme part still opens fine in Word, just falling back to whatever theme
68 /// Word's own default template carries. Only written if `Some` — see `writer.rs`, and
69 /// [`Theme`]'s own doc comment for what is/isn't modeled.
70 pub theme: Option<Theme>,
71 /// Whether Word should track changes (insertions/deletions/formatting changes) made to this
72 /// document from now on (`w:trackRevisions` — confirmed via ECMA-376's own `CT_Settings` schema
73 /// fragment and docx4j's generated `CTSettings` binding; despite the UI calling the feature
74 /// "Track Changes", the underlying element is *not* named `w:trackChanges`, easy to get wrong —
75 /// `CT_OnOff`, in `word/settings.xml`). This is only the simple on/off editing-mode toggle —
76 /// matching the equivalent checkbox under Word's Review > Track Changes menu — not a way to
77 /// author individual tracked revisions (`w:ins`/`w:del`/`w:rPrChange`/..) programmatically;
78 /// this crate exposes exactly this one on/off toggle and nothing more — no API here creates
79 /// individual revision marks. `false` (the default) omits `w:trackRevisions` entirely, which is
80 /// equivalent to explicitly disabling it.
81 pub track_changes: bool,
82 /// This document's core properties (`docProps/core.xml`, `CT_CoreProperties` — title, author,
83 /// subject, etc.). Left at its `Default` (every field `None`) writes the same near-empty
84 /// `<cp:coreProperties/>` this crate has always written; setting any field populates just that
85 /// element, since `CT_CoreProperties` is an `xsd:all` group (every child optional, no fixed
86 /// order — confirmed via ECMA-376's own schema fragment, unlike the many strict `xsd:sequence`
87 /// groups this project has had to pin down elsewhere).
88 pub properties: DocumentProperties,
89 /// This document's editing-restriction protection (`w:documentProtection`, `CT_DocProtect`, in
90 /// `word/settings.xml`), or `None` for no restriction. Only the no-password form is modeled
91 /// (`w:enforcement="1"` with no `w:cryptProviderType`/hash/salt) — a password-protected variant
92 /// involves a cryptographic hash of the password plus a random salt, real complexity with no
93 /// proportionate demand seen yet in this project, matching the "simple case first" posture
94 /// already applied elsewhere (e.g. `Run.text_border`'s fixed appearance instead of
95 /// `CT_Border`'s full richness). See [`ProtectionKind`] for which restriction levels are
96 /// modeled.
97 pub protection: Option<ProtectionKind>,
98 /// This document's extended properties (`docProps/app.xml`, `CT_Properties` —
99 /// application-defined metadata, distinct from `docProps/core.xml`'s Dublin Core fields above).
100 /// Only `Company`/`Manager` are modeled — see [`ExtendedProperties`]'s own doc comment for why
101 /// the many statistical fields (`Words`/`Pages`/ `Characters`/..) aren't. Left at its `Default`
102 /// (both fields `None`), this crate still always writes `docProps/app.xml` with just
103 /// `Application` set (unchanged from before this field existed) — see `writer.rs`'s
104 /// `to_extended_properties_xml`.
105 pub extended_properties: ExtendedProperties,
106 /// This document's custom properties (`docProps/custom.xml`, `CT_Properties` — arbitrary
107 /// user-defined name/value metadata pairs). A `Vec` of `(name, value)` pairs, not a map — order
108 /// matters here: it determines the `pid` (property id) each entry gets when written (`2`, `3`,
109 /// `4`. in declaration order; `0`/`1` are reserved by the format and never assigned), so
110 /// insertion order is preserved rather than an arbitrary hash-map order. Only written if
111 /// non-empty, same "optional part" convention as `styles`/`footnotes`/etc. See
112 /// [`CustomPropertyValue`] for which value types are modeled.
113 pub custom_properties: Vec<(String, CustomPropertyValue)>,
114}
115
116/// This document's core properties (`docProps/core.xml`, `CT_CoreProperties`). Every field mirrors
117/// a Dublin Core / Dublin Core Terms element Word's own File > Info panel shows and lets a user
118/// edit — `None` leaves the corresponding XML element out entirely (mirrors Word's own behavior: an
119/// absent element is treated as an empty property, not a validation error). Dates
120/// (`created`/`modified`) are plain ISO-8601/W3CDTF strings (e.g. `"2026-07-17T10:00:00Z"`), not a
121/// dedicated date/time type — this crate has no date/time dependency, and two optional metadata
122/// fields don't justify adding one; the caller is responsible for supplying a valid W3CDTF string,
123/// not validated when writing (same best-effort posture as `Paragraph.numbering_id` referencing a
124/// declaration that may not exist). A handful of `CT_CoreProperties`' children are not modeled here
125/// (`dc:identifier`, `dc:language`, `cp:lastPrinted`, `cp:version`) — niche fields with no real
126/// demand seen yet, same scope-reduction posture as elsewhere in this crate.
127#[derive(Debug, Default, Clone, PartialEq, Eq)]
128pub struct DocumentProperties {
129 /// The document's title (`dc:title`), shown in Word's title bar and File > Info panel.
130 pub title: Option<String>,
131 /// The document's subject (`dc:subject`).
132 pub subject: Option<String>,
133 /// The document's author (`dc:creator`).
134 pub creator: Option<String>,
135 /// Search keywords/tags (`cp:keywords`).
136 pub keywords: Option<String>,
137 /// A longer description of the document (`dc:description`) — shown as "Comments" in Word's own
138 /// File > Info panel despite the underlying element being `dc:description`.
139 pub description: Option<String>,
140 /// The user who last modified the document (`cp:lastModifiedBy`).
141 pub last_modified_by: Option<String>,
142 /// The document's revision number, as a plain string (`cp:revision`, schema-typed as
143 /// `xsd:string`, even though Word itself always populates it with a decimal integer in
144 /// practice).
145 pub revision: Option<String>,
146 /// When the document was created (`dcterms:created`), a W3CDTF string. See this struct's own
147 /// doc comment for the string-not-a-type choice.
148 pub created: Option<String>,
149 /// When the document was last modified (`dcterms:modified`), mirroring `created`.
150 pub modified: Option<String>,
151 /// The document's category (`cp:category`), a free-form classification string distinct from
152 /// `subject`.
153 pub category: Option<String>,
154 /// The document's content status (`cp:contentStatus`, e.g. "Draft", "Final" — a free-form
155 /// string, not a fixed enumeration).
156 pub content_status: Option<String>,
157}
158
159/// Which editing restrictions are enforced on a document (`w:documentProtection/@w:edit`,
160/// `ST_DocProtect`) once `Document.protection` is `Some`. `ST_DocProtect`'s `"none"` value isn't
161/// modeled as its own variant — `Document.protection: Option::None` already expresses "no
162/// restriction", same convention as `Highlight`'s `"none"`/ `VerticalAlign`'s
163/// `"baseline"`/`UnderlineStyle`'s `"none"` elsewhere in this crate.
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub enum ProtectionKind {
166 /// Edits are restricted to regions delimited by matching range permissions — in practice, with
167 /// none declared (this crate doesn't model range permissions), this means no editing at all.
168 ReadOnly,
169 /// Edits are restricted to inserting/deleting comments (plus the same range-permission
170 /// carve-out as `ReadOnly`).
171 Comments,
172 /// Edits are tracked as revisions, and applications shouldn't allow turning tracking back off
173 /// while this is enforced — per ECMA-376, this value "shall imply the presence of the
174 /// `trackRevisions` element", though this crate does not automatically set
175 /// `Document.track_changes` when this variant is used (best-effort, not validated, same posture
176 /// as `Hyperlink::Internal`'s anchor not being checked against a real bookmark).
177 TrackedChanges,
178 /// Edits are restricted to form fields in sections marked as form-protected (this crate doesn't
179 /// model per-section `formProt`, so in practice this behaves like `ReadOnly` for any document
180 /// it writes).
181 Forms,
182}
183
184impl ProtectionKind {
185 /// This kind's `w:edit` attribute value (`ST_DocProtect`).
186 pub fn attribute_value(self) -> &'static str {
187 match self {
188 ProtectionKind::ReadOnly => "readOnly",
189 ProtectionKind::Comments => "comments",
190 ProtectionKind::TrackedChanges => "trackedChanges",
191 ProtectionKind::Forms => "forms",
192 }
193 }
194
195 /// Parses a `w:edit` attribute value (`ST_DocProtect`) into a [`ProtectionKind`], or `None` for
196 /// `"none"` or any unrecognized value — mirrors this crate's usual forgiving-on-read policy.
197 pub fn from_attribute_value(value: &str) -> Option<Self> {
198 match value {
199 "readOnly" => Some(ProtectionKind::ReadOnly),
200 "comments" => Some(ProtectionKind::Comments),
201 "trackedChanges" => Some(ProtectionKind::TrackedChanges),
202 "forms" => Some(ProtectionKind::Forms),
203 _ => None,
204 }
205 }
206}
207
208/// This document's extended properties (`docProps/app.xml`, `CT_Properties` — confirmed via
209/// ECMA-376's own schema fragment to be an `xsd:all` group, same "no fixed order" shape as
210/// `CT_CoreProperties`). Only `Company`/`Manager` are modeled here: the two fields a user actually
211/// edits (shown right next to Title/Author/Category in Word's own Summary tab, alongside the
212/// `docProps/core.xml` fields already modeled by [`DocumentProperties`]). `CT_Properties` has many
213/// more children (`Words`, `Pages`, `Characters`, `Lines`, `Paragraphs`, `TotalTime`,
214/// `Application`, `AppVersion`, `DocSecurity`, `ScaleCrop`, `LinksUpToDate`..) — these are
215/// statistics/environment fields Word itself computes when it saves a document, not meaningful for
216/// a generation library to set by hand, so they're deliberately left out (this crate's `writer.rs`
217/// still always sets `Application` to identify itself as the producer, same as before this struct
218/// existed, but that value isn't exposed as a settable field here).
219#[derive(Debug, Default, Clone, PartialEq, Eq)]
220pub struct ExtendedProperties {
221 /// The document's associated company/organization (`Company`).
222 pub company: Option<String>,
223 /// The document's manager (`Manager`).
224 pub manager: Option<String>,
225}
226
227impl ExtendedProperties {
228 /// Creates an empty set of extended properties (every field `None`).
229 pub fn new() -> Self {
230 Self::default()
231 }
232
233 /// Sets the document's company and returns it for chaining.
234 pub fn with_company(mut self, company: impl Into<String>) -> Self {
235 self.company = Some(company.into());
236 self
237 }
238
239 /// Sets the document's manager and returns it for chaining.
240 pub fn with_manager(mut self, manager: impl Into<String>) -> Self {
241 self.manager = Some(manager.into());
242 self
243 }
244}
245
246/// A single custom document property's value (`docProps/custom.xml`, `CT_Property`'s value
247/// `xsd:choice` — this crate models 4 of its many variant types: `vt:lpwstr` (`Text`), `vt:bool`
248/// (`Bool`), `vt:i4` (`Int`), `vt:r8` (`Number`)). `CT_Property` also allows a date variant
249/// (`vt:filetime`) and several others (vectors, blobs, currency..) not modeled here — same "simple
250/// case first" scope reduction as `DocumentProperties.created`/`.modified` not getting a dedicated
251/// date/time type either (no `chrono` dependency for this crate).
252#[derive(Debug, Clone, PartialEq)]
253pub enum CustomPropertyValue {
254 /// A text value (`vt:lpwstr`).
255 Text(String),
256 /// A boolean value (`vt:bool`).
257 Bool(bool),
258 /// A 32-bit signed integer value (`vt:i4`).
259 Int(i32),
260 /// A 64-bit floating-point value (`vt:r8`).
261 Number(f64),
262}
263
264/// The content of a header or footer: a sequence of block-level content (paragraphs and tables) —
265/// the same content model WordprocessingML gives the document body (`CT_HdrFtr` reuses
266/// `EG_BlockLevelElts`, the exact group `w:body` uses), just without a `w:body` wrapper or section
267/// properties of its own.
268// `Eq` dropped (was `PartialEq, Eq`): transitively contains `Vec<Block>` -> `Block::Paragraph` ->
269// `Run` -> `RunContent::Image`/`RunContent::Chart`, neither `drawing::ShapeProperties` nor
270// `chart::ChartSpace` implementing `Eq` — Same reasoning applies to every other type below in this
271// transitive closure (`Block`, `Paragraph`, `Run`, `Table`/`TableRow`/`TableCell`,
272// `StructuredDocumentTag`, `Note`, `Comment`).
273#[derive(Debug, Default, Clone, PartialEq)]
274pub struct HeaderFooter {
275 /// The blocks that make up this header/footer's content, in order.
276 pub blocks: Vec<Block>,
277}
278
279/// A document's page setup (`w:sectPr`'s `pgSz`/`pgMar`/`cols`, `CT_PageSz`/
280/// `CT_PageMar`/`CT_Columns`) — size, orientation, margins, and an optional equal-width
281/// multi-column layout. See `Document.page_setup`'s doc comment for why only one, document-wide
282/// page setup is modeled (no mid-document section breaks).
283#[derive(Debug, Clone, Copy, PartialEq, Eq)]
284pub struct PageSetup {
285 /// The page's width, in twips (`w:pgSz/@w`).
286 pub width_twips: u32,
287 /// The page's height, in twips (`w:pgSz/@h`).
288 pub height_twips: u32,
289 /// The page's orientation (`w:pgSz/@orient`). Note that real Word output also swaps
290 /// `width_twips`/`height_twips` so the page is actually wider than tall for `Landscape` — this
291 /// crate does not do that automatically (setting `Landscape` alone, without also swapping the
292 /// dimensions, produces a schema-valid but visually portrait-shaped "landscape" page); the
293 /// caller is responsible for passing dimensions that already match the chosen orientation, same
294 /// "no hidden magic" convention as the rest of this crate.
295 pub orientation: Orientation,
296 /// The page's top margin, in twips (`w:pgMar/@top`).
297 pub margin_top_twips: u32,
298 /// The page's bottom margin, in twips (`w:pgMar/@bottom`).
299 pub margin_bottom_twips: u32,
300 /// The page's left margin, in twips (`w:pgMar/@left`).
301 pub margin_left_twips: u32,
302 /// The page's right margin, in twips (`w:pgMar/@right`).
303 pub margin_right_twips: u32,
304 /// The distance from the top of the page to the header's content, in twips (`w:pgMar/@header`).
305 pub margin_header_twips: u32,
306 /// The distance from the bottom of the page to the footer's content, in twips
307 /// (`w:pgMar/@footer`).
308 pub margin_footer_twips: u32,
309 /// Extra binding-side margin, in twips (`w:pgMar/@gutter`) — added to the left margin (or the
310 /// right, under `w:sectPr/@rtlGutter`, not modeled here; always left-side).
311 pub margin_gutter_twips: u32,
312 /// The number of equal-width columns the page's text flows into (`w:cols`, `CT_Columns`), or
313 /// `None` to omit `w:cols` entirely (a single column, Word's default). When `Some`, written as
314 /// `w:cols/@num` with `w:equalWidth="true"` and a fixed `w:space`
315 /// (`DEFAULT_COLUMN_SPACING_TWIPS`, matching Word's own default spacing between columns) —
316 /// `CT_Columns` also allows unequal, individually-sized columns via a repeated `w:col` child,
317 /// not modeled here, same "equal widths only" scope reduction as `Table.column_widths` versus
318 /// per-column-width columns is NOT (`Table.column_widths` sets individual widths; this only
319 /// sets a column *count*, all equal) — kept deliberately simple since the common case
320 /// (newsletter-style equal columns) doesn't need more.
321 pub columns: Option<u32>,
322}
323
324impl Default for PageSetup {
325 /// A4 portrait with the standard margins this crate has always written (fixed since) —
326 /// preserves old behavior exactly for a `Document` that doesn't set `page_setup` explicitly.
327 fn default() -> Self {
328 Self {
329 width_twips: 11_906,
330 height_twips: 16_838,
331 orientation: Orientation::Portrait,
332 margin_top_twips: 1_417,
333 margin_bottom_twips: 1_417,
334 margin_left_twips: 1_417,
335 margin_right_twips: 1_417,
336 margin_header_twips: 708,
337 margin_footer_twips: 708,
338 margin_gutter_twips: 0,
339 columns: None,
340 }
341 }
342}
343
344impl PageSetup {
345 /// Creates a page setup with this crate's previous fixed defaults (A4 portrait, standard
346 /// margins) — the same as [`PageSetup::default`], spelled out as a constructor for consistency
347 /// with every other type in this crate.
348 pub fn new() -> Self {
349 Self::default()
350 }
351
352 /// Sets the page's width/height, in twips, and returns it for chaining.
353 pub fn with_size_twips(mut self, width_twips: u32, height_twips: u32) -> Self {
354 self.width_twips = width_twips;
355 self.height_twips = height_twips;
356 self
357 }
358
359 /// Sets the page's orientation and returns it for chaining. See `orientation`'s doc comment —
360 /// does not swap `width_twips`/ `height_twips` automatically.
361 pub fn with_orientation(mut self, orientation: Orientation) -> Self {
362 self.orientation = orientation;
363 self
364 }
365
366 /// Sets the page's top/bottom/left/right margins, in twips, and returns it for chaining.
367 pub fn with_margins_twips(mut self, top: u32, bottom: u32, left: u32, right: u32) -> Self {
368 self.margin_top_twips = top;
369 self.margin_bottom_twips = bottom;
370 self.margin_left_twips = left;
371 self.margin_right_twips = right;
372 self
373 }
374
375 /// Sets the header/footer margins, in twips, and returns it for chaining.
376 pub fn with_header_footer_margins_twips(mut self, header: u32, footer: u32) -> Self {
377 self.margin_header_twips = header;
378 self.margin_footer_twips = footer;
379 self
380 }
381
382 /// Sets the binding-side gutter margin, in twips, and returns it for chaining.
383 pub fn with_gutter_twips(mut self, gutter: u32) -> Self {
384 self.margin_gutter_twips = gutter;
385 self
386 }
387
388 /// Sets the number of equal-width columns and returns it for chaining. See `columns`'s doc
389 /// comment.
390 pub fn with_columns(mut self, columns: u32) -> Self {
391 self.columns = Some(columns);
392 self
393 }
394}
395
396/// A page's orientation (`w:pgSz/@orient`, `ST_PageOrientation` — exactly 2 values).
397#[derive(Debug, Clone, Copy, PartialEq, Eq)]
398pub enum Orientation {
399 Portrait,
400 Landscape,
401}
402
403impl Orientation {
404 /// This orientation's `w:pgSz`/`orient` value (`ST_PageOrientation`).
405 pub fn attribute_value(self) -> &'static str {
406 match self {
407 Orientation::Portrait => "portrait",
408 Orientation::Landscape => "landscape",
409 }
410 }
411
412 /// Parses a `w:pgSz`/`orient` value (`ST_PageOrientation`) back into an [`Orientation`], or
413 /// `None` for an unrecognized value.
414 pub fn from_attribute_value(value: &str) -> Option<Self> {
415 match value {
416 "portrait" => Some(Orientation::Portrait),
417 "landscape" => Some(Orientation::Landscape),
418 _ => None,
419 }
420 }
421}
422
423/// A single block-level element of a document body: a paragraph, a table, or a structured document
424/// tag (content control) wrapping more blocks. Real WordprocessingML documents freely interleave
425/// these (e.g. a paragraph, then a table, then another paragraph), so this is modeled as a sequence
426/// of blocks rather than separate lists per kind — matching
427/// `EG_ContentBlockContent`/`EG_BlockLevelElts`, the actual schema group
428/// `w:body`/`w:sdtContent`/etc. share.
429#[derive(Debug, Clone, PartialEq)]
430pub enum Block {
431 Paragraph(Paragraph),
432 Table(Table),
433 StructuredDocumentTag(StructuredDocumentTag),
434}
435
436/// A paragraph: a sequence of runs, plus paragraph-level formatting.
437#[derive(Debug, Default, Clone, PartialEq)]
438pub struct Paragraph {
439 /// The runs that make up this paragraph, in order.
440 pub runs: Vec<Run>,
441 /// The paragraph's horizontal alignment, or `None` to leave it unspecified (inherits from the
442 /// paragraph style / Word's default). Overrides the alignment of `style_id`'s style, if any.
443 pub alignment: Option<Alignment>,
444 /// The id of a paragraph-type [`Style`] to apply (`w:pStyle`), or `None` to use the document's
445 /// default paragraph style. The referenced style must actually be declared in `Document.styles`
446 /// for the document to be well-formed — this is not validated when writing.
447 pub style_id: Option<String>,
448 /// The id of a [`NumberingDefinition`] to apply (`w:numPr/w:numId`), making this paragraph an
449 /// item of that list, or `None` for a regular paragraph. The referenced definition must
450 /// actually be declared in `Document.numbering_definitions` for the document to be well-formed
451 /// — not validated when writing, same convention as `style_id`.
452 pub numbering_id: Option<u32>,
453 /// Which indent level of `numbering_id`'s list this paragraph is at (`w:numPr/w:ilvl`,
454 /// 0-indexed). Only meaningful when `numbering_id` is `Some`; must be within the range of
455 /// levels the referenced [`NumberingDefinition`] actually declares — not validated when
456 /// writing.
457 pub numbering_level: u8,
458 /// This paragraph's line spacing (`w:spacing/@line`, written together with
459 /// `w:spacing/@lineRule="auto"`), or `None` to leave it unspecified. Unlike
460 /// `space_before`/`space_after` (raw twips), this is in `w:spacing`'s "auto" units — 240ths of
461 /// a single line, so `240` is single spacing, `360` is 1.5 lines, `480` is double — matching
462 /// what Word's own line-spacing dropdown offers. The twips-based `atLeast`/`exact` line-height
463 /// modes (`w:lineRule`'s other two values, meant for exact point-based line heights rather than
464 /// a multiple of the current font) aren't modeled.
465 pub line_spacing: Option<u32>,
466 /// Extra space before this paragraph, in twips (`w:spacing/@before`, `ST_TwipsMeasure`,
467 /// twentieths of a point) — raw units, no conversion, matching `Run.character_spacing`'s
468 /// precedent. `None` leaves it unspecified. `w:spacing/@beforeAutospacing` (letting Word
469 /// compute it automatically instead) isn't modeled.
470 pub space_before: Option<u32>,
471 /// Extra space after this paragraph, in twips, mirroring `space_before`.
472 pub space_after: Option<u32>,
473 /// This paragraph's background shading color (`w:shd/@fill`), mirroring `Run.shading_color` —
474 /// see that field's doc comment for the same `val`/`color` fixed-pair convention.
475 pub shading_color: Option<String>,
476 /// Whether this paragraph has a border around it (`w:pBdr`), mirroring `Run.text_border` — a
477 /// simple on/off switch with the same fixed appearance (`val="single" sz="4" space="0"
478 /// color="auto"`), applied to all four sides (`top`/`left`/`bottom`/`right`). `CT_PBdr`'s
479 /// `between` (a rule drawn between paragraphs sharing this same border, when several
480 /// consecutive paragraphs all have it) and `bar` (a vertical bar in the margin, used for
481 /// revision-style paragraph marking) aren't modeled, same scope-reduction posture as
482 /// `text_border` not exposing all of `CT_Border`'s richness.
483 pub border: bool,
484 /// Whether this paragraph should stay on the same page as the one following it (`w:keepNext`,
485 /// `CT_OnOff`) — Word moves both to the next page together rather than letting a page break
486 /// fall between them. Commonly set on heading paragraphs, so a heading never ends up alone at
487 /// the bottom of a page with its content starting on the next one.
488 pub keep_with_next: bool,
489 /// Whether every line of this paragraph must stay together on the same page (`w:keepLines`,
490 /// `CT_OnOff`) — prevents the paragraph itself from being split across a page break partway
491 /// through.
492 pub keep_lines_together: bool,
493 /// Whether this paragraph always starts on a new page (`w:pageBreakBefore`, `CT_OnOff`) — a
494 /// page break is forced immediately before it, regardless of how much room is left on the
495 /// current page. Distinct from `RunContent::Break(BreakKind::Page)`, which inserts an explicit
496 /// page break wherever it appears within a run's content, mid-paragraph; this is a
497 /// paragraph-level property instead.
498 pub page_break_before: bool,
499 /// This paragraph's custom tab stops (`w:tabs`, `CT_Tabs`), in order. Left empty (the default)
500 /// to fall back entirely to Word's own default tab stops (evenly spaced, typically every 720
501 /// twips/half inch) — setting even one custom stop here does not disable Word's defaults beyond
502 /// it, per ECMA-376.
503 pub tabs: Vec<TabStop>,
504 /// Whether to ignore the spacing above/below this paragraph when the paragraph immediately
505 /// before/after it shares the same paragraph style (`w:contextualSpacing`, `CT_OnOff`) —
506 /// commonly set on list item styles, so consecutive list items don't get extra vertical gaps
507 /// between them even if the style itself sets `space_before`/ `space_after`.
508 pub contextual_spacing: bool,
509}
510
511/// A single custom tab stop (`w:tab`, `CT_TabStop`), one entry of `Paragraph.tabs`.
512#[derive(Debug, Clone, Copy, PartialEq, Eq)]
513pub struct TabStop {
514 /// The tab stop's position, in twips, measured from the paragraph's left margin (`w:tab/@pos`,
515 /// `ST_SignedTwipsMeasure` — signed, since a tab stop can be positioned left of the margin
516 /// under a negative indent).
517 pub position_twips: i32,
518 /// How text is aligned relative to this tab stop (`w:tab/@val`).
519 pub alignment: TabStopAlignment,
520 /// The leader character repeated between the preceding text and this tab stop
521 /// (`w:tab/@leader`), or `None` for a blank leader (Word's default when the attribute is
522 /// omitted).
523 pub leader: Option<TabLeader>,
524}
525
526impl TabStop {
527 /// Creates a left-aligned tab stop at the given position, with no leader — the most common
528 /// case.
529 pub fn new(position_twips: i32) -> Self {
530 Self {
531 position_twips,
532 alignment: TabStopAlignment::Left,
533 leader: None,
534 }
535 }
536
537 /// Sets this tab stop's alignment and returns it for chaining.
538 pub fn with_alignment(mut self, alignment: TabStopAlignment) -> Self {
539 self.alignment = alignment;
540 self
541 }
542
543 /// Sets this tab stop's leader character and returns it for chaining.
544 pub fn with_leader(mut self, leader: TabLeader) -> Self {
545 self.leader = Some(leader);
546 self
547 }
548}
549
550/// How text is aligned relative to a [`TabStop`] (`w:tab/@val`, `ST_TabJc`).
551///
552/// `ST_TabJc` also defines `clear` (removes an inherited tab stop from a base style, not meaningful
553/// when authoring a fresh document) and `num` (a legacy "list tab" tied to outline numbering) —
554/// neither modeled here, same scope reduction as elsewhere in this crate.
555#[derive(Debug, Clone, Copy, PartialEq, Eq)]
556pub enum TabStopAlignment {
557 Left,
558 Center,
559 Right,
560 Decimal,
561 Bar,
562}
563
564impl TabStopAlignment {
565 /// This alignment's `w:tab`/`val` value (`ST_TabJc`).
566 pub fn attribute_value(self) -> &'static str {
567 match self {
568 TabStopAlignment::Left => "left",
569 TabStopAlignment::Center => "center",
570 TabStopAlignment::Right => "right",
571 TabStopAlignment::Decimal => "decimal",
572 TabStopAlignment::Bar => "bar",
573 }
574 }
575
576 /// Parses a `w:tab`/`val` value (`ST_TabJc`) back into a [`TabStopAlignment`], or `None` for
577 /// `clear`/`num`/an unrecognized value.
578 pub fn from_attribute_value(value: &str) -> Option<Self> {
579 match value {
580 "left" => Some(TabStopAlignment::Left),
581 "center" => Some(TabStopAlignment::Center),
582 "right" => Some(TabStopAlignment::Right),
583 "decimal" => Some(TabStopAlignment::Decimal),
584 "bar" => Some(TabStopAlignment::Bar),
585 _ => None,
586 }
587 }
588}
589
590/// A [`TabStop`]'s leader character (`w:tab/@leader`, `ST_TabTlc`) — repeated between the preceding
591/// text and the tab stop itself (e.g. dotted leaders in a table of contents).
592///
593/// `ST_TabTlc` also defines `none`, explicitly clearing any inherited leader — not modeled as its
594/// own variant here, since `TabStop.leader: Option<TabLeader>` already expresses "no leader" via
595/// `None`, same convention as [`Highlight`]'s own `"none"` value.
596#[derive(Debug, Clone, Copy, PartialEq, Eq)]
597pub enum TabLeader {
598 Dot,
599 Hyphen,
600 Underscore,
601 Heavy,
602 MiddleDot,
603}
604
605impl TabLeader {
606 /// This leader's `w:tab`/`leader` value (`ST_TabTlc`).
607 pub fn attribute_value(self) -> &'static str {
608 match self {
609 TabLeader::Dot => "dot",
610 TabLeader::Hyphen => "hyphen",
611 TabLeader::Underscore => "underscore",
612 TabLeader::Heavy => "heavy",
613 TabLeader::MiddleDot => "middleDot",
614 }
615 }
616
617 /// Parses a `w:tab`/`leader` value (`ST_TabTlc`) back into a [`TabLeader`], or `None` for
618 /// `"none"` or an unrecognized value.
619 pub fn from_attribute_value(value: &str) -> Option<Self> {
620 match value {
621 "dot" => Some(TabLeader::Dot),
622 "hyphen" => Some(TabLeader::Hyphen),
623 "underscore" => Some(TabLeader::Underscore),
624 "heavy" => Some(TabLeader::Heavy),
625 "middleDot" => Some(TabLeader::MiddleDot),
626 _ => None,
627 }
628 }
629}
630
631/// A run: a contiguous span of content (text, an inline image, or a field) sharing the same
632/// formatting.
633#[derive(Debug, Default, Clone, PartialEq)]
634pub struct Run {
635 /// The run's content.
636 pub content: RunContent,
637 /// Whether the run is bold (`<w:b/>`, WordprocessingML `CT_RPr`). Meaningful for
638 /// [`RunContent::Text`] and [`RunContent::Field`]; ignored when writing an image run.
639 pub bold: bool,
640 /// Whether the run is italic (`<w:i/>`).
641 pub italic: bool,
642 /// The run's underline style (`<w:u w:val="..">`, `CT_Underline`), or `None` for no underline.
643 /// See [`UnderlineStyle`]'s doc comment for the full palette of styles (`ST_Underline`).
644 pub underline: Option<UnderlineStyle>,
645 /// The color of `underline`'s line (`w:u`'s `color` attribute), as a plain 6-digit RGB hex
646 /// string, or `None` to leave it unset (matches the text color — visually the same outcome as
647 /// `CT_Underline`'s other `color` choice, the literal string `"auto"`, which isn't modeled
648 /// separately, same convention as `Run.color`). Only meaningful when `underline` is `Some`;
649 /// theme-relative colors (`w:themeColor`/`w:themeTint`/`w:themeShade`) aren't modeled either,
650 /// same scope limit as `color` itself.
651 pub underline_color: Option<String>,
652 /// The run's text color (`<w:color w:val="..">`, `CT_Color`), as a plain 6-digit RGB hex string
653 /// (no leading `#`, e.g. `"FF0000"`), or `None` to leave it unset (inherits from the
654 /// paragraph/character style, or Word's own default — visually the same outcome as `CT_Color`'s
655 /// other `val` choice, the literal string `"auto"`, which isn't modeled separately since
656 /// omitting the element entirely already achieves it). Theme-relative colors
657 /// (`w:themeColor`/`w:themeTint`/ `w:themeShade`, `CT_Color`'s other attributes) aren't modeled
658 /// either — same scope limit as [`Theme`] itself, which only lets a document's theme be *set*,
659 /// not referenced from formatting.
660 pub color: Option<String>,
661 /// The run's font size in whole points (e.g. `12`), or `None` to leave it unset. Written as
662 /// `<w:sz w:val="..">`/`<w:szCs w:val="..">` (`CT_HpsMeasure`, in half-points — `24` for a 12pt
663 /// run — the writer does the doubling), both set to the same value since complex-script sizing
664 /// isn't modeled separately. WordprocessingML's `w:sz` actually allows half-point granularity
665 /// (e.g. 10.5pt), but only whole points are modeled here — no real need for finer granularity
666 /// has come up.
667 pub font_size: Option<u16>,
668 /// The run's font family/typeface (e.g. `"Calibri"`), or `None` to leave it unset. Written as
669 /// `<w:rFonts w:ascii=".." w:hAnsi="..">` (`CT_Fonts`), both set to the same value;
670 /// `w:eastAsia`/`w:cs` (East Asian / complex-script typefaces) aren't modeled, same scope limit
671 /// as [`FontScheme`]'s Latin-only typefaces.
672 pub font_family: Option<String>,
673 /// The run's highlight color (`<w:highlight w:val="..">`, `CT_Highlight` — a fixed
674 /// "highlighter" palette, distinct from `color`), or `None` for no highlighting. See
675 /// [`Highlight`]'s doc comment.
676 pub highlight: Option<Highlight>,
677 /// Whether the run is struck through (`<w:strike/>`, `CT_OnOff`) — a single horizontal line
678 /// through the text. WordprocessingML also defines `w:dstrike` (double strikethrough), not
679 /// modeled here.
680 pub strike: bool,
681 /// The run's vertical alignment (`<w:vertAlign w:val="..">`, `CT_VerticalAlignRun`) —
682 /// superscript or subscript, or `None` for the normal baseline position. See
683 /// [`VerticalAlign`]'s doc comment.
684 pub vertical_align: Option<VerticalAlign>,
685 /// Whether lowercase letters in the run are displayed as small capital letters
686 /// (`<w:smallCaps/>`, `CT_OnOff`) — a display-only effect, the underlying text isn't changed.
687 /// Distinct from `all_caps`, which affects lowercase AND uppercase letters the same way.
688 pub small_caps: bool,
689 /// Whether all characters in the run are displayed as capital letters (`<w:caps/>`, `CT_OnOff`)
690 /// — like `small_caps`, a display-only effect. `small_caps` and `all_caps` are mutually
691 /// exclusive in practice (Word's UI won't let both apply to the same text at once), but that
692 /// isn't enforced here — setting both just lets `all_caps` visually win, matching real Word
693 /// rendering.
694 pub all_caps: bool,
695 /// The run's background shading color (`<w:shd w:val="clear" w:color="auto" w:fill="..">`,
696 /// `CT_Shd`), as a plain 6-digit RGB hex string, or `None` for no shading. Unlike `highlight`'s
697 /// fixed 16-color "highlighter" palette, this is an arbitrary color, closer to a text
698 /// background fill — `val`/`color` (the shading pattern and its foreground color) are always
699 /// written as the fixed `"clear"`/`"auto"` pair for a plain solid fill, matching this crate's
700 /// convention for schema-mandatory-but-out-of-scope attributes (see `Theme`'s `fmtScheme`);
701 /// patterned shading (crosshatch, stripes, etc.) and `w:themeFill` aren't modeled.
702 pub shading_color: Option<String>,
703 /// Whether the run has a border around its text (`<w:bdr w:val="single" w:sz="4" w:space="0"
704 /// w:color="auto"/>`, `CT_Border`) — a simple on/off flag rather than exposing `CT_Border`'s
705 /// full richness (border style, width, spacing, color, shadow, frame effect), since this
706 /// property is niche enough that a single sensible fixed appearance (thin single black line, no
707 /// gap) covers the common case; broader configurability can be added later if a real need comes
708 /// up, same posture as `strike` not modeling `w:dstrike`.
709 pub text_border: bool,
710 /// Character spacing adjustment (`<w:spacing w:val="..">`, `CT_SignedTwipsMeasure`), in twips
711 /// (twentieths of a point) — positive expands letter spacing, negative condenses it — or `None`
712 /// to leave it unset.
713 pub character_spacing: Option<i16>,
714 /// The run's vertical position, raised or lowered from the normal baseline (`<w:position
715 /// w:val="..">`, `CT_SignedHpsMeasure`), in half-points — positive raises, negative lowers — or
716 /// `None` to leave it at the baseline. Distinct from `vertical_align` (superscript/subscript):
717 /// `position` shifts the baseline without shrinking the font size, `vertical_align` does both
718 /// together.
719 pub vertical_position: Option<i16>,
720 /// Whether the run's text is hidden (`<w:vanish/>`, `CT_OnOff`) — present in the document but
721 /// not displayed or printed by default (Word's Home > Show/Hide formatting marks toggle reveals
722 /// it).
723 pub hidden: bool,
724 /// The id of a character-type [`Style`] to apply (`w:rStyle`), or `None`. Like
725 /// `Paragraph.style_id`, not validated against `Document.styles` when writing.
726 pub style_id: Option<String>,
727 /// A hyperlink to apply to this run (`w:hyperlink`), or `None`. See [`Hyperlink`]'s doc comment
728 /// for how this reconciles with the underlying XML, where a hyperlink actually wraps a run
729 /// rather than being one of its properties.
730 pub hyperlink: Option<Hyperlink>,
731 /// The ids of the [`Comment`]s (`Document.comments`) whose range covers this run — usually
732 /// empty, usually a single id, but a run can fall inside several overlapping comment ranges at
733 /// once (real Word documents do have overlapping comments), hence a `Vec` rather than an
734 /// `Option<u32>`.
735 ///
736 /// Strictly, in the underlying XML, a comment range is not a run property either:
737 /// `w:commentRangeStart`/`w:commentRangeEnd` (`CT_MarkupRange`) are empty siblings of `w:r`
738 /// marking where a range starts/ends, and `w:commentReference` (the balloon anchor mark) is its
739 /// own run placed right after the matching `w:commentRangeEnd` — none of which the caller has
740 /// to place by hand here, unlike [`NoteReference`]. The writer derives all three automatically:
741 /// consecutive runs sharing an id become one `w:commentRangeStart`/`w:commentRangeEnd` pair
742 /// spanning them (not one pair per run — that would emit the same id's start/end repeatedly,
743 /// breaking the pairing `CT_MarkupRange`'s doc comment describes), with the matching
744 /// `w:commentReference` written immediately after the range closes (see `write_paragraph` in
745 /// `writer.rs`). Only comment ranges within a single paragraph are supported — WordprocessingML
746 /// does allow a range to span several paragraphs or even wrap a whole table ("cross structure"
747 /// annotations, ECMA-376 §2.13.4), out of scope for now, the same kind of deliberate scope
748 /// limit as header/footer's "default"-only variant.
749 pub comment_ids: Vec<u32>,
750 /// The [`Bookmark`]s whose range covers this run — same range-tracking convention as
751 /// `comment_ids`, but each bookmark's id/name pair travels directly with the run rather than
752 /// being centralized in a separate `Document`-level list: unlike a comment reference (which
753 /// only ever needs an id, since the name/author/date lives in `word/comments.xml`, a whole
754 /// separate part), `w:bookmarkStart` carries its `w:name` right there on the inline marker
755 /// itself, so there is no separate part `write_paragraph` would need to consult.
756 ///
757 /// Strictly, in the underlying XML, a bookmark range is not a run property either —
758 /// `w:bookmarkStart`/`w:bookmarkEnd` (`CT_Bookmark`/ `CT_MarkupRange`) are empty siblings of
759 /// `w:r`, members of the same `EG_PContent` choice group as `w:commentRangeStart`/
760 /// `commentRangeEnd`, so the writer derives them the same way: runs sharing a bookmark id
761 /// (repeated by the caller on each one, same asymmetry as `comment_ids`) become one
762 /// `w:bookmarkStart`/ `w:bookmarkEnd` pair spanning them. Only bookmark ranges within a single
763 /// paragraph are supported (not a range spanning several paragraphs, nor a zero-width "point"
764 /// bookmark with no run content at all) — same deliberate scope limit as `comment_ids`.
765 pub bookmarks: Vec<Bookmark>,
766}
767
768/// A named bookmark (`w:bookmarkStart`'s `w:name`, `CT_Bookmark`), attached to one or more runs via
769/// `Run.bookmarks` to mark which run(s) fall within its range. `Hyperlink::Internal`'s anchor name
770/// is meant to match a bookmark's `name` here for the resulting link to actually go anywhere in
771/// Word — not validated when writing (same best-effort posture as `Paragraph.numbering_id`
772/// referencing a declaration that may not exist).
773#[derive(Debug, Clone, PartialEq, Eq)]
774pub struct Bookmark {
775 /// This bookmark's id (`w:bookmarkStart`/`w:bookmarkEnd`'s `w:id`), pairing a start with its
776 /// matching end — purely an internal wiring detail, never referenced from outside the document
777 /// (unlike `name`, which is what `Hyperlink::Internal`'s anchor actually points to).
778 pub id: u32,
779 /// This bookmark's name (`w:bookmarkStart`'s `w:name`), what `Hyperlink::Internal(name)` refers
780 /// to.
781 pub name: String,
782}
783
784impl Bookmark {
785 /// Creates a new bookmark with the given id and name. The id only needs to be unique among a
786 /// paragraph's currently-open bookmarks (mirroring `comment_ids`'s own uniqueness scope) — this
787 /// crate does not track or validate uniqueness across the whole document.
788 pub fn new(id: u32, name: impl Into<String>) -> Self {
789 Self {
790 id,
791 name: name.into(),
792 }
793 }
794}
795
796/// A hyperlink attached to a run (`w:hyperlink`, `CT_Hyperlink`): either a link to an external URL,
797/// or a link to a bookmark elsewhere in the same document.
798///
799/// Strictly, in the underlying XML, a hyperlink is not a run property: `<w:hyperlink>` wraps its
800/// own nested run(s) (`CT_Hyperlink`'s content is `EG_PContent`, the very same group `w:p` itself
801/// uses), making it a sibling of `<w:r>` in a paragraph's content, much like [`Field`]'s
802/// `<w:fldSimple>`. It is modeled here as a property on [`Run`] instead (alongside `style_id`,
803/// `bold`..) for a simpler, more consistent public API — the run's own text/formatting still apply
804/// normally, the hyperlink just adds link behavior on top. When several consecutive runs each set a
805/// hyperlink, every one of them is written as its own separate `<w:hyperlink>` (and, for an
806/// external link, its own OPC relationship, even when two runs happen to share the same URL) rather
807/// than merged into a single wrapper — simpler and safer than trying to detect and dedupe repeated
808/// URLs across runs.
809#[derive(Debug, Clone, PartialEq, Eq)]
810pub enum Hyperlink {
811 /// A link to an external URL (`r:id`, an OPC relationship with `TargetMode="External"` — the
812 /// target is never resolved or validated as a package part, unlike an image's `r:embed`).
813 External(String),
814 /// A link to a bookmark elsewhere in the same document (`w:anchor`), with no relationship at
815 /// all. The anchor name is meant to match a [`Bookmark::name`] set via `Run.bookmarks`
816 /// somewhere in the document, but this is not validated when writing — an `Internal` hyperlink
817 /// to a name that isn't actually bookmarked anywhere still produces a well-formed `.docx`, just
818 /// a dead link in Word.
819 Internal(String),
820}
821
822/// A named style declared in `word/styles.xml` (`CT_Style`), applied to paragraphs or runs by id
823/// (`Paragraph.style_id`/`Run.style_id`).
824///
825/// Only the handful of fields covering the common case — a name, an optional base style to inherit
826/// from, and the same character/paragraph formatting already modeled on [`Run`]/[`Paragraph`] — are
827/// modeled. `CT_Style` has many more (`next`, `link`, `hidden`, `uiPriority`, table-style-specific
828/// properties..), out of scope for now; a style marked as the type's default (`CT_Style`'s
829/// `default` attribute) isn't modeled either — Word falls back to its own built-in default when
830/// none is declared, which is sufficient for a first version.
831#[derive(Debug, Clone, PartialEq, Eq)]
832pub struct Style {
833 /// The style's id (`w:styleId`), referenced by `Paragraph.style_id`/ `Run.style_id`.
834 pub id: String,
835 /// The style's human-readable name (`w:name`), shown in Word's style picker.
836 pub name: String,
837 /// Whether this is a paragraph or character style (`w:type`, `ST_StyleType` — only these two of
838 /// its four values are modeled; `table`/`numbering` styles are out of scope for now).
839 pub kind: StyleKind,
840 /// The id of another style this one inherits from (`w:basedOn`), or `None`.
841 pub based_on: Option<String>,
842 /// Whether text in this style is bold. Meaningful for both style kinds: a paragraph style's run
843 /// formatting is the default for text typed in that paragraph, unless a run/character style
844 /// overrides it.
845 pub bold: bool,
846 /// Whether text in this style is italic.
847 pub italic: bool,
848 /// This style's underline style, mirroring `Run.underline`.
849 pub underline: Option<UnderlineStyle>,
850 /// This style's underline color, mirroring `Run.underline_color`.
851 pub underline_color: Option<String>,
852 /// This style's text color, mirroring `Run.color` — see that field's doc comment.
853 pub color: Option<String>,
854 /// This style's font size in whole points, mirroring `Run.font_size`.
855 pub font_size: Option<u16>,
856 /// This style's font family/typeface, mirroring `Run.font_family`.
857 pub font_family: Option<String>,
858 /// This style's highlight color, mirroring `Run.highlight`.
859 pub highlight: Option<Highlight>,
860 /// Whether text in this style is struck through, mirroring `Run.strike`.
861 pub strike: bool,
862 /// This style's vertical alignment, mirroring `Run.vertical_align`.
863 pub vertical_align: Option<VerticalAlign>,
864 /// Whether text in this style is displayed as small caps, mirroring `Run.small_caps`.
865 pub small_caps: bool,
866 /// Whether text in this style is displayed as all caps, mirroring `Run.all_caps`.
867 pub all_caps: bool,
868 /// This style's background shading color, mirroring `Run.shading_color`.
869 pub shading_color: Option<String>,
870 /// Whether text in this style has a border, mirroring `Run.text_border`.
871 pub text_border: bool,
872 /// This style's character spacing adjustment, mirroring `Run.character_spacing`.
873 pub character_spacing: Option<i16>,
874 /// This style's vertical position, mirroring `Run.vertical_position`.
875 pub vertical_position: Option<i16>,
876 /// Whether text in this style is hidden, mirroring `Run.hidden`.
877 pub hidden: bool,
878 /// The paragraph alignment this style sets. Only meaningful for [`StyleKind::Paragraph`];
879 /// ignored (not written) for a character style, which has no paragraph-level formatting of its
880 /// own.
881 pub alignment: Option<Alignment>,
882 /// This style's line spacing, mirroring `Paragraph.line_spacing`. Only meaningful for
883 /// [`StyleKind::Paragraph`], same restriction as `alignment`.
884 pub line_spacing: Option<u32>,
885 /// This style's extra space before a paragraph, mirroring `Paragraph.space_before`. Only
886 /// meaningful for [`StyleKind::Paragraph`].
887 pub space_before: Option<u32>,
888 /// This style's extra space after a paragraph, mirroring `Paragraph.space_after`. Only
889 /// meaningful for [`StyleKind::Paragraph`].
890 pub space_after: Option<u32>,
891 /// This style's paragraph background shading color, mirroring `Paragraph.shading_color`.
892 /// Distinct from `shading_color` above (that one mirrors `Run.shading_color`, a run-level
893 /// `w:shd` inside `w:rPr`) — this is the paragraph-level `w:shd` inside `w:pPr`; a paragraph
894 /// style can carry both at once (default run shading AND a paragraph background), which is why
895 /// they need separate fields. Only meaningful for [`StyleKind::Paragraph`].
896 pub paragraph_shading_color: Option<String>,
897 /// Whether this style draws a border around the paragraph, mirroring `Paragraph.border`.
898 /// Distinct from `text_border` above for the same reason as `paragraph_shading_color` vs.
899 /// `shading_color` — a paragraph-level `w:pBdr` inside `w:pPr`, not the run-level `w:bdr`
900 /// inside `w:rPr`. Only meaningful for [`StyleKind::Paragraph`].
901 pub paragraph_border: bool,
902 /// This style's keep-with-next setting, mirroring `Paragraph.keep_with_next`. Only meaningful
903 /// for [`StyleKind::Paragraph`] — commonly set directly on a built-in "Heading" style rather
904 /// than on every individual heading paragraph.
905 pub keep_with_next: bool,
906 /// This style's keep-lines-together setting, mirroring `Paragraph.keep_lines_together`. Only
907 /// meaningful for [`StyleKind::Paragraph`].
908 pub keep_lines_together: bool,
909 /// This style's page-break-before setting, mirroring `Paragraph.page_break_before`. Only
910 /// meaningful for [`StyleKind::Paragraph`].
911 pub page_break_before: bool,
912 /// This style's custom tab stops, mirroring `Paragraph.tabs`. Only meaningful for
913 /// [`StyleKind::Paragraph`].
914 pub tabs: Vec<TabStop>,
915 /// This style's contextual-spacing setting, mirroring `Paragraph.contextual_spacing`. Only
916 /// meaningful for [`StyleKind::Paragraph`] — this is exactly the property Word's own built-in
917 /// "List Paragraph" style sets, so consecutive list items don't get extra gaps between them.
918 pub contextual_spacing: bool,
919}
920
921/// Which kind of content a [`Style`] applies to (`ST_StyleType`).
922#[derive(Debug, Clone, Copy, PartialEq, Eq)]
923pub enum StyleKind {
924 /// Applies to a whole paragraph (`Paragraph.style_id`).
925 Paragraph,
926 /// Applies to a run (`Run.style_id`).
927 Character,
928}
929
930impl StyleKind {
931 /// This kind's `w:type` attribute value (`ST_StyleType`).
932 pub fn attribute_value(self) -> &'static str {
933 match self {
934 StyleKind::Paragraph => "paragraph",
935 StyleKind::Character => "character",
936 }
937 }
938}
939
940/// A numbering (list) definition declared in `word/numbering.xml`, referenced from paragraphs
941/// (`Paragraph.numbering_id`) by id.
942///
943/// WordprocessingML actually splits a list definition into two linked elements: `w:abstractNum`
944/// (the real level formatting, `CT_AbstractNum`) and `w:num` (`CT_Num`, a thin indirection
945/// paragraphs reference by `numId`, pointing at an `abstractNumId`) — this indirection exists so
946/// several `w:num` entries can share one `w:abstractNum`, which real Word documents do use, but
947/// which this crate doesn't need for the common case of one standalone list. So this type collapses
948/// both into one value: `id` corresponds to `w:num`'s `numId` (what a paragraph actually
949/// references), and a dedicated `w:abstractNum` is always written alongside it internally — see
950/// `writer.rs`. Reading back a real, externally-authored document that DOES share one
951/// `w:abstractNum` across several `w:num` entries still works (each `w:num` becomes its own
952/// [`NumberingDefinition`], with its `levels` resolved through whichever `w:abstractNum` it points
953/// to), just without preserving that sharing structure on a round trip.
954#[derive(Debug, Clone, PartialEq, Eq)]
955pub struct NumberingDefinition {
956 /// This definition's id (`w:num`'s `w:numId`), referenced by `Paragraph.numbering_id`.
957 pub id: u32,
958 /// The formatting for each indent level this list defines, in order (index 0 is the outermost
959 /// level, `w:lvl`'s `ilvl="0"`). WordprocessingML allows up to 9 (`ilvl` 0-8); most real lists
960 /// only need 1-3.
961 pub levels: Vec<ListLevel>,
962}
963
964/// A single indent level of a [`NumberingDefinition`] (`w:lvl`, `CT_Lvl`).
965///
966/// Only the handful of properties needed to produce a correctly-formatted, correctly-indented list
967/// are modeled (`numFmt`, `lvlText`, and the indentation `CT_Lvl`'s own `pPr` sets) — not `pStyle`
968/// (a paragraph style tied to the level), `lvlPicBulletId` (a picture bullet), `lvlJc`
969/// (WordprocessingML lets a level's own number/bullet be right/center aligned within its indent —
970/// always left here, matching the common case), `suff` (what follows the number: tab/space/nothing
971/// — always a tab, Word's default), or per-level run formatting (`rPr`) — a deliberately plain
972/// bullet/number, styled the same as the surrounding text, rather than needing this crate to also
973/// model fonts just to reproduce Word's own Wingdings/Symbol-font bullet glyphs.
974#[derive(Debug, Clone, PartialEq, Eq)]
975pub struct ListLevel {
976 /// How this level's number/bullet is formatted (`w:numFmt`).
977 pub format: NumberFormat,
978 /// The level's number/bullet template (`w:lvlText`'s `val`) — a literal bullet character for
979 /// [`NumberFormat::Bullet`] (e.g. `"•"`), or a placeholder template for the numbered formats,
980 /// where `%1` is this level's own counter and `%2`/`%3`/. refer to ancestor levels' counters
981 /// (e.g. `"%1."` for a simple "1.", or `"%1.%2."` for a second level that repeats its parent's
982 /// number, matching Word's own default multilevel numbering).
983 pub text: String,
984 /// This level's left indent, in twips (1/20 pt) — `w:pPr/w:ind`'s `left` attribute.
985 pub indent_twips: i32,
986 /// How far the number/bullet hangs to the left of the following text's left edge, in twips —
987 /// `w:pPr/w:ind`'s `hanging` attribute. Word's own default lists always use `360` regardless of
988 /// level.
989 pub hanging_twips: i32,
990}
991
992/// How a [`ListLevel`]'s number/bullet is formatted (`w:numFmt`, `ST_NumberFormat`).
993///
994/// `ST_NumberFormat` defines dozens of values (`chicago`, `hex`, `ideographDigital`, several
995/// locale-specific numbering systems..); only the handful in everyday use in Word's own list
996/// gallery are modeled.
997#[derive(Debug, Clone, Copy, PartialEq, Eq)]
998pub enum NumberFormat {
999 /// An unordered (bulleted) list.
1000 Bullet,
1001 /// "1, 2, 3..".
1002 Decimal,
1003 /// "a, b, c..".
1004 LowerLetter,
1005 /// "A, B, C..".
1006 UpperLetter,
1007 /// "i, ii, iii..".
1008 LowerRoman,
1009 /// "I, II, III..".
1010 UpperRoman,
1011}
1012
1013impl NumberFormat {
1014 /// This format's `w:numFmt`/`val` value (`ST_NumberFormat`).
1015 pub fn attribute_value(self) -> &'static str {
1016 match self {
1017 NumberFormat::Bullet => "bullet",
1018 NumberFormat::Decimal => "decimal",
1019 NumberFormat::LowerLetter => "lowerLetter",
1020 NumberFormat::UpperLetter => "upperLetter",
1021 NumberFormat::LowerRoman => "lowerRoman",
1022 NumberFormat::UpperRoman => "upperRoman",
1023 }
1024 }
1025}
1026
1027/// A reference to a footnote or endnote (`<w:footnoteReference>`/ `<w:endnoteReference>`, both
1028/// `CT_FtnEdnRef`), placed in the document body to point at a [`Note`] declared in
1029/// `Document.footnotes`/ `Document.endnotes` by id.
1030///
1031/// Unlike [`Hyperlink`] or [`Field`], this genuinely is run content in the underlying XML —
1032/// `footnoteReference`/`endnoteReference` are members of `EG_RunInnerContent`, the same group `w:t`
1033/// belongs to, so no wrapping element is needed around the run.
1034#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1035pub enum NoteReference {
1036 /// References a footnote by id (`Document.footnotes`).
1037 Footnote(u32),
1038 /// References an endnote by id (`Document.endnotes`).
1039 Endnote(u32),
1040}
1041
1042/// Which kind of note a [`RunContent::NoteMarker`] renders the number of
1043/// (`<w:footnoteRef/>`/`<w:endnoteRef/>`, both `CT_Empty`).
1044///
1045/// Used as the first run of a footnote/endnote's own first paragraph, to render its own number
1046/// inline with its text — e.g. the superscript "1" at the very start of a footnote's text. Distinct
1047/// from [`NoteReference`], which is what goes in the document body to point *at* a note; this is
1048/// what goes *inside* the note itself, rendering its own number.
1049#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1050pub enum NoteKind {
1051 Footnote,
1052 Endnote,
1053}
1054
1055/// A single footnote or endnote's content (`CT_FtnEdn`) — the same block-level content model as the
1056/// document body (`EG_BlockLevelElts`, the same group `w:body`/[`HeaderFooter`] use), referenced
1057/// from the body by id via [`NoteReference`].
1058///
1059/// Only `type="normal"` notes are modeled. Real Word documents also always carry two boilerplate
1060/// notes per part (`type="separator"`/ `type="continuationSeparator"`, the horizontal rule Word
1061/// draws above its footnotes) — this crate never writes them; Word simply falls back to its own
1062/// built-in default separator when they're absent. Any such special notes found when reading an
1063/// existing document are silently skipped rather than exposed here.
1064#[derive(Debug, Default, Clone, PartialEq)]
1065pub struct Note {
1066 /// This note's id, referenced by [`NoteReference`].
1067 pub id: u32,
1068 /// The blocks that make up this note's content, in order.
1069 pub blocks: Vec<Block>,
1070}
1071
1072/// A single comment's content (`<w:comment>`, `CT_Comment`), declared in `word/comments.xml` and
1073/// anchored to a range of the body via `Run.comment_ids` — see that field's doc comment for how the
1074/// underlying `w:commentRangeStart`/`w:commentRangeEnd`/`w:commentReference` markers are derived
1075/// automatically rather than modeled here.
1076///
1077/// `CT_Comment` extends `CT_TrackChange` (`id`, `author`, `date`) and adds `initials`, plus the
1078/// same block-level content model as the document body (`EG_BlockLevelElts`, like
1079/// [`Note`]/[`HeaderFooter`]). A `<w:annotationRef/>` (`CT_Empty`) may optionally mark this
1080/// comment's own reference mark inside its content — not modeled: ECMA-376 §2.13.4.1 explicitly
1081/// allows omitting it ("an annotation reference mark may be added. by the consumer"), and Word's
1082/// Reviewing pane balloon already shows `author`/`date`/`initials` from this element's own
1083/// attributes without needing it, unlike a footnote/endnote's number marker (`NoteMarker`), which
1084/// genuinely is the only thing that renders the note's number at all.
1085#[derive(Debug, Default, Clone, PartialEq)]
1086pub struct Comment {
1087 /// This comment's id, referenced by `Run.comment_ids`.
1088 pub id: u32,
1089 /// The comment's author (`w:author`), shown in Word's Reviewing pane.
1090 pub author: Option<String>,
1091 /// The author's initials (`w:initials`), shown next to the comment's reference mark in the
1092 /// body.
1093 pub initials: Option<String>,
1094 /// When the comment was made (`w:date`), stored verbatim as a plain `xs:dateTime` string (e.g.
1095 /// `"2024-01-01T12:00:00Z"`) rather than parsed into a real date/time type — this crate doesn't
1096 /// otherwise need a date/time dependency, so round-tripping this one attribute as text avoids
1097 /// adding one just for this. The caller is responsible for a valid `xs:dateTime` value if
1098 /// Word's own date display matters.
1099 pub date: Option<String>,
1100 /// The blocks that make up this comment's content, in order.
1101 pub blocks: Vec<Block>,
1102}
1103
1104/// A block-level structured document tag / content control (`<w:sdt>`, `CT_SdtBlock`), wrapping one
1105/// or more paragraphs/tables — one of the three alternatives `EG_ContentBlockContent` allows
1106/// anywhere a [`Block`] can appear (body, header/footer, table cell content, a note's or comment's
1107/// own content..), hence a [`Block`] variant rather than its own separate top-level list the way
1108/// [`Note`]/[`Comment`] are.
1109///
1110/// WordprocessingML defines a dozen-plus specific content-control "types" (plain text, rich text,
1111/// date picker, drop-down list, combo box, picture, citation, group, bibliography, equation,
1112/// built-in document part..) via `w:sdtPr`'s `text`/`richText`/`date`/`dropDownList`/. child
1113/// (`CT_SdtPr`'s own inner `choice`) — none of these are modeled here, only the type-agnostic
1114/// identifying metadata (`id`/`tag`/`alias`) and the content itself. SDTs remain a niche,
1115/// fast-evolving corner of the format with little real demand seen yet for anything beyond
1116/// read-only handling, so unlike every other feature in this crate there is deliberately no
1117/// *creation*/write API for SDTs at all. Omitting the type indicator entirely is schema-valid
1118/// regardless (`CT_SdtPr`'s inner `choice` has `minOccurs="0"`): Word treats a content control with
1119/// no declared type as a generic one, still fully functional.
1120///
1121/// Only the block-level form (`CT_SdtBlock`, wherever a [`Block`] can appear) is modeled —
1122/// WordprocessingML also defines inline-level (wrapping run content inside a paragraph), row-level
1123/// and cell-level variants (`CT_SdtRun`/`CT_SdtRow`/`CT_SdtCell`), none of which are supported yet.
1124/// A block-level content control *can* appear inside a table cell, though — since
1125/// [`TableCell::blocks`] is `Vec<Block>`, the same as everywhere else a [`Block`] is valid — it's
1126/// specifically the dedicated `CT_SdtCell` form (matching the cell's own boundaries rather than
1127/// wrapping arbitrary content within it) that isn't modeled.
1128#[derive(Debug, Default, Clone, PartialEq)]
1129pub struct StructuredDocumentTag {
1130 /// A unique identifier Word assigns the control (`w:sdtPr/w:id`, `CT_DecimalNumber` — signed
1131 /// per the schema, though Word's own generator only ever produces positive values in practice).
1132 pub id: Option<i32>,
1133 /// The programmatic tag (`w:sdtPr/w:tag`) — the id automation code (VBA, the OpenXML SDK..)
1134 /// uses to find this control by, distinct from the human-facing `alias`.
1135 pub tag: Option<String>,
1136 /// The friendly/display name (`w:sdtPr/w:alias`) shown in Word's UI (e.g. the control's tab on
1137 /// the Developer ribbon) — distinct from `tag`.
1138 pub alias: Option<String>,
1139 /// The blocks that make up this control's content, in order.
1140 pub blocks: Vec<Block>,
1141}
1142
1143/// A document's theme (`word/theme/theme1.xml`, `CT_OfficeStyleSheet`, root element `<a:theme>`) —
1144/// the combination of color scheme and font scheme behind Word's own "Theme Colors"/"Theme Fonts"
1145/// palettes, and behind what Word's built-in heading styles (Heading 1-9, Title..) resolve to,
1146/// since those are defined in terms of theme slots inside Word's own built-in style definitions
1147/// rather than anything in `Document.styles`. This crate's own [`Style`]/[`Run`] formatting never
1148/// references a theme slot — `color`/`highlight`/`underline_color` are always literal values, never
1149/// `w:themeColor` — so a [`Theme`] mainly matters for controlling those built-in styles' appearance
1150/// and for round-tripping a document's existing theme unchanged.
1151///
1152/// A theme part also requires `fmtScheme` (a fill/line/effect "style matrix", `CT_StyleMatrix` —
1153/// `CT_BaseStyles`'s three children, `clrScheme`/`fontScheme`/`fmtScheme`, are all
1154/// `minOccurs="1"`), which is relevant almost entirely to DrawingML shapes/charts, out of scope for
1155/// `word-ooxml` entirely (see [`Image`]'s doc comment on what DrawingML content this crate does
1156/// support). It is always written as a fixed copy of Office's own built-in default rather than also
1157/// modeled here: each of `fmtScheme`'s four lists (`fillStyleLst`/`lnStyleLst`/`effectStyleLst`/
1158/// `bgFillStyleLst`) is a `minOccurs="3" maxOccurs="3"` sequence of DrawingML fill/line/effect
1159/// definitions, non-trivial to also expose as configurable, and no real need for that has come up —
1160/// the same deliberate scope limit as the fixed default `w:sectPr` written since.
1161/// `objectDefaults`/`extraClrSchemeLst` (both `minOccurs="0"` on `CT_OfficeStyleSheet` itself) are
1162/// omitted entirely rather than also given a fixed default, since omitting them outright is simpler
1163/// and just as schema-valid.
1164#[derive(Debug, Clone, PartialEq, Eq)]
1165pub struct Theme {
1166 /// The theme's name (`<a:theme name="..">`), shown in Word's Design > Themes gallery. Also
1167 /// reused, unchanged, as the `name` attribute of the nested `<a:clrScheme>`/`<a:fontScheme>`
1168 /// elements when writing — real Word-authored themes usually give all three the same name (e.g.
1169 /// `"Office"`), and this crate doesn't model them separately.
1170 pub name: String,
1171 /// The theme's 12-color palette.
1172 pub colors: ColorScheme,
1173 /// The theme's heading/body typefaces.
1174 pub fonts: FontScheme,
1175}
1176
1177/// A theme's 12-color palette (`<a:clrScheme>`, `CT_ColorScheme`): two dark/ light pairs, six
1178/// accents, and two hyperlink colors — the palette behind Word's "Theme Colors" swatches and
1179/// `w:themeColor`-based direct formatting (`CT_Color`'s theme-color choice, not modeled on [`Run`]/
1180/// [`Style`] itself, which only ever carry literal RGB colors).
1181///
1182/// Every slot is stored as a plain 6-digit RGB hex string (no leading `#`, e.g. `"4F81BD"`),
1183/// written as `<a:srgbClr val="..">`. `CT_Color`'s other choice, `<a:sysClr>` (binding a slot to a
1184/// live Windows system color, with a fallback RGB for non-Windows renderers) — what Word's own
1185/// default theme actually uses for `dk1`/`lt1` (bound to `windowText`/`window`) — is not modeled
1186/// for writing; a plain `srgbClr` is schema-valid in its place and renders identically outside of
1187/// that live system-color binding, which this crate has no way to honor anyway (a document it
1188/// writes isn't tied to any running Windows session). Reading back a real, externally-authored
1189/// theme that does use `<a:sysClr>` for a slot still works: its fallback RGB (`lastClr`) is
1190/// captured instead of the live binding, which is lost on a round trip — see `reader.rs`.
1191#[derive(Debug, Clone, PartialEq, Eq)]
1192pub struct ColorScheme {
1193 pub dark1: String,
1194 pub light1: String,
1195 pub dark2: String,
1196 pub light2: String,
1197 pub accent1: String,
1198 pub accent2: String,
1199 pub accent3: String,
1200 pub accent4: String,
1201 pub accent5: String,
1202 pub accent6: String,
1203 pub hyperlink: String,
1204 pub followed_hyperlink: String,
1205}
1206
1207/// A theme's font scheme (`<a:fontScheme>`, `CT_FontScheme`): a heading ("major") and body
1208/// ("minor") typeface, behind Word's "Theme Fonts" and built-in heading styles.
1209///
1210/// Only the Latin typeface of each is modeled (`<a:latin typeface="..">`). `CT_FontCollection` also
1211/// requires an East Asian and complex-script typeface (`ea`/`cs`, each `minOccurs="1"`) and allows
1212/// a further list of per-script fallback typefaces (`font`, e.g. `script="Jpan"`) — the writer
1213/// always emits `ea`/`cs` with an empty `typeface=""` (schema-valid, and exactly what a Latin-only
1214/// real Word theme does) and omits the per-script fallback list entirely (its own `minOccurs="0"`,
1215/// so this is valid too); Word simply falls back to its own built-in per-script defaults. Reading a
1216/// real theme that does declare `ea`/`cs`/per-script fonts silently ignores them, same scope limit
1217/// as writing.
1218#[derive(Debug, Clone, PartialEq, Eq)]
1219pub struct FontScheme {
1220 pub major_latin: String,
1221 pub minor_latin: String,
1222}
1223
1224/// A run's text highlight color (`<w:highlight w:val="..">`, `CT_Highlight`, `ST_HighlightColor`) —
1225/// a fixed palette of "highlighter" colors applied as a background behind a run's text, distinct
1226/// from `Run.color` (an arbitrary literal RGB text color) and from an arbitrary shaded background
1227/// fill (`w:shd`, not modeled).
1228///
1229/// `ST_HighlightColor` also defines the value `"none"`, explicitly clearing any inherited
1230/// highlighting — not modeled as its own variant here, since `Run.highlight: Option<Highlight>`
1231/// already expresses "no highlight" via `None` (omitting `w:highlight` entirely achieves the same
1232/// visual result for a run with nothing to inherit from).
1233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1234pub enum Highlight {
1235 Black,
1236 Blue,
1237 Cyan,
1238 DarkBlue,
1239 DarkCyan,
1240 DarkGray,
1241 DarkGreen,
1242 DarkMagenta,
1243 DarkRed,
1244 DarkYellow,
1245 Green,
1246 LightGray,
1247 Magenta,
1248 Red,
1249 White,
1250 Yellow,
1251}
1252
1253impl Highlight {
1254 /// This highlight's `w:highlight`/`val` value (`ST_HighlightColor`).
1255 pub fn attribute_value(self) -> &'static str {
1256 match self {
1257 Highlight::Black => "black",
1258 Highlight::Blue => "blue",
1259 Highlight::Cyan => "cyan",
1260 Highlight::DarkBlue => "darkBlue",
1261 Highlight::DarkCyan => "darkCyan",
1262 Highlight::DarkGray => "darkGray",
1263 Highlight::DarkGreen => "darkGreen",
1264 Highlight::DarkMagenta => "darkMagenta",
1265 Highlight::DarkRed => "darkRed",
1266 Highlight::DarkYellow => "darkYellow",
1267 Highlight::Green => "green",
1268 Highlight::LightGray => "lightGray",
1269 Highlight::Magenta => "magenta",
1270 Highlight::Red => "red",
1271 Highlight::White => "white",
1272 Highlight::Yellow => "yellow",
1273 }
1274 }
1275
1276 /// Parses a `w:highlight`/`val` value (`ST_HighlightColor`) back into a [`Highlight`], or
1277 /// `None` for `"none"` or an unrecognized value.
1278 pub fn from_attribute_value(value: &str) -> Option<Self> {
1279 match value {
1280 "black" => Some(Highlight::Black),
1281 "blue" => Some(Highlight::Blue),
1282 "cyan" => Some(Highlight::Cyan),
1283 "darkBlue" => Some(Highlight::DarkBlue),
1284 "darkCyan" => Some(Highlight::DarkCyan),
1285 "darkGray" => Some(Highlight::DarkGray),
1286 "darkGreen" => Some(Highlight::DarkGreen),
1287 "darkMagenta" => Some(Highlight::DarkMagenta),
1288 "darkRed" => Some(Highlight::DarkRed),
1289 "darkYellow" => Some(Highlight::DarkYellow),
1290 "green" => Some(Highlight::Green),
1291 "lightGray" => Some(Highlight::LightGray),
1292 "magenta" => Some(Highlight::Magenta),
1293 "red" => Some(Highlight::Red),
1294 "white" => Some(Highlight::White),
1295 "yellow" => Some(Highlight::Yellow),
1296 _ => None,
1297 }
1298 }
1299}
1300
1301/// A run's vertical alignment (`<w:vertAlign w:val="..">`, `CT_VerticalAlignRun`,
1302/// `ST_VerticalAlignRun`) — superscript or subscript text, rendered smaller and shifted above/below
1303/// the normal baseline without otherwise altering the run's own font size.
1304///
1305/// `ST_VerticalAlignRun` also defines the value `"baseline"`, the normal, non-shifted position —
1306/// not modeled as its own variant here, since `Run.vertical_align: Option<VerticalAlign>` already
1307/// expresses "normal position" via `None` (omitting `w:vertAlign` entirely achieves the same visual
1308/// result for a run with nothing to inherit from), same convention as [`Highlight`]'s own `"none"`
1309/// value.
1310#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1311pub enum VerticalAlign {
1312 Superscript,
1313 Subscript,
1314}
1315
1316impl VerticalAlign {
1317 /// This vertical alignment's `w:vertAlign`/`val` value (`ST_VerticalAlignRun`).
1318 pub fn attribute_value(self) -> &'static str {
1319 match self {
1320 VerticalAlign::Superscript => "superscript",
1321 VerticalAlign::Subscript => "subscript",
1322 }
1323 }
1324
1325 /// Parses a `w:vertAlign`/`val` value (`ST_VerticalAlignRun`) back into a [`VerticalAlign`], or
1326 /// `None` for `"baseline"` or an unrecognized value.
1327 pub fn from_attribute_value(value: &str) -> Option<Self> {
1328 match value {
1329 "superscript" => Some(VerticalAlign::Superscript),
1330 "subscript" => Some(VerticalAlign::Subscript),
1331 _ => None,
1332 }
1333 }
1334}
1335
1336/// A run's underline style (`<w:u w:val="..">`, `CT_Underline`, `ST_Underline`) — the pattern of
1337/// the line drawn beneath a run's text (single, double, thick, dotted, dashed, wavy, and
1338/// combinations of those), distinct from `Run.underline_color` (the line's color, a separate
1339/// attribute on the same `w:u` element).
1340///
1341/// `ST_Underline` also defines the value `"none"`, explicitly clearing any inherited underline —
1342/// not modeled as its own variant here, since `Run.underline: Option<UnderlineStyle>` already
1343/// expresses "no underline" via `None` (omitting `w:u` entirely achieves the same visual result for
1344/// a run with nothing to inherit from), same convention as [`Highlight`]'s own `"none"` value and
1345/// [`VerticalAlign`]'s `"baseline"`.
1346#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1347pub enum UnderlineStyle {
1348 Single,
1349 Words,
1350 Double,
1351 Thick,
1352 Dotted,
1353 DottedHeavy,
1354 Dash,
1355 DashedHeavy,
1356 DashLong,
1357 DashLongHeavy,
1358 DotDash,
1359 DashDotHeavy,
1360 DotDotDash,
1361 DashDotDotHeavy,
1362 Wave,
1363 WavyHeavy,
1364 WavyDouble,
1365}
1366
1367impl UnderlineStyle {
1368 /// This underline style's `w:u`/`val` value (`ST_Underline`).
1369 pub fn attribute_value(self) -> &'static str {
1370 match self {
1371 UnderlineStyle::Single => "single",
1372 UnderlineStyle::Words => "words",
1373 UnderlineStyle::Double => "double",
1374 UnderlineStyle::Thick => "thick",
1375 UnderlineStyle::Dotted => "dotted",
1376 UnderlineStyle::DottedHeavy => "dottedHeavy",
1377 UnderlineStyle::Dash => "dash",
1378 UnderlineStyle::DashedHeavy => "dashedHeavy",
1379 UnderlineStyle::DashLong => "dashLong",
1380 UnderlineStyle::DashLongHeavy => "dashLongHeavy",
1381 UnderlineStyle::DotDash => "dotDash",
1382 UnderlineStyle::DashDotHeavy => "dashDotHeavy",
1383 UnderlineStyle::DotDotDash => "dotDotDash",
1384 UnderlineStyle::DashDotDotHeavy => "dashDotDotHeavy",
1385 UnderlineStyle::Wave => "wave",
1386 UnderlineStyle::WavyHeavy => "wavyHeavy",
1387 UnderlineStyle::WavyDouble => "wavyDouble",
1388 }
1389 }
1390
1391 /// Parses a `w:u`/`val` value (`ST_Underline`) back into an [`UnderlineStyle`], or `None` for
1392 /// `"none"` or an unrecognized value.
1393 pub fn from_attribute_value(value: &str) -> Option<Self> {
1394 match value {
1395 "single" => Some(UnderlineStyle::Single),
1396 "words" => Some(UnderlineStyle::Words),
1397 "double" => Some(UnderlineStyle::Double),
1398 "thick" => Some(UnderlineStyle::Thick),
1399 "dotted" => Some(UnderlineStyle::Dotted),
1400 "dottedHeavy" => Some(UnderlineStyle::DottedHeavy),
1401 "dash" => Some(UnderlineStyle::Dash),
1402 "dashedHeavy" => Some(UnderlineStyle::DashedHeavy),
1403 "dashLong" => Some(UnderlineStyle::DashLong),
1404 "dashLongHeavy" => Some(UnderlineStyle::DashLongHeavy),
1405 "dotDash" => Some(UnderlineStyle::DotDash),
1406 "dashDotHeavy" => Some(UnderlineStyle::DashDotHeavy),
1407 "dotDotDash" => Some(UnderlineStyle::DotDotDash),
1408 "dashDotDotHeavy" => Some(UnderlineStyle::DashDotDotHeavy),
1409 "wave" => Some(UnderlineStyle::Wave),
1410 "wavyHeavy" => Some(UnderlineStyle::WavyHeavy),
1411 "wavyDouble" => Some(UnderlineStyle::WavyDouble),
1412 _ => None,
1413 }
1414 }
1415}
1416
1417/// The content of a [`Run`]: text, a single inline image, a field (e.g. a page number), a reference
1418/// to a footnote/endnote, or a footnote/endnote's own number marker.
1419///
1420/// WordprocessingML's `EG_RunInnerContent` technically allows a run to mix several kinds of content
1421/// (e.g. text followed by a line break followed by more text), but that generality isn't modeled
1422/// yet — a run is either all text, a single image, a single field, a single note reference/marker,
1423/// or a single break. Split content across multiple runs (e.g. one text run, then a break run, then
1424/// another text run) if that's needed — this crate always represents a break as a run of its own,
1425/// rather than mixing content kinds within a single run. Note that a [`Field`] is, strictly, not
1426/// really "inside" a run in the underlying XML (`<w:fldSimple>` is a sibling of `<w:r>`, wrapping
1427/// its own inner run) — it is modeled here as run content anyway, for a simpler, more consistent
1428/// public API; see `writer.rs`'s `write_field` for how this is reconciled when serializing.
1429/// `NoteReference`/`NoteMarker`/`Break`/ `CarriageReturn`, by contrast, really are plain run
1430/// content (`EG_RunInnerContent` members), no reconciliation needed.
1431#[derive(Debug, Clone, PartialEq)]
1432pub enum RunContent {
1433 Text(String),
1434 /// An inline image. Boxed for the same reason as `Chart` below — once `Chart` was boxed,
1435 /// clippy's `large_enum_variant` moved on to flag this as the new largest variant (an `Image`
1436 /// carries its own encoded bytes plus shape-formatting fields).
1437 Image(Box<Image>),
1438 Field(Field),
1439 NoteReference(NoteReference),
1440 NoteMarker(NoteKind),
1441 /// An embedded chart (`<w:drawing><wp:inline>` wrapping a `<c:chart r:id="..">` graphic frame
1442 /// instead of a picture). See [`EmbeddedChart`]'s doc comment. Boxed for the same reason as
1443 /// `powerpoint-ooxml`'s `Shape::Chart` — a chart's own nested `ChartSpace` otherwise forces
1444 /// every `RunContent`, whatever its real kind, to pay for the largest variant's size.
1445 Chart(Box<EmbeddedChart>),
1446 /// An explicit page/column/line break (`<w:br>`, `CT_Br`). See [`BreakKind`]'s doc comment.
1447 Break(BreakKind),
1448 /// A simple carriage return / line break (`<w:cr/>`, `CT_Empty`) — distinct from
1449 /// `Break(BreakKind::TextWrapping)`: both produce a line break, but `w:cr` is
1450 /// WordprocessingML's older, simpler element for it (no `type`/`clear` attributes at all),
1451 /// while `w:br` with `type="textWrapping"` is the modern, more general one. This crate writes
1452 /// whichever the caller explicitly asks for; both round-trip back to their own distinct variant
1453 /// when read.
1454 CarriageReturn,
1455}
1456
1457impl Default for RunContent {
1458 fn default() -> Self {
1459 RunContent::Text(String::new())
1460 }
1461}
1462
1463/// The kind of an explicit break (`<w:br w:type="..">`, `CT_Br`, `ST_BrType`), see
1464/// [`RunContent::Break`].
1465#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1466pub enum BreakKind {
1467 /// Forces a page break at this point in the text.
1468 Page,
1469 /// Forces a column break (only meaningful in a multi-column section, see
1470 /// [`PageSetup::columns`]).
1471 Column,
1472 /// A line break within the same paragraph (the text continues on the next line without starting
1473 /// a new paragraph) — `ST_BrType`'s default value when `w:br`'s `type` attribute is omitted
1474 /// entirely.
1475 TextWrapping,
1476}
1477
1478impl BreakKind {
1479 /// This break kind's `w:br`/`type` value (`ST_BrType`).
1480 pub fn attribute_value(self) -> &'static str {
1481 match self {
1482 BreakKind::Page => "page",
1483 BreakKind::Column => "column",
1484 BreakKind::TextWrapping => "textWrapping",
1485 }
1486 }
1487
1488 /// Parses a `w:br`/`type` value (`ST_BrType`) back into a [`BreakKind`], defaulting to
1489 /// `TextWrapping` for a missing/ unrecognized value — matching `ST_BrType`'s own documented
1490 /// default for an omitted `type` attribute.
1491 pub fn from_attribute_value(value: Option<&str>) -> Self {
1492 match value {
1493 Some("page") => BreakKind::Page,
1494 Some("column") => BreakKind::Column,
1495 _ => BreakKind::TextWrapping,
1496 }
1497 }
1498}
1499
1500/// A WordprocessingML "simple field" (`<w:fldSimple w:instr="..">`, `CT_SimpleField`) — a value
1501/// Word computes and keeps up to date, most commonly a page number.
1502///
1503/// Only the two most common, simplest field codes are modeled. WordprocessingML supports many more
1504/// (`DATE`, `AUTHOR`, `TOC`..), most of which need the more elaborate "complex field" construct
1505/// (`w:fldChar` begin/separate/end, spanning several runs) rather than `fldSimple` to update
1506/// correctly in Word, so they are out of scope for now.
1507#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1508pub enum Field {
1509 /// The current page number (`PAGE` field code).
1510 PageNumber,
1511 /// The total number of pages in the document (`NUMPAGES` field code).
1512 TotalPages,
1513}
1514
1515impl Field {
1516 /// The field's instruction text (`w:instr`, `CT_SimpleField`'s required attribute).
1517 pub fn instruction(self) -> &'static str {
1518 match self {
1519 Field::PageNumber => "PAGE",
1520 Field::TotalPages => "NUMPAGES",
1521 }
1522 }
1523}
1524
1525/// An inline image (`<w:drawing><wp:inline>..`), embedded in a run.
1526///
1527/// Structure is a minimal `wp:inline`/`pic:pic` skeleton (no rotation, cropping or other DrawingML
1528/// effects yet).
1529#[derive(Debug, Clone, PartialEq)]
1530pub struct Image {
1531 /// The raw, encoded image bytes (e.g. the bytes of a `.png` file).
1532 pub data: Vec<u8>,
1533 /// The image's encoding, also used to pick the media part's extension and content type.
1534 pub format: ImageFormat,
1535 /// The width the image is displayed at, in EMUs (English Metric Units; 914,400 EMU per inch —
1536 /// `CT_PositiveSize2D`'s `cx`). Display size, not necessarily the image's native pixel size.
1537 pub width_emu: i64,
1538 /// The height the image is displayed at, in EMUs (`cy`).
1539 pub height_emu: i64,
1540 /// Alt text / description (`wp:docPr`'s `descr` attribute). May be empty.
1541 pub description: String,
1542 /// Additional shape formatting (fill/line/rotation) for the picture's own `pic:spPr`. `None`
1543 /// (the only value this crate produced before this point existed) preserves the exact
1544 /// fixed-rectangle geometry `writer.rs::write_drawing` always wrote; `Some` enriches that
1545 /// `spPr` with the given content (reusing
1546 /// `drawing::write_shape_properties`/`drawing::read_shape_properties`) without changing the
1547 /// fixed `a:xfrm`/size handling, which stays governed by `width_emu`/`height_emu` as before.
1548 /// Purely additive: no existing round-trip test is affected since none of them ever set this
1549 /// field.
1550 pub shape_properties: Option<drawing::ShapeProperties>,
1551}
1552
1553/// The number of EMUs (English Metric Units) per inch, used to convert pixel dimensions to the EMU
1554/// units WordprocessingML expects.
1555pub const EMU_PER_INCH: i64 = 914_400;
1556
1557/// An image's file format, also determining its media content type.
1558///
1559/// Only the handful of raster formats Word natively displays inline are modeled; vector formats
1560/// (e.g. EMF/WMF metafiles) and other DrawingML content (shapes, charts) are out of scope for now.
1561#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1562pub enum ImageFormat {
1563 Png,
1564 Jpeg,
1565 Gif,
1566 Bmp,
1567}
1568
1569/// A table: a sequence of rows.
1570///
1571/// Column count is always derived from the widest row (accounting for `TableCell.horizontal_span`),
1572/// never stored explicitly. Column WIDTHS default to equal, fixed widths (see `writer.rs`'s
1573/// `DEFAULT_COLUMN_WIDTH_TWIPS`) unless `column_widths` overrides them.
1574#[derive(Debug, Default, Clone, PartialEq)]
1575pub struct Table {
1576 /// The rows that make up this table, in order.
1577 pub rows: Vec<TableRow>,
1578 /// Per-column preferred widths, in twips (twentieths of a point, `w:gridCol/@w`'s own unit —
1579 /// see `Run.character_spacing`'s doc comment for the same "expose the raw unit, no conversion"
1580 /// convention used throughout this crate), one entry per grid column in order. Left empty (the
1581 /// default) to keep the writer's previous behavior: every column gets the same fixed default
1582 /// width. When non-empty, entries beyond the table's real column count are simply unused, and a
1583 /// real column count exceeding `column_widths.len()` falls back to the default width for the
1584 /// remaining columns — not validated or required to match exactly, same "best-effort, not
1585 /// strictly enforced" policy as `Paragraph.numbering_id`/`style_id` referencing a declaration
1586 /// that may or may not actually exist.
1587 ///
1588 /// Setting this also makes the writer emit `<w:tblLayout w:type="fixed"/>`
1589 /// (`CT_TblLayoutType`'s two values are `fixed`/ `autofit`, `autofit` being Word's default when
1590 /// the element is omitted entirely). This matters a lot in practice: under the default
1591 /// `autofit` layout, Word treats `tblGrid`'s widths as mere starting suggestions and actively
1592 /// reflows columns based on cell content ("the presence of long non-breaking content can widen
1593 /// a column, proportionally shrinking the others" — ECMA-376's own worked example for
1594 /// `ST_TblLayoutType`) — so explicit widths would often be silently overridden without `fixed`
1595 /// layout forcing Word to honor them regardless of content.
1596 pub column_widths: Vec<u32>,
1597 /// The color of the table's own border lines (`w:tblBorders`'s sides' `color` attribute, a
1598 /// plain 6-digit RGB hex string), or `None` to use the fixed `"auto"` (black) this crate has
1599 /// always used. Overrides all six sides (`top`/`left`/`bottom`/`right`/`insideH`/`insideV`)
1600 /// uniformly — WordprocessingML allows each side its own independent color, but a single
1601 /// uniform color covers the common case and matches the fixed-appearance convention already
1602 /// used for `Paragraph.border`/`Run.text_border`/`TableCell.border`.
1603 pub border_color: Option<String>,
1604 /// The table's own horizontal alignment/positioning on the page (`w:tblPr/w:jc`, `CT_JcTable`)
1605 /// — where the whole table sits relative to the page margins, distinct from
1606 /// `Paragraph.alignment` (which aligns text within a paragraph). `None` leaves it unspecified
1607 /// (Word's default, effectively left-aligned). Reuses [`Alignment`] even though `CT_JcTable`'s
1608 /// own enumeration (`ST_JcTable`) is actually a different, smaller type than `ST_Jc` (no
1609 /// `distribute`, but it does still cover `left`/`center`/`right`/`both`) — the values this
1610 /// crate models for `Alignment` (`Left`/`Center`/`Right`/`Justify`) all happen to also be valid
1611 /// `ST_JcTable` values, so sharing the type avoids a near-duplicate enum.
1612 pub alignment: Option<Alignment>,
1613 /// The table's indentation from the leading margin, in twips (`w:tblPr/w:tblInd`,
1614 /// `CT_TblWidth`, always written with `w:type="dxa"`), or `None` to leave it unspecified (no
1615 /// extra indentation).
1616 pub indent_twips: Option<u32>,
1617 /// The id of a table-type named style to apply (`w:tblPr/w:tblStyle`), or `None`. Unlike
1618 /// `Paragraph.style_id`/`Run.style_id`, this crate does not model *declaring* a table style in
1619 /// `Document.styles` (`CT_Style`'s `table`-kind styles carry their own rich conditional
1620 /// formatting — `CT_TblStylePr` per table region: first row, banding, corner cells. — a large
1621 /// surface with no real need seen yet, matching the scope reduction already applied to
1622 /// `StyleKind`, which only models `paragraph`/`character`). Referencing one of Word's built-in
1623 /// table style ids (e.g. `"TableGrid"`, `"LightList-Accent1"`) still works perfectly well
1624 /// without a local declaration, the same way referencing a built-in paragraph style like
1625 /// `"Heading1"` does — Word resolves those from its own built-in style set when the document
1626 /// doesn't declare them itself.
1627 pub style_id: Option<String>,
1628}
1629
1630/// A table row: a sequence of cells.
1631#[derive(Debug, Default, Clone, PartialEq)]
1632pub struct TableRow {
1633 /// The cells that make up this row, in order.
1634 pub cells: Vec<TableCell>,
1635 /// Whether this row repeats as a header row on every page the table spans
1636 /// (`w:trPr/w:tblHeader`, `CT_OnOff`) — only meaningful, per ECMA-376, when set on one or more
1637 /// of the table's topmost rows (Word stops honoring it on the first row where it isn't set);
1638 /// not validated here, same "caller's responsibility" convention as `Paragraph.numbering_id`
1639 /// referencing a declared list.
1640 pub repeat_as_header_row: bool,
1641 /// This row's height, in twips (`w:trPr/w:trHeight/@w:val`), or `None` to leave it unspecified
1642 /// (Word sizes the row to fit its content). Always written with `w:hRule="atLeast"`
1643 /// (`ST_HeightRule`'s other values, `exact` — which can clip content taller than the given
1644 /// height — and `auto` — equivalent to omitting the element entirely — aren't modeled), meaning
1645 /// this is a *minimum* height Word may still exceed for taller content, never a hard cap.
1646 pub height_twips: Option<u32>,
1647 /// Whether this row is prevented from splitting across a page break (`w:trPr/w:cantSplit`,
1648 /// `CT_OnOff`) — when `true`, Word moves the whole row to the next page rather than breaking it
1649 /// mid-row.
1650 pub cant_split: bool,
1651}
1652
1653/// A table cell: a sequence of blocks (paragraphs, and optionally a nested table). `CT_Tc` reuses
1654/// the same block-level content model as the document body — `w:tbl` is a valid child of `w:tc` per
1655/// the schema — so a [`Block::Table`] here nests a table inside the cell, and a
1656/// [`Block::StructuredDocumentTag`] wraps more blocks the same way it does anywhere else a `Block`
1657/// is valid.
1658///
1659/// WordprocessingML requires every cell's content to actually END with a paragraph — not just
1660/// contain one somewhere (`CT_Tc`'s block-level content group has `minOccurs="1"`, but real Word
1661/// additionally expects the *last* child to be `w:p`, even after a nested table; violating this
1662/// causes Word's repair prompt on open, the same class of "schema-valid but not what Word actually
1663/// wants" bug this project has been bitten by before). The writer enforces this automatically —
1664/// appending an empty trailing paragraph whenever `blocks` is empty, or its last entry isn't
1665/// already a [`Block::Paragraph`] — so any [`TableCell`] is always valid to write regardless of
1666/// what the caller put in `blocks`.
1667#[derive(Debug, Default, Clone, PartialEq)]
1668pub struct TableCell {
1669 /// The blocks that make up this cell's content, in order.
1670 pub blocks: Vec<Block>,
1671 /// How many grid columns this cell spans, for a horizontal merge (`w:gridSpan`,
1672 /// `CT_DecimalNumber`) — `None` (or `Some(1)`, treated identically) means an ordinary, unmerged
1673 /// cell occupying exactly one column; `Some(n)` with `n > 1` means this cell visually replaces
1674 /// `n` consecutive cells in the row, which must NOT also be present in `TableRow.cells` (this
1675 /// crate follows WordprocessingML's own model: a horizontally merged row simply has fewer
1676 /// `w:tc` elements than the table's column count, not extra "merged-away" cells to skip — this
1677 /// leaves column-counting entirely up to the caller).
1678 pub horizontal_span: Option<u32>,
1679 /// This cell's role in a vertical merge (`w:vMerge`, `CT_VMerge`), or `None` for a cell that
1680 /// isn't part of any vertical merge (and, per ECMA-376, closes any vertically merged group of
1681 /// *preceding* cells in the same column). See [`VerticalMerge`]'s doc comment for how a
1682 /// vertical merge spanning several rows is actually expressed: unlike a horizontal merge, EVERY
1683 /// row still needs one `TableCell` per column — there's no way to omit a cell vertically, only
1684 /// mark it as continuing the merge above it.
1685 pub vertical_merge: Option<VerticalMerge>,
1686 /// Whether this cell has a border around it (`w:tcPr/w:tcBorders`), mirroring
1687 /// `Paragraph.border`/`Run.text_border`'s fixed on/off appearance (`val="single" sz="4"
1688 /// space="0" color="auto"` on all four sides). `CT_TcBorders`'s `insideH`/`insideV` (only
1689 /// meaningful when a cell's borders are set individually across a merged region) and
1690 /// `tl2br`/`tr2bl` (diagonal lines) aren't modeled, same scope reduction as `Run.text_border`
1691 /// not exposing `CT_Border`'s full richness.
1692 pub border: bool,
1693 /// This cell's own background shading color (`w:tcPr/w:shd`), as a plain 6-digit RGB hex
1694 /// string, mirroring `Run.shading_color`/ `Paragraph.shading_color` — same fixed
1695 /// `val="clear"`/`color="auto"` pair, just written inside `w:tcPr` instead of `w:rPr`/`w:pPr`.
1696 /// `None` leaves the cell's background unset (inherits the table's/row's own shading, if any —
1697 /// not modeled at those levels here).
1698 pub shading_color: Option<String>,
1699 /// This cell's vertical alignment of its content within the cell's full height
1700 /// (`w:tcPr/w:vAlign`), or `None` to leave it unspecified (Word's default, effectively
1701 /// top-aligned). See [`CellVerticalAlign`]'s doc comment.
1702 pub vertical_alignment: Option<CellVerticalAlign>,
1703 /// This cell's top margin override, in twips (`w:tcPr/w:tcMar/w:top`, always written with
1704 /// `w:type="dxa"`), or `None` to use the table's own default cell margins (Word's built-in
1705 /// default, since this crate doesn't model table-wide `w:tblCellMar` either). See
1706 /// `margin_left_twips` for why these are four separate fields rather than one struct.
1707 pub margin_top_twips: Option<u32>,
1708 /// This cell's left margin override, in twips, mirroring `margin_top_twips`. Four separate
1709 /// `Option<u32>` fields (rather than a single small struct) match this crate's existing
1710 /// convention for `Paragraph.space_before`/`space_after` — plain, independently optional fields
1711 /// rather than introducing a new small aggregate type for four numbers.
1712 pub margin_left_twips: Option<u32>,
1713 /// This cell's bottom margin override, in twips, mirroring `margin_top_twips`.
1714 pub margin_bottom_twips: Option<u32>,
1715 /// This cell's right margin override, in twips, mirroring `margin_top_twips`.
1716 pub margin_right_twips: Option<u32>,
1717 /// This cell's text flow direction (`w:tcPr/w:textDirection`), or `None` for the normal,
1718 /// horizontal left-to-right flow (`ST_TextDirection`'s `lrTb`, Word's default when the element
1719 /// is omitted). See [`TextDirection`]'s doc comment for which rotated values are modeled.
1720 pub text_direction: Option<TextDirection>,
1721}
1722
1723/// A table cell's role in a vertical merge (`w:vMerge`, `CT_VMerge`'s `val` attribute, `ST_Merge` —
1724/// exactly 2 values).
1725///
1726/// A vertical merge spanning several rows is expressed as ONE cell per row, all in the same column,
1727/// not a single cell with a "row span" count the way `horizontal_span` works for a horizontal merge
1728/// — WordprocessingML has no row-span concept at all, only this restart/continue chain: the topmost
1729/// cell of the merged group is [`VerticalMerge::Restart`], and every cell directly below it, for as
1730/// many rows as the merge covers, is [`VerticalMerge::Continue`]. Word derives the merge's actual
1731/// content from the `Restart` cell alone; a `Continue` cell's own paragraphs are conventionally
1732/// left empty (not enforced here) since they're never displayed.
1733#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1734pub enum VerticalMerge {
1735 /// Starts a new vertically merged group of cells in this column.
1736 Restart,
1737 /// Continues the vertically merged group started by the nearest `Restart` cell above it in the
1738 /// same column.
1739 Continue,
1740}
1741
1742impl VerticalMerge {
1743 /// This merge role's `w:val` attribute value (`ST_Merge`).
1744 pub fn attribute_value(self) -> &'static str {
1745 match self {
1746 VerticalMerge::Restart => "restart",
1747 VerticalMerge::Continue => "continue",
1748 }
1749 }
1750}
1751
1752/// A table cell's vertical alignment of its content (`w:tcPr/w:vAlign`, `CT_VerticalJc`,
1753/// `ST_VerticalJc`) — where the cell's paragraphs sit within the cell's full height, when that
1754/// height exceeds the content's natural height (e.g. because a neighboring cell in the same row is
1755/// taller, or `TableRow.height_twips` forces extra height). Distinct from
1756/// `Paragraph.alignment`/`Table.alignment` (both horizontal).
1757#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1758pub enum CellVerticalAlign {
1759 Top,
1760 Center,
1761 Bottom,
1762}
1763
1764impl CellVerticalAlign {
1765 /// This vertical alignment's `w:vAlign`/`val` value (`ST_VerticalJc`).
1766 pub fn attribute_value(self) -> &'static str {
1767 match self {
1768 CellVerticalAlign::Top => "top",
1769 CellVerticalAlign::Center => "center",
1770 CellVerticalAlign::Bottom => "bottom",
1771 }
1772 }
1773
1774 /// Parses a `w:vAlign`/`val` value (`ST_VerticalJc`) back into a [`CellVerticalAlign`], or
1775 /// `None` for an unrecognized value.
1776 pub fn from_attribute_value(value: &str) -> Option<Self> {
1777 match value {
1778 "top" => Some(CellVerticalAlign::Top),
1779 "center" => Some(CellVerticalAlign::Center),
1780 "bottom" => Some(CellVerticalAlign::Bottom),
1781 _ => None,
1782 }
1783 }
1784}
1785
1786/// A table cell's text flow direction (`w:tcPr/w:textDirection`, `CT_TextDirection`,
1787/// `ST_TextDirection`) — used to rotate a cell's text 90 degrees, a common way to fit a narrow
1788/// column header (e.g. a label running down a data table's leftmost column).
1789///
1790/// `ST_TextDirection` actually defines many more values than these two (`lr`, `tb`, `lrV`, `tbV`,
1791/// `lrTb`, `tbRl`, `btLr`, `lrTbV`, `tbLrV`, `tbRlV` — a mix of modern WordprocessingML values and
1792/// legacy WordPerfect-compatibility ones); only the two rotated options exposed by Word's own
1793/// Table Properties dialog (under Text Direction) are modeled. The normal, non-rotated flow
1794/// (`lrTb`) isn't modeled as its own variant, since `TableCell.text_direction:
1795/// Option<TextDirection>` already expresses it via `None` (omitting `w:textDirection` entirely
1796/// achieves the same visual result), same convention as [`VerticalAlign`]'s
1797/// `"baseline"`/[`Highlight`]'s `"none"`.
1798#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1799pub enum TextDirection {
1800 /// Text reads top-to-bottom, columns flowing right-to-left — Word's "Rotate all text 90°"
1801 /// (`ST_TextDirection`'s `tbRl`).
1802 TopToBottom,
1803 /// Text reads bottom-to-top, columns flowing left-to-right — Word's "Rotate all text 270°"
1804 /// (`ST_TextDirection`'s `btLr`).
1805 BottomToTop,
1806}
1807
1808impl TextDirection {
1809 /// This text direction's `w:textDirection`/`val` value (`ST_TextDirection`).
1810 pub fn attribute_value(self) -> &'static str {
1811 match self {
1812 TextDirection::TopToBottom => "tbRl",
1813 TextDirection::BottomToTop => "btLr",
1814 }
1815 }
1816
1817 /// Parses a `w:textDirection`/`val` value (`ST_TextDirection`) back into a [`TextDirection`],
1818 /// or `None` for `"lrTb"` or any other unmodeled value.
1819 pub fn from_attribute_value(value: &str) -> Option<Self> {
1820 match value {
1821 "tbRl" => Some(TextDirection::TopToBottom),
1822 "btLr" => Some(TextDirection::BottomToTop),
1823 _ => None,
1824 }
1825 }
1826}
1827
1828/// A paragraph's horizontal alignment (WordprocessingML `CT_Jc`/`ST_Jc`).
1829///
1830/// Only the four values Word's UI exposes directly are modeled; `ST_Jc` defines several more
1831/// (`distribute`, `thaiDistribute`, `mediumKashida`, `highKashida`, `lowKashida`, `numTab`), all
1832/// specialized enough to add later if/when a real need for them comes up.
1833#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1834pub enum Alignment {
1835 Left,
1836 Center,
1837 Right,
1838 /// "Justify" in Word's UI; `w:val="both"` in the markup.
1839 Justify,
1840}
1841
1842impl Document {
1843 /// Creates an empty document.
1844 pub fn new() -> Self {
1845 Self::default()
1846 }
1847
1848 /// Appends a paragraph block and returns the document for chaining.
1849 pub fn with_paragraph(mut self, paragraph: Paragraph) -> Self {
1850 self.blocks.push(Block::Paragraph(paragraph));
1851 self
1852 }
1853
1854 /// Appends a table block and returns the document for chaining.
1855 pub fn with_table(mut self, table: Table) -> Self {
1856 self.blocks.push(Block::Table(table));
1857 self
1858 }
1859
1860 /// Appends a paragraph with the given text and returns a mutable reference to it, for further
1861 /// in-place styling (e.g. `document.add_paragraph("..").with_alignment(..)` isn't possible
1862 /// since `with_*` consumes `self` — reach for `paragraph.alignment =..` or
1863 /// `paragraph.runs.push(..)` on the returned reference instead). A `&mut self` counterpart to
1864 /// [`Self::with_paragraph`], for the common case of building a document by appending to it in a
1865 /// loop, where the consuming builder would otherwise force a `document =
1866 /// document.with_paragraph(..)` reassignment on every iteration.
1867 pub fn add_paragraph(&mut self, text: impl Into<String>) -> &mut Paragraph {
1868 self.blocks
1869 .push(Block::Paragraph(Paragraph::with_text(text)));
1870 let Some(Block::Paragraph(paragraph)) = self.blocks.last_mut() else {
1871 unreachable!("the block just pushed is always a Block::Paragraph")
1872 };
1873 paragraph
1874 }
1875
1876 /// Iterates over the document's paragraphs only, skipping tables. A convenience for the common
1877 /// case of not caring about tables; use `blocks` directly to see paragraphs and tables in their
1878 /// actual reading order.
1879 pub fn paragraphs(&self) -> impl Iterator<Item = &Paragraph> {
1880 self.blocks.iter().filter_map(|block| match block {
1881 Block::Paragraph(paragraph) => Some(paragraph),
1882 Block::Table(_) | Block::StructuredDocumentTag(_) => None,
1883 })
1884 }
1885
1886 /// Iterates over the document's tables only, skipping paragraphs.
1887 pub fn tables(&self) -> impl Iterator<Item = &Table> {
1888 self.blocks.iter().filter_map(|block| match block {
1889 Block::Table(table) => Some(table),
1890 Block::Paragraph(_) | Block::StructuredDocumentTag(_) => None,
1891 })
1892 }
1893
1894 /// Sets the document's default header and returns it for chaining.
1895 pub fn with_header(mut self, header: HeaderFooter) -> Self {
1896 self.header = Some(header);
1897 self
1898 }
1899
1900 /// Sets the document's default footer and returns it for chaining.
1901 pub fn with_footer(mut self, footer: HeaderFooter) -> Self {
1902 self.footer = Some(footer);
1903 self
1904 }
1905
1906 /// Sets the document's first-page header and returns it for chaining. See `header_first`'s doc
1907 /// comment.
1908 pub fn with_header_first(mut self, header: HeaderFooter) -> Self {
1909 self.header_first = Some(header);
1910 self
1911 }
1912
1913 /// Sets the document's first-page footer and returns it for chaining.
1914 pub fn with_footer_first(mut self, footer: HeaderFooter) -> Self {
1915 self.footer_first = Some(footer);
1916 self
1917 }
1918
1919 /// Sets the document's even-page header and returns it for chaining. See `header_even`'s doc
1920 /// comment.
1921 pub fn with_header_even(mut self, header: HeaderFooter) -> Self {
1922 self.header_even = Some(header);
1923 self
1924 }
1925
1926 /// Sets the document's even-page footer and returns it for chaining.
1927 pub fn with_footer_even(mut self, footer: HeaderFooter) -> Self {
1928 self.footer_even = Some(footer);
1929 self
1930 }
1931
1932 /// Sets the document's page setup and returns it for chaining.
1933 pub fn with_page_setup(mut self, page_setup: PageSetup) -> Self {
1934 self.page_setup = page_setup;
1935 self
1936 }
1937
1938 /// Appends a named style declaration and returns the document for chaining.
1939 pub fn with_style(mut self, style: Style) -> Self {
1940 self.styles.push(style);
1941 self
1942 }
1943
1944 /// Appends a numbering (list) definition and returns the document for chaining.
1945 pub fn with_numbering_definition(mut self, definition: NumberingDefinition) -> Self {
1946 self.numbering_definitions.push(definition);
1947 self
1948 }
1949
1950 /// Appends a footnote declaration and returns the document for chaining.
1951 pub fn with_footnote(mut self, note: Note) -> Self {
1952 self.footnotes.push(note);
1953 self
1954 }
1955
1956 /// Appends an endnote declaration and returns the document for chaining.
1957 pub fn with_endnote(mut self, note: Note) -> Self {
1958 self.endnotes.push(note);
1959 self
1960 }
1961
1962 /// Appends a comment declaration and returns the document for chaining.
1963 pub fn with_comment(mut self, comment: Comment) -> Self {
1964 self.comments.push(comment);
1965 self
1966 }
1967
1968 /// Appends a structured document tag block and returns the document for chaining.
1969 pub fn with_structured_document_tag(mut self, sdt: StructuredDocumentTag) -> Self {
1970 self.blocks.push(Block::StructuredDocumentTag(sdt));
1971 self
1972 }
1973
1974 /// Sets the document's theme and returns it for chaining.
1975 pub fn with_theme(mut self, theme: Theme) -> Self {
1976 self.theme = Some(theme);
1977 self
1978 }
1979
1980 /// Sets whether Word should track changes made to this document from now on and returns it for
1981 /// chaining. See `track_changes`'s doc comment.
1982 pub fn with_track_changes(mut self, track_changes: bool) -> Self {
1983 self.track_changes = track_changes;
1984 self
1985 }
1986
1987 /// Sets the document's core properties (title, author, etc.) and returns it for chaining. See
1988 /// [`DocumentProperties`]'s doc comment.
1989 pub fn with_properties(mut self, properties: DocumentProperties) -> Self {
1990 self.properties = properties;
1991 self
1992 }
1993
1994 /// Sets which editing restrictions are enforced on the document and returns it for chaining.
1995 /// See [`ProtectionKind`]'s doc comment.
1996 pub fn with_protection(mut self, protection: ProtectionKind) -> Self {
1997 self.protection = Some(protection);
1998 self
1999 }
2000
2001 /// Sets the document's extended properties (company/manager) and returns it for chaining. See
2002 /// [`ExtendedProperties`]'s doc comment.
2003 pub fn with_extended_properties(mut self, extended_properties: ExtendedProperties) -> Self {
2004 self.extended_properties = extended_properties;
2005 self
2006 }
2007
2008 /// Appends a custom property (name/value pair) and returns the document for chaining. See
2009 /// `custom_properties`'s doc comment for why order matters here.
2010 pub fn with_custom_property(
2011 mut self,
2012 name: impl Into<String>,
2013 value: CustomPropertyValue,
2014 ) -> Self {
2015 self.custom_properties.push((name.into(), value));
2016 self
2017 }
2018}
2019
2020impl DocumentProperties {
2021 /// Creates an empty set of document properties (every field `None`).
2022 pub fn new() -> Self {
2023 Self::default()
2024 }
2025
2026 /// Sets the document's title and returns it for chaining.
2027 pub fn with_title(mut self, title: impl Into<String>) -> Self {
2028 self.title = Some(title.into());
2029 self
2030 }
2031
2032 /// Sets the document's subject and returns it for chaining.
2033 pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
2034 self.subject = Some(subject.into());
2035 self
2036 }
2037
2038 /// Sets the document's author and returns it for chaining.
2039 pub fn with_creator(mut self, creator: impl Into<String>) -> Self {
2040 self.creator = Some(creator.into());
2041 self
2042 }
2043
2044 /// Sets the document's search keywords and returns it for chaining.
2045 pub fn with_keywords(mut self, keywords: impl Into<String>) -> Self {
2046 self.keywords = Some(keywords.into());
2047 self
2048 }
2049
2050 /// Sets the document's description and returns it for chaining. Shown as "Comments" in Word's
2051 /// own File > Info panel — see this field's doc comment.
2052 pub fn with_description(mut self, description: impl Into<String>) -> Self {
2053 self.description = Some(description.into());
2054 self
2055 }
2056
2057 /// Sets who last modified the document and returns it for chaining.
2058 pub fn with_last_modified_by(mut self, last_modified_by: impl Into<String>) -> Self {
2059 self.last_modified_by = Some(last_modified_by.into());
2060 self
2061 }
2062
2063 /// Sets the document's revision number (as a string) and returns it for chaining.
2064 pub fn with_revision(mut self, revision: impl Into<String>) -> Self {
2065 self.revision = Some(revision.into());
2066 self
2067 }
2068
2069 /// Sets when the document was created (a W3CDTF string, e.g. `"2026-07-17T10:00:00Z"`) and
2070 /// returns it for chaining. Not validated — see this struct's doc comment.
2071 pub fn with_created(mut self, created: impl Into<String>) -> Self {
2072 self.created = Some(created.into());
2073 self
2074 }
2075
2076 /// Sets when the document was last modified (a W3CDTF string), mirroring `with_created`, and
2077 /// returns it for chaining.
2078 pub fn with_modified(mut self, modified: impl Into<String>) -> Self {
2079 self.modified = Some(modified.into());
2080 self
2081 }
2082
2083 /// Sets the document's category and returns it for chaining.
2084 pub fn with_category(mut self, category: impl Into<String>) -> Self {
2085 self.category = Some(category.into());
2086 self
2087 }
2088
2089 /// Sets the document's content status (e.g. `"Draft"`, `"Final"`) and returns it for chaining.
2090 pub fn with_content_status(mut self, content_status: impl Into<String>) -> Self {
2091 self.content_status = Some(content_status.into());
2092 self
2093 }
2094}
2095
2096impl HeaderFooter {
2097 /// Creates an empty header/footer.
2098 pub fn new() -> Self {
2099 Self::default()
2100 }
2101
2102 /// Appends a paragraph block and returns it for chaining.
2103 pub fn with_paragraph(mut self, paragraph: Paragraph) -> Self {
2104 self.blocks.push(Block::Paragraph(paragraph));
2105 self
2106 }
2107
2108 /// Appends a table block and returns it for chaining.
2109 pub fn with_table(mut self, table: Table) -> Self {
2110 self.blocks.push(Block::Table(table));
2111 self
2112 }
2113
2114 /// Appends a structured document tag block and returns it for chaining.
2115 pub fn with_structured_document_tag(mut self, sdt: StructuredDocumentTag) -> Self {
2116 self.blocks.push(Block::StructuredDocumentTag(sdt));
2117 self
2118 }
2119
2120 /// Iterates over this header/footer's paragraphs only, skipping tables.
2121 pub fn paragraphs(&self) -> impl Iterator<Item = &Paragraph> {
2122 self.blocks.iter().filter_map(|block| match block {
2123 Block::Paragraph(paragraph) => Some(paragraph),
2124 Block::Table(_) | Block::StructuredDocumentTag(_) => None,
2125 })
2126 }
2127
2128 /// Iterates over this header/footer's tables only, skipping paragraphs.
2129 pub fn tables(&self) -> impl Iterator<Item = &Table> {
2130 self.blocks.iter().filter_map(|block| match block {
2131 Block::Table(table) => Some(table),
2132 Block::Paragraph(_) | Block::StructuredDocumentTag(_) => None,
2133 })
2134 }
2135}
2136
2137impl Note {
2138 /// Creates an empty note with the given id.
2139 pub fn new(id: u32) -> Self {
2140 Self {
2141 id,
2142 blocks: Vec::new(),
2143 }
2144 }
2145
2146 /// A ready-made footnote: a single paragraph starting with the footnote's own number marker, a
2147 /// space, then the given text — the same shape a real Word-authored footnote uses (see
2148 /// [`Note`]'s doc comment). Use [`Note::new`] plus `with_paragraph`/`with_table` instead for
2149 /// full control (e.g. a note spanning several paragraphs).
2150 pub fn footnote_with_text(id: u32, text: impl Into<String>) -> Self {
2151 Self::with_marker_and_text(id, NoteKind::Footnote, text)
2152 }
2153
2154 /// A ready-made endnote, mirroring [`Note::footnote_with_text`].
2155 pub fn endnote_with_text(id: u32, text: impl Into<String>) -> Self {
2156 Self::with_marker_and_text(id, NoteKind::Endnote, text)
2157 }
2158
2159 fn with_marker_and_text(id: u32, kind: NoteKind, text: impl Into<String>) -> Self {
2160 Self {
2161 id,
2162 blocks: vec![Block::Paragraph(
2163 Paragraph::new()
2164 .with_run(Run::with_note_marker(kind))
2165 .with_run(Run::new(" "))
2166 .with_run(Run::new(text)),
2167 )],
2168 }
2169 }
2170
2171 /// Appends a paragraph block and returns the note for chaining.
2172 pub fn with_paragraph(mut self, paragraph: Paragraph) -> Self {
2173 self.blocks.push(Block::Paragraph(paragraph));
2174 self
2175 }
2176
2177 /// Appends a table block and returns the note for chaining.
2178 pub fn with_table(mut self, table: Table) -> Self {
2179 self.blocks.push(Block::Table(table));
2180 self
2181 }
2182
2183 /// Iterates over this note's paragraphs only, skipping tables.
2184 pub fn paragraphs(&self) -> impl Iterator<Item = &Paragraph> {
2185 self.blocks.iter().filter_map(|block| match block {
2186 Block::Paragraph(paragraph) => Some(paragraph),
2187 Block::Table(_) | Block::StructuredDocumentTag(_) => None,
2188 })
2189 }
2190
2191 /// Iterates over this note's tables only, skipping paragraphs.
2192 pub fn tables(&self) -> impl Iterator<Item = &Table> {
2193 self.blocks.iter().filter_map(|block| match block {
2194 Block::Table(table) => Some(table),
2195 Block::Paragraph(_) | Block::StructuredDocumentTag(_) => None,
2196 })
2197 }
2198}
2199
2200impl Comment {
2201 /// Creates an empty comment with the given id and no author/initials/ date set.
2202 pub fn new(id: u32) -> Self {
2203 Self {
2204 id,
2205 ..Self::default()
2206 }
2207 }
2208
2209 /// A ready-made comment: a single paragraph with the given text. Use [`Comment::new`] plus
2210 /// `with_paragraph`/`with_table` instead for full control (e.g. a comment spanning several
2211 /// paragraphs). Unlike [`Note::footnote_with_text`], no marker run is needed at the start — see
2212 /// [`Comment`]'s doc comment for why `annotationRef` isn't modeled.
2213 pub fn with_text(id: u32, text: impl Into<String>) -> Self {
2214 Self {
2215 id,
2216 blocks: vec![Block::Paragraph(Paragraph::with_text(text))],
2217 ..Self::default()
2218 }
2219 }
2220
2221 /// Sets the comment's author and returns it for chaining.
2222 pub fn with_author(mut self, author: impl Into<String>) -> Self {
2223 self.author = Some(author.into());
2224 self
2225 }
2226
2227 /// Sets the author's initials and returns it for chaining.
2228 pub fn with_initials(mut self, initials: impl Into<String>) -> Self {
2229 self.initials = Some(initials.into());
2230 self
2231 }
2232
2233 /// Sets the comment's date (a plain `xs:dateTime` string, e.g. `"2024-01-01T12:00:00Z"` — see
2234 /// the field's doc comment) and returns it for chaining.
2235 pub fn with_date(mut self, date: impl Into<String>) -> Self {
2236 self.date = Some(date.into());
2237 self
2238 }
2239
2240 /// Appends a paragraph block and returns the comment for chaining.
2241 pub fn with_paragraph(mut self, paragraph: Paragraph) -> Self {
2242 self.blocks.push(Block::Paragraph(paragraph));
2243 self
2244 }
2245
2246 /// Appends a table block and returns the comment for chaining.
2247 pub fn with_table(mut self, table: Table) -> Self {
2248 self.blocks.push(Block::Table(table));
2249 self
2250 }
2251
2252 /// Iterates over this comment's paragraphs only, skipping tables.
2253 pub fn paragraphs(&self) -> impl Iterator<Item = &Paragraph> {
2254 self.blocks.iter().filter_map(|block| match block {
2255 Block::Paragraph(paragraph) => Some(paragraph),
2256 Block::Table(_) | Block::StructuredDocumentTag(_) => None,
2257 })
2258 }
2259
2260 /// Iterates over this comment's tables only, skipping paragraphs.
2261 pub fn tables(&self) -> impl Iterator<Item = &Table> {
2262 self.blocks.iter().filter_map(|block| match block {
2263 Block::Table(table) => Some(table),
2264 Block::Paragraph(_) | Block::StructuredDocumentTag(_) => None,
2265 })
2266 }
2267}
2268
2269impl StructuredDocumentTag {
2270 /// Creates an empty structured document tag with no id/tag/alias set.
2271 pub fn new() -> Self {
2272 Self::default()
2273 }
2274
2275 /// Sets the control's id and returns it for chaining.
2276 pub fn with_id(mut self, id: i32) -> Self {
2277 self.id = Some(id);
2278 self
2279 }
2280
2281 /// Sets the control's programmatic tag and returns it for chaining.
2282 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
2283 self.tag = Some(tag.into());
2284 self
2285 }
2286
2287 /// Sets the control's friendly/display name and returns it for chaining.
2288 pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
2289 self.alias = Some(alias.into());
2290 self
2291 }
2292
2293 /// Appends a paragraph block and returns the control for chaining.
2294 pub fn with_paragraph(mut self, paragraph: Paragraph) -> Self {
2295 self.blocks.push(Block::Paragraph(paragraph));
2296 self
2297 }
2298
2299 /// Appends a table block and returns the control for chaining.
2300 pub fn with_table(mut self, table: Table) -> Self {
2301 self.blocks.push(Block::Table(table));
2302 self
2303 }
2304
2305 /// Iterates over this control's paragraphs only, skipping tables and nested structured document
2306 /// tags.
2307 pub fn paragraphs(&self) -> impl Iterator<Item = &Paragraph> {
2308 self.blocks.iter().filter_map(|block| match block {
2309 Block::Paragraph(paragraph) => Some(paragraph),
2310 Block::Table(_) | Block::StructuredDocumentTag(_) => None,
2311 })
2312 }
2313
2314 /// Iterates over this control's tables only, skipping paragraphs and nested structured document
2315 /// tags.
2316 pub fn tables(&self) -> impl Iterator<Item = &Table> {
2317 self.blocks.iter().filter_map(|block| match block {
2318 Block::Table(table) => Some(table),
2319 Block::Paragraph(_) | Block::StructuredDocumentTag(_) => None,
2320 })
2321 }
2322}
2323
2324impl Theme {
2325 /// Creates a theme with the given name, colors and fonts.
2326 pub fn new(name: impl Into<String>, colors: ColorScheme, fonts: FontScheme) -> Self {
2327 Self {
2328 name: name.into(),
2329 colors,
2330 fonts,
2331 }
2332 }
2333
2334 /// Office's own built-in default theme: its classic "Office" color palette and Cambria/Calibri
2335 /// heading/body fonts — the same theme a brand-new blank document uses.
2336 pub fn office_default() -> Self {
2337 Self::new(
2338 "Office",
2339 ColorScheme::office_default(),
2340 FontScheme::office_default(),
2341 )
2342 }
2343}
2344
2345impl ColorScheme {
2346 /// Creates a color scheme from its 12 slots, each a 6-digit RGB hex string (no leading `#`).
2347 #[allow(clippy::too_many_arguments)]
2348 pub fn new(
2349 dark1: impl Into<String>,
2350 light1: impl Into<String>,
2351 dark2: impl Into<String>,
2352 light2: impl Into<String>,
2353 accent1: impl Into<String>,
2354 accent2: impl Into<String>,
2355 accent3: impl Into<String>,
2356 accent4: impl Into<String>,
2357 accent5: impl Into<String>,
2358 accent6: impl Into<String>,
2359 hyperlink: impl Into<String>,
2360 followed_hyperlink: impl Into<String>,
2361 ) -> Self {
2362 Self {
2363 dark1: dark1.into(),
2364 light1: light1.into(),
2365 dark2: dark2.into(),
2366 light2: light2.into(),
2367 accent1: accent1.into(),
2368 accent2: accent2.into(),
2369 accent3: accent3.into(),
2370 accent4: accent4.into(),
2371 accent5: accent5.into(),
2372 accent6: accent6.into(),
2373 hyperlink: hyperlink.into(),
2374 followed_hyperlink: followed_hyperlink.into(),
2375 }
2376 }
2377
2378 /// Office's own built-in default 12-color palette (the classic "Office" theme colors:
2379 /// `dk1`/`lt1` as plain black/white rather than Word's own live `sysClr` binding — see
2380 /// [`ColorScheme`]'s doc comment).
2381 pub fn office_default() -> Self {
2382 Self::new(
2383 "000000", "FFFFFF", "1F497D", "EEECE1", "4F81BD", "C0504D", "9BBB59", "8064A2",
2384 "4BACC6", "F79646", "0000FF", "800080",
2385 )
2386 }
2387}
2388
2389impl FontScheme {
2390 /// Creates a font scheme with the given heading ("major") and body ("minor") Latin typefaces.
2391 pub fn new(major_latin: impl Into<String>, minor_latin: impl Into<String>) -> Self {
2392 Self {
2393 major_latin: major_latin.into(),
2394 minor_latin: minor_latin.into(),
2395 }
2396 }
2397
2398 /// Office's own built-in default fonts: Cambria for headings, Calibri for body text.
2399 pub fn office_default() -> Self {
2400 Self::new("Cambria", "Calibri")
2401 }
2402}
2403
2404impl Paragraph {
2405 /// Creates an empty paragraph.
2406 pub fn new() -> Self {
2407 Self::default()
2408 }
2409
2410 /// Creates a paragraph containing a single run with the given text.
2411 pub fn with_text(text: impl Into<String>) -> Self {
2412 Self {
2413 runs: vec![Run::new(text)],
2414 ..Self::default()
2415 }
2416 }
2417
2418 /// Appends a run and returns the paragraph for chaining.
2419 pub fn with_run(mut self, run: Run) -> Self {
2420 self.runs.push(run);
2421 self
2422 }
2423
2424 /// Sets the paragraph's alignment and returns it for chaining.
2425 pub fn with_alignment(mut self, alignment: Alignment) -> Self {
2426 self.alignment = Some(alignment);
2427 self
2428 }
2429
2430 /// Sets the paragraph style to apply (by id) and returns it for chaining.
2431 pub fn with_style_id(mut self, style_id: impl Into<String>) -> Self {
2432 self.style_id = Some(style_id.into());
2433 self
2434 }
2435
2436 /// Makes this paragraph an item of `numbering_id`'s list, at the given indent level
2437 /// (0-indexed), and returns it for chaining.
2438 pub fn with_numbering(mut self, numbering_id: u32, level: u8) -> Self {
2439 self.numbering_id = Some(numbering_id);
2440 self.numbering_level = level;
2441 self
2442 }
2443
2444 /// Sets the paragraph's line spacing (in `w:spacing`'s "auto" units — `240` is single, `360` is
2445 /// 1.5 lines, `480` is double) and returns it for chaining.
2446 pub fn with_line_spacing(mut self, line_spacing: u32) -> Self {
2447 self.line_spacing = Some(line_spacing);
2448 self
2449 }
2450
2451 /// Sets extra space before this paragraph, in twips, and returns it for chaining.
2452 pub fn with_space_before(mut self, space_before: u32) -> Self {
2453 self.space_before = Some(space_before);
2454 self
2455 }
2456
2457 /// Sets extra space after this paragraph, in twips, and returns it for chaining.
2458 pub fn with_space_after(mut self, space_after: u32) -> Self {
2459 self.space_after = Some(space_after);
2460 self
2461 }
2462
2463 /// Sets the paragraph's background shading color and returns it for chaining.
2464 pub fn with_shading_color(mut self, shading_color: impl Into<String>) -> Self {
2465 self.shading_color = Some(shading_color.into());
2466 self
2467 }
2468
2469 /// Sets whether this paragraph has a border around it and returns it for chaining.
2470 pub fn with_border(mut self, border: bool) -> Self {
2471 self.border = border;
2472 self
2473 }
2474
2475 /// Sets whether this paragraph stays on the same page as the one following it and returns it
2476 /// for chaining. See `keep_with_next`'s doc comment.
2477 pub fn with_keep_with_next(mut self, keep_with_next: bool) -> Self {
2478 self.keep_with_next = keep_with_next;
2479 self
2480 }
2481
2482 /// Sets whether every line of this paragraph must stay together on the same page and returns it
2483 /// for chaining. See `keep_lines_together`'s doc comment.
2484 pub fn with_keep_lines_together(mut self, keep_lines_together: bool) -> Self {
2485 self.keep_lines_together = keep_lines_together;
2486 self
2487 }
2488
2489 /// Sets whether this paragraph always starts on a new page and returns it for chaining. See
2490 /// `page_break_before`'s doc comment.
2491 pub fn with_page_break_before(mut self, page_break_before: bool) -> Self {
2492 self.page_break_before = page_break_before;
2493 self
2494 }
2495
2496 /// Appends a custom tab stop and returns the paragraph for chaining.
2497 pub fn with_tab(mut self, tab: TabStop) -> Self {
2498 self.tabs.push(tab);
2499 self
2500 }
2501
2502 /// Sets whether to ignore spacing above/below this paragraph when adjacent to a paragraph of
2503 /// the same style, and returns it for chaining. See `contextual_spacing`'s doc comment.
2504 pub fn with_contextual_spacing(mut self, contextual_spacing: bool) -> Self {
2505 self.contextual_spacing = contextual_spacing;
2506 self
2507 }
2508
2509 /// The paragraph's text: the concatenation of its text runs' text. Image and field runs
2510 /// contribute nothing (not even a placeholder character).
2511 pub fn text(&self) -> String {
2512 self.runs.iter().filter_map(Run::text).collect()
2513 }
2514}
2515
2516impl Run {
2517 /// Creates a text run with the given text and no formatting.
2518 pub fn new(text: impl Into<String>) -> Self {
2519 Self {
2520 content: RunContent::Text(text.into()),
2521 ..Self::default()
2522 }
2523 }
2524
2525 /// Creates a run containing a single inline image.
2526 pub fn with_image(image: Image) -> Self {
2527 Self {
2528 content: RunContent::Image(Box::new(image)),
2529 ..Self::default()
2530 }
2531 }
2532
2533 /// Creates a run containing a single embedded chart.
2534 pub fn with_chart(chart: EmbeddedChart) -> Self {
2535 Self {
2536 content: RunContent::Chart(Box::new(chart)),
2537 ..Self::default()
2538 }
2539 }
2540
2541 /// Creates a run containing a single field (e.g. a page number).
2542 pub fn with_field(field: Field) -> Self {
2543 Self {
2544 content: RunContent::Field(field),
2545 ..Self::default()
2546 }
2547 }
2548
2549 /// Creates a run whose content is a single explicit break (`w:br`, page/column/line). See
2550 /// [`BreakKind`]'s doc comment.
2551 pub fn with_break(kind: BreakKind) -> Self {
2552 Self {
2553 content: RunContent::Break(kind),
2554 ..Self::default()
2555 }
2556 }
2557
2558 /// Creates a run whose content is a single carriage return (`w:cr`). See
2559 /// `RunContent::CarriageReturn`'s doc comment for how this differs from
2560 /// `with_break(BreakKind::TextWrapping)`.
2561 pub fn with_carriage_return() -> Self {
2562 Self {
2563 content: RunContent::CarriageReturn,
2564 ..Self::default()
2565 }
2566 }
2567
2568 /// This run's text, or `None` if it is an image, chart, field, note, break or carriage return
2569 /// run.
2570 pub fn text(&self) -> Option<&str> {
2571 match &self.content {
2572 RunContent::Text(text) => Some(text.as_str()),
2573 RunContent::Image(_)
2574 | RunContent::Chart(_)
2575 | RunContent::Field(_)
2576 | RunContent::NoteReference(_)
2577 | RunContent::NoteMarker(_)
2578 | RunContent::Break(_)
2579 | RunContent::CarriageReturn => None,
2580 }
2581 }
2582
2583 /// This run's image, or `None` if it isn't an image run.
2584 pub fn image(&self) -> Option<&Image> {
2585 match &self.content {
2586 RunContent::Image(image) => Some(image.as_ref()),
2587 RunContent::Text(_)
2588 | RunContent::Chart(_)
2589 | RunContent::Field(_)
2590 | RunContent::NoteReference(_)
2591 | RunContent::NoteMarker(_)
2592 | RunContent::Break(_)
2593 | RunContent::CarriageReturn => None,
2594 }
2595 }
2596
2597 /// This run's chart, or `None` if it isn't a chart run.
2598 pub fn chart(&self) -> Option<&EmbeddedChart> {
2599 match &self.content {
2600 RunContent::Chart(chart) => Some(chart.as_ref()),
2601 RunContent::Text(_)
2602 | RunContent::Image(_)
2603 | RunContent::Field(_)
2604 | RunContent::NoteReference(_)
2605 | RunContent::NoteMarker(_)
2606 | RunContent::Break(_)
2607 | RunContent::CarriageReturn => None,
2608 }
2609 }
2610
2611 /// This run's field, or `None` if it isn't a field run.
2612 pub fn field(&self) -> Option<Field> {
2613 match &self.content {
2614 RunContent::Field(field) => Some(*field),
2615 RunContent::Text(_)
2616 | RunContent::Image(_)
2617 | RunContent::Chart(_)
2618 | RunContent::NoteReference(_)
2619 | RunContent::NoteMarker(_)
2620 | RunContent::Break(_)
2621 | RunContent::CarriageReturn => None,
2622 }
2623 }
2624
2625 /// Sets whether the run is bold and returns it for chaining.
2626 pub fn with_bold(mut self, bold: bool) -> Self {
2627 self.bold = bold;
2628 self
2629 }
2630
2631 /// Sets whether the run is italic and returns it for chaining.
2632 pub fn with_italic(mut self, italic: bool) -> Self {
2633 self.italic = italic;
2634 self
2635 }
2636
2637 /// Sets the run's underline style and returns it for chaining.
2638 pub fn with_underline(mut self, underline: UnderlineStyle) -> Self {
2639 self.underline = Some(underline);
2640 self
2641 }
2642
2643 /// Sets the color of the run's underline (a 6-digit RGB hex string, no leading `#`) and returns
2644 /// it for chaining. Only meaningful once `with_underline` has also been called.
2645 pub fn with_underline_color(mut self, underline_color: impl Into<String>) -> Self {
2646 self.underline_color = Some(underline_color.into());
2647 self
2648 }
2649
2650 /// Sets the run's text color (a 6-digit RGB hex string, no leading `#`) and returns it for
2651 /// chaining.
2652 pub fn with_color(mut self, color: impl Into<String>) -> Self {
2653 self.color = Some(color.into());
2654 self
2655 }
2656
2657 /// Sets the run's font size in whole points and returns it for chaining.
2658 pub fn with_font_size(mut self, font_size: u16) -> Self {
2659 self.font_size = Some(font_size);
2660 self
2661 }
2662
2663 /// Sets the run's font family/typeface and returns it for chaining.
2664 pub fn with_font_family(mut self, font_family: impl Into<String>) -> Self {
2665 self.font_family = Some(font_family.into());
2666 self
2667 }
2668
2669 /// Sets the run's highlight color and returns it for chaining.
2670 pub fn with_highlight(mut self, highlight: Highlight) -> Self {
2671 self.highlight = Some(highlight);
2672 self
2673 }
2674
2675 /// Sets whether the run is struck through and returns it for chaining.
2676 pub fn with_strike(mut self, strike: bool) -> Self {
2677 self.strike = strike;
2678 self
2679 }
2680
2681 /// Sets the run's vertical alignment (superscript/subscript) and returns it for chaining.
2682 pub fn with_vertical_align(mut self, vertical_align: VerticalAlign) -> Self {
2683 self.vertical_align = Some(vertical_align);
2684 self
2685 }
2686
2687 /// Sets whether the run is displayed as small caps and returns it for chaining.
2688 pub fn with_small_caps(mut self, small_caps: bool) -> Self {
2689 self.small_caps = small_caps;
2690 self
2691 }
2692
2693 /// Sets whether the run is displayed as all caps and returns it for chaining.
2694 pub fn with_all_caps(mut self, all_caps: bool) -> Self {
2695 self.all_caps = all_caps;
2696 self
2697 }
2698
2699 /// Sets the run's background shading color (a 6-digit RGB hex string, no leading `#`) and
2700 /// returns it for chaining.
2701 pub fn with_shading_color(mut self, shading_color: impl Into<String>) -> Self {
2702 self.shading_color = Some(shading_color.into());
2703 self
2704 }
2705
2706 /// Sets whether the run has a border around its text and returns it for chaining.
2707 pub fn with_text_border(mut self, text_border: bool) -> Self {
2708 self.text_border = text_border;
2709 self
2710 }
2711
2712 /// Sets the run's character spacing adjustment, in twips, and returns it for chaining.
2713 pub fn with_character_spacing(mut self, character_spacing: i16) -> Self {
2714 self.character_spacing = Some(character_spacing);
2715 self
2716 }
2717
2718 /// Sets the run's vertical position, raised or lowered from the baseline, in half-points, and
2719 /// returns it for chaining.
2720 pub fn with_vertical_position(mut self, vertical_position: i16) -> Self {
2721 self.vertical_position = Some(vertical_position);
2722 self
2723 }
2724
2725 /// Sets whether the run's text is hidden and returns it for chaining.
2726 pub fn with_hidden(mut self, hidden: bool) -> Self {
2727 self.hidden = hidden;
2728 self
2729 }
2730
2731 /// Sets the character style to apply (by id) and returns it for chaining.
2732 pub fn with_style_id(mut self, style_id: impl Into<String>) -> Self {
2733 self.style_id = Some(style_id.into());
2734 self
2735 }
2736
2737 /// Sets the hyperlink to apply to this run and returns it for chaining.
2738 pub fn with_hyperlink(mut self, hyperlink: Hyperlink) -> Self {
2739 self.hyperlink = Some(hyperlink);
2740 self
2741 }
2742
2743 /// Marks this run as falling inside the given [`Comment`]'s range (see `comment_ids`'s doc
2744 /// comment) and returns it for chaining. Call repeatedly to place a run inside several
2745 /// overlapping comments' ranges at once.
2746 pub fn with_comment(mut self, comment_id: u32) -> Self {
2747 self.comment_ids.push(comment_id);
2748 self
2749 }
2750
2751 /// Marks this run as falling inside the given [`Bookmark`]'s range (see `bookmarks`'s doc
2752 /// comment) and returns it for chaining. Call repeatedly to place a run inside several
2753 /// overlapping bookmarks' ranges at once, or on consecutive runs to span a bookmark across more
2754 /// than one run — repeat the exact same `Bookmark` (same id, same name) on every run the
2755 /// bookmark should cover.
2756 pub fn with_bookmark(mut self, bookmark: Bookmark) -> Self {
2757 self.bookmarks.push(bookmark);
2758 self
2759 }
2760
2761 /// Creates a run referencing a footnote/endnote (`<w:footnoteReference>`/
2762 /// `<w:endnoteReference>`), to be placed in the document body.
2763 ///
2764 /// Word always renders this reference in the "FootnoteReference"/ "EndnoteReference" built-in
2765 /// character style (a superscript number) — real Word documents rely on Word's own built-in
2766 /// latent style definitions for these well-known style ids, with no need to declare a matching
2767 /// [`Style`] in `Document.styles`, so this constructor sets `style_id` accordingly. Override it
2768 /// afterwards (`with_style_id`) if a different style is genuinely needed.
2769 pub fn with_note_reference(reference: NoteReference) -> Self {
2770 let style_id = match reference {
2771 NoteReference::Footnote(_) => "FootnoteReference",
2772 NoteReference::Endnote(_) => "EndnoteReference",
2773 };
2774 Self {
2775 content: RunContent::NoteReference(reference),
2776 style_id: Some(style_id.to_string()),
2777 ..Self::default()
2778 }
2779 }
2780
2781 /// Creates a run rendering a footnote/endnote's own number marker
2782 /// (`<w:footnoteRef/>`/`<w:endnoteRef/>`) — meant to be the first run of the note's own first
2783 /// paragraph (see [`Note::footnote_with_text`]/ [`Note::endnote_with_text`] for a convenience
2784 /// that builds this automatically). Sets `style_id` the same way [`Run::with_note_reference`]
2785 /// does, for the same reason.
2786 pub fn with_note_marker(kind: NoteKind) -> Self {
2787 let style_id = match kind {
2788 NoteKind::Footnote => "FootnoteReference",
2789 NoteKind::Endnote => "EndnoteReference",
2790 };
2791 Self {
2792 content: RunContent::NoteMarker(kind),
2793 style_id: Some(style_id.to_string()),
2794 ..Self::default()
2795 }
2796 }
2797}
2798
2799impl Image {
2800 /// Creates an image with an explicit display size in EMUs.
2801 pub fn new(
2802 data: impl Into<Vec<u8>>,
2803 format: ImageFormat,
2804 width_emu: i64,
2805 height_emu: i64,
2806 ) -> Self {
2807 Self {
2808 data: data.into(),
2809 format,
2810 width_emu,
2811 height_emu,
2812 description: String::new(),
2813 shape_properties: None,
2814 }
2815 }
2816
2817 /// Creates an image with its display size given in pixels, converted to EMUs assuming 96 DPI (a
2818 /// common screen resolution default, and Word's own default when it can't otherwise determine
2819 /// an image's intended size).
2820 pub fn from_pixels(
2821 data: impl Into<Vec<u8>>,
2822 format: ImageFormat,
2823 width_px: u32,
2824 height_px: u32,
2825 ) -> Self {
2826 const DEFAULT_DPI: i64 = 96;
2827 Self::new(
2828 data,
2829 format,
2830 (i64::from(width_px) * EMU_PER_INCH) / DEFAULT_DPI,
2831 (i64::from(height_px) * EMU_PER_INCH) / DEFAULT_DPI,
2832 )
2833 }
2834
2835 /// Sets the image's alt text / description and returns it for chaining.
2836 pub fn with_description(mut self, description: impl Into<String>) -> Self {
2837 self.description = description.into();
2838 self
2839 }
2840
2841 /// Sets additional shape formatting (fill/line/rotation) for this picture's own `pic:spPr` and
2842 /// returns it for chaining. See [`Image::shape_properties`]'s doc comment.
2843 pub fn with_shape_properties(mut self, shape_properties: drawing::ShapeProperties) -> Self {
2844 self.shape_properties = Some(shape_properties);
2845 self
2846 }
2847}
2848
2849impl ImageFormat {
2850 /// The file extension used for this format's media part (e.g. `word/media/image1.png`), without
2851 /// the leading dot.
2852 pub fn extension(self) -> &'static str {
2853 match self {
2854 ImageFormat::Png => "png",
2855 ImageFormat::Jpeg => "jpg",
2856 ImageFormat::Gif => "gif",
2857 ImageFormat::Bmp => "bmp",
2858 }
2859 }
2860
2861 /// The media part's content type (`[Content_Types].xml` `Override` value).
2862 pub fn content_type(self) -> &'static str {
2863 match self {
2864 ImageFormat::Png => "image/png",
2865 ImageFormat::Jpeg => "image/jpeg",
2866 ImageFormat::Gif => "image/gif",
2867 ImageFormat::Bmp => "image/bmp",
2868 }
2869 }
2870
2871 /// Guesses the format from a part name's or file name's extension (case-insensitive), or `None`
2872 /// if it isn't one of the formats we support.
2873 pub fn from_extension(name: &str) -> Option<Self> {
2874 match name.rsplit('.').next()?.to_lowercase().as_str() {
2875 "png" => Some(ImageFormat::Png),
2876 "jpg" | "jpeg" => Some(ImageFormat::Jpeg),
2877 "gif" => Some(ImageFormat::Gif),
2878 "bmp" => Some(ImageFormat::Bmp),
2879 _ => None,
2880 }
2881 }
2882}
2883
2884// ============================================================================,: an embedded chart,
2885// inline in a run. ============================================================================
2886
2887/// An inline chart (`<w:drawing><wp:inline>..`), embedded in a run — [`RunContent::Chart`]'s
2888/// payload. Structurally the same `wp:inline` skeleton as [`Image`] (`wp:extent`/`wp:docPr`,
2889/// wrapping an `a:graphic`/`a:graphicData`), but the `a:graphicData` wraps a `<c:chart r:id="..">`
2890/// graphic frame instead of a `pic:pic` (see `writer.rs::write_embedded_chart`) — its own
2891/// graphic-frame wiring rather than a picture's, even though both live under `wp:inline` on the
2892/// Word side.
2893///
2894/// A deliberate scope decision, since chart-in-document support is still an evolving corner of the
2895/// format: **cached values only**, no embedded `.xlsx` workbook as a data source. This crate's
2896/// `chart::ChartSpace` already carries every series' cached values directly
2897/// (`chart::NumericDataSource`/`StringDataSource`), so a Word chart embedded this way renders
2898/// correctly in Word without any companion workbook part — it just can't be "edited" via Word's
2899/// "Edit Data in Excel" feature the way a real Word-native chart (created through Word's own UI)
2900/// can.
2901#[derive(Debug, Clone, PartialEq)]
2902pub struct EmbeddedChart {
2903 pub chart_space: chart::ChartSpace,
2904 /// The width the chart is displayed at, in EMUs (`wp:extent`'s `cx`).
2905 pub width_emu: i64,
2906 /// The height the chart is displayed at, in EMUs (`cy`).
2907 pub height_emu: i64,
2908 /// Alt text / description (`wp:docPr`'s `descr`/`name` attributes). May be empty.
2909 pub name: String,
2910}
2911
2912impl EmbeddedChart {
2913 /// Creates an embedded chart with an explicit display size in EMUs.
2914 pub fn new(chart_space: chart::ChartSpace, width_emu: i64, height_emu: i64) -> Self {
2915 Self {
2916 chart_space,
2917 width_emu,
2918 height_emu,
2919 name: String::new(),
2920 }
2921 }
2922
2923 /// Sets the chart's alt text / name and returns it for chaining.
2924 pub fn with_name(mut self, name: impl Into<String>) -> Self {
2925 self.name = name.into();
2926 self
2927 }
2928}
2929
2930impl Table {
2931 /// Creates an empty table.
2932 pub fn new() -> Self {
2933 Self::default()
2934 }
2935
2936 /// Appends a row and returns the table for chaining.
2937 pub fn with_row(mut self, row: TableRow) -> Self {
2938 self.rows.push(row);
2939 self
2940 }
2941
2942 /// Sets this table's per-column preferred widths (in twips, one entry per grid column) and
2943 /// returns it for chaining. See `column_widths`'s doc comment for how this also switches the
2944 /// table to a fixed (rather than autofit) layout.
2945 pub fn with_column_widths(mut self, column_widths: Vec<u32>) -> Self {
2946 self.column_widths = column_widths;
2947 self
2948 }
2949
2950 /// Sets this table's border color and returns it for chaining. See `border_color`'s doc
2951 /// comment.
2952 pub fn with_border_color(mut self, border_color: impl Into<String>) -> Self {
2953 self.border_color = Some(border_color.into());
2954 self
2955 }
2956
2957 /// Sets this table's own horizontal alignment/positioning and returns it for chaining. See
2958 /// `alignment`'s doc comment.
2959 pub fn with_alignment(mut self, alignment: Alignment) -> Self {
2960 self.alignment = Some(alignment);
2961 self
2962 }
2963
2964 /// Sets this table's indentation, in twips, and returns it for chaining. See `indent_twips`'s
2965 /// doc comment.
2966 pub fn with_indent_twips(mut self, indent_twips: u32) -> Self {
2967 self.indent_twips = Some(indent_twips);
2968 self
2969 }
2970
2971 /// Sets the id of a named table style to apply and returns it for chaining. See `style_id`'s
2972 /// doc comment.
2973 pub fn with_style_id(mut self, style_id: impl Into<String>) -> Self {
2974 self.style_id = Some(style_id.into());
2975 self
2976 }
2977}
2978
2979impl TableRow {
2980 /// Creates an empty row.
2981 pub fn new() -> Self {
2982 Self::default()
2983 }
2984
2985 /// Appends a cell and returns the row for chaining.
2986 pub fn with_cell(mut self, cell: TableCell) -> Self {
2987 self.cells.push(cell);
2988 self
2989 }
2990
2991 /// Marks this row as repeating as a header row on every page and returns it for chaining. See
2992 /// `repeat_as_header_row`'s doc comment.
2993 pub fn with_repeat_as_header_row(mut self, repeat_as_header_row: bool) -> Self {
2994 self.repeat_as_header_row = repeat_as_header_row;
2995 self
2996 }
2997
2998 /// Sets this row's height, in twips, and returns it for chaining. See `height_twips`'s doc
2999 /// comment.
3000 pub fn with_height_twips(mut self, height_twips: u32) -> Self {
3001 self.height_twips = Some(height_twips);
3002 self
3003 }
3004
3005 /// Marks this row as unable to split across a page break and returns it for chaining. See
3006 /// `cant_split`'s doc comment.
3007 pub fn with_cant_split(mut self, cant_split: bool) -> Self {
3008 self.cant_split = cant_split;
3009 self
3010 }
3011}
3012
3013impl TableCell {
3014 /// Creates an empty cell (still valid to write: the writer emits a single empty paragraph for
3015 /// it, as WordprocessingML requires).
3016 pub fn new() -> Self {
3017 Self::default()
3018 }
3019
3020 /// Creates a cell containing a single paragraph with the given text.
3021 pub fn with_text(text: impl Into<String>) -> Self {
3022 Self {
3023 blocks: vec![Block::Paragraph(Paragraph::with_text(text))],
3024 ..Self::default()
3025 }
3026 }
3027
3028 /// Appends a paragraph block and returns the cell for chaining.
3029 pub fn with_paragraph(mut self, paragraph: Paragraph) -> Self {
3030 self.blocks.push(Block::Paragraph(paragraph));
3031 self
3032 }
3033
3034 /// Appends a nested table block and returns the cell for chaining.
3035 ///
3036 /// WordprocessingML (`CT_Tc`) allows a `w:tbl` inside a cell's content. The writer
3037 /// automatically appends a trailing empty paragraph after the nested table if this cell doesn't
3038 /// already end with one, since Word requires cell content to end in a paragraph (see
3039 /// [`TableCell`]'s doc comment).
3040 pub fn with_table(mut self, table: Table) -> Self {
3041 self.blocks.push(Block::Table(table));
3042 self
3043 }
3044
3045 /// Iterates over this cell's paragraphs only, skipping any nested table.
3046 pub fn paragraphs(&self) -> impl Iterator<Item = &Paragraph> {
3047 self.blocks.iter().filter_map(|block| match block {
3048 Block::Paragraph(paragraph) => Some(paragraph),
3049 Block::Table(_) | Block::StructuredDocumentTag(_) => None,
3050 })
3051 }
3052
3053 /// Iterates over this cell's nested tables only, skipping paragraphs.
3054 pub fn tables(&self) -> impl Iterator<Item = &Table> {
3055 self.blocks.iter().filter_map(|block| match block {
3056 Block::Table(table) => Some(table),
3057 Block::Paragraph(_) | Block::StructuredDocumentTag(_) => None,
3058 })
3059 }
3060
3061 /// Sets how many grid columns this cell spans (for a horizontal merge) and returns it for
3062 /// chaining. See `horizontal_span`'s doc comment for how this interacts with `TableRow.cells`.
3063 pub fn with_horizontal_span(mut self, horizontal_span: u32) -> Self {
3064 self.horizontal_span = Some(horizontal_span);
3065 self
3066 }
3067
3068 /// Sets this cell's role in a vertical merge and returns it for chaining.
3069 pub fn with_vertical_merge(mut self, vertical_merge: VerticalMerge) -> Self {
3070 self.vertical_merge = Some(vertical_merge);
3071 self
3072 }
3073
3074 /// Sets whether this cell has a border around it and returns it for chaining. See `border`'s
3075 /// doc comment.
3076 pub fn with_border(mut self, border: bool) -> Self {
3077 self.border = border;
3078 self
3079 }
3080
3081 /// Sets this cell's background shading color and returns it for chaining. See `shading_color`'s
3082 /// doc comment.
3083 pub fn with_shading_color(mut self, shading_color: impl Into<String>) -> Self {
3084 self.shading_color = Some(shading_color.into());
3085 self
3086 }
3087
3088 /// Sets this cell's vertical alignment and returns it for chaining. See `vertical_alignment`'s
3089 /// doc comment.
3090 pub fn with_vertical_alignment(mut self, vertical_alignment: CellVerticalAlign) -> Self {
3091 self.vertical_alignment = Some(vertical_alignment);
3092 self
3093 }
3094
3095 /// Sets this cell's margin overrides (top/left/bottom/right, in twips) and returns it for
3096 /// chaining. See `margin_top_twips`'s doc comment.
3097 pub fn with_margins_twips(mut self, top: u32, left: u32, bottom: u32, right: u32) -> Self {
3098 self.margin_top_twips = Some(top);
3099 self.margin_left_twips = Some(left);
3100 self.margin_bottom_twips = Some(bottom);
3101 self.margin_right_twips = Some(right);
3102 self
3103 }
3104
3105 /// Sets this cell's text flow direction and returns it for chaining. See `text_direction`'s doc
3106 /// comment.
3107 pub fn with_text_direction(mut self, text_direction: TextDirection) -> Self {
3108 self.text_direction = Some(text_direction);
3109 self
3110 }
3111
3112 /// The cell's text: its paragraphs' text, joined with newlines. Any nested table's text is not
3113 /// included — use [`TableCell::tables`] to reach it.
3114 pub fn text(&self) -> String {
3115 self.paragraphs()
3116 .map(Paragraph::text)
3117 .collect::<Vec<_>>()
3118 .join("\n")
3119 }
3120}
3121
3122impl Style {
3123 /// Creates a style with the given id, display name and kind, and no formatting.
3124 pub fn new(id: impl Into<String>, name: impl Into<String>, kind: StyleKind) -> Self {
3125 Self {
3126 id: id.into(),
3127 name: name.into(),
3128 kind,
3129 based_on: None,
3130 bold: false,
3131 italic: false,
3132 underline: None,
3133 underline_color: None,
3134 color: None,
3135 font_size: None,
3136 font_family: None,
3137 highlight: None,
3138 strike: false,
3139 vertical_align: None,
3140 small_caps: false,
3141 all_caps: false,
3142 shading_color: None,
3143 text_border: false,
3144 character_spacing: None,
3145 vertical_position: None,
3146 hidden: false,
3147 alignment: None,
3148 line_spacing: None,
3149 space_before: None,
3150 space_after: None,
3151 paragraph_shading_color: None,
3152 paragraph_border: false,
3153 keep_with_next: false,
3154 keep_lines_together: false,
3155 page_break_before: false,
3156 tabs: Vec::new(),
3157 contextual_spacing: false,
3158 }
3159 }
3160
3161 /// Sets the style this one inherits from (by id) and returns it for chaining.
3162 pub fn with_based_on(mut self, style_id: impl Into<String>) -> Self {
3163 self.based_on = Some(style_id.into());
3164 self
3165 }
3166
3167 /// Sets whether text in this style is bold and returns it for chaining.
3168 pub fn with_bold(mut self, bold: bool) -> Self {
3169 self.bold = bold;
3170 self
3171 }
3172
3173 /// Sets whether text in this style is italic and returns it for chaining.
3174 pub fn with_italic(mut self, italic: bool) -> Self {
3175 self.italic = italic;
3176 self
3177 }
3178
3179 /// Sets this style's underline style and returns it for chaining.
3180 pub fn with_underline(mut self, underline: UnderlineStyle) -> Self {
3181 self.underline = Some(underline);
3182 self
3183 }
3184
3185 /// Sets the color of this style's underline (a 6-digit RGB hex string, no leading `#`) and
3186 /// returns it for chaining. Only meaningful once `with_underline` has also been called.
3187 pub fn with_underline_color(mut self, underline_color: impl Into<String>) -> Self {
3188 self.underline_color = Some(underline_color.into());
3189 self
3190 }
3191
3192 /// Sets this style's text color (a 6-digit RGB hex string, no leading `#`) and returns it for
3193 /// chaining.
3194 pub fn with_color(mut self, color: impl Into<String>) -> Self {
3195 self.color = Some(color.into());
3196 self
3197 }
3198
3199 /// Sets this style's font size in whole points and returns it for chaining.
3200 pub fn with_font_size(mut self, font_size: u16) -> Self {
3201 self.font_size = Some(font_size);
3202 self
3203 }
3204
3205 /// Sets this style's font family/typeface and returns it for chaining.
3206 pub fn with_font_family(mut self, font_family: impl Into<String>) -> Self {
3207 self.font_family = Some(font_family.into());
3208 self
3209 }
3210
3211 /// Sets this style's highlight color and returns it for chaining.
3212 pub fn with_highlight(mut self, highlight: Highlight) -> Self {
3213 self.highlight = Some(highlight);
3214 self
3215 }
3216
3217 /// Sets whether text in this style is struck through and returns it for chaining.
3218 pub fn with_strike(mut self, strike: bool) -> Self {
3219 self.strike = strike;
3220 self
3221 }
3222
3223 /// Sets this style's vertical alignment (superscript/subscript) and returns it for chaining.
3224 pub fn with_vertical_align(mut self, vertical_align: VerticalAlign) -> Self {
3225 self.vertical_align = Some(vertical_align);
3226 self
3227 }
3228
3229 /// Sets whether text in this style is displayed as small caps and returns it for chaining.
3230 pub fn with_small_caps(mut self, small_caps: bool) -> Self {
3231 self.small_caps = small_caps;
3232 self
3233 }
3234
3235 /// Sets whether text in this style is displayed as all caps and returns it for chaining.
3236 pub fn with_all_caps(mut self, all_caps: bool) -> Self {
3237 self.all_caps = all_caps;
3238 self
3239 }
3240
3241 /// Sets this style's background shading color (a 6-digit RGB hex string, no leading `#`) and
3242 /// returns it for chaining.
3243 pub fn with_shading_color(mut self, shading_color: impl Into<String>) -> Self {
3244 self.shading_color = Some(shading_color.into());
3245 self
3246 }
3247
3248 /// Sets whether text in this style has a border and returns it for chaining.
3249 pub fn with_text_border(mut self, text_border: bool) -> Self {
3250 self.text_border = text_border;
3251 self
3252 }
3253
3254 /// Sets this style's character spacing adjustment, in twips, and returns it for chaining.
3255 pub fn with_character_spacing(mut self, character_spacing: i16) -> Self {
3256 self.character_spacing = Some(character_spacing);
3257 self
3258 }
3259
3260 /// Sets this style's vertical position, raised or lowered from the baseline, in half-points,
3261 /// and returns it for chaining.
3262 pub fn with_vertical_position(mut self, vertical_position: i16) -> Self {
3263 self.vertical_position = Some(vertical_position);
3264 self
3265 }
3266
3267 /// Sets whether text in this style is hidden and returns it for chaining.
3268 pub fn with_hidden(mut self, hidden: bool) -> Self {
3269 self.hidden = hidden;
3270 self
3271 }
3272
3273 /// Sets this style's paragraph alignment and returns it for chaining. Only meaningful for
3274 /// [`StyleKind::Paragraph`] — see the field's doc comment.
3275 pub fn with_alignment(mut self, alignment: Alignment) -> Self {
3276 self.alignment = Some(alignment);
3277 self
3278 }
3279
3280 /// Sets this style's line spacing (in `w:spacing`'s "auto" units — see
3281 /// `Paragraph.line_spacing`'s doc comment) and returns it for chaining. Only meaningful for
3282 /// [`StyleKind::Paragraph`].
3283 pub fn with_line_spacing(mut self, line_spacing: u32) -> Self {
3284 self.line_spacing = Some(line_spacing);
3285 self
3286 }
3287
3288 /// Sets this style's extra space before a paragraph, in twips, and returns it for chaining.
3289 /// Only meaningful for [`StyleKind::Paragraph`].
3290 pub fn with_space_before(mut self, space_before: u32) -> Self {
3291 self.space_before = Some(space_before);
3292 self
3293 }
3294
3295 /// Sets this style's extra space after a paragraph, in twips, and returns it for chaining. Only
3296 /// meaningful for [`StyleKind::Paragraph`].
3297 pub fn with_space_after(mut self, space_after: u32) -> Self {
3298 self.space_after = Some(space_after);
3299 self
3300 }
3301
3302 /// Sets this style's paragraph background shading color and returns it for chaining. Only
3303 /// meaningful for [`StyleKind::Paragraph`] — see `paragraph_shading_color`'s doc comment for
3304 /// how this differs from `with_shading_color`.
3305 pub fn with_paragraph_shading_color(
3306 mut self,
3307 paragraph_shading_color: impl Into<String>,
3308 ) -> Self {
3309 self.paragraph_shading_color = Some(paragraph_shading_color.into());
3310 self
3311 }
3312
3313 /// Sets whether this style draws a border around the paragraph and returns it for chaining.
3314 /// Only meaningful for [`StyleKind::Paragraph`] — see `paragraph_border`'s doc comment for how
3315 /// this differs from `with_text_border`.
3316 pub fn with_paragraph_border(mut self, paragraph_border: bool) -> Self {
3317 self.paragraph_border = paragraph_border;
3318 self
3319 }
3320
3321 /// Sets whether this style keeps its paragraph on the same page as the one following it and
3322 /// returns it for chaining. Only meaningful for [`StyleKind::Paragraph`]. See
3323 /// `keep_with_next`'s doc comment.
3324 pub fn with_keep_with_next(mut self, keep_with_next: bool) -> Self {
3325 self.keep_with_next = keep_with_next;
3326 self
3327 }
3328
3329 /// Sets whether this style keeps its paragraph's lines together on the same page and returns it
3330 /// for chaining. Only meaningful for [`StyleKind::Paragraph`]. See `keep_lines_together`'s doc
3331 /// comment.
3332 pub fn with_keep_lines_together(mut self, keep_lines_together: bool) -> Self {
3333 self.keep_lines_together = keep_lines_together;
3334 self
3335 }
3336
3337 /// Sets whether this style forces a page break before its paragraph and returns it for
3338 /// chaining. Only meaningful for [`StyleKind::Paragraph`]. See `page_break_before`'s doc
3339 /// comment.
3340 pub fn with_page_break_before(mut self, page_break_before: bool) -> Self {
3341 self.page_break_before = page_break_before;
3342 self
3343 }
3344
3345 /// Appends a custom tab stop and returns this style for chaining. Only meaningful for
3346 /// [`StyleKind::Paragraph`]. See `tabs`'s doc comment.
3347 pub fn with_tab(mut self, tab: TabStop) -> Self {
3348 self.tabs.push(tab);
3349 self
3350 }
3351
3352 /// Sets whether this style ignores spacing above/below when adjacent to a paragraph sharing the
3353 /// same style and returns it for chaining. Only meaningful for [`StyleKind::Paragraph`]. See
3354 /// `contextual_spacing`'s doc comment.
3355 pub fn with_contextual_spacing(mut self, contextual_spacing: bool) -> Self {
3356 self.contextual_spacing = contextual_spacing;
3357 self
3358 }
3359}
3360
3361impl NumberingDefinition {
3362 /// Creates a numbering definition with the given id and levels (fully custom — see
3363 /// [`NumberingDefinition::bullet`]/ [`NumberingDefinition::decimal`] for ready-made common
3364 /// cases).
3365 pub fn new(id: u32, levels: Vec<ListLevel>) -> Self {
3366 Self { id, levels }
3367 }
3368
3369 /// A ready-made 3-level bulleted (unordered) list, using the same bullet characters, format and
3370 /// indentation Word's own default "Bullets" gallery entry uses when it doesn't need to fall
3371 /// back to a Wingdings/Symbol-font glyph — this crate doesn't model fonts, so plain Unicode
3372 /// bullet characters are used instead of Word's own Wingdings/Symbol ones (which would render
3373 /// as the wrong glyph without the matching font declared on the run).
3374 pub fn bullet(id: u32) -> Self {
3375 Self::new(
3376 id,
3377 vec![
3378 ListLevel::new(NumberFormat::Bullet, "\u{2022}", 720, 360), // •
3379 ListLevel::new(NumberFormat::Bullet, "\u{25E6}", 1440, 360), // ◦
3380 ListLevel::new(NumberFormat::Bullet, "\u{25AA}", 2160, 360), // ▪
3381 ],
3382 )
3383 }
3384
3385 /// A ready-made 3-level numbered (ordered) list, using the same cumulative numbering (each
3386 /// level repeats its ancestors' counters, e.g. "1.1.1.") and indentation Word's own default
3387 /// "Numbering" gallery entry uses.
3388 pub fn decimal(id: u32) -> Self {
3389 Self::new(
3390 id,
3391 vec![
3392 ListLevel::new(NumberFormat::Decimal, "%1.", 360, 360),
3393 ListLevel::new(NumberFormat::Decimal, "%1.%2.", 792, 432),
3394 ListLevel::new(NumberFormat::Decimal, "%1.%2.%3.", 1224, 504),
3395 ],
3396 )
3397 }
3398}
3399
3400impl ListLevel {
3401 /// Creates a single indent level with explicit formatting and indentation.
3402 pub fn new(
3403 format: NumberFormat,
3404 text: impl Into<String>,
3405 indent_twips: i32,
3406 hanging_twips: i32,
3407 ) -> Self {
3408 Self {
3409 format,
3410 text: text.into(),
3411 indent_twips,
3412 hanging_twips,
3413 }
3414 }
3415}