Skip to main content

ooxml_wml/
writer.rs

1//! Document writing and serialization.
2//!
3//! This module provides functionality for creating new Word documents
4//! using generated types and ToXml serializers.
5
6use crate::document::{
7    AppProperties, CoreProperties, serialize_app_properties, serialize_core_properties,
8};
9use crate::error::Result;
10use crate::generated_serializers::ToXml;
11use crate::types;
12use ooxml_opc::{PackageWriter, Relationship, Relationships, content_type, rel_type};
13use ooxml_xml::{PositionedNode, RawXmlElement, RawXmlNode};
14use std::collections::HashMap;
15use std::fs::File;
16use std::io::{BufWriter, Seek, Write};
17use std::path::Path;
18
19/// WordprocessingML namespace.
20pub const NS_W: &str = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
21/// Relationships namespace.
22pub const NS_R: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
23/// WordprocessingML Drawing namespace.
24pub const NS_WP: &str = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
25/// DrawingML main namespace.
26pub const NS_A: &str = "http://schemas.openxmlformats.org/drawingml/2006/main";
27/// Picture namespace.
28pub const NS_PIC: &str = "http://schemas.openxmlformats.org/drawingml/2006/picture";
29/// Word Processing Shapes namespace (text boxes).
30pub const NS_WPS: &str = "http://schemas.microsoft.com/office/word/2010/wordprocessingShape";
31/// Markup Compatibility namespace.
32pub const NS_MC: &str = "http://schemas.openxmlformats.org/markup-compatibility/2006";
33
34/// Standard namespace declarations used on root elements.
35const NS_DECLS: &[(&str, &str)] = &[
36    ("xmlns:w", NS_W),
37    ("xmlns:r", NS_R),
38    ("xmlns:wp", NS_WP),
39    ("xmlns:a", NS_A),
40    ("xmlns:pic", NS_PIC),
41];
42
43/// A pending image to be written to the package.
44#[derive(Clone)]
45pub struct PendingImage {
46    /// Raw image data.
47    pub data: Vec<u8>,
48    /// Content type (e.g., "image/png").
49    pub content_type: String,
50    /// Assigned relationship ID.
51    pub rel_id: String,
52    /// Generated filename (e.g., "image1.png").
53    pub filename: String,
54}
55
56/// A pending hyperlink to be written to relationships.
57#[derive(Clone)]
58pub struct PendingHyperlink {
59    /// Relationship ID.
60    pub rel_id: String,
61    /// Target URL.
62    pub url: String,
63}
64
65/// List type for creating numbered or bulleted lists.
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum ListType {
68    /// Bulleted list (uses bullet character).
69    Bullet,
70    /// Numbered list (uses decimal numbers: 1, 2, 3...).
71    Decimal,
72    /// Lowercase letter list (a, b, c...).
73    LowerLetter,
74    /// Uppercase letter list (A, B, C...).
75    UpperLetter,
76    /// Lowercase Roman numerals (i, ii, iii...).
77    LowerRoman,
78    /// Uppercase Roman numerals (I, II, III...).
79    UpperRoman,
80}
81
82/// A numbering definition to be written to numbering.xml.
83#[derive(Clone)]
84pub struct PendingNumbering {
85    /// Abstract numbering ID.
86    pub abstract_num_id: u32,
87    /// Concrete numbering ID (used in numPr).
88    pub num_id: u32,
89    /// List type (None when using custom levels).
90    pub list_type: Option<ListType>,
91    /// Custom levels (None when using simple list_type).
92    pub custom_levels: Option<Vec<NumberingLevel>>,
93}
94
95/// A numbering level definition for custom lists.
96///
97/// Used with `DocumentBuilder::add_custom_list()` to create multi-level
98/// or custom-formatted numbered/bulleted lists.
99///
100/// ECMA-376 Part 1, Section 17.9.6 (`w:lvl`).
101#[derive(Debug, Clone)]
102pub struct NumberingLevel {
103    /// Level index (0-based). Level 0 is the outermost list level.
104    pub ilvl: u32,
105    /// Number format (e.g., Decimal, LowerRoman, Bullet).
106    pub format: ListType,
107    /// Starting value for this level.
108    pub start: u32,
109    /// Level text pattern, e.g., `"%1."` for "1.", `"%1.%2."` for nested.
110    /// For bullet lists, use the bullet character directly (e.g., `"\u{2022}"`).
111    pub text: String,
112    /// Left indentation in twips for this level (default: 720 × (ilvl + 1)).
113    pub indent_left: Option<u32>,
114    /// Hanging indentation in twips (default: 360).
115    pub hanging: Option<u32>,
116}
117
118impl NumberingLevel {
119    /// Create a bullet level at the given depth.
120    pub fn bullet(ilvl: u32) -> Self {
121        Self {
122            ilvl,
123            format: ListType::Bullet,
124            start: 1,
125            text: "\u{2022}".to_string(),
126            indent_left: Some(720 * (ilvl + 1)),
127            hanging: Some(360),
128        }
129    }
130
131    /// Create a decimal (numbered) level at the given depth.
132    pub fn decimal(ilvl: u32) -> Self {
133        Self {
134            ilvl,
135            format: ListType::Decimal,
136            start: 1,
137            text: format!("%{}.", ilvl + 1),
138            indent_left: Some(720 * (ilvl + 1)),
139            hanging: Some(360),
140        }
141    }
142}
143
144/// Type of header or footer.
145///
146/// ECMA-376 Part 1, Section 17.18.36 (ST_HdrFtr).
147#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
148pub enum HeaderFooterType {
149    /// Default header/footer used on most pages.
150    #[default]
151    Default,
152    /// Header/footer for the first page only.
153    First,
154    /// Header/footer for even pages (when different odd/even is enabled).
155    Even,
156}
157
158impl HeaderFooterType {
159    /// Parse from the `w:type` attribute value.
160    pub fn parse(s: &str) -> Self {
161        match s {
162            "first" => Self::First,
163            "even" => Self::Even,
164            _ => Self::Default,
165        }
166    }
167
168    /// Convert to the `w:type` attribute value.
169    pub fn as_str(&self) -> &'static str {
170        match self {
171            Self::Default => "default",
172            Self::First => "first",
173            Self::Even => "even",
174        }
175    }
176}
177
178/// Text wrapping type for anchored images.
179///
180/// ECMA-376 Part 1, Section 20.4.2.3.
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
182pub enum WrapType {
183    /// No wrapping - text flows over/under the image.
184    #[default]
185    None,
186    /// Square wrapping - text wraps around a bounding box.
187    Square,
188    /// Tight wrapping - text wraps closely around the image shape.
189    Tight,
190    /// Through wrapping - text wraps through transparent areas.
191    Through,
192    /// Top and bottom - text only above and below.
193    TopAndBottom,
194}
195
196/// A pending header to be written to the package.
197#[derive(Clone)]
198pub struct PendingHeader {
199    /// Header content.
200    pub body: types::HeaderFooter,
201    /// Assigned relationship ID.
202    pub rel_id: String,
203    /// Header type (default, first, even).
204    pub header_type: HeaderFooterType,
205    /// Generated filename (e.g., "header1.xml").
206    pub filename: String,
207}
208
209/// A pending footer to be written to the package.
210#[derive(Clone)]
211pub struct PendingFooter {
212    /// Footer content.
213    pub body: types::HeaderFooter,
214    /// Assigned relationship ID.
215    pub rel_id: String,
216    /// Footer type (default, first, even).
217    pub footer_type: HeaderFooterType,
218    /// Generated filename (e.g., "footer1.xml").
219    pub filename: String,
220}
221
222/// A pending footnote to be written to the package.
223#[derive(Clone)]
224pub struct PendingFootnote {
225    /// Footnote ID (referenced by FootnoteReference).
226    pub id: i32,
227    /// Footnote content.
228    pub body: types::FootnoteEndnote,
229}
230
231/// A pending endnote to be written to the package.
232#[derive(Clone)]
233pub struct PendingEndnote {
234    /// Endnote ID (referenced by EndnoteReference).
235    pub id: i32,
236    /// Endnote content.
237    pub body: types::FootnoteEndnote,
238}
239
240// =============================================================================
241// Settings types
242// =============================================================================
243
244/// Options for the `word/settings.xml` document settings part.
245///
246/// ECMA-376 Part 1, Section 17.15 (Settings).
247#[cfg(feature = "wml-settings")]
248#[derive(Debug, Clone, Default)]
249pub struct DocumentSettingsOptions {
250    /// Default tab stop width in twips (maps to `<w:defaultTabStop>`).
251    pub default_tab_stop: Option<u32>,
252    /// Whether different headers/footers for odd/even pages are enabled
253    /// (maps to `<w:evenAndOddHeaders/>`).
254    pub even_and_odd_headers: bool,
255    /// Whether tracked changes are active (maps to `<w:trackChanges/>`).
256    pub track_changes: bool,
257    /// Root RSID for the document (maps to `<w:rsidRoot w:val="..."/>`).
258    pub rsid_root: Option<String>,
259    /// Whether to include a compatibility mode setting for Word 2013+
260    /// (`<w:compat><w:compatSetting w:name="compatibilityMode" w:val="15"/></w:compat>`).
261    pub compat_mode: bool,
262}
263
264// =============================================================================
265// Chart types
266// =============================================================================
267
268/// A pending chart to be written to the package.
269#[cfg(feature = "wml-charts")]
270#[derive(Clone)]
271pub struct PendingChart {
272    /// Chart XML bytes.
273    pub data: Vec<u8>,
274    /// Assigned relationship ID.
275    pub rel_id: String,
276    /// Generated filename (e.g., "chart1.xml").
277    pub filename: String,
278}
279
280/// A pending comment to be written to the package.
281#[derive(Clone)]
282pub struct PendingComment {
283    /// Comment ID (referenced by CommentReference and comment ranges).
284    pub id: i32,
285    /// Comment author.
286    pub author: Option<String>,
287    /// Comment date (ISO 8601 format).
288    pub date: Option<String>,
289    /// Comment initials.
290    pub initials: Option<String>,
291    /// Comment content.
292    pub body: types::Comment,
293}
294
295/// Builder for header content.
296///
297/// Provides a fluent API for building header content.
298pub struct HeaderBuilder<'a> {
299    builder: &'a mut DocumentBuilder,
300    rel_id: String,
301}
302
303impl<'a> HeaderBuilder<'a> {
304    /// Get a mutable reference to the header body.
305    pub fn body_mut(&mut self) -> &mut types::HeaderFooter {
306        &mut self
307            .builder
308            .headers
309            .get_mut(&self.rel_id)
310            .expect("header should exist")
311            .body
312    }
313
314    /// Add a paragraph with text to the header.
315    pub fn add_paragraph(&mut self, text: &str) -> &mut Self {
316        self.body_mut().add_paragraph().add_run().set_text(text);
317        self
318    }
319
320    /// Get the relationship ID for this header.
321    pub fn rel_id(&self) -> &str {
322        &self.rel_id
323    }
324}
325
326/// Builder for footer content.
327///
328/// Provides a fluent API for building footer content.
329pub struct FooterBuilder<'a> {
330    builder: &'a mut DocumentBuilder,
331    rel_id: String,
332}
333
334impl<'a> FooterBuilder<'a> {
335    /// Get a mutable reference to the footer body.
336    pub fn body_mut(&mut self) -> &mut types::HeaderFooter {
337        &mut self
338            .builder
339            .footers
340            .get_mut(&self.rel_id)
341            .expect("footer should exist")
342            .body
343    }
344
345    /// Add a paragraph with text to the footer.
346    pub fn add_paragraph(&mut self, text: &str) -> &mut Self {
347        self.body_mut().add_paragraph().add_run().set_text(text);
348        self
349    }
350
351    /// Get the relationship ID for this footer.
352    pub fn rel_id(&self) -> &str {
353        &self.rel_id
354    }
355}
356
357/// Builder for footnote content.
358pub struct FootnoteBuilder<'a> {
359    builder: &'a mut DocumentBuilder,
360    id: i32,
361}
362
363impl<'a> FootnoteBuilder<'a> {
364    /// Get a mutable reference to the footnote body.
365    pub fn body_mut(&mut self) -> &mut types::FootnoteEndnote {
366        &mut self
367            .builder
368            .footnotes
369            .get_mut(&self.id)
370            .expect("footnote should exist")
371            .body
372    }
373
374    /// Add a paragraph with text to the footnote.
375    pub fn add_paragraph(&mut self, text: &str) -> &mut Self {
376        self.body_mut().add_paragraph().add_run().set_text(text);
377        self
378    }
379
380    /// Get the footnote ID for use in FootnoteReference.
381    ///
382    /// The returned ID is always positive (user-created footnotes start at 1).
383    pub fn id(&self) -> u32 {
384        self.id as u32
385    }
386}
387
388/// Builder for endnote content.
389pub struct EndnoteBuilder<'a> {
390    builder: &'a mut DocumentBuilder,
391    id: i32,
392}
393
394impl<'a> EndnoteBuilder<'a> {
395    /// Get a mutable reference to the endnote body.
396    pub fn body_mut(&mut self) -> &mut types::FootnoteEndnote {
397        &mut self
398            .builder
399            .endnotes
400            .get_mut(&self.id)
401            .expect("endnote should exist")
402            .body
403    }
404
405    /// Add a paragraph with text to the endnote.
406    pub fn add_paragraph(&mut self, text: &str) -> &mut Self {
407        self.body_mut().add_paragraph().add_run().set_text(text);
408        self
409    }
410
411    /// Get the endnote ID for use in EndnoteReference.
412    ///
413    /// The returned ID is always positive (user-created endnotes start at 1).
414    pub fn id(&self) -> u32 {
415        self.id as u32
416    }
417}
418
419/// Builder for comment content.
420pub struct CommentBuilder<'a> {
421    builder: &'a mut DocumentBuilder,
422    id: i32,
423}
424
425impl<'a> CommentBuilder<'a> {
426    /// Get a mutable reference to the comment body.
427    pub fn body_mut(&mut self) -> &mut types::Comment {
428        &mut self
429            .builder
430            .comments
431            .get_mut(&self.id)
432            .expect("comment should exist")
433            .body
434    }
435
436    /// Add a paragraph with text to the comment.
437    pub fn add_paragraph(&mut self, text: &str) -> &mut Self {
438        self.body_mut().add_paragraph().add_run().set_text(text);
439        self
440    }
441
442    /// Set the comment author.
443    pub fn set_author(&mut self, author: &str) -> &mut Self {
444        self.builder
445            .comments
446            .get_mut(&self.id)
447            .expect("comment should exist")
448            .author = Some(author.to_string());
449        self
450    }
451
452    /// Set the comment date (ISO 8601 format, e.g., "2024-01-15T10:30:00Z").
453    pub fn set_date(&mut self, date: &str) -> &mut Self {
454        self.builder
455            .comments
456            .get_mut(&self.id)
457            .expect("comment should exist")
458            .date = Some(date.to_string());
459        self
460    }
461
462    /// Set the comment initials.
463    pub fn set_initials(&mut self, initials: &str) -> &mut Self {
464        self.builder
465            .comments
466            .get_mut(&self.id)
467            .expect("comment should exist")
468            .initials = Some(initials.to_string());
469        self
470    }
471
472    /// Get the comment ID for use in CommentReference and comment ranges.
473    ///
474    /// The returned ID is always positive (user-created comments start at 0).
475    pub fn id(&self) -> u32 {
476        self.id as u32
477    }
478}
479
480// =============================================================================
481// Drawing types (writer-only, produce DrawingML XML for images)
482// =============================================================================
483
484/// A text box embedded in a drawing.
485///
486/// Produces `<wps:wsp>` inside a `<wp:inline>` drawing element.
487/// ECMA-376 Part 1 §20.4; WPS namespace §§ (Word Processing Shapes).
488#[derive(Debug, Clone)]
489pub struct TextBox {
490    /// Text content of the box.
491    pub text: String,
492    /// Width in EMUs (914400 = 1 inch). Defaults to 1 inch.
493    pub width_emu: i64,
494    /// Height in EMUs. Defaults to 0.5 inch.
495    pub height_emu: i64,
496}
497
498impl TextBox {
499    /// Create a new text box with the given content.
500    pub fn new(text: impl Into<String>) -> Self {
501        Self {
502            text: text.into(),
503            width_emu: 914400,  // 1 inch
504            height_emu: 457200, // 0.5 inch
505        }
506    }
507
508    /// Set the width in EMUs.
509    pub fn set_width_emu(&mut self, emu: i64) -> &mut Self {
510        self.width_emu = emu;
511        self
512    }
513
514    /// Set the height in EMUs.
515    pub fn set_height_emu(&mut self, emu: i64) -> &mut Self {
516        self.height_emu = emu;
517        self
518    }
519
520    /// Set the width in inches.
521    pub fn set_width_inches(&mut self, inches: f64) -> &mut Self {
522        self.width_emu = (inches * 914400.0) as i64;
523        self
524    }
525
526    /// Set the height in inches.
527    pub fn set_height_inches(&mut self, inches: f64) -> &mut Self {
528        self.height_emu = (inches * 914400.0) as i64;
529        self
530    }
531}
532
533/// A drawing container for images.
534///
535/// This is a writer-side helper that produces the DrawingML XML for inline
536/// and anchored images. Use `Drawing::build()` to convert to a generated
537/// `types::CTDrawing` that can be added to a run.
538#[derive(Debug, Clone, Default)]
539pub struct Drawing {
540    /// Inline images in this drawing.
541    images: Vec<InlineImage>,
542    /// Anchored (floating) images in this drawing.
543    anchored_images: Vec<AnchoredImage>,
544    /// Text boxes in this drawing.
545    text_boxes: Vec<TextBox>,
546}
547
548impl Drawing {
549    /// Create an empty drawing.
550    pub fn new() -> Self {
551        Self::default()
552    }
553
554    /// Get inline images in this drawing.
555    pub fn images(&self) -> &[InlineImage] {
556        &self.images
557    }
558
559    /// Get mutable reference to inline images.
560    pub fn images_mut(&mut self) -> &mut Vec<InlineImage> {
561        &mut self.images
562    }
563
564    /// Add an inline image to this drawing.
565    pub fn add_image(&mut self, rel_id: impl Into<String>) -> &mut InlineImage {
566        self.images.push(InlineImage::new(rel_id));
567        self.images.last_mut().unwrap()
568    }
569
570    /// Get anchored (floating) images in this drawing.
571    pub fn anchored_images(&self) -> &[AnchoredImage] {
572        &self.anchored_images
573    }
574
575    /// Get mutable reference to anchored images.
576    pub fn anchored_images_mut(&mut self) -> &mut Vec<AnchoredImage> {
577        &mut self.anchored_images
578    }
579
580    /// Add an anchored (floating) image to this drawing.
581    pub fn add_anchored_image(&mut self, rel_id: impl Into<String>) -> &mut AnchoredImage {
582        self.anchored_images.push(AnchoredImage::new(rel_id));
583        self.anchored_images.last_mut().unwrap()
584    }
585
586    /// Add a text box to this drawing.
587    ///
588    /// Returns a mutable reference to the `TextBox` for further configuration.
589    /// The text box is rendered as a `<wps:wsp>` element inside a `<wp:inline>` wrapper.
590    ///
591    /// # Example
592    ///
593    /// ```
594    /// use ooxml_wml::writer::Drawing;
595    ///
596    /// let mut drawing = Drawing::new();
597    /// drawing.add_text_box("Hello from a text box")
598    ///        .set_width_inches(2.0)
599    ///        .set_height_inches(1.0);
600    /// ```
601    pub fn add_text_box(&mut self, text: impl Into<String>) -> &mut TextBox {
602        self.text_boxes.push(TextBox::new(text));
603        self.text_boxes.last_mut().unwrap()
604    }
605
606    /// Get text boxes in this drawing.
607    pub fn text_boxes(&self) -> &[TextBox] {
608        &self.text_boxes
609    }
610
611    /// Convert this drawing to a generated `CTDrawing` type.
612    ///
613    /// The `doc_id` counter is incremented for each image to produce unique IDs.
614    pub fn build(self, doc_id: &mut usize) -> types::CTDrawing {
615        let mut children = Vec::new();
616        let mut child_idx = 0usize;
617
618        for image in &self.images {
619            let elem = build_inline_image_element(image, *doc_id);
620            children.push(PositionedNode::new(child_idx, RawXmlNode::Element(elem)));
621            child_idx += 1;
622            *doc_id += 1;
623        }
624
625        for image in &self.anchored_images {
626            let elem = build_anchored_image_element(image, *doc_id);
627            children.push(PositionedNode::new(child_idx, RawXmlNode::Element(elem)));
628            child_idx += 1;
629            *doc_id += 1;
630        }
631
632        for text_box in &self.text_boxes {
633            let elem = build_text_box_element(text_box, *doc_id);
634            children.push(PositionedNode::new(child_idx, RawXmlNode::Element(elem)));
635            child_idx += 1;
636            *doc_id += 1;
637        }
638
639        types::CTDrawing {
640            #[cfg(feature = "extra-children")]
641            extra_children: children,
642        }
643    }
644}
645
646/// An inline image in a drawing.
647///
648/// Represents an image embedded in the document via DrawingML.
649/// References image data through a relationship ID.
650#[derive(Debug, Clone)]
651pub struct InlineImage {
652    /// Relationship ID referencing the image file (e.g., "rId4").
653    rel_id: String,
654    /// Width in EMUs (English Metric Units). 914400 EMUs = 1 inch.
655    width_emu: Option<i64>,
656    /// Height in EMUs.
657    height_emu: Option<i64>,
658    /// Optional description/alt text for the image.
659    description: Option<String>,
660}
661
662impl InlineImage {
663    /// Create a new inline image with the given relationship ID.
664    pub fn new(rel_id: impl Into<String>) -> Self {
665        Self {
666            rel_id: rel_id.into(),
667            width_emu: None,
668            height_emu: None,
669            description: None,
670        }
671    }
672
673    /// Get the relationship ID.
674    pub fn rel_id(&self) -> &str {
675        &self.rel_id
676    }
677
678    /// Get width in EMUs (914400 EMUs = 1 inch).
679    pub fn width_emu(&self) -> Option<i64> {
680        self.width_emu
681    }
682
683    /// Get height in EMUs.
684    pub fn height_emu(&self) -> Option<i64> {
685        self.height_emu
686    }
687
688    /// Get width in inches.
689    pub fn width_inches(&self) -> Option<f64> {
690        self.width_emu.map(|e| e as f64 / 914400.0)
691    }
692
693    /// Get height in inches.
694    pub fn height_inches(&self) -> Option<f64> {
695        self.height_emu.map(|e| e as f64 / 914400.0)
696    }
697
698    /// Set width in EMUs.
699    pub fn set_width_emu(&mut self, emu: i64) -> &mut Self {
700        self.width_emu = Some(emu);
701        self
702    }
703
704    /// Set height in EMUs.
705    pub fn set_height_emu(&mut self, emu: i64) -> &mut Self {
706        self.height_emu = Some(emu);
707        self
708    }
709
710    /// Set width in inches.
711    pub fn set_width_inches(&mut self, inches: f64) -> &mut Self {
712        self.width_emu = Some((inches * 914400.0) as i64);
713        self
714    }
715
716    /// Set height in inches.
717    pub fn set_height_inches(&mut self, inches: f64) -> &mut Self {
718        self.height_emu = Some((inches * 914400.0) as i64);
719        self
720    }
721
722    /// Get the description/alt text.
723    pub fn description(&self) -> Option<&str> {
724        self.description.as_deref()
725    }
726
727    /// Set the description/alt text.
728    pub fn set_description(&mut self, desc: impl Into<String>) -> &mut Self {
729        self.description = Some(desc.into());
730        self
731    }
732}
733
734/// An anchored (floating) image in a drawing.
735///
736/// Represents an image positioned relative to a reference point with text
737/// wrapping options. Unlike inline images, anchored images can float and wrap.
738/// ECMA-376 Part 1, Section 20.4.2.3 (anchor).
739#[derive(Debug, Clone)]
740pub struct AnchoredImage {
741    /// Relationship ID referencing the image file (e.g., "rId4").
742    rel_id: String,
743    /// Width in EMUs (English Metric Units). 914400 EMUs = 1 inch.
744    width_emu: Option<i64>,
745    /// Height in EMUs.
746    height_emu: Option<i64>,
747    /// Optional description/alt text for the image.
748    description: Option<String>,
749    /// Whether the image is behind text (true) or in front (false).
750    behind_doc: bool,
751    /// Horizontal position offset from the reference in EMUs.
752    pos_x: i64,
753    /// Vertical position offset from the reference in EMUs.
754    pos_y: i64,
755    /// Text wrapping mode.
756    wrap_type: WrapType,
757}
758
759impl AnchoredImage {
760    /// Create a new anchored image with the given relationship ID.
761    pub fn new(rel_id: impl Into<String>) -> Self {
762        Self {
763            rel_id: rel_id.into(),
764            width_emu: None,
765            height_emu: None,
766            description: None,
767            behind_doc: false,
768            pos_x: 0,
769            pos_y: 0,
770            wrap_type: WrapType::None,
771        }
772    }
773
774    /// Get the relationship ID.
775    pub fn rel_id(&self) -> &str {
776        &self.rel_id
777    }
778
779    /// Get width in EMUs (914400 EMUs = 1 inch).
780    pub fn width_emu(&self) -> Option<i64> {
781        self.width_emu
782    }
783
784    /// Get height in EMUs.
785    pub fn height_emu(&self) -> Option<i64> {
786        self.height_emu
787    }
788
789    /// Get width in inches.
790    pub fn width_inches(&self) -> Option<f64> {
791        self.width_emu.map(|e| e as f64 / 914400.0)
792    }
793
794    /// Get height in inches.
795    pub fn height_inches(&self) -> Option<f64> {
796        self.height_emu.map(|e| e as f64 / 914400.0)
797    }
798
799    /// Set width in EMUs.
800    pub fn set_width_emu(&mut self, emu: i64) -> &mut Self {
801        self.width_emu = Some(emu);
802        self
803    }
804
805    /// Set height in EMUs.
806    pub fn set_height_emu(&mut self, emu: i64) -> &mut Self {
807        self.height_emu = Some(emu);
808        self
809    }
810
811    /// Set width in inches.
812    pub fn set_width_inches(&mut self, inches: f64) -> &mut Self {
813        self.width_emu = Some((inches * 914400.0) as i64);
814        self
815    }
816
817    /// Set height in inches.
818    pub fn set_height_inches(&mut self, inches: f64) -> &mut Self {
819        self.height_emu = Some((inches * 914400.0) as i64);
820        self
821    }
822
823    /// Get the description/alt text.
824    pub fn description(&self) -> Option<&str> {
825        self.description.as_deref()
826    }
827
828    /// Set the description/alt text.
829    pub fn set_description(&mut self, desc: impl Into<String>) -> &mut Self {
830        self.description = Some(desc.into());
831        self
832    }
833
834    /// Check if the image is behind document text.
835    pub fn is_behind_doc(&self) -> bool {
836        self.behind_doc
837    }
838
839    /// Set whether the image is behind document text.
840    pub fn set_behind_doc(&mut self, behind: bool) -> &mut Self {
841        self.behind_doc = behind;
842        self
843    }
844
845    /// Get horizontal position offset in EMUs.
846    pub fn pos_x(&self) -> i64 {
847        self.pos_x
848    }
849
850    /// Get vertical position offset in EMUs.
851    pub fn pos_y(&self) -> i64 {
852        self.pos_y
853    }
854
855    /// Set horizontal position offset in EMUs.
856    pub fn set_pos_x(&mut self, emu: i64) -> &mut Self {
857        self.pos_x = emu;
858        self
859    }
860
861    /// Set vertical position offset in EMUs.
862    pub fn set_pos_y(&mut self, emu: i64) -> &mut Self {
863        self.pos_y = emu;
864        self
865    }
866
867    /// Get the text wrapping type.
868    pub fn wrap_type(&self) -> WrapType {
869        self.wrap_type
870    }
871
872    /// Set the text wrapping type.
873    pub fn set_wrap_type(&mut self, wrap: WrapType) -> &mut Self {
874        self.wrap_type = wrap;
875        self
876    }
877}
878
879// =============================================================================
880// DocumentBuilder
881// =============================================================================
882
883/// Builder for creating new Word documents.
884pub struct DocumentBuilder {
885    document: types::Document,
886    /// Pending images to write, keyed by rel_id.
887    images: HashMap<String, PendingImage>,
888    /// Pending hyperlinks, keyed by rel_id.
889    hyperlinks: HashMap<String, PendingHyperlink>,
890    /// Numbering definitions, keyed by num_id.
891    numberings: HashMap<u32, PendingNumbering>,
892    /// Styles to write to word/styles.xml, if any.
893    styles: Option<types::Styles>,
894    /// Pending headers, keyed by rel_id.
895    headers: HashMap<String, PendingHeader>,
896    /// Pending footers, keyed by rel_id.
897    footers: HashMap<String, PendingFooter>,
898    /// Pending footnotes, keyed by ID.
899    footnotes: HashMap<i32, PendingFootnote>,
900    /// Pending endnotes, keyed by ID.
901    endnotes: HashMap<i32, PendingEndnote>,
902    /// Pending comments, keyed by ID.
903    comments: HashMap<i32, PendingComment>,
904    /// Document settings options to write to word/settings.xml.
905    #[cfg(feature = "wml-settings")]
906    settings: Option<DocumentSettingsOptions>,
907    /// Pending charts, keyed by rel_id.
908    #[cfg(feature = "wml-charts")]
909    charts: HashMap<String, PendingChart>,
910    /// Counter for generating unique chart IDs.
911    #[cfg(feature = "wml-charts")]
912    next_chart_id: u32,
913    /// Core document properties to write to docProps/core.xml.
914    core_properties: Option<CoreProperties>,
915    /// Extended application properties to write to docProps/app.xml.
916    app_properties: Option<AppProperties>,
917    /// Counter for generating unique IDs.
918    next_rel_id: u32,
919    /// Counter for generating unique numbering IDs.
920    next_num_id: u32,
921    /// Counter for generating unique header IDs.
922    next_header_id: u32,
923    /// Counter for generating unique footer IDs.
924    next_footer_id: u32,
925    /// Counter for generating unique footnote IDs.
926    /// Starts at 1 because 0 is reserved for the separator footnote.
927    next_footnote_id: i32,
928    /// Counter for generating unique endnote IDs.
929    /// Starts at 1 because 0 is reserved for the separator endnote.
930    next_endnote_id: i32,
931    /// Counter for generating unique comment IDs.
932    next_comment_id: i32,
933    /// Counter for generating unique drawing/image IDs.
934    next_drawing_id: usize,
935}
936
937impl Default for DocumentBuilder {
938    fn default() -> Self {
939        Self::new()
940    }
941}
942
943impl DocumentBuilder {
944    /// Create a new document builder.
945    pub fn new() -> Self {
946        let document = types::Document {
947            #[cfg(feature = "wml-styling")]
948            background: None,
949            body: Some(Box::new(types::Body::default())),
950            conformance: None,
951            #[cfg(feature = "extra-attrs")]
952            extra_attrs: std::collections::HashMap::new(),
953            #[cfg(feature = "extra-children")]
954            extra_children: Vec::new(),
955        };
956
957        Self {
958            document,
959            images: HashMap::new(),
960            hyperlinks: HashMap::new(),
961            numberings: HashMap::new(),
962            styles: None,
963            headers: HashMap::new(),
964            footers: HashMap::new(),
965            footnotes: HashMap::new(),
966            endnotes: HashMap::new(),
967            comments: HashMap::new(),
968            #[cfg(feature = "wml-settings")]
969            settings: None,
970            #[cfg(feature = "wml-charts")]
971            charts: HashMap::new(),
972            #[cfg(feature = "wml-charts")]
973            next_chart_id: 1,
974            core_properties: None,
975            app_properties: None,
976            next_rel_id: 1,
977            next_num_id: 1,
978            next_header_id: 1,
979            next_footer_id: 1,
980            next_footnote_id: 1,
981            next_endnote_id: 1,
982            next_comment_id: 0,
983            next_drawing_id: 1,
984        }
985    }
986
987    /// Add an image and return its relationship ID.
988    ///
989    /// The image data will be written to the package when save() is called.
990    /// Use the returned rel_id when adding an InlineImage to a Run.
991    pub fn add_image(&mut self, data: Vec<u8>, content_type: &str) -> String {
992        let id = self.next_rel_id;
993        self.next_rel_id += 1;
994
995        let rel_id = format!("rId{}", id);
996        let ext = extension_from_content_type(content_type);
997        let filename = format!("image{}.{}", id, ext);
998
999        self.images.insert(
1000            rel_id.clone(),
1001            PendingImage {
1002                data,
1003                content_type: content_type.to_string(),
1004                rel_id: rel_id.clone(),
1005                filename,
1006            },
1007        );
1008
1009        rel_id
1010    }
1011
1012    /// Add a hyperlink and return its relationship ID.
1013    ///
1014    /// Use the returned rel_id when creating a Hyperlink in a paragraph.
1015    pub fn add_hyperlink(&mut self, url: &str) -> String {
1016        let id = self.next_rel_id;
1017        self.next_rel_id += 1;
1018
1019        let rel_id = format!("rId{}", id);
1020
1021        self.hyperlinks.insert(
1022            rel_id.clone(),
1023            PendingHyperlink {
1024                rel_id: rel_id.clone(),
1025                url: url.to_string(),
1026            },
1027        );
1028
1029        rel_id
1030    }
1031
1032    /// Create a list definition and return its numbering ID.
1033    ///
1034    /// Use the returned num_id in NumberingProperties when adding list items.
1035    pub fn add_list(&mut self, list_type: ListType) -> u32 {
1036        let num_id = self.next_num_id;
1037        self.next_num_id += 1;
1038
1039        self.numberings.insert(
1040            num_id,
1041            PendingNumbering {
1042                abstract_num_id: num_id, // Use same ID for simplicity
1043                num_id,
1044                list_type: Some(list_type),
1045                custom_levels: None,
1046            },
1047        );
1048
1049        num_id
1050    }
1051
1052    /// Create a custom multi-level list definition and return its numbering ID.
1053    ///
1054    /// Each `NumberingLevel` defines formatting for a single list level (0-based).
1055    /// Use the returned num_id in NumberingProperties when adding list items.
1056    ///
1057    /// # Example
1058    ///
1059    /// ```
1060    /// use ooxml_wml::writer::{DocumentBuilder, NumberingLevel, ListType};
1061    ///
1062    /// let mut builder = DocumentBuilder::new();
1063    /// let num_id = builder.add_custom_list(vec![
1064    ///     NumberingLevel {
1065    ///         ilvl: 0,
1066    ///         format: ListType::Decimal,
1067    ///         start: 1,
1068    ///         text: "%1.".to_string(),
1069    ///         indent_left: Some(720),
1070    ///         hanging: Some(360),
1071    ///     },
1072    ///     NumberingLevel {
1073    ///         ilvl: 1,
1074    ///         format: ListType::LowerLetter,
1075    ///         start: 1,
1076    ///         text: "%2.".to_string(),
1077    ///         indent_left: Some(1440),
1078    ///         hanging: Some(360),
1079    ///     },
1080    /// ]);
1081    /// ```
1082    ///
1083    /// ECMA-376 Part 1, Section 17.9 (Numbering Definitions).
1084    pub fn add_custom_list(&mut self, levels: Vec<NumberingLevel>) -> u32 {
1085        let num_id = self.next_num_id;
1086        self.next_num_id += 1;
1087
1088        self.numberings.insert(
1089            num_id,
1090            PendingNumbering {
1091                abstract_num_id: num_id,
1092                num_id,
1093                list_type: None,
1094                custom_levels: Some(levels),
1095            },
1096        );
1097
1098        num_id
1099    }
1100
1101    /// Set the full styles for the document.
1102    ///
1103    /// Replaces any previously set styles. The styles will be written to
1104    /// `word/styles.xml` when the document is saved.
1105    ///
1106    /// ECMA-376 Part 1, Section 17.7 (Styles).
1107    pub fn set_styles(&mut self, styles: types::Styles) -> &mut Self {
1108        self.styles = Some(styles);
1109        self
1110    }
1111
1112    /// Add a single style definition.
1113    ///
1114    /// Creates the styles container if it doesn't exist yet. The style will be
1115    /// written to `word/styles.xml` when the document is saved.
1116    ///
1117    /// ECMA-376 Part 1, Section 17.7.4.17 (style).
1118    pub fn add_style(&mut self, style: types::Style) -> &mut Self {
1119        self.styles
1120            .get_or_insert_with(types::Styles::default)
1121            .style
1122            .push(style);
1123        self
1124    }
1125
1126    /// Add a header and return a builder for its content.
1127    ///
1128    /// The header will be automatically linked to the document's section properties.
1129    pub fn add_header(&mut self, header_type: HeaderFooterType) -> HeaderBuilder<'_> {
1130        let id = self.next_rel_id;
1131        self.next_rel_id += 1;
1132        let header_num = self.next_header_id;
1133        self.next_header_id += 1;
1134
1135        let rel_id = format!("rId{}", id);
1136        let filename = format!("header{}.xml", header_num);
1137
1138        self.headers.insert(
1139            rel_id.clone(),
1140            PendingHeader {
1141                body: types::HeaderFooter::default(),
1142                rel_id: rel_id.clone(),
1143                header_type,
1144                filename,
1145            },
1146        );
1147
1148        HeaderBuilder {
1149            builder: self,
1150            rel_id,
1151        }
1152    }
1153
1154    /// Add a footer and return a builder for its content.
1155    ///
1156    /// The footer will be automatically linked to the document's section properties.
1157    pub fn add_footer(&mut self, footer_type: HeaderFooterType) -> FooterBuilder<'_> {
1158        let id = self.next_rel_id;
1159        self.next_rel_id += 1;
1160        let footer_num = self.next_footer_id;
1161        self.next_footer_id += 1;
1162
1163        let rel_id = format!("rId{}", id);
1164        let filename = format!("footer{}.xml", footer_num);
1165
1166        self.footers.insert(
1167            rel_id.clone(),
1168            PendingFooter {
1169                body: types::HeaderFooter::default(),
1170                rel_id: rel_id.clone(),
1171                footer_type,
1172                filename,
1173            },
1174        );
1175
1176        FooterBuilder {
1177            builder: self,
1178            rel_id,
1179        }
1180    }
1181
1182    /// Add a footnote and return a builder for its content.
1183    ///
1184    /// Use the returned `id` when adding a FootnoteReference to a Run.
1185    pub fn add_footnote(&mut self) -> FootnoteBuilder<'_> {
1186        let id = self.next_footnote_id;
1187        self.next_footnote_id += 1;
1188
1189        self.footnotes.insert(
1190            id,
1191            PendingFootnote {
1192                id,
1193                body: types::FootnoteEndnote {
1194                    #[cfg(feature = "wml-comments")]
1195                    r#type: None,
1196                    id: id as i64,
1197                    block_content: Vec::new(),
1198                    #[cfg(feature = "extra-attrs")]
1199                    extra_attrs: std::collections::HashMap::new(),
1200                    #[cfg(feature = "extra-children")]
1201                    extra_children: Vec::new(),
1202                },
1203            },
1204        );
1205
1206        FootnoteBuilder { builder: self, id }
1207    }
1208
1209    /// Add an endnote and return a builder for its content.
1210    ///
1211    /// Use the returned `id` when adding an EndnoteReference to a Run.
1212    pub fn add_endnote(&mut self) -> EndnoteBuilder<'_> {
1213        let id = self.next_endnote_id;
1214        self.next_endnote_id += 1;
1215
1216        self.endnotes.insert(
1217            id,
1218            PendingEndnote {
1219                id,
1220                body: types::FootnoteEndnote {
1221                    #[cfg(feature = "wml-comments")]
1222                    r#type: None,
1223                    id: id as i64,
1224                    block_content: Vec::new(),
1225                    #[cfg(feature = "extra-attrs")]
1226                    extra_attrs: std::collections::HashMap::new(),
1227                    #[cfg(feature = "extra-children")]
1228                    extra_children: Vec::new(),
1229                },
1230            },
1231        );
1232
1233        EndnoteBuilder { builder: self, id }
1234    }
1235
1236    /// Add a comment and return a builder for its content.
1237    ///
1238    /// Use the returned `id` when adding comment ranges and references to the document.
1239    pub fn add_comment(&mut self) -> CommentBuilder<'_> {
1240        let id = self.next_comment_id;
1241        self.next_comment_id += 1;
1242
1243        self.comments.insert(
1244            id,
1245            PendingComment {
1246                id,
1247                author: None,
1248                date: None,
1249                initials: None,
1250                body: types::Comment {
1251                    id: 0,                 // set in build_comments
1252                    author: String::new(), // set in build_comments
1253                    #[cfg(feature = "wml-comments")]
1254                    date: None,
1255                    block_content: Vec::new(),
1256                    #[cfg(feature = "wml-comments")]
1257                    initials: None,
1258                    #[cfg(feature = "extra-attrs")]
1259                    extra_attrs: Default::default(),
1260                    #[cfg(feature = "extra-children")]
1261                    extra_children: Vec::new(),
1262                },
1263            },
1264        );
1265
1266        CommentBuilder { builder: self, id }
1267    }
1268
1269    /// Set the core document properties (title, author, dates, etc.).
1270    ///
1271    /// The properties will be written to `docProps/core.xml` when saved.
1272    ///
1273    /// ECMA-376 Part 2, Section 11 (Core Properties).
1274    pub fn set_core_properties(&mut self, props: CoreProperties) -> &mut Self {
1275        self.core_properties = Some(props);
1276        self
1277    }
1278
1279    /// Set the extended application properties (word count, page count, etc.).
1280    ///
1281    /// The properties will be written to `docProps/app.xml` when saved.
1282    ///
1283    /// ECMA-376 Part 2, Section 11.1 (Extended Properties).
1284    pub fn set_app_properties(&mut self, props: AppProperties) -> &mut Self {
1285        self.app_properties = Some(props);
1286        self
1287    }
1288
1289    /// Set the document settings to write to `word/settings.xml`.
1290    ///
1291    /// ECMA-376 Part 1, Section 17.15 (Document Settings).
1292    #[cfg(feature = "wml-settings")]
1293    pub fn set_settings(&mut self, opts: DocumentSettingsOptions) -> &mut Self {
1294        self.settings = Some(opts);
1295        self
1296    }
1297
1298    /// Embed chart XML and return its relationship ID.
1299    ///
1300    /// The chart XML bytes will be written to `word/charts/chart{n}.xml`.
1301    /// Use the returned `rel_id` with `add_inline_chart` on a paragraph run.
1302    ///
1303    /// ECMA-376 Part 1, Section 21.2 (DrawingML – Charts).
1304    #[cfg(feature = "wml-charts")]
1305    pub fn embed_chart(&mut self, chart_xml: &[u8]) -> crate::error::Result<String> {
1306        let id = self.next_rel_id;
1307        self.next_rel_id += 1;
1308        let chart_num = self.next_chart_id;
1309        self.next_chart_id += 1;
1310
1311        let rel_id = format!("rId{}", id);
1312        let filename = format!("chart{}.xml", chart_num);
1313
1314        self.charts.insert(
1315            rel_id.clone(),
1316            PendingChart {
1317                data: chart_xml.to_vec(),
1318                rel_id: rel_id.clone(),
1319                filename,
1320            },
1321        );
1322
1323        Ok(rel_id)
1324    }
1325
1326    /// Get a mutable reference to the document body.
1327    pub fn body_mut(&mut self) -> &mut types::Body {
1328        self.document
1329            .body
1330            .as_deref_mut()
1331            .expect("document body should exist")
1332    }
1333
1334    /// Add a paragraph with text.
1335    pub fn add_paragraph(&mut self, text: &str) -> &mut Self {
1336        let para = self.body_mut().add_paragraph();
1337        para.add_run().set_text(text);
1338        self
1339    }
1340
1341    /// Convert a Drawing helper to a CTDrawing using the builder's ID counter.
1342    ///
1343    /// Use this to create a `types::CTDrawing` from a `Drawing`, then add it
1344    /// to a run via `run.add_drawing(ct_drawing)`.
1345    pub fn build_drawing(&mut self, drawing: Drawing) -> types::CTDrawing {
1346        drawing.build(&mut self.next_drawing_id)
1347    }
1348
1349    /// Save the document to a file.
1350    pub fn save<P: AsRef<Path>>(self, path: P) -> Result<()> {
1351        let file = File::create(path)?;
1352        let writer = BufWriter::new(file);
1353        self.write(writer)
1354    }
1355
1356    /// Write the document to a writer.
1357    pub fn write<W: Write + Seek>(mut self, writer: W) -> Result<()> {
1358        let mut pkg = PackageWriter::new(writer);
1359
1360        // Add default content types
1361        pkg.add_default_content_type("rels", content_type::RELATIONSHIPS);
1362        pkg.add_default_content_type("xml", content_type::XML);
1363
1364        // Add content types for images
1365        pkg.add_default_content_type("png", "image/png");
1366        pkg.add_default_content_type("jpg", "image/jpeg");
1367        pkg.add_default_content_type("jpeg", "image/jpeg");
1368        pkg.add_default_content_type("gif", "image/gif");
1369
1370        // Build document relationships
1371        let mut doc_rels = Relationships::new();
1372
1373        // Add header/footer references to section properties
1374        if !self.headers.is_empty() || !self.footers.is_empty() {
1375            // Ensure body has section properties
1376            #[cfg(feature = "wml-layout")]
1377            {
1378                let body = self.document.body.as_deref_mut().expect("document body");
1379                if body.sect_pr.is_none() {
1380                    body.sect_pr = Some(Box::new(types::SectionProperties::default()));
1381                }
1382                let sect_pr = body.sect_pr.as_deref_mut().unwrap();
1383
1384                // Add header references
1385                for header in self.headers.values() {
1386                    let hdr_ref = types::HeaderFooterReference {
1387                        id: header.rel_id.clone(),
1388                        r#type: match header.header_type {
1389                            HeaderFooterType::Default => types::STHdrFtr::Default,
1390                            HeaderFooterType::First => types::STHdrFtr::First,
1391                            HeaderFooterType::Even => types::STHdrFtr::Even,
1392                        },
1393                        #[cfg(feature = "extra-attrs")]
1394                        extra_attrs: std::collections::HashMap::new(),
1395                    };
1396                    sect_pr
1397                        .header_footer_refs
1398                        .push(types::HeaderFooterRef::HeaderReference(Box::new(hdr_ref)));
1399                }
1400
1401                // Add footer references
1402                for footer in self.footers.values() {
1403                    let ftr_ref = types::HeaderFooterReference {
1404                        id: footer.rel_id.clone(),
1405                        r#type: match footer.footer_type {
1406                            HeaderFooterType::Default => types::STHdrFtr::Default,
1407                            HeaderFooterType::First => types::STHdrFtr::First,
1408                            HeaderFooterType::Even => types::STHdrFtr::Even,
1409                        },
1410                        #[cfg(feature = "extra-attrs")]
1411                        extra_attrs: std::collections::HashMap::new(),
1412                    };
1413                    sect_pr
1414                        .header_footer_refs
1415                        .push(types::HeaderFooterRef::FooterReference(Box::new(ftr_ref)));
1416                }
1417            }
1418        }
1419
1420        // Set namespace declarations on the document's extra_attrs
1421        #[cfg(feature = "extra-attrs")]
1422        {
1423            for &(key, value) in NS_DECLS {
1424                self.document
1425                    .extra_attrs
1426                    .insert(key.to_string(), value.to_string());
1427            }
1428        }
1429
1430        // Write document.xml
1431        let doc_xml = serialize_to_xml_bytes(&self.document, "w:document")?;
1432        pkg.add_part(
1433            "word/document.xml",
1434            content_type::WORDPROCESSING_DOCUMENT,
1435            &doc_xml,
1436        )?;
1437
1438        // Write package relationships
1439        let mut pkg_rels = Relationships::new();
1440        pkg_rels.add(Relationship::new(
1441            "rId1",
1442            rel_type::OFFICE_DOCUMENT,
1443            "word/document.xml",
1444        ));
1445
1446        // Write core properties if set
1447        if let Some(ref core_props) = self.core_properties {
1448            let core_xml = serialize_core_properties(core_props)?;
1449            pkg.add_part(
1450                "docProps/core.xml",
1451                content_type::CORE_PROPERTIES,
1452                &core_xml,
1453            )?;
1454            pkg_rels.add(Relationship::new(
1455                "rId2",
1456                rel_type::CORE_PROPERTIES,
1457                "docProps/core.xml",
1458            ));
1459        }
1460
1461        // Write app properties if set
1462        if let Some(ref app_props) = self.app_properties {
1463            let app_xml = serialize_app_properties(app_props)?;
1464            pkg.add_part(
1465                "docProps/app.xml",
1466                content_type::EXTENDED_PROPERTIES,
1467                &app_xml,
1468            )?;
1469            pkg_rels.add(Relationship::new(
1470                "rId3",
1471                rel_type::EXTENDED_PROPERTIES,
1472                "docProps/app.xml",
1473            ));
1474        }
1475
1476        pkg.add_part(
1477            "_rels/.rels",
1478            content_type::RELATIONSHIPS,
1479            pkg_rels.serialize().as_bytes(),
1480        )?;
1481
1482        // Add image relationships and write image files
1483        for image in self.images.values() {
1484            doc_rels.add(Relationship::new(
1485                &image.rel_id,
1486                rel_type::IMAGE,
1487                format!("media/{}", image.filename),
1488            ));
1489
1490            let image_path = format!("word/media/{}", image.filename);
1491            pkg.add_part(&image_path, &image.content_type, &image.data)?;
1492        }
1493
1494        // Add hyperlink relationships (external)
1495        for hyperlink in self.hyperlinks.values() {
1496            doc_rels.add(Relationship::external(
1497                &hyperlink.rel_id,
1498                rel_type::HYPERLINK,
1499                &hyperlink.url,
1500            ));
1501        }
1502
1503        // Write headers and add relationships
1504        for header in self.headers.values() {
1505            let header_xml = serialize_with_namespaces(&header.body, "w:hdr")?;
1506            let header_path = format!("word/{}", header.filename);
1507            pkg.add_part(
1508                &header_path,
1509                content_type::WORDPROCESSING_HEADER,
1510                &header_xml,
1511            )?;
1512
1513            doc_rels.add(Relationship::new(
1514                &header.rel_id,
1515                rel_type::HEADER,
1516                &header.filename,
1517            ));
1518        }
1519
1520        // Write footers and add relationships
1521        for footer in self.footers.values() {
1522            let footer_xml = serialize_with_namespaces(&footer.body, "w:ftr")?;
1523            let footer_path = format!("word/{}", footer.filename);
1524            pkg.add_part(
1525                &footer_path,
1526                content_type::WORDPROCESSING_FOOTER,
1527                &footer_xml,
1528            )?;
1529
1530            doc_rels.add(Relationship::new(
1531                &footer.rel_id,
1532                rel_type::FOOTER,
1533                &footer.filename,
1534            ));
1535        }
1536
1537        // Write footnotes.xml if we have any footnotes
1538        if !self.footnotes.is_empty() {
1539            let fns = build_footnotes(&self.footnotes);
1540            let footnotes_xml = serialize_with_namespaces(&fns, "w:footnotes")?;
1541            pkg.add_part(
1542                "word/footnotes.xml",
1543                content_type::WORDPROCESSING_FOOTNOTES,
1544                &footnotes_xml,
1545            )?;
1546
1547            let footnotes_rel_id = format!("rId{}", self.next_rel_id);
1548            self.next_rel_id += 1;
1549            doc_rels.add(Relationship::new(
1550                &footnotes_rel_id,
1551                rel_type::FOOTNOTES,
1552                "footnotes.xml",
1553            ));
1554        }
1555
1556        // Write endnotes.xml if we have any endnotes
1557        if !self.endnotes.is_empty() {
1558            let ens = build_endnotes(&self.endnotes);
1559            let endnotes_xml = serialize_with_namespaces(&ens, "w:endnotes")?;
1560            pkg.add_part(
1561                "word/endnotes.xml",
1562                content_type::WORDPROCESSING_ENDNOTES,
1563                &endnotes_xml,
1564            )?;
1565
1566            let endnotes_rel_id = format!("rId{}", self.next_rel_id);
1567            self.next_rel_id += 1;
1568            doc_rels.add(Relationship::new(
1569                &endnotes_rel_id,
1570                rel_type::ENDNOTES,
1571                "endnotes.xml",
1572            ));
1573        }
1574
1575        // Write comments.xml if we have any comments
1576        if !self.comments.is_empty() {
1577            let comments = build_comments(&self.comments);
1578            let comments_xml = serialize_with_namespaces(&comments, "w:comments")?;
1579            pkg.add_part(
1580                "word/comments.xml",
1581                content_type::WORDPROCESSING_COMMENTS,
1582                &comments_xml,
1583            )?;
1584
1585            let comments_rel_id = format!("rId{}", self.next_rel_id);
1586            self.next_rel_id += 1;
1587            doc_rels.add(Relationship::new(
1588                &comments_rel_id,
1589                rel_type::COMMENTS,
1590                "comments.xml",
1591            ));
1592        }
1593
1594        // Write styles.xml if we have style definitions
1595        if let Some(ref styles) = self.styles {
1596            let styles_xml = serialize_with_namespaces(styles, "w:styles")?;
1597            pkg.add_part(
1598                "word/styles.xml",
1599                content_type::WORDPROCESSING_STYLES,
1600                &styles_xml,
1601            )?;
1602
1603            let styles_rel_id = format!("rId{}", self.next_rel_id);
1604            self.next_rel_id += 1;
1605            doc_rels.add(Relationship::new(
1606                &styles_rel_id,
1607                rel_type::STYLES,
1608                "styles.xml",
1609            ));
1610        }
1611
1612        // Write numbering.xml if we have any numbering definitions
1613        if !self.numberings.is_empty() {
1614            let numbering = build_numbering(&self.numberings);
1615            let num_xml = serialize_with_namespaces(&numbering, "w:numbering")?;
1616            pkg.add_part(
1617                "word/numbering.xml",
1618                content_type::WORDPROCESSING_NUMBERING,
1619                &num_xml,
1620            )?;
1621
1622            let num_rel_id = format!("rId{}", self.next_rel_id);
1623            self.next_rel_id += 1;
1624            doc_rels.add(Relationship::new(
1625                &num_rel_id,
1626                rel_type::NUMBERING,
1627                "numbering.xml",
1628            ));
1629        }
1630
1631        // Write settings.xml if settings were configured
1632        #[cfg(feature = "wml-settings")]
1633        if let Some(ref settings_opts) = self.settings {
1634            let settings_xml = build_settings_xml(settings_opts);
1635            pkg.add_part(
1636                "word/settings.xml",
1637                "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml",
1638                settings_xml.as_bytes(),
1639            )?;
1640
1641            let settings_rel_id = format!("rId{}", self.next_rel_id);
1642            self.next_rel_id += 1;
1643            doc_rels.add(Relationship::new(
1644                &settings_rel_id,
1645                rel_type::SETTINGS,
1646                "settings.xml",
1647            ));
1648        }
1649
1650        // Write charts and add relationships
1651        #[cfg(feature = "wml-charts")]
1652        for chart in self.charts.values() {
1653            let chart_path = format!("word/charts/{}", chart.filename);
1654            pkg.add_part(
1655                &chart_path,
1656                "application/vnd.openxmlformats-officedocument.drawingml.chart+xml",
1657                &chart.data,
1658            )?;
1659
1660            doc_rels.add(Relationship::new(
1661                &chart.rel_id,
1662                rel_type::CHART,
1663                format!("charts/{}", chart.filename),
1664            ));
1665        }
1666
1667        pkg.add_part(
1668            "word/_rels/document.xml.rels",
1669            content_type::RELATIONSHIPS,
1670            doc_rels.serialize().as_bytes(),
1671        )?;
1672
1673        pkg.finish()?;
1674        Ok(())
1675    }
1676}
1677
1678// =============================================================================
1679// Serialization helpers
1680// =============================================================================
1681
1682/// Serialize a ToXml value to bytes with XML declaration prepended.
1683fn serialize_to_xml_bytes(value: &impl ToXml, tag: &str) -> Result<Vec<u8>> {
1684    let inner = Vec::new();
1685    let mut writer = quick_xml::Writer::new(inner);
1686    value.write_element(tag, &mut writer)?;
1687    let inner = writer.into_inner();
1688    let mut buf = Vec::with_capacity(
1689        b"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n".len() + inner.len(),
1690    );
1691    buf.extend_from_slice(b"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n");
1692    buf.extend_from_slice(&inner);
1693    Ok(buf)
1694}
1695
1696/// Serialize a ToXml value with namespace declarations injected into the
1697/// root element's start tag. This is needed for types that don't have
1698/// `extra_attrs` (like Footnotes, Endnotes, Comments, Numbering, HeaderFooter).
1699fn serialize_with_namespaces(value: &impl ToXml, tag: &str) -> Result<Vec<u8>> {
1700    use quick_xml::events::{BytesEnd, BytesStart, Event};
1701
1702    let inner = Vec::new();
1703    let mut writer = quick_xml::Writer::new(inner);
1704
1705    // Write start tag with namespace declarations + type's own attrs
1706    let start = BytesStart::new(tag);
1707    let start = value.write_attrs(start);
1708    let mut start = start;
1709    for &(key, val) in NS_DECLS {
1710        start.push_attribute((key, val));
1711    }
1712
1713    if value.is_empty_element() {
1714        writer.write_event(Event::Empty(start))?;
1715    } else {
1716        writer.write_event(Event::Start(start))?;
1717        value.write_children(&mut writer)?;
1718        writer.write_event(Event::End(BytesEnd::new(tag)))?;
1719    }
1720
1721    let inner = writer.into_inner();
1722    let mut buf = Vec::with_capacity(
1723        b"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n".len() + inner.len(),
1724    );
1725    buf.extend_from_slice(b"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n");
1726    buf.extend_from_slice(&inner);
1727    Ok(buf)
1728}
1729
1730// =============================================================================
1731// Build generated types from pending data
1732// =============================================================================
1733
1734/// Build a separator footnote/endnote (required by Word).
1735///
1736/// Creates a FootnoteEndnote with a single paragraph containing a single run
1737/// with a separator or continuation separator element.
1738fn build_separator_ftn_edn(id: i64, ftn_type: types::STFtnEdn) -> types::FootnoteEndnote {
1739    let separator_content = match ftn_type {
1740        types::STFtnEdn::Separator => types::RunContent::Separator(Box::new(types::CTEmpty)),
1741        types::STFtnEdn::ContinuationSeparator => {
1742            types::RunContent::ContinuationSeparator(Box::new(types::CTEmpty))
1743        }
1744        _ => unreachable!("only Separator and ContinuationSeparator expected"),
1745    };
1746
1747    let run = types::Run {
1748        #[cfg(feature = "wml-track-changes")]
1749        rsid_r_pr: None,
1750        #[cfg(feature = "wml-track-changes")]
1751        rsid_del: None,
1752        #[cfg(feature = "wml-track-changes")]
1753        rsid_r: None,
1754        #[cfg(feature = "wml-styling")]
1755        r_pr: None,
1756        run_content: vec![separator_content],
1757        #[cfg(feature = "extra-attrs")]
1758        extra_attrs: std::collections::HashMap::new(),
1759        #[cfg(feature = "extra-children")]
1760        extra_children: Vec::new(),
1761    };
1762
1763    let para = types::Paragraph {
1764        #[cfg(feature = "wml-track-changes")]
1765        rsid_r_pr: None,
1766        #[cfg(feature = "wml-track-changes")]
1767        rsid_r: None,
1768        #[cfg(feature = "wml-track-changes")]
1769        rsid_del: None,
1770        #[cfg(feature = "wml-track-changes")]
1771        rsid_p: None,
1772        #[cfg(feature = "wml-track-changes")]
1773        rsid_r_default: None,
1774        #[cfg(feature = "wml-styling")]
1775        p_pr: None,
1776        paragraph_content: vec![types::ParagraphContent::R(Box::new(run))],
1777        #[cfg(feature = "extra-attrs")]
1778        extra_attrs: std::collections::HashMap::new(),
1779        #[cfg(feature = "extra-children")]
1780        extra_children: Vec::new(),
1781    };
1782
1783    types::FootnoteEndnote {
1784        #[cfg(feature = "wml-comments")]
1785        r#type: Some(ftn_type),
1786        id,
1787        block_content: vec![types::BlockContent::P(Box::new(para))],
1788        #[cfg(feature = "extra-attrs")]
1789        extra_attrs: std::collections::HashMap::new(),
1790        #[cfg(feature = "extra-children")]
1791        extra_children: Vec::new(),
1792    }
1793}
1794
1795/// Build a Footnotes type from pending footnotes.
1796fn build_footnotes(footnotes: &HashMap<i32, PendingFootnote>) -> types::Footnotes {
1797    let mut fns = types::Footnotes {
1798        footnote: Vec::new(),
1799        #[cfg(feature = "extra-children")]
1800        extra_children: Vec::new(),
1801    };
1802
1803    // Add separator footnotes (required by Word)
1804    fns.footnote
1805        .push(build_separator_ftn_edn(-1, types::STFtnEdn::Separator));
1806    fns.footnote.push(build_separator_ftn_edn(
1807        0,
1808        types::STFtnEdn::ContinuationSeparator,
1809    ));
1810
1811    // Add user footnotes sorted by ID
1812    let mut sorted: Vec<_> = footnotes.values().collect();
1813    sorted.sort_by_key(|f| f.id);
1814    for footnote in sorted {
1815        fns.footnote.push(footnote.body.clone());
1816    }
1817
1818    fns
1819}
1820
1821/// Build an Endnotes type from pending endnotes.
1822fn build_endnotes(endnotes: &HashMap<i32, PendingEndnote>) -> types::Endnotes {
1823    let mut ens = types::Endnotes {
1824        endnote: Vec::new(),
1825        #[cfg(feature = "extra-children")]
1826        extra_children: Vec::new(),
1827    };
1828
1829    // Add separator endnotes (required by Word)
1830    ens.endnote
1831        .push(build_separator_ftn_edn(-1, types::STFtnEdn::Separator));
1832    ens.endnote.push(build_separator_ftn_edn(
1833        0,
1834        types::STFtnEdn::ContinuationSeparator,
1835    ));
1836
1837    // Add user endnotes sorted by ID
1838    let mut sorted: Vec<_> = endnotes.values().collect();
1839    sorted.sort_by_key(|e| e.id);
1840    for endnote in sorted {
1841        ens.endnote.push(endnote.body.clone());
1842    }
1843
1844    ens
1845}
1846
1847/// Build a Comments type from pending comments.
1848fn build_comments(comments: &HashMap<i32, PendingComment>) -> types::Comments {
1849    let mut result = types::Comments {
1850        comment: Vec::new(),
1851        #[cfg(feature = "extra-children")]
1852        extra_children: Vec::new(),
1853    };
1854
1855    // Sort comments by ID for deterministic output
1856    let mut sorted: Vec<_> = comments.values().collect();
1857    sorted.sort_by_key(|c| c.id);
1858
1859    for pc in sorted {
1860        let mut comment = pc.body.clone();
1861        comment.id = pc.id as i64;
1862        if let Some(ref author) = pc.author {
1863            comment.author = author.clone();
1864        }
1865        #[cfg(feature = "wml-comments")]
1866        if let Some(ref date) = pc.date {
1867            comment.date = Some(date.clone());
1868        }
1869        #[cfg(feature = "wml-comments")]
1870        if let Some(ref initials) = pc.initials {
1871            comment.initials = Some(initials.clone());
1872        }
1873        result.comment.push(comment);
1874    }
1875
1876    result
1877}
1878
1879/// Map ListType to STNumberFormat and level text.
1880fn list_type_to_num_fmt_and_text(list_type: ListType) -> (types::STNumberFormat, &'static str) {
1881    match list_type {
1882        ListType::Bullet => (types::STNumberFormat::Bullet, "\u{2022}"),
1883        ListType::Decimal => (types::STNumberFormat::Decimal, "%1."),
1884        ListType::LowerLetter => (types::STNumberFormat::LowerLetter, "%1."),
1885        ListType::UpperLetter => (types::STNumberFormat::UpperLetter, "%1."),
1886        ListType::LowerRoman => (types::STNumberFormat::LowerRoman, "%1."),
1887        ListType::UpperRoman => (types::STNumberFormat::UpperRoman, "%1."),
1888    }
1889}
1890
1891/// Map ListType to STNumberFormat.
1892#[cfg(feature = "wml-numbering")]
1893fn list_type_to_num_fmt(list_type: ListType) -> types::STNumberFormat {
1894    match list_type {
1895        ListType::Bullet => types::STNumberFormat::Bullet,
1896        ListType::Decimal => types::STNumberFormat::Decimal,
1897        ListType::LowerLetter => types::STNumberFormat::LowerLetter,
1898        ListType::UpperLetter => types::STNumberFormat::UpperLetter,
1899        ListType::LowerRoman => types::STNumberFormat::LowerRoman,
1900        ListType::UpperRoman => types::STNumberFormat::UpperRoman,
1901    }
1902}
1903
1904/// Build a single `Level` from a `NumberingLevel` spec.
1905fn build_level_from_spec(spec: &NumberingLevel) -> types::Level {
1906    #[cfg(feature = "wml-numbering")]
1907    let is_bullet = spec.format == ListType::Bullet;
1908    #[cfg(feature = "wml-numbering")]
1909    let indent_left = spec.indent_left.unwrap_or(720 * (spec.ilvl + 1));
1910    #[cfg(feature = "wml-numbering")]
1911    let hanging = spec.hanging.unwrap_or(360);
1912
1913    types::Level {
1914        ilvl: spec.ilvl as i64,
1915        #[cfg(feature = "wml-numbering")]
1916        tplc: None,
1917        #[cfg(feature = "wml-numbering")]
1918        tentative: None,
1919        #[cfg(feature = "wml-numbering")]
1920        start: Some(Box::new(types::CTDecimalNumber {
1921            value: spec.start as i64,
1922            #[cfg(feature = "extra-attrs")]
1923            extra_attrs: std::collections::HashMap::new(),
1924        })),
1925        #[cfg(feature = "wml-numbering")]
1926        num_fmt: Some(Box::new(types::CTNumFmt {
1927            value: list_type_to_num_fmt(spec.format),
1928            format: None,
1929            #[cfg(feature = "extra-attrs")]
1930            extra_attrs: std::collections::HashMap::new(),
1931        })),
1932        #[cfg(feature = "wml-numbering")]
1933        lvl_restart: None,
1934        #[cfg(feature = "wml-numbering")]
1935        paragraph_style: None,
1936        #[cfg(feature = "wml-numbering")]
1937        is_lgl: None,
1938        #[cfg(feature = "wml-numbering")]
1939        suff: None,
1940        #[cfg(feature = "wml-numbering")]
1941        lvl_text: Some(Box::new(types::CTLevelText {
1942            value: Some(spec.text.clone()),
1943            null: None,
1944            #[cfg(feature = "extra-attrs")]
1945            extra_attrs: std::collections::HashMap::new(),
1946        })),
1947        #[cfg(feature = "wml-numbering")]
1948        lvl_pic_bullet_id: None,
1949        #[cfg(feature = "wml-numbering")]
1950        legacy: None,
1951        #[cfg(feature = "wml-numbering")]
1952        lvl_jc: Some(Box::new(types::CTJc {
1953            value: types::STJc::Left,
1954            #[cfg(feature = "extra-attrs")]
1955            extra_attrs: std::collections::HashMap::new(),
1956        })),
1957        #[cfg(feature = "wml-numbering")]
1958        p_pr: Some(Box::new(build_level_paragraph_properties(
1959            indent_left,
1960            hanging,
1961        ))),
1962        #[cfg(feature = "wml-numbering")]
1963        r_pr: if is_bullet {
1964            Some(Box::new(build_bullet_run_properties()))
1965        } else {
1966            None
1967        },
1968        #[cfg(feature = "extra-attrs")]
1969        extra_attrs: std::collections::HashMap::new(),
1970        #[cfg(feature = "extra-children")]
1971        extra_children: Vec::new(),
1972    }
1973}
1974
1975/// Build paragraph properties for a numbering level (indentation).
1976#[cfg(feature = "wml-numbering")]
1977fn build_level_paragraph_properties(indent_left: u32, hanging: u32) -> types::CTPPrGeneral {
1978    let ind = types::CTInd {
1979        #[cfg(feature = "wml-styling")]
1980        left: Some(indent_left.to_string()),
1981        #[cfg(feature = "wml-styling")]
1982        hanging: Some(hanging.to_string()),
1983        ..Default::default()
1984    };
1985    // Suppress unused variable warnings when wml-styling is off
1986    let _ = indent_left;
1987    let _ = hanging;
1988    types::CTPPrGeneral {
1989        indentation: Some(Box::new(ind)),
1990        ..Default::default()
1991    }
1992}
1993
1994/// Build a Numbering type from pending numbering definitions.
1995fn build_numbering(numberings: &HashMap<u32, PendingNumbering>) -> types::Numbering {
1996    let mut numbering = types::Numbering {
1997        #[cfg(feature = "wml-numbering")]
1998        num_pic_bullet: Vec::new(),
1999        abstract_num: Vec::new(),
2000        num: Vec::new(),
2001        #[cfg(feature = "wml-numbering")]
2002        num_id_mac_at_cleanup: None,
2003        #[cfg(feature = "extra-children")]
2004        extra_children: Vec::new(),
2005    };
2006
2007    // Sort numberings by num_id for deterministic output
2008    let mut sorted: Vec<_> = numberings.values().collect();
2009    sorted.sort_by_key(|n| n.num_id);
2010
2011    for pn in &sorted {
2012        let levels: Vec<types::Level> = if let Some(ref custom_levels) = pn.custom_levels {
2013            // Custom multi-level list
2014            custom_levels.iter().map(build_level_from_spec).collect()
2015        } else if let Some(list_type) = pn.list_type {
2016            // Simple single-level list (backwards-compatible path)
2017            let (_num_fmt, _lvl_text) = list_type_to_num_fmt_and_text(list_type);
2018            let spec = NumberingLevel {
2019                ilvl: 0,
2020                format: list_type,
2021                start: 1,
2022                text: _lvl_text.to_string(),
2023                indent_left: Some(720),
2024                hanging: Some(360),
2025            };
2026            vec![build_level_from_spec(&spec)]
2027        } else {
2028            Vec::new()
2029        };
2030
2031        let abs = types::AbstractNumbering {
2032            abstract_num_id: pn.abstract_num_id as i64,
2033            #[cfg(feature = "wml-numbering")]
2034            nsid: None,
2035            #[cfg(feature = "wml-numbering")]
2036            multi_level_type: None,
2037            #[cfg(feature = "wml-numbering")]
2038            tmpl: None,
2039            #[cfg(feature = "wml-numbering")]
2040            name: None,
2041            #[cfg(feature = "wml-numbering")]
2042            style_link: None,
2043            #[cfg(feature = "wml-numbering")]
2044            num_style_link: None,
2045            lvl: levels,
2046            #[cfg(feature = "extra-attrs")]
2047            extra_attrs: std::collections::HashMap::new(),
2048            #[cfg(feature = "extra-children")]
2049            extra_children: Vec::new(),
2050        };
2051        numbering.abstract_num.push(abs);
2052
2053        let inst = types::NumberingInstance {
2054            num_id: pn.num_id as i64,
2055            abstract_num_id: Box::new(types::CTDecimalNumber {
2056                value: pn.abstract_num_id as i64,
2057                #[cfg(feature = "extra-attrs")]
2058                extra_attrs: std::collections::HashMap::new(),
2059            }),
2060            #[cfg(feature = "wml-numbering")]
2061            lvl_override: Vec::new(),
2062            #[cfg(feature = "extra-attrs")]
2063            extra_attrs: std::collections::HashMap::new(),
2064            #[cfg(feature = "extra-children")]
2065            extra_children: Vec::new(),
2066        };
2067        numbering.num.push(inst);
2068    }
2069
2070    numbering
2071}
2072
2073/// Build run properties for bullet list levels (Symbol font).
2074#[cfg(feature = "wml-styling")]
2075fn build_bullet_run_properties() -> types::RunProperties {
2076    types::RunProperties {
2077        fonts: Some(Box::new(types::Fonts {
2078            ascii: Some("Symbol".to_string()),
2079            h_ansi: Some("Symbol".to_string()),
2080            hint: Some(types::STHint::Default),
2081            ..Default::default()
2082        })),
2083        ..Default::default()
2084    }
2085}
2086
2087/// Stub for when wml-styling is not enabled.
2088#[cfg(not(feature = "wml-styling"))]
2089#[allow(dead_code)]
2090fn build_bullet_run_properties() -> types::RunProperties {
2091    types::RunProperties::default()
2092}
2093
2094// =============================================================================
2095// Settings XML builder
2096// =============================================================================
2097
2098/// Build the raw XML for `word/settings.xml` from the configured options.
2099///
2100/// ECMA-376 Part 1, Section 17.15.1 (`w:settings`).
2101#[cfg(feature = "wml-settings")]
2102fn build_settings_xml(opts: &DocumentSettingsOptions) -> String {
2103    let mut xml = String::from(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#);
2104    xml.push_str("\r\n");
2105    xml.push_str(
2106        r#"<w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">"#,
2107    );
2108
2109    if let Some(tab_stop) = opts.default_tab_stop {
2110        xml.push_str(&format!(r#"<w:defaultTabStop w:val="{}"/>"#, tab_stop));
2111    }
2112
2113    if opts.even_and_odd_headers {
2114        xml.push_str("<w:evenAndOddHeaders/>");
2115    }
2116
2117    if opts.track_changes {
2118        xml.push_str("<w:trackChanges/>");
2119    }
2120
2121    if let Some(ref rsid) = opts.rsid_root {
2122        xml.push_str(&format!(r#"<w:rsidRoot w:val="{}"/>"#, rsid));
2123    }
2124
2125    if opts.compat_mode {
2126        xml.push_str(concat!(
2127            "<w:compat>",
2128            r#"<w:compatSetting w:name="compatibilityMode" "#,
2129            r#"w:uri="http://schemas.microsoft.com/office/word" "#,
2130            r#"w:val="15"/>"#,
2131            "</w:compat>",
2132        ));
2133    }
2134
2135    xml.push_str("</w:settings>");
2136    xml
2137}
2138
2139// =============================================================================
2140// Drawing XML element builders
2141// =============================================================================
2142
2143/// Build the `a:graphic` element containing a picture reference.
2144fn build_graphic_element(rel_id: &str, width: i64, height: i64, doc_id: usize) -> RawXmlElement {
2145    let blip = RawXmlElement {
2146        name: "a:blip".to_string(),
2147        attributes: vec![("r:embed".to_string(), rel_id.to_string())],
2148        children: vec![],
2149        self_closing: true,
2150    };
2151
2152    let fill_rect = RawXmlElement {
2153        name: "a:fillRect".to_string(),
2154        attributes: vec![],
2155        children: vec![],
2156        self_closing: true,
2157    };
2158
2159    let stretch = RawXmlElement {
2160        name: "a:stretch".to_string(),
2161        attributes: vec![],
2162        children: vec![RawXmlNode::Element(fill_rect)],
2163        self_closing: false,
2164    };
2165
2166    let blip_fill = RawXmlElement {
2167        name: "pic:blipFill".to_string(),
2168        attributes: vec![],
2169        children: vec![RawXmlNode::Element(blip), RawXmlNode::Element(stretch)],
2170        self_closing: false,
2171    };
2172
2173    let cnv_pr = RawXmlElement {
2174        name: "pic:cNvPr".to_string(),
2175        attributes: vec![
2176            ("id".to_string(), doc_id.to_string()),
2177            ("name".to_string(), format!("Picture {}", doc_id)),
2178        ],
2179        children: vec![],
2180        self_closing: true,
2181    };
2182
2183    let cnv_pic_pr = RawXmlElement {
2184        name: "pic:cNvPicPr".to_string(),
2185        attributes: vec![],
2186        children: vec![],
2187        self_closing: true,
2188    };
2189
2190    let nv_pic_pr = RawXmlElement {
2191        name: "pic:nvPicPr".to_string(),
2192        attributes: vec![],
2193        children: vec![RawXmlNode::Element(cnv_pr), RawXmlNode::Element(cnv_pic_pr)],
2194        self_closing: false,
2195    };
2196
2197    let off = RawXmlElement {
2198        name: "a:off".to_string(),
2199        attributes: vec![
2200            ("x".to_string(), "0".to_string()),
2201            ("y".to_string(), "0".to_string()),
2202        ],
2203        children: vec![],
2204        self_closing: true,
2205    };
2206
2207    let ext = RawXmlElement {
2208        name: "a:ext".to_string(),
2209        attributes: vec![
2210            ("cx".to_string(), width.to_string()),
2211            ("cy".to_string(), height.to_string()),
2212        ],
2213        children: vec![],
2214        self_closing: true,
2215    };
2216
2217    let xfrm = RawXmlElement {
2218        name: "a:xfrm".to_string(),
2219        attributes: vec![],
2220        children: vec![RawXmlNode::Element(off), RawXmlNode::Element(ext)],
2221        self_closing: false,
2222    };
2223
2224    let av_lst = RawXmlElement {
2225        name: "a:avLst".to_string(),
2226        attributes: vec![],
2227        children: vec![],
2228        self_closing: true,
2229    };
2230
2231    let prst_geom = RawXmlElement {
2232        name: "a:prstGeom".to_string(),
2233        attributes: vec![("prst".to_string(), "rect".to_string())],
2234        children: vec![RawXmlNode::Element(av_lst)],
2235        self_closing: false,
2236    };
2237
2238    let sp_pr = RawXmlElement {
2239        name: "pic:spPr".to_string(),
2240        attributes: vec![],
2241        children: vec![RawXmlNode::Element(xfrm), RawXmlNode::Element(prst_geom)],
2242        self_closing: false,
2243    };
2244
2245    let pic = RawXmlElement {
2246        name: "pic:pic".to_string(),
2247        attributes: vec![],
2248        children: vec![
2249            RawXmlNode::Element(nv_pic_pr),
2250            RawXmlNode::Element(blip_fill),
2251            RawXmlNode::Element(sp_pr),
2252        ],
2253        self_closing: false,
2254    };
2255
2256    let graphic_data = RawXmlElement {
2257        name: "a:graphicData".to_string(),
2258        attributes: vec![(
2259            "uri".to_string(),
2260            "http://schemas.openxmlformats.org/drawingml/2006/picture".to_string(),
2261        )],
2262        children: vec![RawXmlNode::Element(pic)],
2263        self_closing: false,
2264    };
2265
2266    RawXmlElement {
2267        name: "a:graphic".to_string(),
2268        attributes: vec![],
2269        children: vec![RawXmlNode::Element(graphic_data)],
2270        self_closing: false,
2271    }
2272}
2273
2274/// Build the `wp:inline` element for an inline image.
2275fn build_inline_image_element(image: &InlineImage, doc_id: usize) -> RawXmlElement {
2276    let width_emu = image.width_emu.unwrap_or(914400);
2277    let height_emu = image.height_emu.unwrap_or(914400);
2278    let desc = image.description.as_deref().unwrap_or("Image");
2279
2280    let extent = RawXmlElement {
2281        name: "wp:extent".to_string(),
2282        attributes: vec![
2283            ("cx".to_string(), width_emu.to_string()),
2284            ("cy".to_string(), height_emu.to_string()),
2285        ],
2286        children: vec![],
2287        self_closing: true,
2288    };
2289
2290    let doc_pr = RawXmlElement {
2291        name: "wp:docPr".to_string(),
2292        attributes: vec![
2293            ("id".to_string(), doc_id.to_string()),
2294            ("name".to_string(), format!("Picture {}", doc_id)),
2295            ("descr".to_string(), desc.to_string()),
2296        ],
2297        children: vec![],
2298        self_closing: true,
2299    };
2300
2301    let graphic_frame_locks = RawXmlElement {
2302        name: "a:graphicFrameLocks".to_string(),
2303        attributes: vec![("noChangeAspect".to_string(), "1".to_string())],
2304        children: vec![],
2305        self_closing: true,
2306    };
2307
2308    let cnv_graphic_frame_pr = RawXmlElement {
2309        name: "wp:cNvGraphicFramePr".to_string(),
2310        attributes: vec![],
2311        children: vec![RawXmlNode::Element(graphic_frame_locks)],
2312        self_closing: false,
2313    };
2314
2315    let graphic = build_graphic_element(&image.rel_id, width_emu, height_emu, doc_id);
2316
2317    RawXmlElement {
2318        name: "wp:inline".to_string(),
2319        attributes: vec![
2320            ("distT".to_string(), "0".to_string()),
2321            ("distB".to_string(), "0".to_string()),
2322            ("distL".to_string(), "0".to_string()),
2323            ("distR".to_string(), "0".to_string()),
2324        ],
2325        children: vec![
2326            RawXmlNode::Element(extent),
2327            RawXmlNode::Element(doc_pr),
2328            RawXmlNode::Element(cnv_graphic_frame_pr),
2329            RawXmlNode::Element(graphic),
2330        ],
2331        self_closing: false,
2332    }
2333}
2334
2335/// Build the wrap type element for an anchored image.
2336fn build_wrap_element(wrap_type: WrapType) -> RawXmlElement {
2337    match wrap_type {
2338        WrapType::None => RawXmlElement {
2339            name: "wp:wrapNone".to_string(),
2340            attributes: vec![],
2341            children: vec![],
2342            self_closing: true,
2343        },
2344        WrapType::Square => RawXmlElement {
2345            name: "wp:wrapSquare".to_string(),
2346            attributes: vec![("wrapText".to_string(), "bothSides".to_string())],
2347            children: vec![],
2348            self_closing: true,
2349        },
2350        WrapType::Tight => {
2351            let polygon = build_default_wrap_polygon();
2352            RawXmlElement {
2353                name: "wp:wrapTight".to_string(),
2354                attributes: vec![("wrapText".to_string(), "bothSides".to_string())],
2355                children: vec![RawXmlNode::Element(polygon)],
2356                self_closing: false,
2357            }
2358        }
2359        WrapType::Through => {
2360            let polygon = build_default_wrap_polygon();
2361            RawXmlElement {
2362                name: "wp:wrapThrough".to_string(),
2363                attributes: vec![("wrapText".to_string(), "bothSides".to_string())],
2364                children: vec![RawXmlNode::Element(polygon)],
2365                self_closing: false,
2366            }
2367        }
2368        WrapType::TopAndBottom => RawXmlElement {
2369            name: "wp:wrapTopAndBottom".to_string(),
2370            attributes: vec![],
2371            children: vec![],
2372            self_closing: true,
2373        },
2374    }
2375}
2376
2377/// Build a default rectangular wrap polygon.
2378fn build_default_wrap_polygon() -> RawXmlElement {
2379    let start = RawXmlElement {
2380        name: "wp:start".to_string(),
2381        attributes: vec![
2382            ("x".to_string(), "0".to_string()),
2383            ("y".to_string(), "0".to_string()),
2384        ],
2385        children: vec![],
2386        self_closing: true,
2387    };
2388
2389    let line_to_1 = RawXmlElement {
2390        name: "wp:lineTo".to_string(),
2391        attributes: vec![
2392            ("x".to_string(), "0".to_string()),
2393            ("y".to_string(), "21600".to_string()),
2394        ],
2395        children: vec![],
2396        self_closing: true,
2397    };
2398
2399    let line_to_2 = RawXmlElement {
2400        name: "wp:lineTo".to_string(),
2401        attributes: vec![
2402            ("x".to_string(), "21600".to_string()),
2403            ("y".to_string(), "21600".to_string()),
2404        ],
2405        children: vec![],
2406        self_closing: true,
2407    };
2408
2409    let line_to_3 = RawXmlElement {
2410        name: "wp:lineTo".to_string(),
2411        attributes: vec![
2412            ("x".to_string(), "21600".to_string()),
2413            ("y".to_string(), "0".to_string()),
2414        ],
2415        children: vec![],
2416        self_closing: true,
2417    };
2418
2419    let line_to_4 = RawXmlElement {
2420        name: "wp:lineTo".to_string(),
2421        attributes: vec![
2422            ("x".to_string(), "0".to_string()),
2423            ("y".to_string(), "0".to_string()),
2424        ],
2425        children: vec![],
2426        self_closing: true,
2427    };
2428
2429    RawXmlElement {
2430        name: "wp:wrapPolygon".to_string(),
2431        attributes: vec![("edited".to_string(), "0".to_string())],
2432        children: vec![
2433            RawXmlNode::Element(start),
2434            RawXmlNode::Element(line_to_1),
2435            RawXmlNode::Element(line_to_2),
2436            RawXmlNode::Element(line_to_3),
2437            RawXmlNode::Element(line_to_4),
2438        ],
2439        self_closing: false,
2440    }
2441}
2442
2443/// Build the `wp:anchor` element for an anchored (floating) image.
2444fn build_anchored_image_element(image: &AnchoredImage, doc_id: usize) -> RawXmlElement {
2445    let width_emu = image.width_emu.unwrap_or(914400);
2446    let height_emu = image.height_emu.unwrap_or(914400);
2447    let desc = image.description.as_deref().unwrap_or("Image");
2448    let behind_doc = if image.behind_doc { "1" } else { "0" };
2449
2450    let simple_pos = RawXmlElement {
2451        name: "wp:simplePos".to_string(),
2452        attributes: vec![
2453            ("x".to_string(), "0".to_string()),
2454            ("y".to_string(), "0".to_string()),
2455        ],
2456        children: vec![],
2457        self_closing: true,
2458    };
2459
2460    let pos_offset_h = RawXmlElement {
2461        name: "wp:posOffset".to_string(),
2462        attributes: vec![],
2463        children: vec![RawXmlNode::Text(image.pos_x.to_string())],
2464        self_closing: false,
2465    };
2466
2467    let position_h = RawXmlElement {
2468        name: "wp:positionH".to_string(),
2469        attributes: vec![("relativeFrom".to_string(), "column".to_string())],
2470        children: vec![RawXmlNode::Element(pos_offset_h)],
2471        self_closing: false,
2472    };
2473
2474    let pos_offset_v = RawXmlElement {
2475        name: "wp:posOffset".to_string(),
2476        attributes: vec![],
2477        children: vec![RawXmlNode::Text(image.pos_y.to_string())],
2478        self_closing: false,
2479    };
2480
2481    let position_v = RawXmlElement {
2482        name: "wp:positionV".to_string(),
2483        attributes: vec![("relativeFrom".to_string(), "paragraph".to_string())],
2484        children: vec![RawXmlNode::Element(pos_offset_v)],
2485        self_closing: false,
2486    };
2487
2488    let extent = RawXmlElement {
2489        name: "wp:extent".to_string(),
2490        attributes: vec![
2491            ("cx".to_string(), width_emu.to_string()),
2492            ("cy".to_string(), height_emu.to_string()),
2493        ],
2494        children: vec![],
2495        self_closing: true,
2496    };
2497
2498    let effect_extent = RawXmlElement {
2499        name: "wp:effectExtent".to_string(),
2500        attributes: vec![
2501            ("l".to_string(), "0".to_string()),
2502            ("t".to_string(), "0".to_string()),
2503            ("r".to_string(), "0".to_string()),
2504            ("b".to_string(), "0".to_string()),
2505        ],
2506        children: vec![],
2507        self_closing: true,
2508    };
2509
2510    let wrap = build_wrap_element(image.wrap_type);
2511
2512    let doc_pr = RawXmlElement {
2513        name: "wp:docPr".to_string(),
2514        attributes: vec![
2515            ("id".to_string(), doc_id.to_string()),
2516            ("name".to_string(), format!("Picture {}", doc_id)),
2517            ("descr".to_string(), desc.to_string()),
2518        ],
2519        children: vec![],
2520        self_closing: true,
2521    };
2522
2523    let graphic_frame_locks = RawXmlElement {
2524        name: "a:graphicFrameLocks".to_string(),
2525        attributes: vec![("noChangeAspect".to_string(), "1".to_string())],
2526        children: vec![],
2527        self_closing: true,
2528    };
2529
2530    let cnv_graphic_frame_pr = RawXmlElement {
2531        name: "wp:cNvGraphicFramePr".to_string(),
2532        attributes: vec![],
2533        children: vec![RawXmlNode::Element(graphic_frame_locks)],
2534        self_closing: false,
2535    };
2536
2537    let graphic = build_graphic_element(&image.rel_id, width_emu, height_emu, doc_id);
2538
2539    RawXmlElement {
2540        name: "wp:anchor".to_string(),
2541        attributes: vec![
2542            ("distT".to_string(), "0".to_string()),
2543            ("distB".to_string(), "0".to_string()),
2544            ("distL".to_string(), "114300".to_string()),
2545            ("distR".to_string(), "114300".to_string()),
2546            ("simplePos".to_string(), "0".to_string()),
2547            ("relativeHeight".to_string(), "251658240".to_string()),
2548            ("behindDoc".to_string(), behind_doc.to_string()),
2549            ("locked".to_string(), "0".to_string()),
2550            ("layoutInCell".to_string(), "1".to_string()),
2551            ("allowOverlap".to_string(), "1".to_string()),
2552        ],
2553        children: vec![
2554            RawXmlNode::Element(simple_pos),
2555            RawXmlNode::Element(position_h),
2556            RawXmlNode::Element(position_v),
2557            RawXmlNode::Element(extent),
2558            RawXmlNode::Element(effect_extent),
2559            RawXmlNode::Element(wrap),
2560            RawXmlNode::Element(doc_pr),
2561            RawXmlNode::Element(cnv_graphic_frame_pr),
2562            RawXmlNode::Element(graphic),
2563        ],
2564        self_closing: false,
2565    }
2566}
2567
2568// =============================================================================
2569// Text box element builder
2570// =============================================================================
2571
2572/// Build a `wp:inline` element containing a `wps:wsp` text box.
2573///
2574/// Produces the minimal structure required by Word:
2575/// ```xml
2576/// <wp:inline>
2577///   <wp:extent cx="..." cy="..."/>
2578///   <wp:docPr id="..." name="Text Box ..."/>
2579///   <a:graphic>
2580///     <a:graphicData uri="http://schemas.microsoft.com/office/word/2010/wordprocessingShape">
2581///       <wps:wsp>
2582///         <wps:spPr>
2583///           <a:xfrm><a:off x="0" y="0"/><a:ext cx="..." cy="..."/></a:xfrm>
2584///           <a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
2585///         </wps:spPr>
2586///         <wps:txbx>
2587///           <w:txbxContent>
2588///             <w:p><w:r><w:t>...</w:t></w:r></w:p>
2589///           </w:txbxContent>
2590///         </wps:txbx>
2591///       </wps:wsp>
2592///     </a:graphicData>
2593///   </a:graphic>
2594/// </wp:inline>
2595/// ```
2596///
2597/// ECMA-376 Part 1 §20.4; Word Processing Shapes spec.
2598fn build_text_box_element(text_box: &TextBox, doc_id: usize) -> RawXmlElement {
2599    let w = text_box.width_emu;
2600    let h = text_box.height_emu;
2601
2602    // wps:spPr children
2603    let off = RawXmlElement {
2604        name: "a:off".to_string(),
2605        attributes: vec![
2606            ("x".to_string(), "0".to_string()),
2607            ("y".to_string(), "0".to_string()),
2608        ],
2609        children: vec![],
2610        self_closing: true,
2611    };
2612    let ext = RawXmlElement {
2613        name: "a:ext".to_string(),
2614        attributes: vec![
2615            ("cx".to_string(), w.to_string()),
2616            ("cy".to_string(), h.to_string()),
2617        ],
2618        children: vec![],
2619        self_closing: true,
2620    };
2621    let xfrm = RawXmlElement {
2622        name: "a:xfrm".to_string(),
2623        attributes: vec![],
2624        children: vec![RawXmlNode::Element(off), RawXmlNode::Element(ext)],
2625        self_closing: false,
2626    };
2627    let av_lst = RawXmlElement {
2628        name: "a:avLst".to_string(),
2629        attributes: vec![],
2630        children: vec![],
2631        self_closing: true,
2632    };
2633    let prst_geom = RawXmlElement {
2634        name: "a:prstGeom".to_string(),
2635        attributes: vec![("prst".to_string(), "rect".to_string())],
2636        children: vec![RawXmlNode::Element(av_lst)],
2637        self_closing: false,
2638    };
2639    let sp_pr = RawXmlElement {
2640        name: "wps:spPr".to_string(),
2641        attributes: vec![],
2642        children: vec![RawXmlNode::Element(xfrm), RawXmlNode::Element(prst_geom)],
2643        self_closing: false,
2644    };
2645
2646    // wps:txbx > w:txbxContent > w:p > w:r > w:t
2647    let t_node = RawXmlElement {
2648        name: "w:t".to_string(),
2649        attributes: vec![("xml:space".to_string(), "preserve".to_string())],
2650        children: vec![RawXmlNode::Text(text_box.text.clone())],
2651        self_closing: false,
2652    };
2653    let r_node = RawXmlElement {
2654        name: "w:r".to_string(),
2655        attributes: vec![],
2656        children: vec![RawXmlNode::Element(t_node)],
2657        self_closing: false,
2658    };
2659    let p_node = RawXmlElement {
2660        name: "w:p".to_string(),
2661        attributes: vec![],
2662        children: vec![RawXmlNode::Element(r_node)],
2663        self_closing: false,
2664    };
2665    let txbx_content = RawXmlElement {
2666        name: "w:txbxContent".to_string(),
2667        attributes: vec![],
2668        children: vec![RawXmlNode::Element(p_node)],
2669        self_closing: false,
2670    };
2671    let txbx = RawXmlElement {
2672        name: "wps:txbx".to_string(),
2673        attributes: vec![],
2674        children: vec![RawXmlNode::Element(txbx_content)],
2675        self_closing: false,
2676    };
2677
2678    // wps:wsp
2679    let wsp = RawXmlElement {
2680        name: "wps:wsp".to_string(),
2681        attributes: vec![],
2682        children: vec![RawXmlNode::Element(sp_pr), RawXmlNode::Element(txbx)],
2683        self_closing: false,
2684    };
2685
2686    // a:graphicData
2687    let graphic_data = RawXmlElement {
2688        name: "a:graphicData".to_string(),
2689        attributes: vec![("uri".to_string(), NS_WPS.to_string())],
2690        children: vec![RawXmlNode::Element(wsp)],
2691        self_closing: false,
2692    };
2693
2694    // a:graphic
2695    let graphic = RawXmlElement {
2696        name: "a:graphic".to_string(),
2697        attributes: vec![],
2698        children: vec![RawXmlNode::Element(graphic_data)],
2699        self_closing: false,
2700    };
2701
2702    // wp:docPr
2703    let doc_pr = RawXmlElement {
2704        name: "wp:docPr".to_string(),
2705        attributes: vec![
2706            ("id".to_string(), doc_id.to_string()),
2707            ("name".to_string(), format!("Text Box {}", doc_id)),
2708        ],
2709        children: vec![],
2710        self_closing: true,
2711    };
2712
2713    // wp:extent
2714    let extent = RawXmlElement {
2715        name: "wp:extent".to_string(),
2716        attributes: vec![
2717            ("cx".to_string(), w.to_string()),
2718            ("cy".to_string(), h.to_string()),
2719        ],
2720        children: vec![],
2721        self_closing: true,
2722    };
2723
2724    // wp:inline
2725    RawXmlElement {
2726        name: "wp:inline".to_string(),
2727        attributes: vec![
2728            ("distT".to_string(), "0".to_string()),
2729            ("distB".to_string(), "0".to_string()),
2730            ("distL".to_string(), "0".to_string()),
2731            ("distR".to_string(), "0".to_string()),
2732        ],
2733        children: vec![
2734            RawXmlNode::Element(extent),
2735            RawXmlNode::Element(doc_pr),
2736            RawXmlNode::Element(graphic),
2737        ],
2738        self_closing: false,
2739    }
2740}
2741
2742// =============================================================================
2743// Utility functions
2744// =============================================================================
2745
2746/// Get file extension from MIME content type.
2747fn extension_from_content_type(content_type: &str) -> &'static str {
2748    match content_type {
2749        "image/png" => "png",
2750        "image/jpeg" => "jpg",
2751        "image/gif" => "gif",
2752        "image/bmp" => "bmp",
2753        "image/tiff" => "tiff",
2754        "image/webp" => "webp",
2755        "image/svg+xml" => "svg",
2756        "image/x-emf" | "image/emf" => "emf",
2757        "image/x-wmf" | "image/wmf" => "wmf",
2758        _ => "bin",
2759    }
2760}
2761
2762#[cfg(test)]
2763mod tests {
2764    use super::*;
2765
2766    #[test]
2767    fn test_document_builder_simple() {
2768        let mut builder = DocumentBuilder::new();
2769        builder.add_paragraph("Hello, World!");
2770        builder.add_paragraph("Second paragraph");
2771
2772        let body = builder.document.body.as_ref().unwrap();
2773        assert_eq!(body.block_content.len(), 2);
2774    }
2775
2776    #[test]
2777    fn test_serialize_to_xml_bytes() {
2778        let doc = types::Document {
2779            background: None,
2780            body: Some(Box::new(types::Body::default())),
2781            conformance: None,
2782            #[cfg(feature = "extra-attrs")]
2783            extra_attrs: std::collections::HashMap::new(),
2784            #[cfg(feature = "extra-children")]
2785            extra_children: Vec::new(),
2786        };
2787
2788        let bytes = serialize_to_xml_bytes(&doc, "w:document").unwrap();
2789        let xml = String::from_utf8(bytes).unwrap();
2790        assert!(xml.starts_with("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"));
2791        assert!(xml.contains("w:document"));
2792    }
2793
2794    #[test]
2795    fn test_list_type_mapping() {
2796        let (fmt, text) = list_type_to_num_fmt_and_text(ListType::Bullet);
2797        assert!(matches!(fmt, types::STNumberFormat::Bullet));
2798        assert_eq!(text, "\u{2022}");
2799
2800        let (fmt, text) = list_type_to_num_fmt_and_text(ListType::Decimal);
2801        assert!(matches!(fmt, types::STNumberFormat::Decimal));
2802        assert_eq!(text, "%1.");
2803    }
2804
2805    #[test]
2806    fn test_extension_from_content_type() {
2807        assert_eq!(extension_from_content_type("image/png"), "png");
2808        assert_eq!(extension_from_content_type("image/jpeg"), "jpg");
2809        assert_eq!(extension_from_content_type("image/gif"), "gif");
2810        assert_eq!(extension_from_content_type("unknown/type"), "bin");
2811    }
2812
2813    #[test]
2814    fn test_drawing_build() {
2815        let mut drawing = Drawing::new();
2816        drawing
2817            .add_image("rId1")
2818            .set_width_inches(1.0)
2819            .set_height_inches(1.0);
2820
2821        let mut doc_id = 1;
2822        let ct_drawing = drawing.build(&mut doc_id);
2823        assert_eq!(doc_id, 2);
2824
2825        #[cfg(feature = "extra-children")]
2826        assert_eq!(ct_drawing.extra_children.len(), 1);
2827        let _ = ct_drawing;
2828    }
2829
2830    #[test]
2831    fn test_text_box_build() {
2832        let mut drawing = Drawing::new();
2833        drawing
2834            .add_text_box("Hello, text box!")
2835            .set_width_inches(2.0)
2836            .set_height_inches(1.0);
2837
2838        assert_eq!(drawing.text_boxes().len(), 1);
2839        assert_eq!(drawing.text_boxes()[0].text, "Hello, text box!");
2840        assert_eq!(drawing.text_boxes()[0].width_emu, (2.0 * 914400.0) as i64);
2841
2842        let mut doc_id = 1;
2843        let ct_drawing = drawing.build(&mut doc_id);
2844        assert_eq!(doc_id, 2);
2845        #[cfg(feature = "extra-children")]
2846        assert_eq!(ct_drawing.extra_children.len(), 1);
2847        let _ = ct_drawing;
2848    }
2849
2850    #[test]
2851    #[cfg(all(feature = "extra-attrs", feature = "extra-children"))]
2852    fn test_roundtrip_with_text_box() {
2853        use crate::Document;
2854        use std::io::Cursor;
2855
2856        let mut builder = DocumentBuilder::new();
2857        {
2858            let body = builder.body_mut();
2859            let para = body.add_paragraph();
2860            let run = para.add_run();
2861            // Build drawing inline
2862            let mut drawing = Drawing::new();
2863            drawing.add_text_box("My text box content");
2864            let ct = drawing.build(&mut 1usize.clone());
2865            run.add_drawing(ct);
2866        }
2867
2868        let mut buf = Cursor::new(Vec::new());
2869        builder.write(&mut buf).unwrap();
2870
2871        // Verify the document can be read back
2872        buf.set_position(0);
2873        let doc = Document::from_reader(buf).unwrap();
2874        // Document should have 1 paragraph
2875        let body = doc.body();
2876        assert_eq!(body.block_content.len(), 1);
2877    }
2878
2879    #[test]
2880    fn test_add_custom_list() {
2881        let mut builder = DocumentBuilder::new();
2882        let num_id = builder.add_custom_list(vec![
2883            NumberingLevel {
2884                ilvl: 0,
2885                format: ListType::Decimal,
2886                start: 1,
2887                text: "%1.".to_string(),
2888                indent_left: Some(720),
2889                hanging: Some(360),
2890            },
2891            NumberingLevel {
2892                ilvl: 1,
2893                format: ListType::LowerLetter,
2894                start: 1,
2895                text: "%2.".to_string(),
2896                indent_left: Some(1440),
2897                hanging: Some(360),
2898            },
2899        ]);
2900        assert_eq!(num_id, 1);
2901        assert!(builder.numberings.contains_key(&1));
2902        let pn = &builder.numberings[&1];
2903        assert!(pn.custom_levels.is_some());
2904        assert_eq!(pn.custom_levels.as_ref().unwrap().len(), 2);
2905    }
2906
2907    #[test]
2908    fn test_numbering_level_helpers() {
2909        let bullet = NumberingLevel::bullet(0);
2910        assert_eq!(bullet.ilvl, 0);
2911        assert_eq!(bullet.format, ListType::Bullet);
2912        assert_eq!(bullet.start, 1);
2913        assert_eq!(bullet.indent_left, Some(720));
2914
2915        let decimal = NumberingLevel::decimal(2);
2916        assert_eq!(decimal.ilvl, 2);
2917        assert_eq!(decimal.format, ListType::Decimal);
2918        assert_eq!(decimal.indent_left, Some(2160));
2919    }
2920
2921    #[test]
2922    #[cfg(all(
2923        feature = "wml-numbering",
2924        feature = "extra-attrs",
2925        feature = "extra-children"
2926    ))]
2927    fn test_roundtrip_custom_list() {
2928        use crate::Document;
2929        use crate::ext::BodyExt;
2930        use std::io::Cursor;
2931
2932        let mut builder = DocumentBuilder::new();
2933        let num_id = builder.add_custom_list(vec![NumberingLevel {
2934            ilvl: 0,
2935            format: ListType::Decimal,
2936            start: 1,
2937            text: "%1.".to_string(),
2938            indent_left: Some(720),
2939            hanging: Some(360),
2940        }]);
2941
2942        {
2943            let body = builder.body_mut();
2944            let para = body.add_paragraph();
2945            #[cfg(feature = "wml-styling")]
2946            para.set_numbering(num_id, 0);
2947            para.add_run().set_text("Item one");
2948        }
2949
2950        let mut buf = Cursor::new(Vec::new());
2951        builder.write(&mut buf).unwrap();
2952
2953        buf.set_position(0);
2954        let doc = Document::from_reader(buf).unwrap();
2955        assert_eq!(doc.body().paragraphs().len(), 1);
2956        // Ensure the document serialized OK (numbering.xml was written)
2957        let _ = num_id;
2958    }
2959
2960    #[test]
2961    fn test_core_and_app_properties_roundtrip() {
2962        use crate::Document;
2963        use crate::document::{AppProperties, CoreProperties};
2964        use std::io::Cursor;
2965
2966        let mut builder = DocumentBuilder::new();
2967        builder.add_paragraph("Hello");
2968        builder.set_core_properties(CoreProperties {
2969            title: Some("Test Doc".to_string()),
2970            creator: Some("Test Author".to_string()),
2971            created: Some("2024-01-01T00:00:00Z".to_string()),
2972            ..Default::default()
2973        });
2974        builder.set_app_properties(AppProperties {
2975            application: Some("ooxml-wml".to_string()),
2976            pages: Some(1),
2977            ..Default::default()
2978        });
2979
2980        let mut buffer = Cursor::new(Vec::new());
2981        builder.write(&mut buffer).unwrap();
2982
2983        buffer.set_position(0);
2984        let doc = Document::from_reader(buffer).unwrap();
2985
2986        let core = doc
2987            .core_properties()
2988            .expect("core properties should be present");
2989        assert_eq!(core.title, Some("Test Doc".to_string()));
2990        assert_eq!(core.creator, Some("Test Author".to_string()));
2991        assert_eq!(core.created, Some("2024-01-01T00:00:00Z".to_string()));
2992
2993        let app = doc
2994            .app_properties()
2995            .expect("app properties should be present");
2996        assert_eq!(app.application, Some("ooxml-wml".to_string()));
2997        assert_eq!(app.pages, Some(1));
2998    }
2999
3000    #[test]
3001    fn test_roundtrip_create_and_read() {
3002        use crate::Document;
3003        use crate::ext::BodyExt;
3004        use std::io::Cursor;
3005
3006        // Create a document
3007        let mut builder = DocumentBuilder::new();
3008        builder.add_paragraph("Test content");
3009
3010        // Write to memory
3011        let mut buffer = Cursor::new(Vec::new());
3012        builder.write(&mut buffer).unwrap();
3013
3014        // Read it back
3015        buffer.set_position(0);
3016        let doc = Document::from_reader(buffer).unwrap();
3017
3018        assert_eq!(doc.body().paragraphs().len(), 1);
3019        assert_eq!(doc.text(), "Test content");
3020    }
3021
3022    #[test]
3023    fn test_styles_written_and_readable() {
3024        use crate::Document;
3025        use std::io::Cursor;
3026
3027        // Build a document with a custom paragraph style.
3028        let mut builder = DocumentBuilder::new();
3029        builder.add_paragraph("Styled content");
3030
3031        // Define a simple paragraph style.
3032        let style = types::Style {
3033            r#type: Some(types::STStyleType::Paragraph),
3034            style_id: Some("MyHeading".to_string()),
3035            name: Some(Box::new(types::CTString {
3036                value: "My Heading".to_string(),
3037                #[cfg(feature = "extra-attrs")]
3038                extra_attrs: std::collections::HashMap::new(),
3039            })),
3040            ..Default::default()
3041        };
3042        builder.add_style(style);
3043
3044        // Write to memory
3045        let mut buffer = Cursor::new(Vec::new());
3046        builder.write(&mut buffer).unwrap();
3047
3048        // Read it back and check that styles were preserved
3049        buffer.set_position(0);
3050        let doc = Document::from_reader(buffer).unwrap();
3051
3052        let styles = doc.styles();
3053        assert_eq!(styles.style.len(), 1);
3054        assert_eq!(styles.style[0].style_id.as_deref(), Some("MyHeading"));
3055    }
3056}