Skip to main content

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}