Skip to main content

ooxml_wml/
convenience.rs

1//! Builder methods on generated types for ergonomic document construction.
2//!
3//! These `impl` blocks extend the generated types with convenience methods
4//! for building documents (adding paragraphs, runs, tables, etc.).
5//! They are allowed because the generated types are in the same crate.
6
7use crate::types;
8#[cfg(feature = "extra-children")]
9use ooxml_xml::PositionedNode;
10use ooxml_xml::{RawXmlElement, RawXmlNode};
11
12// =============================================================================
13// Body
14// =============================================================================
15
16impl types::Body {
17    /// Add an empty paragraph and return a mutable reference to it.
18    pub fn add_paragraph(&mut self) -> &mut types::Paragraph {
19        self.block_content
20            .push(types::BlockContent::P(Box::default()));
21        match self.block_content.last_mut().unwrap() {
22            types::BlockContent::P(p) => p.as_mut(),
23            _ => unreachable!(),
24        }
25    }
26
27    /// Add an empty table and return a mutable reference to it.
28    #[cfg(feature = "wml-tables")]
29    pub fn add_table(&mut self) -> &mut types::Table {
30        let table = types::Table {
31            range_markup: Vec::new(),
32            table_properties: Box::new(types::TableProperties::default()),
33            tbl_grid: Box::new(types::TableGrid::default()),
34            rows: Vec::new(),
35            #[cfg(feature = "extra-children")]
36            extra_children: Vec::new(),
37        };
38        self.block_content
39            .push(types::BlockContent::Tbl(Box::new(table)));
40        match self.block_content.last_mut().unwrap() {
41            types::BlockContent::Tbl(t) => t.as_mut(),
42            _ => unreachable!(),
43        }
44    }
45
46    /// Set section properties on the body.
47    #[cfg(feature = "wml-layout")]
48    pub fn set_section_properties(&mut self, sect_pr: types::SectionProperties) {
49        self.sect_pr = Some(Box::new(sect_pr));
50    }
51}
52
53// =============================================================================
54// Paragraph
55// =============================================================================
56
57impl types::Paragraph {
58    /// Add an empty run and return a mutable reference to it.
59    pub fn add_run(&mut self) -> &mut types::Run {
60        self.paragraph_content
61            .push(types::ParagraphContent::R(Box::default()));
62        match self.paragraph_content.last_mut().unwrap() {
63            types::ParagraphContent::R(r) => r.as_mut(),
64            _ => unreachable!(),
65        }
66    }
67
68    /// Add an empty hyperlink and return a mutable reference to it.
69    #[cfg(feature = "wml-hyperlinks")]
70    pub fn add_hyperlink(&mut self) -> &mut types::Hyperlink {
71        self.paragraph_content
72            .push(types::ParagraphContent::Hyperlink(Box::default()));
73        match self.paragraph_content.last_mut().unwrap() {
74            types::ParagraphContent::Hyperlink(h) => h.as_mut(),
75            _ => unreachable!(),
76        }
77    }
78
79    /// Add a bookmark start marker.
80    ///
81    /// This version accepts a `u32` id for ergonomic use in writer APIs.
82    /// ECMA-376 Part 1, Section 17.13.6.1 (`w:bookmarkStart`).
83    pub fn add_bookmark_start_u32(&mut self, id: u32, name: &str) {
84        self.add_bookmark_start(id as i64, name);
85    }
86
87    /// Add a bookmark end marker.
88    ///
89    /// This version accepts a `u32` id for ergonomic use in writer APIs.
90    /// ECMA-376 Part 1, Section 17.13.6.2 (`w:bookmarkEnd`).
91    pub fn add_bookmark_end_u32(&mut self, id: u32) {
92        self.add_bookmark_end(id as i64);
93    }
94
95    /// Add a bookmark start marker.
96    pub fn add_bookmark_start(&mut self, id: i64, name: &str) {
97        let bookmark = types::Bookmark {
98            id,
99            name: name.to_string(),
100            #[cfg(feature = "wml-settings")]
101            displaced_by_custom_xml: None,
102            #[cfg(feature = "wml-tables")]
103            col_first: None,
104            #[cfg(feature = "wml-tables")]
105            col_last: None,
106            #[cfg(feature = "extra-attrs")]
107            extra_attrs: Default::default(),
108        };
109        self.paragraph_content
110            .push(types::ParagraphContent::BookmarkStart(Box::new(bookmark)));
111    }
112
113    /// Add a bookmark end marker.
114    pub fn add_bookmark_end(&mut self, id: i64) {
115        let range = types::CTMarkupRange {
116            id,
117            #[cfg(feature = "wml-settings")]
118            displaced_by_custom_xml: None,
119            #[cfg(feature = "extra-attrs")]
120            extra_attrs: Default::default(),
121        };
122        self.paragraph_content
123            .push(types::ParagraphContent::BookmarkEnd(Box::new(range)));
124    }
125
126    /// Add a comment range start marker.
127    pub fn add_comment_range_start(&mut self, id: u32) {
128        let range = types::CTMarkupRange {
129            id: id as i64,
130            #[cfg(feature = "wml-settings")]
131            displaced_by_custom_xml: None,
132            #[cfg(feature = "extra-attrs")]
133            extra_attrs: Default::default(),
134        };
135        self.paragraph_content
136            .push(types::ParagraphContent::CommentRangeStart(Box::new(range)));
137    }
138
139    /// Add a comment range end marker.
140    pub fn add_comment_range_end(&mut self, id: u32) {
141        let range = types::CTMarkupRange {
142            id: id as i64,
143            #[cfg(feature = "wml-settings")]
144            displaced_by_custom_xml: None,
145            #[cfg(feature = "extra-attrs")]
146            extra_attrs: Default::default(),
147        };
148        self.paragraph_content
149            .push(types::ParagraphContent::CommentRangeEnd(Box::new(range)));
150    }
151
152    /// Set paragraph properties.
153    #[cfg(feature = "wml-styling")]
154    pub fn set_properties(&mut self, props: types::ParagraphProperties) {
155        self.p_pr = Some(Box::new(props));
156    }
157
158    /// Set numbering properties (list membership) on this paragraph.
159    #[cfg(feature = "wml-styling")]
160    pub fn set_numbering(&mut self, num_id: u32, ilvl: u32) {
161        let ppr = self
162            .p_pr
163            .get_or_insert_with(|| Box::new(types::ParagraphProperties::default()));
164        ppr.num_pr = Some(Box::new(types::NumberingProperties {
165            ilvl: Some(Box::new(types::CTDecimalNumber {
166                value: ilvl as i64,
167                #[cfg(feature = "extra-attrs")]
168                extra_attrs: Default::default(),
169            })),
170            num_id: Some(Box::new(types::CTDecimalNumber {
171                value: num_id as i64,
172                #[cfg(feature = "extra-attrs")]
173                extra_attrs: Default::default(),
174            })),
175            numbering_change: None,
176            ins: None,
177            #[cfg(feature = "extra-children")]
178            extra_children: Vec::new(),
179        }));
180    }
181
182    /// Set paragraph alignment.
183    ///
184    /// Use `STJc` variants: `Left`, `Center`, `Right`, `Both` (justified), etc.
185    #[cfg(feature = "wml-styling")]
186    pub fn set_alignment(&mut self, alignment: types::STJc) {
187        let ppr = self
188            .p_pr
189            .get_or_insert_with(|| Box::new(types::ParagraphProperties::default()));
190        ppr.justification = Some(Box::new(types::CTJc {
191            value: alignment,
192            #[cfg(feature = "extra-attrs")]
193            extra_attrs: Default::default(),
194        }));
195    }
196
197    /// Set paragraph spacing (before and after, in twips).
198    #[cfg(feature = "wml-styling")]
199    pub fn set_spacing(&mut self, before: Option<u32>, after: Option<u32>) {
200        let ppr = self
201            .p_pr
202            .get_or_insert_with(|| Box::new(types::ParagraphProperties::default()));
203        ppr.spacing = Some(Box::new(types::CTSpacing {
204            before: before.map(|b| b.to_string()),
205            after: after.map(|a| a.to_string()),
206            ..Default::default()
207        }));
208    }
209
210    /// Set paragraph indentation.
211    #[cfg(feature = "wml-styling")]
212    pub fn set_indent(&mut self, left: Option<u32>, first_line: Option<u32>) {
213        let ppr = self
214            .p_pr
215            .get_or_insert_with(|| Box::new(types::ParagraphProperties::default()));
216        ppr.indentation = Some(Box::new(types::CTInd {
217            left: left.map(|l| l.to_string()),
218            first_line: first_line.map(|fl| fl.to_string()),
219            ..Default::default()
220        }));
221    }
222
223    /// Insert a page break run into this paragraph.
224    ///
225    /// Adds `<w:r><w:br w:type="page"/></w:r>` to the paragraph content.
226    /// ECMA-376 Part 1, Section 17.3.3.1 (`w:br`).
227    pub fn add_page_break(&mut self) -> &mut Self {
228        let mut run = types::Run::default();
229        run.run_content
230            .push(types::RunContent::Br(Box::new(types::CTBr {
231                r#type: Some(types::STBrType::Page),
232                clear: None,
233                #[cfg(feature = "extra-attrs")]
234                extra_attrs: Default::default(),
235            })));
236        self.paragraph_content
237            .push(types::ParagraphContent::R(Box::new(run)));
238        self
239    }
240
241    /// Insert a column break run into this paragraph.
242    ///
243    /// Adds `<w:r><w:br w:type="column"/></w:r>` to the paragraph content.
244    /// ECMA-376 Part 1, Section 17.3.3.1 (`w:br`).
245    pub fn add_column_break(&mut self) -> &mut Self {
246        let mut run = types::Run::default();
247        run.run_content
248            .push(types::RunContent::Br(Box::new(types::CTBr {
249                r#type: Some(types::STBrType::Column),
250                clear: None,
251                #[cfg(feature = "extra-attrs")]
252                extra_attrs: Default::default(),
253            })));
254        self.paragraph_content
255            .push(types::ParagraphContent::R(Box::new(run)));
256        self
257    }
258
259    /// Set space before the paragraph in twips (twentieths of a point).
260    ///
261    /// Modifies the `<w:spacing w:before="..."/>` attribute.
262    /// ECMA-376 Part 1, Section 17.3.1.33 (`w:spacing`).
263    #[cfg(feature = "wml-styling")]
264    pub fn set_space_before(&mut self, twips: u32) -> &mut Self {
265        let ppr = self
266            .p_pr
267            .get_or_insert_with(|| Box::new(types::ParagraphProperties::default()));
268        let spacing = ppr
269            .spacing
270            .get_or_insert_with(|| Box::new(types::CTSpacing::default()));
271        spacing.before = Some(twips.to_string());
272        self
273    }
274
275    /// Set space after the paragraph in twips (twentieths of a point).
276    ///
277    /// Modifies the `<w:spacing w:after="..."/>` attribute.
278    /// ECMA-376 Part 1, Section 17.3.1.33 (`w:spacing`).
279    #[cfg(feature = "wml-styling")]
280    pub fn set_space_after(&mut self, twips: u32) -> &mut Self {
281        let ppr = self
282            .p_pr
283            .get_or_insert_with(|| Box::new(types::ParagraphProperties::default()));
284        let spacing = ppr
285            .spacing
286            .get_or_insert_with(|| Box::new(types::CTSpacing::default()));
287        spacing.after = Some(twips.to_string());
288        self
289    }
290
291    /// Set line spacing in twips.
292    ///
293    /// Sets `<w:spacing w:line="..." w:lineRule="auto"/>`.
294    /// A value of 240 is single-spacing (12pt × 20), 360 is 1.5×, 480 is double.
295    /// ECMA-376 Part 1, Section 17.3.1.33 (`w:spacing`).
296    #[cfg(feature = "wml-styling")]
297    pub fn set_line_spacing(&mut self, twips: u32) -> &mut Self {
298        let ppr = self
299            .p_pr
300            .get_or_insert_with(|| Box::new(types::ParagraphProperties::default()));
301        let spacing = ppr
302            .spacing
303            .get_or_insert_with(|| Box::new(types::CTSpacing::default()));
304        spacing.line = Some(twips.to_string());
305        spacing.line_rule = Some(types::STLineSpacingRule::Auto);
306        self
307    }
308
309    /// Set left indentation in twips.
310    ///
311    /// Sets `<w:ind w:left="..."/>`.
312    /// ECMA-376 Part 1, Section 17.3.1.12 (`w:ind`).
313    #[cfg(feature = "wml-styling")]
314    pub fn set_indent_left(&mut self, twips: u32) -> &mut Self {
315        let ppr = self
316            .p_pr
317            .get_or_insert_with(|| Box::new(types::ParagraphProperties::default()));
318        let ind = ppr
319            .indentation
320            .get_or_insert_with(|| Box::new(types::CTInd::default()));
321        ind.left = Some(twips.to_string());
322        self
323    }
324
325    /// Set right indentation in twips.
326    ///
327    /// Sets `<w:ind w:right="..."/>`.
328    /// ECMA-376 Part 1, Section 17.3.1.12 (`w:ind`).
329    #[cfg(feature = "wml-styling")]
330    pub fn set_indent_right(&mut self, twips: u32) -> &mut Self {
331        let ppr = self
332            .p_pr
333            .get_or_insert_with(|| Box::new(types::ParagraphProperties::default()));
334        let ind = ppr
335            .indentation
336            .get_or_insert_with(|| Box::new(types::CTInd::default()));
337        ind.right = Some(twips.to_string());
338        self
339    }
340
341    /// Set first-line indentation in twips.
342    ///
343    /// Sets `<w:ind w:firstLine="..."/>`. A positive value indents the first
344    /// line; use `hanging` for a hanging indent (not yet exposed directly).
345    /// ECMA-376 Part 1, Section 17.3.1.12 (`w:ind`).
346    #[cfg(feature = "wml-styling")]
347    pub fn set_indent_first_line(&mut self, twips: u32) -> &mut Self {
348        let ppr = self
349            .p_pr
350            .get_or_insert_with(|| Box::new(types::ParagraphProperties::default()));
351        let ind = ppr
352            .indentation
353            .get_or_insert_with(|| Box::new(types::CTInd::default()));
354        ind.first_line = Some(twips.to_string());
355        self
356    }
357
358    /// Set the outline level of this paragraph (0–8, where 0 = body text).
359    ///
360    /// Maps to `<w:outlineLvl w:val="..."/>` in paragraph properties.
361    /// Levels 0–8 correspond to heading levels 1–9 in the document outline.
362    /// ECMA-376 Part 1, Section 17.3.1.20 (`w:outlineLvl`).
363    #[cfg(feature = "wml-styling")]
364    pub fn set_outline_level(&mut self, level: u8) -> &mut Self {
365        let ppr = self
366            .p_pr
367            .get_or_insert_with(|| Box::new(types::ParagraphProperties::default()));
368        ppr.outline_lvl = Some(Box::new(types::CTDecimalNumber {
369            value: level as i64,
370            #[cfg(feature = "extra-attrs")]
371            extra_attrs: Default::default(),
372        }));
373        self
374    }
375}
376
377// =============================================================================
378// Run
379// =============================================================================
380
381impl types::Run {
382    /// Set the text content of this run.
383    pub fn set_text(&mut self, text: impl Into<String>) {
384        let t = types::Text {
385            text: Some(text.into()),
386            #[cfg(feature = "extra-children")]
387            extra_children: Vec::new(),
388        };
389        self.run_content.push(types::RunContent::T(Box::new(t)));
390    }
391
392    /// Set bold on this run. Requires `wml-styling` feature.
393    #[cfg(feature = "wml-styling")]
394    pub fn set_bold(&mut self, bold: bool) {
395        let rpr = self
396            .r_pr
397            .get_or_insert_with(|| Box::new(types::RunProperties::default()));
398        if bold {
399            rpr.bold = Some(Box::new(types::OnOffElement {
400                value: None, // None means "true" for on/off elements
401                #[cfg(feature = "extra-attrs")]
402                extra_attrs: Default::default(),
403            }));
404        } else {
405            rpr.bold = None;
406        }
407    }
408
409    /// Set italic on this run. Requires `wml-styling` feature.
410    #[cfg(feature = "wml-styling")]
411    pub fn set_italic(&mut self, italic: bool) {
412        let rpr = self
413            .r_pr
414            .get_or_insert_with(|| Box::new(types::RunProperties::default()));
415        if italic {
416            rpr.italic = Some(Box::new(types::OnOffElement {
417                value: None,
418                #[cfg(feature = "extra-attrs")]
419                extra_attrs: Default::default(),
420            }));
421        } else {
422            rpr.italic = None;
423        }
424    }
425
426    /// Add a page break to this run.
427    pub fn set_page_break(&mut self) {
428        self.run_content
429            .push(types::RunContent::Br(Box::new(types::CTBr {
430                r#type: Some(types::STBrType::Page),
431                clear: None,
432                #[cfg(feature = "extra-attrs")]
433                extra_attrs: Default::default(),
434            })));
435    }
436
437    /// Set the text color on this run (hex string, e.g. "FF0000" for red).
438    #[cfg(feature = "wml-styling")]
439    pub fn set_color(&mut self, hex: &str) {
440        let rpr = self
441            .r_pr
442            .get_or_insert_with(|| Box::new(types::RunProperties::default()));
443        rpr.color = Some(Box::new(types::CTColor {
444            value: hex.to_string(),
445            theme_color: None,
446            theme_tint: None,
447            theme_shade: None,
448            #[cfg(feature = "extra-attrs")]
449            extra_attrs: Default::default(),
450        }));
451    }
452
453    /// Set the font size in half-points (e.g. 48 = 24pt).
454    #[cfg(feature = "wml-styling")]
455    pub fn set_font_size(&mut self, half_points: i64) {
456        let rpr = self
457            .r_pr
458            .get_or_insert_with(|| Box::new(types::RunProperties::default()));
459        rpr.size = Some(Box::new(types::HpsMeasureElement {
460            value: half_points.to_string(),
461            #[cfg(feature = "extra-attrs")]
462            extra_attrs: Default::default(),
463        }));
464    }
465
466    /// Set strikethrough on this run.
467    #[cfg(feature = "wml-styling")]
468    pub fn set_strikethrough(&mut self, strike: bool) {
469        let rpr = self
470            .r_pr
471            .get_or_insert_with(|| Box::new(types::RunProperties::default()));
472        if strike {
473            rpr.strikethrough = Some(Box::new(types::OnOffElement {
474                value: None,
475                #[cfg(feature = "extra-attrs")]
476                extra_attrs: Default::default(),
477            }));
478        } else {
479            rpr.strikethrough = None;
480        }
481    }
482
483    /// Set underline style on this run.
484    #[cfg(feature = "wml-styling")]
485    pub fn set_underline(&mut self, style: types::STUnderline) {
486        let rpr = self
487            .r_pr
488            .get_or_insert_with(|| Box::new(types::RunProperties::default()));
489        rpr.underline = Some(Box::new(types::CTUnderline {
490            value: Some(style),
491            color: None,
492            theme_color: None,
493            theme_tint: None,
494            theme_shade: None,
495            #[cfg(feature = "extra-attrs")]
496            extra_attrs: Default::default(),
497        }));
498    }
499
500    /// Set fonts on this run.
501    #[cfg(feature = "wml-styling")]
502    pub fn set_fonts(&mut self, fonts: types::Fonts) {
503        let rpr = self
504            .r_pr
505            .get_or_insert_with(|| Box::new(types::RunProperties::default()));
506        rpr.fonts = Some(Box::new(fonts));
507    }
508
509    /// Set run properties.
510    #[cfg(feature = "wml-styling")]
511    pub fn set_properties(&mut self, props: types::RunProperties) {
512        self.r_pr = Some(Box::new(props));
513    }
514
515    /// Add a drawing to this run's inner content.
516    pub fn add_drawing(&mut self, drawing: types::CTDrawing) {
517        self.run_content
518            .push(types::RunContent::Drawing(Box::new(drawing)));
519    }
520
521    /// Add a footnote reference to this run.
522    pub fn add_footnote_ref(&mut self, id: i64) {
523        self.run_content
524            .push(types::RunContent::FootnoteReference(Box::new(
525                types::FootnoteEndnoteRef {
526                    #[cfg(feature = "wml-comments")]
527                    custom_mark_follows: None,
528                    id,
529                    #[cfg(feature = "extra-attrs")]
530                    extra_attrs: Default::default(),
531                },
532            )));
533    }
534
535    /// Add an endnote reference to this run.
536    pub fn add_endnote_ref(&mut self, id: i64) {
537        self.run_content
538            .push(types::RunContent::EndnoteReference(Box::new(
539                types::FootnoteEndnoteRef {
540                    #[cfg(feature = "wml-comments")]
541                    custom_mark_follows: None,
542                    id,
543                    #[cfg(feature = "extra-attrs")]
544                    extra_attrs: Default::default(),
545                },
546            )));
547    }
548
549    /// Add a comment reference to this run.
550    pub fn add_comment_ref(&mut self, id: i64) {
551        self.run_content
552            .push(types::RunContent::CommentReference(Box::new(
553                types::CTMarkup {
554                    id,
555                    #[cfg(feature = "extra-attrs")]
556                    extra_attrs: Default::default(),
557                },
558            )));
559    }
560
561    /// Helper: set an `Option<Box<OnOffElement>>` field to on/off.
562    #[cfg(feature = "wml-styling")]
563    fn set_on_off(field: &mut Option<Box<types::OnOffElement>>, on: bool) {
564        if on {
565            *field = Some(Box::new(types::OnOffElement {
566                value: None, // None means "true" for on/off elements
567                #[cfg(feature = "extra-attrs")]
568                extra_attrs: Default::default(),
569            }));
570        } else {
571            *field = None;
572        }
573    }
574
575    /// Set shadow effect on this run.
576    ///
577    /// Maps to `<w:shadow/>` in run properties.
578    /// ECMA-376 Part 1, Section 17.3.2.31 (`w:shadow`).
579    #[cfg(feature = "wml-styling")]
580    pub fn set_shadow(&mut self, on: bool) {
581        let rpr = self
582            .r_pr
583            .get_or_insert_with(|| Box::new(types::RunProperties::default()));
584        Self::set_on_off(&mut rpr.shadow, on);
585    }
586
587    /// Set outline (hollow) text effect on this run.
588    ///
589    /// Maps to `<w:outline/>` in run properties.
590    /// ECMA-376 Part 1, Section 17.3.2.23 (`w:outline`).
591    #[cfg(feature = "wml-styling")]
592    pub fn set_outline(&mut self, on: bool) {
593        let rpr = self
594            .r_pr
595            .get_or_insert_with(|| Box::new(types::RunProperties::default()));
596        Self::set_on_off(&mut rpr.outline, on);
597    }
598
599    /// Set emboss effect on this run.
600    ///
601    /// Maps to `<w:emboss/>` in run properties.
602    /// ECMA-376 Part 1, Section 17.3.2.13 (`w:emboss`).
603    #[cfg(feature = "wml-styling")]
604    pub fn set_emboss(&mut self, on: bool) {
605        let rpr = self
606            .r_pr
607            .get_or_insert_with(|| Box::new(types::RunProperties::default()));
608        Self::set_on_off(&mut rpr.emboss, on);
609    }
610
611    /// Set imprint (engrave) effect on this run.
612    ///
613    /// Maps to `<w:imprint/>` in run properties.
614    /// ECMA-376 Part 1, Section 17.3.2.18 (`w:imprint`).
615    #[cfg(feature = "wml-styling")]
616    pub fn set_imprint(&mut self, on: bool) {
617        let rpr = self
618            .r_pr
619            .get_or_insert_with(|| Box::new(types::RunProperties::default()));
620        Self::set_on_off(&mut rpr.imprint, on);
621    }
622
623    /// Set small caps on this run.
624    ///
625    /// Maps to `<w:smallCaps/>` in run properties.
626    /// ECMA-376 Part 1, Section 17.3.2.33 (`w:smallCaps`).
627    #[cfg(feature = "wml-styling")]
628    pub fn set_small_caps(&mut self, on: bool) {
629        let rpr = self
630            .r_pr
631            .get_or_insert_with(|| Box::new(types::RunProperties::default()));
632        Self::set_on_off(&mut rpr.small_caps, on);
633    }
634
635    /// Set all caps on this run.
636    ///
637    /// Maps to `<w:caps/>` in run properties.
638    /// ECMA-376 Part 1, Section 17.3.2.5 (`w:caps`).
639    #[cfg(feature = "wml-styling")]
640    pub fn set_all_caps(&mut self, on: bool) {
641        let rpr = self
642            .r_pr
643            .get_or_insert_with(|| Box::new(types::RunProperties::default()));
644        Self::set_on_off(&mut rpr.caps, on);
645    }
646
647    /// Set hidden text (vanish) on this run.
648    ///
649    /// Maps to `<w:vanish/>` in run properties. Hidden text is not rendered
650    /// unless the application is set to show hidden text.
651    /// ECMA-376 Part 1, Section 17.3.2.41 (`w:vanish`).
652    #[cfg(feature = "wml-styling")]
653    pub fn set_vanish(&mut self, on: bool) {
654        let rpr = self
655            .r_pr
656            .get_or_insert_with(|| Box::new(types::RunProperties::default()));
657        Self::set_on_off(&mut rpr.vanish, on);
658    }
659
660    /// Set double strikethrough on this run.
661    ///
662    /// Maps to `<w:dstrike/>` in run properties.
663    /// ECMA-376 Part 1, Section 17.3.2.9 (`w:dstrike`).
664    #[cfg(feature = "wml-styling")]
665    pub fn set_double_strike(&mut self, on: bool) {
666        let rpr = self
667            .r_pr
668            .get_or_insert_with(|| Box::new(types::RunProperties::default()));
669        Self::set_on_off(&mut rpr.dstrike, on);
670    }
671}
672
673// =============================================================================
674// Hyperlink
675// =============================================================================
676
677#[cfg(feature = "wml-hyperlinks")]
678impl types::Hyperlink {
679    /// Add a run to this hyperlink and return a mutable reference.
680    pub fn add_run(&mut self) -> &mut types::Run {
681        self.paragraph_content
682            .push(types::ParagraphContent::R(Box::default()));
683        match self.paragraph_content.last_mut().unwrap() {
684            types::ParagraphContent::R(r) => r.as_mut(),
685            _ => unreachable!(),
686        }
687    }
688
689    /// Set the relationship ID (for external hyperlinks).
690    pub fn set_rel_id(&mut self, rel_id: &str) {
691        self.id = Some(rel_id.to_string());
692    }
693
694    /// Set the anchor (for internal bookmarks).
695    pub fn set_anchor(&mut self, anchor: &str) {
696        self.anchor = Some(anchor.to_string());
697    }
698}
699
700// =============================================================================
701// Table
702// =============================================================================
703
704#[cfg(feature = "wml-tables")]
705impl types::Table {
706    /// Add a row and return a mutable reference.
707    pub fn add_row(&mut self) -> &mut types::CTRow {
708        self.rows.push(types::RowContent::Tr(Box::default()));
709        match self.rows.last_mut().unwrap() {
710            types::RowContent::Tr(r) => r.as_mut(),
711            _ => unreachable!(),
712        }
713    }
714}
715
716#[cfg(feature = "wml-tables")]
717impl types::CTRow {
718    /// Add a cell and return a mutable reference.
719    pub fn add_cell(&mut self) -> &mut types::TableCell {
720        self.cells.push(types::CellContent::Tc(Box::default()));
721        match self.cells.last_mut().unwrap() {
722            types::CellContent::Tc(c) => c.as_mut(),
723            _ => unreachable!(),
724        }
725    }
726}
727
728/// Vertical merge type for table cells.
729///
730/// ECMA-376 Part 1, Section 17.4.84 (`w:vMerge`).
731#[cfg(feature = "wml-tables")]
732#[derive(Debug, Clone, Copy, PartialEq, Eq)]
733pub enum VMergeType {
734    /// The cell starts a vertically merged region (`w:val="restart"`).
735    Restart,
736    /// The cell continues an existing merged region (omitted `w:val`).
737    Continue,
738}
739
740#[cfg(feature = "wml-tables")]
741impl types::TableCell {
742    /// Add a paragraph and return a mutable reference.
743    pub fn add_paragraph(&mut self) -> &mut types::Paragraph {
744        self.block_content
745            .push(types::BlockContent::P(Box::default()));
746        match self.block_content.last_mut().unwrap() {
747            types::BlockContent::P(p) => p.as_mut(),
748            _ => unreachable!(),
749        }
750    }
751
752    /// Set the grid span (number of columns this cell spans).
753    ///
754    /// Maps to `<w:gridSpan w:val="n"/>` in cell properties.
755    /// ECMA-376 Part 1, Section 17.4.17 (`w:gridSpan`).
756    pub fn set_grid_span(&mut self, n: u32) {
757        let tcpr = self
758            .cell_properties
759            .get_or_insert_with(|| Box::new(types::TableCellProperties::default()));
760        tcpr.grid_span = Some(Box::new(types::CTDecimalNumber {
761            value: n as i64,
762            #[cfg(feature = "extra-attrs")]
763            extra_attrs: Default::default(),
764        }));
765    }
766
767    /// Set vertical merge on this cell.
768    ///
769    /// - `VMergeType::Restart` → `<w:vMerge w:val="restart"/>` (starts a merged region)
770    /// - `VMergeType::Continue` → `<w:vMerge/>` (continues a merged region)
771    ///
772    /// ECMA-376 Part 1, Section 17.4.84 (`w:vMerge`).
773    pub fn set_vertical_merge(&mut self, merge_type: VMergeType) {
774        let tcpr = self
775            .cell_properties
776            .get_or_insert_with(|| Box::new(types::TableCellProperties::default()));
777        tcpr.vertical_merge = Some(Box::new(types::CTVMerge {
778            value: match merge_type {
779                VMergeType::Restart => Some(types::STMerge::Restart),
780                VMergeType::Continue => None,
781            },
782            #[cfg(feature = "extra-attrs")]
783            extra_attrs: Default::default(),
784        }));
785    }
786}
787
788/// Border style for table cell borders.
789///
790/// ECMA-376 Part 1, Section 17.18.2 (`ST_Border`).
791#[cfg(feature = "wml-tables")]
792#[derive(Debug, Clone, Copy, PartialEq, Eq)]
793pub enum BorderStyle {
794    /// No border.
795    None,
796    /// Single solid line.
797    Single,
798    /// Double solid lines.
799    Double,
800    /// Dashed line.
801    Dashed,
802    /// Dotted line.
803    Dotted,
804    /// Thick solid line.
805    Thick,
806}
807
808#[cfg(feature = "wml-tables")]
809impl BorderStyle {
810    fn to_st_border(self) -> types::STBorder {
811        match self {
812            BorderStyle::None => types::STBorder::None,
813            BorderStyle::Single => types::STBorder::Single,
814            BorderStyle::Double => types::STBorder::Double,
815            BorderStyle::Dashed => types::STBorder::Dashed,
816            BorderStyle::Dotted => types::STBorder::Dotted,
817            BorderStyle::Thick => types::STBorder::Thick,
818        }
819    }
820}
821
822/// Table width unit.
823///
824/// ECMA-376 Part 1, Section 17.18.87 (`ST_TblWidth`).
825#[cfg(feature = "wml-tables")]
826#[derive(Debug, Clone, Copy, PartialEq, Eq)]
827pub enum TableWidthUnit {
828    /// Twips (twentieths of a point). 1440 = 1 inch.
829    Dxa,
830    /// Percent in fiftieths (5000 = 100%, 2500 = 50%).
831    Pct,
832}
833
834#[cfg(feature = "wml-tables")]
835impl types::CTRow {
836    /// Set the row height in twips.
837    ///
838    /// Maps to `<w:trPr><w:trHeight w:val="..." w:hRule="exact"/></w:trPr>`.
839    /// ECMA-376 Part 1, Section 17.4.81 (`w:trHeight`).
840    pub fn set_height(&mut self, twips: u32) {
841        let row_pr = self
842            .row_properties
843            .get_or_insert_with(|| Box::new(types::TableRowProperties::default()));
844        row_pr.tr_height = Some(Box::new(types::CTHeight {
845            value: Some(twips.to_string()),
846            #[cfg(feature = "wml-tables")]
847            h_rule: Some(types::STHeightRule::Exact),
848            #[cfg(feature = "extra-attrs")]
849            extra_attrs: Default::default(),
850        }));
851    }
852}
853
854#[cfg(feature = "wml-tables")]
855impl types::TableCell {
856    /// Set the background (shading) color of this cell.
857    ///
858    /// The `rgb` value should be a hex string (e.g., `"FF0000"` for red, `"auto"` for automatic).
859    /// Maps to `<w:tcPr><w:shd w:val="clear" w:fill="..." w:color="auto"/></w:tcPr>`.
860    /// ECMA-376 Part 1, Section 17.4.32 (`w:shd`).
861    pub fn set_background_color(&mut self, rgb: &str) {
862        let tcpr = self
863            .cell_properties
864            .get_or_insert_with(|| Box::new(types::TableCellProperties::default()));
865        tcpr.shading = Some(Box::new(types::CTShd {
866            value: types::STShd::Clear,
867            #[cfg(feature = "wml-styling")]
868            fill: Some(rgb.to_string()),
869            #[cfg(feature = "wml-styling")]
870            color: Some("auto".to_string()),
871            #[cfg(feature = "wml-styling")]
872            theme_color: None,
873            #[cfg(feature = "wml-styling")]
874            theme_tint: None,
875            #[cfg(feature = "wml-styling")]
876            theme_shade: None,
877            #[cfg(feature = "wml-styling")]
878            theme_fill: None,
879            #[cfg(feature = "wml-styling")]
880            theme_fill_tint: None,
881            #[cfg(feature = "wml-styling")]
882            theme_fill_shade: None,
883            #[cfg(feature = "extra-attrs")]
884            extra_attrs: Default::default(),
885        }));
886    }
887
888    /// Set all four borders of this cell at once.
889    ///
890    /// `style` is the border style, `width_eights` is the width in eighths of a point
891    /// (e.g., 4 = half a point = 0.5pt), and `color` is a hex color string (e.g., `"000000"`).
892    ///
893    /// ECMA-376 Part 1, Section 17.4.5 (`w:tcBorders`).
894    pub fn set_borders(&mut self, style: BorderStyle, width_eights: u32, color: &str) {
895        self.set_border_top(style, width_eights, color);
896        self.set_border_bottom(style, width_eights, color);
897        self.set_border_left(style, width_eights, color);
898        self.set_border_right(style, width_eights, color);
899    }
900
901    /// Set the top border of this cell.
902    ///
903    /// ECMA-376 Part 1, Section 17.4.5 (`w:tcBorders`).
904    pub fn set_border_top(&mut self, style: BorderStyle, width_eights: u32, color: &str) {
905        let tcpr = self
906            .cell_properties
907            .get_or_insert_with(|| Box::new(types::TableCellProperties::default()));
908        let borders = tcpr
909            .tc_borders
910            .get_or_insert_with(|| Box::new(types::CTTcBorders::default()));
911        borders.top = Some(Box::new(make_cell_border(style, width_eights, color)));
912    }
913
914    /// Set the bottom border of this cell.
915    ///
916    /// ECMA-376 Part 1, Section 17.4.5 (`w:tcBorders`).
917    pub fn set_border_bottom(&mut self, style: BorderStyle, width_eights: u32, color: &str) {
918        let tcpr = self
919            .cell_properties
920            .get_or_insert_with(|| Box::new(types::TableCellProperties::default()));
921        let borders = tcpr
922            .tc_borders
923            .get_or_insert_with(|| Box::new(types::CTTcBorders::default()));
924        borders.bottom = Some(Box::new(make_cell_border(style, width_eights, color)));
925    }
926
927    /// Set the left border of this cell.
928    ///
929    /// ECMA-376 Part 1, Section 17.4.5 (`w:tcBorders`).
930    pub fn set_border_left(&mut self, style: BorderStyle, width_eights: u32, color: &str) {
931        let tcpr = self
932            .cell_properties
933            .get_or_insert_with(|| Box::new(types::TableCellProperties::default()));
934        let borders = tcpr
935            .tc_borders
936            .get_or_insert_with(|| Box::new(types::CTTcBorders::default()));
937        borders.left = Some(Box::new(make_cell_border(style, width_eights, color)));
938    }
939
940    /// Set the right border of this cell.
941    ///
942    /// ECMA-376 Part 1, Section 17.4.5 (`w:tcBorders`).
943    pub fn set_border_right(&mut self, style: BorderStyle, width_eights: u32, color: &str) {
944        let tcpr = self
945            .cell_properties
946            .get_or_insert_with(|| Box::new(types::TableCellProperties::default()));
947        let borders = tcpr
948            .tc_borders
949            .get_or_insert_with(|| Box::new(types::CTTcBorders::default()));
950        borders.right = Some(Box::new(make_cell_border(style, width_eights, color)));
951    }
952
953    /// Set cell padding (margins) in twips.
954    ///
955    /// Maps to `<w:tcPr><w:tcMar .../></w:tcPr>`.
956    /// ECMA-376 Part 1, Section 17.4.44 (`w:tcMar`).
957    pub fn set_padding(&mut self, top: u32, bottom: u32, left: u32, right: u32) {
958        let tcpr = self
959            .cell_properties
960            .get_or_insert_with(|| Box::new(types::TableCellProperties::default()));
961        tcpr.tc_mar = Some(Box::new(types::CTTcMar {
962            top: Some(Box::new(make_tbl_width(top, types::STTblWidth::Dxa))),
963            bottom: Some(Box::new(make_tbl_width(bottom, types::STTblWidth::Dxa))),
964            left: Some(Box::new(make_tbl_width(left, types::STTblWidth::Dxa))),
965            right: Some(Box::new(make_tbl_width(right, types::STTblWidth::Dxa))),
966            #[cfg(feature = "wml-tables")]
967            start: None,
968            #[cfg(feature = "wml-tables")]
969            end: None,
970            #[cfg(feature = "extra-children")]
971            extra_children: Vec::new(),
972        }));
973    }
974}
975
976#[cfg(feature = "wml-tables")]
977impl types::Table {
978    /// Set the preferred width of this table.
979    ///
980    /// `width` is the measurement value; `unit` is the unit type.
981    /// Maps to `<w:tblPr><w:tblW w:w="..." w:type="..."/></w:tblPr>`.
982    /// ECMA-376 Part 1, Section 17.4.63 (`w:tblW`).
983    pub fn set_width(&mut self, width: u32, unit: TableWidthUnit) {
984        let type_ = match unit {
985            TableWidthUnit::Dxa => types::STTblWidth::Dxa,
986            TableWidthUnit::Pct => types::STTblWidth::Pct,
987        };
988        self.table_properties.tbl_w = Some(Box::new(make_tbl_width(width, type_)));
989    }
990}
991
992/// Build a `CTBorder` with the given style, width, and color.
993#[cfg(feature = "wml-tables")]
994fn make_cell_border(style: BorderStyle, width_eights: u32, color: &str) -> types::CTBorder {
995    types::CTBorder {
996        value: style.to_st_border(),
997        #[cfg(feature = "wml-styling")]
998        color: Some(color.to_string()),
999        #[cfg(feature = "wml-styling")]
1000        size: Some(width_eights as u64),
1001        #[cfg(feature = "wml-styling")]
1002        space: Some(0u64),
1003        #[cfg(feature = "wml-styling")]
1004        theme_color: None,
1005        #[cfg(feature = "wml-styling")]
1006        theme_tint: None,
1007        #[cfg(feature = "wml-styling")]
1008        theme_shade: None,
1009        #[cfg(feature = "wml-styling")]
1010        shadow: None,
1011        #[cfg(feature = "wml-styling")]
1012        frame: None,
1013        #[cfg(feature = "extra-attrs")]
1014        extra_attrs: Default::default(),
1015    }
1016}
1017
1018/// Build a `CTTblWidth` with the given value and type.
1019#[cfg(feature = "wml-tables")]
1020fn make_tbl_width(width: u32, type_: types::STTblWidth) -> types::CTTblWidth {
1021    types::CTTblWidth {
1022        width: Some(width.to_string()),
1023        r#type: Some(type_),
1024        #[cfg(feature = "extra-attrs")]
1025        extra_attrs: Default::default(),
1026    }
1027}
1028
1029// =============================================================================
1030// Header/Footer (HeaderFooter)
1031// =============================================================================
1032
1033impl types::HeaderFooter {
1034    /// Add an empty paragraph and return a mutable reference.
1035    pub fn add_paragraph(&mut self) -> &mut types::Paragraph {
1036        self.block_content
1037            .push(types::BlockContent::P(Box::default()));
1038        match self.block_content.last_mut().unwrap() {
1039            types::BlockContent::P(p) => p.as_mut(),
1040            _ => unreachable!(),
1041        }
1042    }
1043}
1044
1045// =============================================================================
1046// Comment
1047// =============================================================================
1048
1049impl types::Comment {
1050    /// Add a paragraph and return a mutable reference.
1051    pub fn add_paragraph(&mut self) -> &mut types::Paragraph {
1052        self.block_content
1053            .push(types::BlockContent::P(Box::default()));
1054        match self.block_content.last_mut().unwrap() {
1055            types::BlockContent::P(p) => p.as_mut(),
1056            _ => unreachable!(),
1057        }
1058    }
1059}
1060
1061// =============================================================================
1062// Footnote/Endnote (FootnoteEndnote)
1063// =============================================================================
1064
1065impl types::FootnoteEndnote {
1066    /// Add a paragraph and return a mutable reference.
1067    pub fn add_paragraph(&mut self) -> &mut types::Paragraph {
1068        self.block_content
1069            .push(types::BlockContent::P(Box::default()));
1070        match self.block_content.last_mut().unwrap() {
1071            types::BlockContent::P(p) => p.as_mut(),
1072            _ => unreachable!(),
1073        }
1074    }
1075}
1076
1077// =============================================================================
1078// Track changes helpers (wml-track-changes)
1079// =============================================================================
1080
1081/// Build a `CTRunTrackChange` containing a single text run.
1082///
1083/// The `text` is placed in a `<w:r><w:t>…</w:t></w:r>` inside the change element.
1084/// ECMA-376 §17.13.5.
1085#[cfg(feature = "wml-track-changes")]
1086fn make_run_track_change(
1087    id: i64,
1088    author: &str,
1089    date: Option<&str>,
1090    text: &str,
1091) -> types::CTRunTrackChange {
1092    let t = types::Text {
1093        text: Some(text.to_string()),
1094        #[cfg(feature = "extra-children")]
1095        extra_children: Vec::new(),
1096    };
1097    let run = types::Run {
1098        #[cfg(feature = "wml-track-changes")]
1099        rsid_r_pr: None,
1100        #[cfg(feature = "wml-track-changes")]
1101        rsid_del: None,
1102        #[cfg(feature = "wml-track-changes")]
1103        rsid_r: None,
1104        #[cfg(feature = "wml-styling")]
1105        r_pr: None,
1106        run_content: vec![types::RunContent::T(Box::new(t))],
1107        #[cfg(feature = "extra-attrs")]
1108        extra_attrs: Default::default(),
1109        #[cfg(feature = "extra-children")]
1110        extra_children: Vec::new(),
1111    };
1112    types::CTRunTrackChange {
1113        id,
1114        author: author.to_string(),
1115        date: date.map(|d| d.to_string()),
1116        run_content: vec![types::RunContentChoice::R(Box::new(run))],
1117        #[cfg(feature = "extra-attrs")]
1118        extra_attrs: Default::default(),
1119        #[cfg(feature = "extra-children")]
1120        extra_children: Vec::new(),
1121    }
1122}
1123
1124/// Create a `ParagraphContent::Ins` element wrapping a text run.
1125///
1126/// Use this to add a tracked insertion to a paragraph's `paragraph_content`.
1127///
1128/// # Example
1129///
1130/// ```
1131/// # #[cfg(feature = "wml-track-changes")] {
1132/// use ooxml_wml::convenience::ins_run;
1133/// use ooxml_wml::types;
1134///
1135/// let mut para = types::Paragraph::default();
1136/// para.paragraph_content.push(ins_run(1, "Alice", Some("2026-02-24T12:00:00Z"), "inserted text"));
1137/// # }
1138/// ```
1139///
1140/// ECMA-376 §17.13.5.16 (`w:ins`).
1141#[cfg(feature = "wml-track-changes")]
1142pub fn ins_run(id: i64, author: &str, date: Option<&str>, text: &str) -> types::ParagraphContent {
1143    types::ParagraphContent::Ins(Box::new(make_run_track_change(id, author, date, text)))
1144}
1145
1146/// Create a `ParagraphContent::Del` element wrapping a text run.
1147///
1148/// Use this to add a tracked deletion to a paragraph's `paragraph_content`.
1149///
1150/// # Example
1151///
1152/// ```
1153/// # #[cfg(feature = "wml-track-changes")] {
1154/// use ooxml_wml::convenience::del_run;
1155/// use ooxml_wml::types;
1156///
1157/// let mut para = types::Paragraph::default();
1158/// para.paragraph_content.push(del_run(2, "Bob", None, "deleted text"));
1159/// # }
1160/// ```
1161///
1162/// ECMA-376 §17.13.5.13 (`w:del`).
1163#[cfg(feature = "wml-track-changes")]
1164pub fn del_run(id: i64, author: &str, date: Option<&str>, text: &str) -> types::ParagraphContent {
1165    types::ParagraphContent::Del(Box::new(make_run_track_change(id, author, date, text)))
1166}
1167
1168#[cfg(feature = "wml-track-changes")]
1169impl types::Paragraph {
1170    /// Add a tracked insertion wrapping the given text and return a mutable
1171    /// reference to the `CTRunTrackChange` (ECMA-376 §17.13.5.16).
1172    pub fn add_tracked_insertion(
1173        &mut self,
1174        id: i64,
1175        author: &str,
1176        date: Option<&str>,
1177        text: &str,
1178    ) -> &mut types::CTRunTrackChange {
1179        self.paragraph_content.push(ins_run(id, author, date, text));
1180        match self.paragraph_content.last_mut().unwrap() {
1181            types::ParagraphContent::Ins(tc) => tc.as_mut(),
1182            _ => unreachable!(),
1183        }
1184    }
1185
1186    /// Add a tracked deletion wrapping the given text and return a mutable
1187    /// reference to the `CTRunTrackChange` (ECMA-376 §17.13.5.13).
1188    pub fn add_tracked_deletion(
1189        &mut self,
1190        id: i64,
1191        author: &str,
1192        date: Option<&str>,
1193        text: &str,
1194    ) -> &mut types::CTRunTrackChange {
1195        self.paragraph_content.push(del_run(id, author, date, text));
1196        match self.paragraph_content.last_mut().unwrap() {
1197            types::ParagraphContent::Del(tc) => tc.as_mut(),
1198            _ => unreachable!(),
1199        }
1200    }
1201}
1202
1203// =============================================================================
1204// Form fields (SDT-based)  (wml-settings feature)
1205// =============================================================================
1206
1207/// Type of form field to create inside a Structured Document Tag.
1208///
1209/// ECMA-376 Part 1, Section 17.5.2 (Structured Document Tags).
1210#[cfg(feature = "wml-settings")]
1211#[derive(Debug, Clone)]
1212pub enum FormFieldType {
1213    /// Plain-text input (`<w:text/>`).
1214    PlainText,
1215    /// Rich-text area (`<w:richText/>`).
1216    RichText,
1217    /// Combo box (`<w:comboBox>`).
1218    ComboBox,
1219    /// Drop-down list (`<w:dropDownList>`).
1220    DropDownList,
1221    /// Date picker (`<w:date>`).
1222    DatePicker,
1223}
1224
1225/// Configuration for a form field written as a Structured Document Tag.
1226///
1227/// ECMA-376 Part 1, Section 17.5.2 (`w:sdt`).
1228#[cfg(feature = "wml-settings")]
1229#[derive(Debug, Clone)]
1230pub struct FormFieldConfig {
1231    /// Machine-readable tag (`<w:tag w:val="..."/>`).
1232    pub tag: Option<String>,
1233    /// Human-readable alias/label (`<w:alias w:val="..."/>`).
1234    pub label: Option<String>,
1235    /// Type of form control.
1236    pub field_type: FormFieldType,
1237    /// Initial/default value displayed in the field.
1238    pub default_value: Option<String>,
1239    /// Placeholder text (used as content when no default_value is set).
1240    pub placeholder: Option<String>,
1241    /// Items for combo box / drop-down list fields.
1242    pub list_items: Vec<String>,
1243    /// Date format string for date picker fields (e.g. `"MM/dd/yyyy"`).
1244    pub date_format: Option<String>,
1245}
1246
1247#[cfg(feature = "wml-settings")]
1248impl Default for FormFieldConfig {
1249    fn default() -> Self {
1250        Self {
1251            tag: None,
1252            label: None,
1253            field_type: FormFieldType::PlainText,
1254            default_value: None,
1255            placeholder: None,
1256            list_items: Vec::new(),
1257            date_format: None,
1258        }
1259    }
1260}
1261
1262/// Build a single paragraph containing the given text, for use inside SDT content.
1263#[cfg(feature = "wml-settings")]
1264fn make_text_paragraph(text: &str) -> types::BlockContentChoice {
1265    let t = types::Text {
1266        text: Some(text.to_string()),
1267        #[cfg(feature = "extra-children")]
1268        extra_children: Vec::new(),
1269    };
1270    let mut run = types::Run::default();
1271    run.run_content.push(types::RunContent::T(Box::new(t)));
1272    let mut para = types::Paragraph::default();
1273    para.paragraph_content
1274        .push(types::ParagraphContent::R(Box::new(run)));
1275    types::BlockContentChoice::P(Box::new(para))
1276}
1277
1278/// Build a `CTSdtListItem` from a display string.
1279#[cfg(feature = "wml-settings")]
1280fn make_list_item(text: &str) -> types::CTSdtListItem {
1281    types::CTSdtListItem {
1282        display_text: Some(text.to_string()),
1283        value: Some(text.to_string()),
1284        #[cfg(feature = "extra-attrs")]
1285        extra_attrs: Default::default(),
1286    }
1287}
1288
1289#[cfg(feature = "wml-settings")]
1290impl types::Body {
1291    /// Add a form field as a Structured Document Tag (`<w:sdt>`).
1292    ///
1293    /// Produces a block-level SDT containing an appropriate SDT properties
1294    /// element (`<w:sdtPr>`) and content paragraph with the default value.
1295    ///
1296    /// ECMA-376 Part 1, Section 17.5.2.
1297    pub fn add_form_field(&mut self, config: FormFieldConfig) -> &mut Self {
1298        let content_text = config
1299            .default_value
1300            .as_deref()
1301            .or(config.placeholder.as_deref())
1302            .unwrap_or("")
1303            .to_string();
1304
1305        // Build sdtPr
1306        let mut sdt_pr = types::CTSdtPr::default();
1307
1308        #[cfg(feature = "wml-settings")]
1309        if let Some(ref tag_val) = config.tag {
1310            sdt_pr.tag = Some(Box::new(types::CTString {
1311                value: tag_val.clone(),
1312                #[cfg(feature = "extra-attrs")]
1313                extra_attrs: Default::default(),
1314            }));
1315        }
1316
1317        #[cfg(feature = "wml-settings")]
1318        if let Some(ref alias_val) = config.label {
1319            sdt_pr.alias = Some(Box::new(types::CTString {
1320                value: alias_val.clone(),
1321                #[cfg(feature = "extra-attrs")]
1322                extra_attrs: Default::default(),
1323            }));
1324        }
1325
1326        #[cfg(feature = "wml-settings")]
1327        match config.field_type {
1328            FormFieldType::PlainText => {
1329                sdt_pr.text = Some(Box::new(types::CTSdtText {
1330                    multi_line: None,
1331                    #[cfg(feature = "extra-attrs")]
1332                    extra_attrs: Default::default(),
1333                }));
1334            }
1335            FormFieldType::RichText => {
1336                sdt_pr.rich_text = Some(Box::new(types::CTEmpty));
1337            }
1338            FormFieldType::ComboBox => {
1339                let items = config
1340                    .list_items
1341                    .iter()
1342                    .map(|s| make_list_item(s))
1343                    .collect();
1344                sdt_pr.combo_box = Some(Box::new(types::CTSdtComboBox {
1345                    last_value: None,
1346                    list_item: items,
1347                    #[cfg(feature = "extra-attrs")]
1348                    extra_attrs: Default::default(),
1349                    #[cfg(feature = "extra-children")]
1350                    extra_children: Vec::new(),
1351                }));
1352            }
1353            FormFieldType::DropDownList => {
1354                let items = config
1355                    .list_items
1356                    .iter()
1357                    .map(|s| make_list_item(s))
1358                    .collect();
1359                sdt_pr.drop_down_list = Some(Box::new(types::CTSdtDropDownList {
1360                    last_value: None,
1361                    list_item: items,
1362                    #[cfg(feature = "extra-attrs")]
1363                    extra_attrs: Default::default(),
1364                    #[cfg(feature = "extra-children")]
1365                    extra_children: Vec::new(),
1366                }));
1367            }
1368            FormFieldType::DatePicker => {
1369                let date_format_elem = config.date_format.as_deref().map(|fmt| {
1370                    Box::new(types::CTString {
1371                        value: fmt.to_string(),
1372                        #[cfg(feature = "extra-attrs")]
1373                        extra_attrs: Default::default(),
1374                    })
1375                });
1376                sdt_pr.date = Some(Box::new(types::CTSdtDate {
1377                    date_format: date_format_elem,
1378                    full_date: None,
1379                    lid: None,
1380                    store_mapped_data_as: None,
1381                    calendar: None,
1382                    #[cfg(feature = "extra-attrs")]
1383                    extra_attrs: Default::default(),
1384                    #[cfg(feature = "extra-children")]
1385                    extra_children: Vec::new(),
1386                }));
1387            }
1388        }
1389
1390        let sdt_content = types::CTSdtContentBlock {
1391            block_content: vec![make_text_paragraph(&content_text)],
1392            #[cfg(feature = "extra-children")]
1393            extra_children: Vec::new(),
1394        };
1395
1396        let sdt = types::CTSdtBlock {
1397            sdt_pr: Some(Box::new(sdt_pr)),
1398            sdt_end_pr: None,
1399            sdt_content: Some(Box::new(sdt_content)),
1400            #[cfg(feature = "extra-children")]
1401            extra_children: Vec::new(),
1402        };
1403
1404        self.block_content
1405            .push(types::BlockContent::Sdt(Box::new(sdt)));
1406        self
1407    }
1408}
1409
1410// =============================================================================
1411// Table of Contents  (wml-fields feature)
1412// =============================================================================
1413
1414/// Options for a Table of Contents field inserted by `Body::add_toc()`.
1415///
1416/// ECMA-376 Part 1, Section 17.16 (Field Codes) – `TOC` field.
1417#[cfg(feature = "wml-fields")]
1418#[derive(Debug, Clone)]
1419pub struct TocOptions {
1420    /// Optional heading paragraph (e.g. "Table of Contents").
1421    pub title: Option<String>,
1422    /// Maximum heading level to include (default 3 → H1–H3).
1423    pub max_level: u8,
1424    /// Whether page numbers should be right-aligned with tab leaders.
1425    pub right_align_page_numbers: bool,
1426    /// Whether TOC entries should be hyperlinks.
1427    pub use_hyperlinks: bool,
1428}
1429
1430#[cfg(feature = "wml-fields")]
1431impl Default for TocOptions {
1432    fn default() -> Self {
1433        Self {
1434            title: None,
1435            max_level: 3,
1436            right_align_page_numbers: true,
1437            use_hyperlinks: true,
1438        }
1439    }
1440}
1441
1442/// Build a CTFldChar with the given type and all optional fields set to None.
1443#[cfg(feature = "wml-fields")]
1444fn make_fld_char(fld_char_type: types::STFldCharType) -> types::CTFldChar {
1445    types::CTFldChar {
1446        fld_char_type,
1447        #[cfg(feature = "wml-fields")]
1448        fld_lock: None,
1449        #[cfg(feature = "wml-fields")]
1450        dirty: None,
1451        #[cfg(feature = "wml-fields")]
1452        fld_data: None,
1453        #[cfg(feature = "wml-fields")]
1454        ff_data: None,
1455        #[cfg(feature = "wml-track-changes")]
1456        numbering_change: None,
1457        #[cfg(feature = "extra-attrs")]
1458        extra_attrs: Default::default(),
1459        #[cfg(feature = "extra-children")]
1460        extra_children: Vec::new(),
1461    }
1462}
1463
1464/// Build a single-run paragraph containing a field character run content item.
1465#[cfg(feature = "wml-fields")]
1466#[allow(dead_code)]
1467fn make_fld_char_para(fld_char_type: types::STFldCharType) -> types::Paragraph {
1468    let fld_char = make_fld_char(fld_char_type);
1469    let mut run = types::Run::default();
1470    run.run_content
1471        .push(types::RunContent::FldChar(Box::new(fld_char)));
1472    let mut para = types::Paragraph::default();
1473    para.paragraph_content
1474        .push(types::ParagraphContent::R(Box::new(run)));
1475    para
1476}
1477
1478/// Build an instr-text run inside a paragraph.
1479#[cfg(feature = "wml-fields")]
1480#[allow(dead_code)]
1481fn make_instr_text_para(instr: &str) -> types::Paragraph {
1482    let t = types::Text {
1483        text: Some(instr.to_string()),
1484        #[cfg(feature = "extra-children")]
1485        extra_children: Vec::new(),
1486    };
1487    let mut run = types::Run::default();
1488    run.run_content
1489        .push(types::RunContent::InstrText(Box::new(t)));
1490    let mut para = types::Paragraph::default();
1491    para.paragraph_content
1492        .push(types::ParagraphContent::R(Box::new(run)));
1493    para
1494}
1495
1496#[cfg(feature = "wml-fields")]
1497impl types::Body {
1498    /// Insert a Table of Contents field at the current position.
1499    ///
1500    /// This writes:
1501    /// 1. An optional title paragraph styled "TOC Heading".
1502    /// 2. A `TOC` field spanning three paragraphs: `fldChar begin`, `instrText`, `fldChar end`.
1503    /// 3. A placeholder paragraph telling the user to update the field.
1504    ///
1505    /// ECMA-376 Part 1, Section 17.16.5.58 (TOC).
1506    pub fn add_toc(&mut self, opts: TocOptions) -> &mut Self {
1507        // 1. Optional title paragraph
1508        if let Some(ref title_text) = opts.title {
1509            let para = self.add_paragraph();
1510            para.add_run().set_text(title_text.as_str());
1511            #[cfg(feature = "wml-styling")]
1512            {
1513                let ppr = para
1514                    .p_pr
1515                    .get_or_insert_with(|| Box::new(types::ParagraphProperties::default()));
1516                ppr.paragraph_style = Some(Box::new(types::CTString {
1517                    value: "TOCHeading".to_string(),
1518                    #[cfg(feature = "extra-attrs")]
1519                    extra_attrs: Default::default(),
1520                }));
1521            }
1522        }
1523
1524        // 2. Build the TOC field instruction string
1525        let mut instr = format!(r#" TOC \o "1-{}" "#, opts.max_level);
1526        if opts.use_hyperlinks {
1527            instr.push_str(r"\h ");
1528        }
1529        if opts.right_align_page_numbers {
1530            instr.push_str(r"\z \u ");
1531        }
1532
1533        // Write the field as three runs in a single paragraph:
1534        // <w:r><w:fldChar w:fldCharType="begin"/></w:r>
1535        // <w:r><w:instrText> TOC ... </w:instrText></w:r>
1536        // <w:r><w:fldChar w:fldCharType="separate"/></w:r>
1537        // <w:r><w:fldChar w:fldCharType="end"/></w:r>
1538        let fld_begin = make_fld_char(types::STFldCharType::Begin);
1539        let fld_separate = make_fld_char(types::STFldCharType::Separate);
1540        let fld_end = make_fld_char(types::STFldCharType::End);
1541
1542        let instr_t = types::Text {
1543            text: Some(instr),
1544            #[cfg(feature = "extra-children")]
1545            extra_children: Vec::new(),
1546        };
1547
1548        let mut run_begin = types::Run::default();
1549        run_begin
1550            .run_content
1551            .push(types::RunContent::FldChar(Box::new(fld_begin)));
1552
1553        let mut run_instr = types::Run::default();
1554        run_instr
1555            .run_content
1556            .push(types::RunContent::InstrText(Box::new(instr_t)));
1557
1558        let mut run_separate = types::Run::default();
1559        run_separate
1560            .run_content
1561            .push(types::RunContent::FldChar(Box::new(fld_separate)));
1562
1563        let mut run_end = types::Run::default();
1564        run_end
1565            .run_content
1566            .push(types::RunContent::FldChar(Box::new(fld_end)));
1567
1568        let toc_para = self.add_paragraph();
1569        toc_para
1570            .paragraph_content
1571            .push(types::ParagraphContent::R(Box::new(run_begin)));
1572        toc_para
1573            .paragraph_content
1574            .push(types::ParagraphContent::R(Box::new(run_instr)));
1575        toc_para
1576            .paragraph_content
1577            .push(types::ParagraphContent::R(Box::new(run_separate)));
1578        toc_para
1579            .paragraph_content
1580            .push(types::ParagraphContent::R(Box::new(run_end)));
1581
1582        // 3. Placeholder paragraph
1583        let placeholder = self.add_paragraph();
1584        placeholder
1585            .add_run()
1586            .set_text("[Right-click to update field]");
1587
1588        self
1589    }
1590}
1591
1592// =============================================================================
1593// Office Math (OMath)
1594// =============================================================================
1595
1596/// Office Math namespace (`m:`).
1597///
1598/// ECMA-376 Part 1, Section 22 (Office Math Markup Language).
1599pub const NS_M: &str = "http://schemas.openxmlformats.org/officeDocument/2006/math";
1600
1601/// Builder for Office Math expressions embedded in Word paragraphs.
1602///
1603/// Produces either an inline `<m:oMath>` or a display `<m:oMathPara><m:oMath>`
1604/// element stored as a `RawXmlElement` inside a paragraph run.
1605///
1606/// ECMA-376 Part 1, Section 22.1.2.77 (`m:oMath`).
1607#[derive(Debug, Clone)]
1608pub struct OMathBuilder {
1609    display: bool,
1610    xml_content: String,
1611}
1612
1613impl OMathBuilder {
1614    /// Create an inline math expression containing plain text.
1615    pub fn plain(text: &str) -> Self {
1616        Self {
1617            display: false,
1618            xml_content: format!("<m:r><m:t>{}</m:t></m:r>", xml_escape(text)),
1619        }
1620    }
1621
1622    /// Create a fraction `numerator / denominator`.
1623    ///
1624    /// ECMA-376 Part 1, Section 22.1.2.36 (`m:f`).
1625    pub fn fraction(numerator: &str, denominator: &str) -> Self {
1626        Self {
1627            display: false,
1628            xml_content: format!(
1629                "<m:f><m:num><m:r><m:t>{}</m:t></m:r></m:num>\
1630                 <m:den><m:r><m:t>{}</m:t></m:r></m:den></m:f>",
1631                xml_escape(numerator),
1632                xml_escape(denominator)
1633            ),
1634        }
1635    }
1636
1637    /// Create a superscript expression `base^exp`.
1638    ///
1639    /// ECMA-376 Part 1, Section 22.1.2.105 (`m:sSup`).
1640    pub fn superscript(base: &str, exp: &str) -> Self {
1641        Self {
1642            display: false,
1643            xml_content: format!(
1644                "<m:sSup><m:e><m:r><m:t>{}</m:t></m:r></m:e>\
1645                 <m:sup><m:r><m:t>{}</m:t></m:r></m:sup></m:sSup>",
1646                xml_escape(base),
1647                xml_escape(exp)
1648            ),
1649        }
1650    }
1651
1652    /// Create a subscript expression `base_sub`.
1653    ///
1654    /// ECMA-376 Part 1, Section 22.1.2.98 (`m:sSub`).
1655    pub fn subscript(base: &str, sub: &str) -> Self {
1656        Self {
1657            display: false,
1658            xml_content: format!(
1659                "<m:sSub><m:e><m:r><m:t>{}</m:t></m:r></m:e>\
1660                 <m:sub><m:r><m:t>{}</m:t></m:r></m:sub></m:sSub>",
1661                xml_escape(base),
1662                xml_escape(sub)
1663            ),
1664        }
1665    }
1666
1667    /// Create a radical (square root) of `base`.
1668    ///
1669    /// ECMA-376 Part 1, Section 22.1.2.79 (`m:rad`).
1670    pub fn radical(base: &str) -> Self {
1671        Self {
1672            display: false,
1673            xml_content: format!(
1674                "<m:rad><m:radPr><m:degHide m:val=\"1\"/></m:radPr>\
1675                 <m:deg/><m:e><m:r><m:t>{}</m:t></m:r></m:e></m:rad>",
1676                xml_escape(base)
1677            ),
1678        }
1679    }
1680
1681    /// Set display (block) mode. Wraps the expression in `<m:oMathPara>`.
1682    pub fn as_display(mut self) -> Self {
1683        self.display = true;
1684        self
1685    }
1686
1687    /// Build a `RawXmlElement` suitable for use inside a paragraph.
1688    ///
1689    /// The element is `<m:oMath>` (inline) or
1690    /// `<m:oMathPara><m:oMath>…</m:oMath></m:oMathPara>` (display).
1691    pub fn build(self) -> RawXmlElement {
1692        let omath = RawXmlElement {
1693            name: "m:oMath".to_string(),
1694            attributes: vec![("xmlns:m".to_string(), NS_M.to_string())],
1695            children: vec![RawXmlNode::Text(self.xml_content)],
1696            self_closing: false,
1697        };
1698
1699        if self.display {
1700            RawXmlElement {
1701                name: "m:oMathPara".to_string(),
1702                attributes: vec![("xmlns:m".to_string(), NS_M.to_string())],
1703                children: vec![RawXmlNode::Element({
1704                    // inner oMath without the xmlns repetition
1705                    RawXmlElement {
1706                        name: "m:oMath".to_string(),
1707                        attributes: vec![],
1708                        children: vec![RawXmlNode::Text({
1709                            // re-use xml_content (moved into omath above) from display wrapper
1710                            // We need to re-derive it — use omath.children
1711                            match omath.children.into_iter().next() {
1712                                Some(RawXmlNode::Text(t)) => t,
1713                                _ => String::new(),
1714                            }
1715                        })],
1716                        self_closing: false,
1717                    }
1718                })],
1719                self_closing: false,
1720            }
1721        } else {
1722            omath
1723        }
1724    }
1725}
1726
1727/// Escape XML special characters in text content.
1728fn xml_escape(s: &str) -> String {
1729    s.replace('&', "&amp;")
1730        .replace('<', "&lt;")
1731        .replace('>', "&gt;")
1732        .replace('"', "&quot;")
1733        .replace('\'', "&apos;")
1734}
1735
1736impl types::Paragraph {
1737    /// Add an Office Math expression to this paragraph.
1738    ///
1739    /// The math element (`<m:oMath>` or `<m:oMathPara>`) is stored as a
1740    /// `RawXmlElement` inside an extra-children slot on a synthetic run.
1741    ///
1742    /// ECMA-376 Part 1, Section 22.1.2.77 (`m:oMath`).
1743    #[cfg(feature = "extra-children")]
1744    pub fn add_math(&mut self, builder: OMathBuilder) -> &mut Self {
1745        let elem = builder.build();
1746        // Math elements live as siblings of runs in the paragraph's extra_children,
1747        // using a PositionedNode so they serialize in document order.
1748        let idx = self.paragraph_content.len();
1749        self.extra_children
1750            .push(PositionedNode::new(idx, RawXmlNode::Element(elem)));
1751        self
1752    }
1753}
1754
1755// =============================================================================
1756// Inline chart drawing
1757// =============================================================================
1758
1759impl types::Paragraph {
1760    /// Add an inline chart drawing reference to this paragraph.
1761    ///
1762    /// Inserts a `<w:drawing><wp:inline>…<c:chart r:id="rel_id"/>…</wp:inline></w:drawing>`
1763    /// element referencing the chart part identified by `rel_id`.
1764    ///
1765    /// Use `DocumentBuilder::embed_chart` to obtain a `rel_id` first.
1766    ///
1767    /// ECMA-376 Part 1, Section 20.4.2.8 (inline).
1768    #[cfg(feature = "wml-charts")]
1769    pub fn add_inline_chart(&mut self, rel_id: &str, width_emu: i64, height_emu: i64) -> &mut Self {
1770        let drawing_elem = build_chart_inline_element(rel_id, width_emu, height_emu);
1771        let drawing = types::CTDrawing {
1772            #[cfg(feature = "extra-children")]
1773            extra_children: vec![PositionedNode::new(0, RawXmlNode::Element(drawing_elem))],
1774        };
1775        let mut run = types::Run::default();
1776        run.run_content
1777            .push(types::RunContent::Drawing(Box::new(drawing)));
1778        self.paragraph_content
1779            .push(types::ParagraphContent::R(Box::new(run)));
1780        self
1781    }
1782}
1783
1784/// Build the `<wp:inline>` element referencing a chart.
1785#[cfg(feature = "wml-charts")]
1786fn build_chart_inline_element(rel_id: &str, width_emu: i64, height_emu: i64) -> RawXmlElement {
1787    // <c:chart r:id="rel_id"/>
1788    let chart_ref = RawXmlElement {
1789        name: "c:chart".to_string(),
1790        attributes: vec![
1791            (
1792                "xmlns:c".to_string(),
1793                "http://schemas.openxmlformats.org/drawingml/2006/chart".to_string(),
1794            ),
1795            (
1796                "xmlns:r".to_string(),
1797                "http://schemas.openxmlformats.org/officeDocument/2006/relationships".to_string(),
1798            ),
1799            ("r:id".to_string(), rel_id.to_string()),
1800        ],
1801        children: vec![],
1802        self_closing: true,
1803    };
1804
1805    // <a:graphicData uri="...chart...">
1806    let graphic_data = RawXmlElement {
1807        name: "a:graphicData".to_string(),
1808        attributes: vec![(
1809            "uri".to_string(),
1810            "http://schemas.openxmlformats.org/drawingml/2006/chart".to_string(),
1811        )],
1812        children: vec![RawXmlNode::Element(chart_ref)],
1813        self_closing: false,
1814    };
1815
1816    // <a:graphic>
1817    let graphic = RawXmlElement {
1818        name: "a:graphic".to_string(),
1819        attributes: vec![(
1820            "xmlns:a".to_string(),
1821            "http://schemas.openxmlformats.org/drawingml/2006/main".to_string(),
1822        )],
1823        children: vec![RawXmlNode::Element(graphic_data)],
1824        self_closing: false,
1825    };
1826
1827    // <wp:extent cx="..." cy="..."/>
1828    let extent = RawXmlElement {
1829        name: "wp:extent".to_string(),
1830        attributes: vec![
1831            ("cx".to_string(), width_emu.to_string()),
1832            ("cy".to_string(), height_emu.to_string()),
1833        ],
1834        children: vec![],
1835        self_closing: true,
1836    };
1837
1838    // <wp:docPr id="1" name="Chart 1"/>
1839    let doc_pr = RawXmlElement {
1840        name: "wp:docPr".to_string(),
1841        attributes: vec![
1842            ("id".to_string(), "1".to_string()),
1843            ("name".to_string(), "Chart 1".to_string()),
1844        ],
1845        children: vec![],
1846        self_closing: true,
1847    };
1848
1849    // <wp:inline>
1850    RawXmlElement {
1851        name: "wp:inline".to_string(),
1852        attributes: vec![
1853            (
1854                "xmlns:wp".to_string(),
1855                "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
1856                    .to_string(),
1857            ),
1858            ("distT".to_string(), "0".to_string()),
1859            ("distB".to_string(), "0".to_string()),
1860            ("distL".to_string(), "0".to_string()),
1861            ("distR".to_string(), "0".to_string()),
1862        ],
1863        children: vec![
1864            RawXmlNode::Element(extent),
1865            RawXmlNode::Element(doc_pr),
1866            RawXmlNode::Element(graphic),
1867        ],
1868        self_closing: false,
1869    }
1870}
1871
1872// =============================================================================
1873// Tests for new features
1874// =============================================================================
1875
1876#[cfg(test)]
1877mod feature_tests {
1878    use super::*;
1879
1880    // -------------------------------------------------------------------------
1881    // Settings
1882    // -------------------------------------------------------------------------
1883
1884    #[test]
1885    #[cfg(feature = "wml-settings")]
1886    fn test_settings_xml_content() {
1887        use crate::writer::DocumentSettingsOptions;
1888
1889        // Directly test the build_settings_xml helper by calling via the module
1890        // (We can't call build_settings_xml directly since it's private, so we
1891        //  verify the public API generates the right document structure.)
1892        let opts = DocumentSettingsOptions {
1893            default_tab_stop: Some(720),
1894            even_and_odd_headers: true,
1895            track_changes: true,
1896            rsid_root: Some("AB12CD34".to_string()),
1897            compat_mode: true,
1898        };
1899        // Just verify struct construction compiles and no panics
1900        let _ = opts;
1901    }
1902
1903    #[test]
1904    #[cfg(all(
1905        feature = "wml-settings",
1906        feature = "extra-attrs",
1907        feature = "extra-children"
1908    ))]
1909    fn test_settings_roundtrip() {
1910        use crate::Document;
1911        use crate::writer::{DocumentBuilder, DocumentSettingsOptions};
1912        use std::io::Cursor;
1913
1914        let mut builder = DocumentBuilder::new();
1915        builder.set_settings(DocumentSettingsOptions {
1916            default_tab_stop: Some(720),
1917            even_and_odd_headers: true,
1918            track_changes: false,
1919            rsid_root: None,
1920            compat_mode: false,
1921        });
1922        builder.add_paragraph("Hello");
1923
1924        let mut buf = Cursor::new(Vec::new());
1925        builder.write(&mut buf).unwrap();
1926
1927        // Re-open and verify the document is readable
1928        buf.set_position(0);
1929        let doc = Document::from_reader(buf).unwrap();
1930        let body = doc.body();
1931        assert!(!body.block_content.is_empty());
1932    }
1933
1934    // -------------------------------------------------------------------------
1935    // Form fields
1936    // -------------------------------------------------------------------------
1937
1938    #[test]
1939    #[cfg(feature = "wml-settings")]
1940    fn test_form_field_plain_text() {
1941        let mut body = types::Body::default();
1942        body.add_form_field(FormFieldConfig {
1943            tag: Some("myTag".to_string()),
1944            label: Some("My Field".to_string()),
1945            field_type: FormFieldType::PlainText,
1946            default_value: Some("default".to_string()),
1947            ..Default::default()
1948        });
1949
1950        assert_eq!(body.block_content.len(), 1);
1951        match &body.block_content[0] {
1952            types::BlockContent::Sdt(sdt) => {
1953                let sdt_pr = sdt.sdt_pr.as_ref().expect("sdt_pr should be present");
1954                assert_eq!(sdt_pr.tag.as_ref().unwrap().value, "myTag");
1955                assert_eq!(sdt_pr.alias.as_ref().unwrap().value, "My Field");
1956                assert!(sdt_pr.text.is_some(), "text element should be set");
1957                let content = sdt.sdt_content.as_ref().expect("sdt_content");
1958                assert_eq!(content.block_content.len(), 1);
1959            }
1960            _ => panic!("expected Sdt block content"),
1961        }
1962    }
1963
1964    #[test]
1965    #[cfg(feature = "wml-settings")]
1966    fn test_form_field_dropdown() {
1967        let mut body = types::Body::default();
1968        body.add_form_field(FormFieldConfig {
1969            field_type: FormFieldType::DropDownList,
1970            list_items: vec!["Option A".to_string(), "Option B".to_string()],
1971            ..Default::default()
1972        });
1973
1974        match &body.block_content[0] {
1975            types::BlockContent::Sdt(sdt) => {
1976                let sdt_pr = sdt.sdt_pr.as_ref().unwrap();
1977                let dd = sdt_pr.drop_down_list.as_ref().expect("drop_down_list");
1978                assert_eq!(dd.list_item.len(), 2);
1979                assert_eq!(dd.list_item[0].display_text.as_deref(), Some("Option A"));
1980                assert_eq!(dd.list_item[1].display_text.as_deref(), Some("Option B"));
1981            }
1982            _ => panic!("expected Sdt"),
1983        }
1984    }
1985
1986    #[test]
1987    #[cfg(feature = "wml-settings")]
1988    fn test_form_field_date_picker() {
1989        let mut body = types::Body::default();
1990        body.add_form_field(FormFieldConfig {
1991            field_type: FormFieldType::DatePicker,
1992            date_format: Some("MM/dd/yyyy".to_string()),
1993            ..Default::default()
1994        });
1995
1996        match &body.block_content[0] {
1997            types::BlockContent::Sdt(sdt) => {
1998                let sdt_pr = sdt.sdt_pr.as_ref().unwrap();
1999                let date = sdt_pr.date.as_ref().expect("date element");
2000                assert_eq!(date.date_format.as_ref().unwrap().value, "MM/dd/yyyy");
2001            }
2002            _ => panic!("expected Sdt"),
2003        }
2004    }
2005
2006    // -------------------------------------------------------------------------
2007    // Table of contents
2008    // -------------------------------------------------------------------------
2009
2010    #[test]
2011    #[cfg(feature = "wml-fields")]
2012    fn test_toc_basic() {
2013        let mut body = types::Body::default();
2014        body.add_toc(TocOptions {
2015            title: Some("Contents".to_string()),
2016            max_level: 3,
2017            right_align_page_numbers: true,
2018            use_hyperlinks: true,
2019        });
2020
2021        // Should have: title para + TOC field para + placeholder para = 3 paragraphs
2022        let paras: Vec<_> = body
2023            .block_content
2024            .iter()
2025            .filter_map(|b| match b {
2026                types::BlockContent::P(p) => Some(p),
2027                _ => None,
2028            })
2029            .collect();
2030        assert_eq!(
2031            paras.len(),
2032            3,
2033            "expected title + field + placeholder paragraphs"
2034        );
2035
2036        // The field paragraph should have 4 run content items (begin, instrText, separate, end)
2037        let field_para = &paras[1];
2038        assert_eq!(
2039            field_para.paragraph_content.len(),
2040            4,
2041            "TOC paragraph should have 4 run items"
2042        );
2043    }
2044
2045    #[test]
2046    #[cfg(feature = "wml-fields")]
2047    fn test_toc_no_title() {
2048        let mut body = types::Body::default();
2049        body.add_toc(TocOptions::default());
2050
2051        // Should have: TOC field para + placeholder para = 2 paragraphs
2052        let paras: Vec<_> = body
2053            .block_content
2054            .iter()
2055            .filter_map(|b| match b {
2056                types::BlockContent::P(p) => Some(p),
2057                _ => None,
2058            })
2059            .collect();
2060        assert_eq!(paras.len(), 2, "expected field + placeholder paragraphs");
2061    }
2062
2063    #[test]
2064    #[cfg(feature = "wml-fields")]
2065    fn test_toc_instr_text_contains_level() {
2066        let mut body = types::Body::default();
2067        body.add_toc(TocOptions {
2068            title: None,
2069            max_level: 2,
2070            right_align_page_numbers: false,
2071            use_hyperlinks: false,
2072        });
2073
2074        let field_para = match &body.block_content[0] {
2075            types::BlockContent::P(p) => p,
2076            _ => panic!("expected paragraph"),
2077        };
2078
2079        // Second run content item is InstrText
2080        match &field_para.paragraph_content[1] {
2081            types::ParagraphContent::R(run) => match &run.run_content[0] {
2082                types::RunContent::InstrText(t) => {
2083                    let instr = t.text.as_deref().unwrap_or("");
2084                    assert!(
2085                        instr.contains("1-2"),
2086                        "should contain level range 1-2, got: {}",
2087                        instr
2088                    );
2089                }
2090                _ => panic!("expected InstrText"),
2091            },
2092            _ => panic!("expected run"),
2093        }
2094    }
2095
2096    // -------------------------------------------------------------------------
2097    // OMathBuilder
2098    // -------------------------------------------------------------------------
2099
2100    #[test]
2101    fn test_omath_plain_build() {
2102        let builder = OMathBuilder::plain("x");
2103        let elem = builder.build();
2104        assert_eq!(elem.name, "m:oMath");
2105        assert!(!elem.attributes.is_empty(), "should have xmlns:m");
2106        assert_eq!(elem.children.len(), 1);
2107        match &elem.children[0] {
2108            RawXmlNode::Text(t) => assert!(t.contains("m:r"), "text should wrap in m:r: {}", t),
2109            _ => panic!("expected text node"),
2110        }
2111    }
2112
2113    #[test]
2114    fn test_omath_fraction_build() {
2115        let builder = OMathBuilder::fraction("a", "b");
2116        let elem = builder.build();
2117        match &elem.children[0] {
2118            RawXmlNode::Text(t) => {
2119                assert!(t.contains("m:f"), "should contain fraction: {}", t);
2120                assert!(t.contains("m:num"), "should contain numerator: {}", t);
2121                assert!(t.contains("m:den"), "should contain denominator: {}", t);
2122            }
2123            _ => panic!("expected text node"),
2124        }
2125    }
2126
2127    #[test]
2128    fn test_omath_superscript_build() {
2129        let builder = OMathBuilder::superscript("x", "2");
2130        let elem = builder.build();
2131        match &elem.children[0] {
2132            RawXmlNode::Text(t) => {
2133                assert!(t.contains("m:sSup"), "should contain sSup: {}", t);
2134                assert!(t.contains("m:e"), "should contain base: {}", t);
2135                assert!(t.contains("m:sup"), "should contain exp: {}", t);
2136            }
2137            _ => panic!("expected text node"),
2138        }
2139    }
2140
2141    #[test]
2142    fn test_omath_subscript_build() {
2143        let builder = OMathBuilder::subscript("x", "i");
2144        let elem = builder.build();
2145        match &elem.children[0] {
2146            RawXmlNode::Text(t) => assert!(t.contains("m:sSub"), "{}", t),
2147            _ => panic!(),
2148        }
2149    }
2150
2151    #[test]
2152    fn test_omath_radical_build() {
2153        let builder = OMathBuilder::radical("x");
2154        let elem = builder.build();
2155        match &elem.children[0] {
2156            RawXmlNode::Text(t) => assert!(t.contains("m:rad"), "{}", t),
2157            _ => panic!(),
2158        }
2159    }
2160
2161    #[test]
2162    fn test_omath_display_wraps_in_para() {
2163        let builder = OMathBuilder::plain("x").as_display();
2164        let elem = builder.build();
2165        assert_eq!(
2166            elem.name, "m:oMathPara",
2167            "display should be wrapped in oMathPara"
2168        );
2169        assert_eq!(elem.children.len(), 1);
2170        match &elem.children[0] {
2171            RawXmlNode::Element(inner) => assert_eq!(inner.name, "m:oMath"),
2172            _ => panic!("expected oMath element child"),
2173        }
2174    }
2175
2176    #[test]
2177    #[cfg(feature = "extra-children")]
2178    fn test_paragraph_add_math() {
2179        let mut para = types::Paragraph::default();
2180        para.add_math(OMathBuilder::plain("y = mx + b"));
2181        assert_eq!(para.extra_children.len(), 1);
2182    }
2183
2184    // -------------------------------------------------------------------------
2185    // Inline chart
2186    // -------------------------------------------------------------------------
2187
2188    #[test]
2189    #[cfg(all(
2190        feature = "wml-charts",
2191        feature = "extra-children",
2192        feature = "extra-attrs"
2193    ))]
2194    fn test_embed_chart_and_add_inline() {
2195        use crate::writer::DocumentBuilder;
2196        use std::io::Cursor;
2197
2198        let chart_xml = br#"<?xml version="1.0" encoding="UTF-8"?><c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart"/>"#;
2199
2200        let mut builder = DocumentBuilder::new();
2201        let rel_id = builder.embed_chart(chart_xml).unwrap();
2202        assert!(
2203            rel_id.starts_with("rId"),
2204            "rel_id should be rId-prefixed: {}",
2205            rel_id
2206        );
2207
2208        // Add a paragraph with the inline chart
2209        {
2210            let body = builder.body_mut();
2211            let para = body.add_paragraph();
2212            para.add_inline_chart(&rel_id, 3000000, 2000000);
2213        }
2214
2215        // Write to a buffer — should not panic
2216        let mut buf = Cursor::new(Vec::new());
2217        builder.write(&mut buf).unwrap();
2218        assert!(!buf.get_ref().is_empty(), "output should not be empty");
2219    }
2220
2221    #[test]
2222    #[cfg(feature = "wml-charts")]
2223    fn test_chart_inline_element_structure() {
2224        let elem = build_chart_inline_element("rId5", 3000000, 2000000);
2225        assert_eq!(elem.name, "wp:inline");
2226        // Should contain extent, docPr, and graphic children
2227        assert_eq!(elem.children.len(), 3);
2228    }
2229}
2230
2231#[cfg(test)]
2232mod tests {
2233    use super::*;
2234
2235    // -------------------------------------------------------------------------
2236    // Page / column breaks
2237    // -------------------------------------------------------------------------
2238
2239    #[test]
2240    fn test_paragraph_add_page_break() {
2241        let mut para = types::Paragraph::default();
2242        para.add_page_break();
2243
2244        assert_eq!(para.paragraph_content.len(), 1);
2245        match &para.paragraph_content[0] {
2246            types::ParagraphContent::R(run) => {
2247                assert_eq!(run.run_content.len(), 1);
2248                match &run.run_content[0] {
2249                    types::RunContent::Br(br) => {
2250                        assert_eq!(br.r#type, Some(types::STBrType::Page));
2251                    }
2252                    _ => panic!("expected Br content"),
2253                }
2254            }
2255            _ => panic!("expected R content"),
2256        }
2257    }
2258
2259    #[test]
2260    fn test_paragraph_add_column_break() {
2261        let mut para = types::Paragraph::default();
2262        para.add_column_break();
2263
2264        assert_eq!(para.paragraph_content.len(), 1);
2265        match &para.paragraph_content[0] {
2266            types::ParagraphContent::R(run) => {
2267                assert_eq!(run.run_content.len(), 1);
2268                match &run.run_content[0] {
2269                    types::RunContent::Br(br) => {
2270                        assert_eq!(br.r#type, Some(types::STBrType::Column));
2271                    }
2272                    _ => panic!("expected Br content"),
2273                }
2274            }
2275            _ => panic!("expected R content"),
2276        }
2277    }
2278
2279    // -------------------------------------------------------------------------
2280    // Paragraph spacing
2281    // -------------------------------------------------------------------------
2282
2283    #[test]
2284    #[cfg(feature = "wml-styling")]
2285    fn test_set_space_before() {
2286        let mut para = types::Paragraph::default();
2287        para.set_space_before(240);
2288
2289        let ppr = para.p_pr.as_ref().unwrap();
2290        let spacing = ppr.spacing.as_ref().unwrap();
2291        assert_eq!(spacing.before.as_deref(), Some("240"));
2292        assert!(spacing.after.is_none());
2293    }
2294
2295    #[test]
2296    #[cfg(feature = "wml-styling")]
2297    fn test_set_space_after() {
2298        let mut para = types::Paragraph::default();
2299        para.set_space_after(160);
2300
2301        let ppr = para.p_pr.as_ref().unwrap();
2302        let spacing = ppr.spacing.as_ref().unwrap();
2303        assert_eq!(spacing.after.as_deref(), Some("160"));
2304        assert!(spacing.before.is_none());
2305    }
2306
2307    #[test]
2308    #[cfg(feature = "wml-styling")]
2309    fn test_set_line_spacing() {
2310        let mut para = types::Paragraph::default();
2311        para.set_line_spacing(360); // 1.5x spacing
2312
2313        let ppr = para.p_pr.as_ref().unwrap();
2314        let spacing = ppr.spacing.as_ref().unwrap();
2315        assert_eq!(spacing.line.as_deref(), Some("360"));
2316        assert_eq!(spacing.line_rule, Some(types::STLineSpacingRule::Auto));
2317    }
2318
2319    #[test]
2320    #[cfg(feature = "wml-styling")]
2321    fn test_spacing_accumulates() {
2322        let mut para = types::Paragraph::default();
2323        para.set_space_before(240);
2324        para.set_space_after(120);
2325        para.set_line_spacing(480);
2326
2327        let ppr = para.p_pr.as_ref().unwrap();
2328        let spacing = ppr.spacing.as_ref().unwrap();
2329        assert_eq!(spacing.before.as_deref(), Some("240"));
2330        assert_eq!(spacing.after.as_deref(), Some("120"));
2331        assert_eq!(spacing.line.as_deref(), Some("480"));
2332    }
2333
2334    // -------------------------------------------------------------------------
2335    // Paragraph indentation
2336    // -------------------------------------------------------------------------
2337
2338    #[test]
2339    #[cfg(feature = "wml-styling")]
2340    fn test_set_indent_left() {
2341        let mut para = types::Paragraph::default();
2342        para.set_indent_left(720);
2343
2344        let ppr = para.p_pr.as_ref().unwrap();
2345        let ind = ppr.indentation.as_ref().unwrap();
2346        assert_eq!(ind.left.as_deref(), Some("720"));
2347    }
2348
2349    #[test]
2350    #[cfg(feature = "wml-styling")]
2351    fn test_set_indent_right() {
2352        let mut para = types::Paragraph::default();
2353        para.set_indent_right(360);
2354
2355        let ppr = para.p_pr.as_ref().unwrap();
2356        let ind = ppr.indentation.as_ref().unwrap();
2357        assert_eq!(ind.right.as_deref(), Some("360"));
2358    }
2359
2360    #[test]
2361    #[cfg(feature = "wml-styling")]
2362    fn test_set_indent_first_line() {
2363        let mut para = types::Paragraph::default();
2364        para.set_indent_first_line(180);
2365
2366        let ppr = para.p_pr.as_ref().unwrap();
2367        let ind = ppr.indentation.as_ref().unwrap();
2368        assert_eq!(ind.first_line.as_deref(), Some("180"));
2369    }
2370
2371    #[test]
2372    #[cfg(feature = "wml-styling")]
2373    fn test_indentation_accumulates() {
2374        let mut para = types::Paragraph::default();
2375        para.set_indent_left(720);
2376        para.set_indent_right(360);
2377        para.set_indent_first_line(180);
2378
2379        let ppr = para.p_pr.as_ref().unwrap();
2380        let ind = ppr.indentation.as_ref().unwrap();
2381        assert_eq!(ind.left.as_deref(), Some("720"));
2382        assert_eq!(ind.right.as_deref(), Some("360"));
2383        assert_eq!(ind.first_line.as_deref(), Some("180"));
2384    }
2385
2386    // -------------------------------------------------------------------------
2387    // Outline level
2388    // -------------------------------------------------------------------------
2389
2390    #[test]
2391    #[cfg(feature = "wml-styling")]
2392    fn test_set_outline_level() {
2393        let mut para = types::Paragraph::default();
2394        para.set_outline_level(1); // heading level 2
2395
2396        let ppr = para.p_pr.as_ref().unwrap();
2397        let lvl = ppr.outline_lvl.as_ref().unwrap();
2398        assert_eq!(lvl.value, 1);
2399    }
2400
2401    // -------------------------------------------------------------------------
2402    // Run property details
2403    // -------------------------------------------------------------------------
2404
2405    #[test]
2406    #[cfg(feature = "wml-styling")]
2407    fn test_run_set_shadow() {
2408        let mut run = types::Run::default();
2409        run.set_shadow(true);
2410        assert!(run.r_pr.as_ref().unwrap().shadow.is_some());
2411        run.set_shadow(false);
2412        assert!(run.r_pr.as_ref().unwrap().shadow.is_none());
2413    }
2414
2415    #[test]
2416    #[cfg(feature = "wml-styling")]
2417    fn test_run_set_outline() {
2418        let mut run = types::Run::default();
2419        run.set_outline(true);
2420        assert!(run.r_pr.as_ref().unwrap().outline.is_some());
2421        run.set_outline(false);
2422        assert!(run.r_pr.as_ref().unwrap().outline.is_none());
2423    }
2424
2425    #[test]
2426    #[cfg(feature = "wml-styling")]
2427    fn test_run_set_emboss() {
2428        let mut run = types::Run::default();
2429        run.set_emboss(true);
2430        assert!(run.r_pr.as_ref().unwrap().emboss.is_some());
2431    }
2432
2433    #[test]
2434    #[cfg(feature = "wml-styling")]
2435    fn test_run_set_imprint() {
2436        let mut run = types::Run::default();
2437        run.set_imprint(true);
2438        assert!(run.r_pr.as_ref().unwrap().imprint.is_some());
2439    }
2440
2441    #[test]
2442    #[cfg(feature = "wml-styling")]
2443    fn test_run_set_small_caps() {
2444        let mut run = types::Run::default();
2445        run.set_small_caps(true);
2446        assert!(run.r_pr.as_ref().unwrap().small_caps.is_some());
2447        run.set_small_caps(false);
2448        assert!(run.r_pr.as_ref().unwrap().small_caps.is_none());
2449    }
2450
2451    #[test]
2452    #[cfg(feature = "wml-styling")]
2453    fn test_run_set_all_caps() {
2454        let mut run = types::Run::default();
2455        run.set_all_caps(true);
2456        assert!(run.r_pr.as_ref().unwrap().caps.is_some());
2457        run.set_all_caps(false);
2458        assert!(run.r_pr.as_ref().unwrap().caps.is_none());
2459    }
2460
2461    #[test]
2462    #[cfg(feature = "wml-styling")]
2463    fn test_run_set_vanish() {
2464        let mut run = types::Run::default();
2465        run.set_vanish(true);
2466        assert!(run.r_pr.as_ref().unwrap().vanish.is_some());
2467        run.set_vanish(false);
2468        assert!(run.r_pr.as_ref().unwrap().vanish.is_none());
2469    }
2470
2471    #[test]
2472    #[cfg(feature = "wml-styling")]
2473    fn test_run_set_double_strike() {
2474        let mut run = types::Run::default();
2475        run.set_double_strike(true);
2476        assert!(run.r_pr.as_ref().unwrap().dstrike.is_some());
2477        run.set_double_strike(false);
2478        assert!(run.r_pr.as_ref().unwrap().dstrike.is_none());
2479    }
2480
2481    // -------------------------------------------------------------------------
2482    // Table merged cells
2483    // -------------------------------------------------------------------------
2484
2485    #[test]
2486    #[cfg(feature = "wml-tables")]
2487    fn test_cell_set_grid_span() {
2488        let mut cell = types::TableCell::default();
2489        cell.set_grid_span(3);
2490
2491        let tcpr = cell.cell_properties.as_ref().unwrap();
2492        let gs = tcpr.grid_span.as_ref().unwrap();
2493        assert_eq!(gs.value, 3);
2494    }
2495
2496    #[test]
2497    #[cfg(feature = "wml-tables")]
2498    fn test_cell_set_vertical_merge_restart() {
2499        let mut cell = types::TableCell::default();
2500        cell.set_vertical_merge(VMergeType::Restart);
2501
2502        let tcpr = cell.cell_properties.as_ref().unwrap();
2503        let vm = tcpr.vertical_merge.as_ref().unwrap();
2504        assert_eq!(vm.value, Some(types::STMerge::Restart));
2505    }
2506
2507    #[test]
2508    #[cfg(feature = "wml-tables")]
2509    fn test_cell_set_vertical_merge_continue() {
2510        let mut cell = types::TableCell::default();
2511        cell.set_vertical_merge(VMergeType::Continue);
2512
2513        let tcpr = cell.cell_properties.as_ref().unwrap();
2514        let vm = tcpr.vertical_merge.as_ref().unwrap();
2515        assert_eq!(vm.value, None);
2516    }
2517
2518    // -------------------------------------------------------------------------
2519    // Roundtrip tests
2520    // -------------------------------------------------------------------------
2521
2522    #[test]
2523    #[cfg(all(
2524        feature = "wml-styling",
2525        feature = "extra-attrs",
2526        feature = "extra-children"
2527    ))]
2528    fn test_roundtrip_page_break() {
2529        use crate::Document;
2530        use crate::writer::DocumentBuilder;
2531        use std::io::Cursor;
2532
2533        let mut builder = DocumentBuilder::new();
2534        let body = builder.body_mut();
2535        let para = body.add_paragraph();
2536        para.add_page_break();
2537
2538        let mut buf = Cursor::new(Vec::new());
2539        builder.write(&mut buf).unwrap();
2540
2541        buf.set_position(0);
2542        let doc = Document::from_reader(buf).unwrap();
2543        let body_ref = doc.body();
2544        assert!(!body_ref.block_content.is_empty());
2545    }
2546
2547    #[test]
2548    #[cfg(all(
2549        feature = "wml-styling",
2550        feature = "extra-attrs",
2551        feature = "extra-children"
2552    ))]
2553    fn test_roundtrip_spacing_and_indent() {
2554        use crate::Document;
2555        use crate::ext::BodyExt;
2556        use crate::writer::DocumentBuilder;
2557        use std::io::Cursor;
2558
2559        let mut builder = DocumentBuilder::new();
2560        {
2561            let body = builder.body_mut();
2562            let para = body.add_paragraph();
2563            para.set_space_before(240);
2564            para.set_space_after(120);
2565            para.set_line_spacing(360);
2566            para.set_indent_left(720);
2567            para.set_outline_level(0);
2568            para.add_run().set_text("test spacing");
2569        }
2570
2571        let mut buf = Cursor::new(Vec::new());
2572        builder.write(&mut buf).unwrap();
2573
2574        buf.set_position(0);
2575        let doc = Document::from_reader(buf).unwrap();
2576        let body_ref = doc.body();
2577        assert_eq!(body_ref.paragraphs().len(), 1);
2578
2579        let p = body_ref.paragraphs()[0];
2580        let ppr = p.p_pr.as_ref().unwrap();
2581        let spacing = ppr.spacing.as_ref().unwrap();
2582        assert_eq!(spacing.before.as_deref(), Some("240"));
2583        assert_eq!(spacing.after.as_deref(), Some("120"));
2584    }
2585
2586    #[test]
2587    #[cfg(all(
2588        feature = "wml-tables",
2589        feature = "extra-attrs",
2590        feature = "extra-children"
2591    ))]
2592    fn test_roundtrip_grid_span_and_vmerge() {
2593        use crate::Document;
2594        use crate::ext::BodyExt;
2595        use crate::writer::DocumentBuilder;
2596        use std::io::Cursor;
2597
2598        let mut builder = DocumentBuilder::new();
2599        {
2600            let body = builder.body_mut();
2601            let tbl = body.add_table();
2602            let row = tbl.add_row();
2603            let cell = row.add_cell();
2604            cell.set_grid_span(2);
2605            cell.set_vertical_merge(VMergeType::Restart);
2606            cell.add_paragraph().add_run().set_text("merged");
2607        }
2608
2609        let mut buf = Cursor::new(Vec::new());
2610        builder.write(&mut buf).unwrap();
2611
2612        buf.set_position(0);
2613        let doc = Document::from_reader(buf).unwrap();
2614        let body_ref = doc.body();
2615        assert!(!body_ref.tables().is_empty());
2616
2617        let tbl = body_ref.tables()[0];
2618        let row = match &tbl.rows[0] {
2619            crate::types::RowContent::Tr(r) => r,
2620            _ => panic!("expected Tr"),
2621        };
2622        let cell = match &row.cells[0] {
2623            crate::types::CellContent::Tc(c) => c,
2624            _ => panic!("expected Tc"),
2625        };
2626        let tcpr = cell.cell_properties.as_ref().unwrap();
2627        assert_eq!(tcpr.grid_span.as_ref().unwrap().value, 2);
2628        assert_eq!(
2629            tcpr.vertical_merge.as_ref().unwrap().value,
2630            Some(crate::types::STMerge::Restart)
2631        );
2632    }
2633
2634    // -------------------------------------------------------------------------
2635    // Bookmarks (u32 wrappers)
2636    // -------------------------------------------------------------------------
2637
2638    #[test]
2639    fn test_add_bookmark_start_u32() {
2640        let mut para = types::Paragraph::default();
2641        para.add_bookmark_start_u32(1, "myBookmark");
2642
2643        assert_eq!(para.paragraph_content.len(), 1);
2644        match &para.paragraph_content[0] {
2645            types::ParagraphContent::BookmarkStart(bm) => {
2646                assert_eq!(bm.id, 1);
2647                assert_eq!(bm.name, "myBookmark");
2648            }
2649            _ => panic!("expected BookmarkStart"),
2650        }
2651    }
2652
2653    #[test]
2654    fn test_add_bookmark_end_u32() {
2655        let mut para = types::Paragraph::default();
2656        para.add_bookmark_end_u32(42);
2657
2658        assert_eq!(para.paragraph_content.len(), 1);
2659        match &para.paragraph_content[0] {
2660            types::ParagraphContent::BookmarkEnd(bm) => {
2661                assert_eq!(bm.id, 42);
2662            }
2663            _ => panic!("expected BookmarkEnd"),
2664        }
2665    }
2666
2667    // -------------------------------------------------------------------------
2668    // Table row height
2669    // -------------------------------------------------------------------------
2670
2671    #[test]
2672    #[cfg(feature = "wml-tables")]
2673    fn test_row_set_height() {
2674        let mut row = types::CTRow::default();
2675        row.set_height(720);
2676
2677        let row_pr = row.row_properties.as_ref().unwrap();
2678        let height = row_pr.tr_height.as_ref().unwrap();
2679        assert_eq!(height.value.as_deref(), Some("720"));
2680        assert_eq!(height.h_rule, Some(types::STHeightRule::Exact));
2681    }
2682
2683    // -------------------------------------------------------------------------
2684    // Table cell background color
2685    // -------------------------------------------------------------------------
2686
2687    #[test]
2688    #[cfg(feature = "wml-tables")]
2689    fn test_cell_set_background_color() {
2690        let mut cell = types::TableCell::default();
2691        cell.set_background_color("FF0000");
2692
2693        let tcpr = cell.cell_properties.as_ref().unwrap();
2694        let shd = tcpr.shading.as_ref().unwrap();
2695        assert_eq!(shd.value, types::STShd::Clear);
2696        #[cfg(feature = "wml-styling")]
2697        assert_eq!(shd.fill.as_deref(), Some("FF0000"));
2698    }
2699
2700    // -------------------------------------------------------------------------
2701    // Table cell borders
2702    // -------------------------------------------------------------------------
2703
2704    #[test]
2705    #[cfg(feature = "wml-tables")]
2706    fn test_cell_set_borders() {
2707        let mut cell = types::TableCell::default();
2708        cell.set_borders(BorderStyle::Single, 4, "000000");
2709
2710        let tcpr = cell.cell_properties.as_ref().unwrap();
2711        let borders = tcpr.tc_borders.as_ref().unwrap();
2712        assert!(borders.top.is_some());
2713        assert!(borders.bottom.is_some());
2714        assert!(borders.left.is_some());
2715        assert!(borders.right.is_some());
2716        let top = borders.top.as_ref().unwrap();
2717        assert_eq!(top.value, types::STBorder::Single);
2718        #[cfg(feature = "wml-styling")]
2719        assert_eq!(top.size, Some(4u64));
2720    }
2721
2722    #[test]
2723    #[cfg(feature = "wml-tables")]
2724    fn test_cell_set_border_top_only() {
2725        let mut cell = types::TableCell::default();
2726        cell.set_border_top(BorderStyle::Dashed, 8, "AABBCC");
2727
2728        let tcpr = cell.cell_properties.as_ref().unwrap();
2729        let borders = tcpr.tc_borders.as_ref().unwrap();
2730        assert!(borders.top.is_some());
2731        assert!(borders.bottom.is_none());
2732        assert!(borders.left.is_none());
2733        assert!(borders.right.is_none());
2734    }
2735
2736    // -------------------------------------------------------------------------
2737    // Table cell padding
2738    // -------------------------------------------------------------------------
2739
2740    #[test]
2741    #[cfg(feature = "wml-tables")]
2742    fn test_cell_set_padding() {
2743        let mut cell = types::TableCell::default();
2744        cell.set_padding(100, 100, 200, 200);
2745
2746        let tcpr = cell.cell_properties.as_ref().unwrap();
2747        let mar = tcpr.tc_mar.as_ref().unwrap();
2748        let top = mar.top.as_ref().unwrap();
2749        assert_eq!(top.width.as_deref(), Some("100"));
2750        assert_eq!(top.r#type, Some(types::STTblWidth::Dxa));
2751        let left = mar.left.as_ref().unwrap();
2752        assert_eq!(left.width.as_deref(), Some("200"));
2753    }
2754
2755    // -------------------------------------------------------------------------
2756    // Table width
2757    // -------------------------------------------------------------------------
2758
2759    #[test]
2760    #[cfg(feature = "wml-tables")]
2761    fn test_table_set_width_dxa() {
2762        let table = types::Table {
2763            range_markup: Vec::new(),
2764            table_properties: Box::new(types::TableProperties::default()),
2765            tbl_grid: Box::new(types::TableGrid::default()),
2766            rows: Vec::new(),
2767            #[cfg(feature = "extra-children")]
2768            extra_children: Vec::new(),
2769        };
2770        let mut body = types::Body::default();
2771        body.block_content
2772            .push(types::BlockContent::Tbl(Box::new(table)));
2773        let tbl = match body.block_content.last_mut().unwrap() {
2774            types::BlockContent::Tbl(t) => t.as_mut(),
2775            _ => unreachable!(),
2776        };
2777        tbl.set_width(9360, TableWidthUnit::Dxa); // 6.5 inches
2778
2779        let tbl_w = tbl.table_properties.tbl_w.as_ref().unwrap();
2780        assert_eq!(tbl_w.width.as_deref(), Some("9360"));
2781        assert_eq!(tbl_w.r#type, Some(types::STTblWidth::Dxa));
2782    }
2783
2784    #[test]
2785    #[cfg(feature = "wml-tables")]
2786    fn test_table_set_width_pct() {
2787        let table = types::Table {
2788            range_markup: Vec::new(),
2789            table_properties: Box::new(types::TableProperties::default()),
2790            tbl_grid: Box::new(types::TableGrid::default()),
2791            rows: Vec::new(),
2792            #[cfg(feature = "extra-children")]
2793            extra_children: Vec::new(),
2794        };
2795        let mut body = types::Body::default();
2796        body.block_content
2797            .push(types::BlockContent::Tbl(Box::new(table)));
2798        let tbl = match body.block_content.last_mut().unwrap() {
2799            types::BlockContent::Tbl(t) => t.as_mut(),
2800            _ => unreachable!(),
2801        };
2802        tbl.set_width(5000, TableWidthUnit::Pct); // 100%
2803
2804        let tbl_w = tbl.table_properties.tbl_w.as_ref().unwrap();
2805        assert_eq!(tbl_w.width.as_deref(), Some("5000"));
2806        assert_eq!(tbl_w.r#type, Some(types::STTblWidth::Pct));
2807    }
2808
2809    // -------------------------------------------------------------------------
2810    // Roundtrip: table cell styling
2811    // -------------------------------------------------------------------------
2812
2813    #[test]
2814    #[cfg(all(
2815        feature = "wml-tables",
2816        feature = "wml-styling",
2817        feature = "extra-attrs",
2818        feature = "extra-children"
2819    ))]
2820    fn test_roundtrip_cell_background_and_borders() {
2821        use crate::Document;
2822        use crate::ext::BodyExt;
2823        use crate::writer::DocumentBuilder;
2824        use std::io::Cursor;
2825
2826        let mut builder = DocumentBuilder::new();
2827        {
2828            let body = builder.body_mut();
2829            let tbl = body.add_table();
2830            let row = tbl.add_row();
2831            let cell = row.add_cell();
2832            cell.set_background_color("FFFF00");
2833            cell.set_borders(BorderStyle::Single, 4, "000000");
2834            cell.set_padding(72, 72, 144, 144);
2835            cell.add_paragraph().add_run().set_text("styled");
2836        }
2837
2838        let mut buf = Cursor::new(Vec::new());
2839        builder.write(&mut buf).unwrap();
2840
2841        buf.set_position(0);
2842        let doc = Document::from_reader(buf).unwrap();
2843        let body_ref = doc.body();
2844        assert!(!body_ref.tables().is_empty());
2845
2846        let tbl = body_ref.tables()[0];
2847        let row = match &tbl.rows[0] {
2848            crate::types::RowContent::Tr(r) => r,
2849            _ => panic!("expected Tr"),
2850        };
2851        let cell = match &row.cells[0] {
2852            crate::types::CellContent::Tc(c) => c,
2853            _ => panic!("expected Tc"),
2854        };
2855        let tcpr = cell.cell_properties.as_ref().unwrap();
2856        let shd = tcpr.shading.as_ref().unwrap();
2857        assert_eq!(shd.value, crate::types::STShd::Clear);
2858        assert_eq!(shd.fill.as_deref(), Some("FFFF00"));
2859
2860        let borders = tcpr.tc_borders.as_ref().unwrap();
2861        assert!(borders.top.is_some());
2862        let top = borders.top.as_ref().unwrap();
2863        assert_eq!(top.value, crate::types::STBorder::Single);
2864
2865        let mar = tcpr.tc_mar.as_ref().unwrap();
2866        let left = mar.left.as_ref().unwrap();
2867        assert_eq!(left.width.as_deref(), Some("144"));
2868    }
2869}