Skip to main content

rdocx_oxml/
document.rs

1//! Document-level elements: `CT_Document` and `CT_Body`.
2
3use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, Event};
4use quick_xml::{Reader, Writer};
5
6use crate::error::Result;
7use crate::header_footer::{HdrFtrRef, HdrFtrType};
8use crate::namespace::{W_NS, matches_local_name};
9use crate::numbering::word_prefixes_at;
10use crate::properties::{get_val_attr, is_word_element};
11use crate::raw_xml::{capture_element, capture_empty_element};
12use crate::shared::{ST_PageOrientation, ST_SectionType};
13use crate::table::CT_Tbl;
14use crate::text::CT_P;
15use crate::units::Twips;
16
17/// Content that can appear in a document body (paragraphs and tables).
18#[derive(Debug, Clone, PartialEq)]
19pub enum BodyContent {
20    Paragraph(CT_P),
21    Table(CT_Tbl),
22    /// Raw XML for unknown elements (bookmarks, SDTs, mc:AlternateContent, etc.)
23    RawXml(Vec<u8>),
24}
25
26/// Column definition for multi-column layouts.
27#[derive(Debug, Clone, PartialEq)]
28pub struct CT_Column {
29    /// Column width in twips
30    pub width: Option<Twips>,
31    /// Space after this column in twips
32    pub space: Option<Twips>,
33}
34
35/// `CT_Columns` — Column layout configuration.
36#[derive(Debug, Clone, PartialEq)]
37pub struct CT_Columns {
38    /// Number of columns (if equal width)
39    pub num: Option<u32>,
40    /// Space between columns in twips (when equal width)
41    pub space: Option<Twips>,
42    /// Whether columns are equal width
43    pub equal_width: Option<bool>,
44    /// Separator line between columns
45    pub sep: Option<bool>,
46    /// Individual column definitions (for unequal widths)
47    pub columns: Vec<CT_Column>,
48}
49
50impl Default for CT_Columns {
51    fn default() -> Self {
52        CT_Columns {
53            num: Some(1),
54            space: Some(Twips(720)),
55            equal_width: Some(true),
56            sep: None,
57            columns: Vec::new(),
58        }
59    }
60}
61
62/// `CT_SectPr` — Section properties (page size, margins, columns, orientation).
63#[derive(Debug, Clone, PartialEq)]
64#[allow(non_snake_case)]
65pub struct CT_SectPr {
66    /// Page width in twips
67    pub page_width: Option<Twips>,
68    /// Page height in twips
69    pub page_height: Option<Twips>,
70    /// Page orientation
71    pub orientation: Option<ST_PageOrientation>,
72    /// Top margin in twips
73    pub margin_top: Option<Twips>,
74    /// Right margin in twips
75    pub margin_right: Option<Twips>,
76    /// Bottom margin in twips
77    pub margin_bottom: Option<Twips>,
78    /// Left margin in twips
79    pub margin_left: Option<Twips>,
80    /// Gutter margin in twips
81    pub gutter: Option<Twips>,
82    /// Header distance from top edge in twips
83    pub header_distance: Option<Twips>,
84    /// Footer distance from bottom edge in twips
85    pub footer_distance: Option<Twips>,
86    /// Section break type
87    pub section_type: Option<ST_SectionType>,
88    /// Column layout
89    pub columns: Option<CT_Columns>,
90    /// Title page (different first page header/footer)
91    pub title_pg: Option<bool>,
92    /// Header references
93    pub header_refs: Vec<HdrFtrRef>,
94    /// Footer references
95    pub footer_refs: Vec<HdrFtrRef>,
96    /// Unknown child elements captured as raw XML.
97    pub extra_xml: Vec<Vec<u8>>,
98}
99
100#[allow(non_snake_case)]
101impl CT_SectPr {
102    /// Default US Letter page with 1-inch margins.
103    pub fn default_letter() -> Self {
104        CT_SectPr {
105            page_width: Some(Twips(12240)),  // 8.5"
106            page_height: Some(Twips(15840)), // 11"
107            orientation: Some(ST_PageOrientation::Portrait),
108            margin_top: Some(Twips(1440)),    // 1"
109            margin_right: Some(Twips(1440)),  // 1"
110            margin_bottom: Some(Twips(1440)), // 1"
111            margin_left: Some(Twips(1440)),   // 1"
112            gutter: Some(Twips(0)),
113            header_distance: Some(Twips(720)),
114            footer_distance: Some(Twips(720)),
115            section_type: None,
116            columns: None,
117            title_pg: None,
118            header_refs: Vec::new(),
119            footer_refs: Vec::new(),
120            extra_xml: Vec::new(),
121        }
122    }
123
124    /// Default A4 page with 1-inch margins.
125    pub fn default_a4() -> Self {
126        CT_SectPr {
127            page_width: Some(Twips(11906)),  // 210mm
128            page_height: Some(Twips(16838)), // 297mm
129            orientation: Some(ST_PageOrientation::Portrait),
130            margin_top: Some(Twips(1440)),
131            margin_right: Some(Twips(1440)),
132            margin_bottom: Some(Twips(1440)),
133            margin_left: Some(Twips(1440)),
134            gutter: Some(Twips(0)),
135            header_distance: Some(Twips(720)),
136            footer_distance: Some(Twips(720)),
137            section_type: None,
138            columns: None,
139            title_pg: None,
140            header_refs: Vec::new(),
141            footer_refs: Vec::new(),
142            extra_xml: Vec::new(),
143        }
144    }
145
146    pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
147        let mut sect = CT_SectPr {
148            page_width: None,
149            page_height: None,
150            orientation: None,
151            margin_top: None,
152            margin_right: None,
153            margin_bottom: None,
154            margin_left: None,
155            gutter: None,
156            header_distance: None,
157            footer_distance: None,
158            section_type: None,
159            columns: None,
160            title_pg: None,
161            header_refs: Vec::new(),
162            footer_refs: Vec::new(),
163            extra_xml: Vec::new(),
164        };
165        let mut buf = Vec::new();
166
167        loop {
168            match reader.read_event_into(&mut buf) {
169                Ok(Event::Empty(ref e)) => {
170                    let name = e.name();
171                    if matches_local_name(name.as_ref(), b"pgSz") {
172                        for attr in e.attributes() {
173                            let attr = attr?;
174                            let key = attr.key.as_ref();
175                            let val_str = std::str::from_utf8(&attr.value)?;
176                            if matches_local_name(key, b"w") {
177                                sect.page_width = Some(Twips(val_str.parse()?));
178                            } else if matches_local_name(key, b"h") {
179                                sect.page_height = Some(Twips(val_str.parse()?));
180                            } else if matches_local_name(key, b"orient") {
181                                sect.orientation = Some(ST_PageOrientation::from_str(val_str)?);
182                            }
183                        }
184                    } else if matches_local_name(name.as_ref(), b"pgMar") {
185                        for attr in e.attributes() {
186                            let attr = attr?;
187                            let key = attr.key.as_ref();
188                            let val: i32 = std::str::from_utf8(&attr.value)?.parse()?;
189                            if matches_local_name(key, b"top") {
190                                sect.margin_top = Some(Twips(val));
191                            } else if matches_local_name(key, b"right")
192                                || matches_local_name(key, b"end")
193                            {
194                                sect.margin_right = Some(Twips(val));
195                            } else if matches_local_name(key, b"bottom") {
196                                sect.margin_bottom = Some(Twips(val));
197                            } else if matches_local_name(key, b"left")
198                                || matches_local_name(key, b"start")
199                            {
200                                sect.margin_left = Some(Twips(val));
201                            } else if matches_local_name(key, b"gutter") {
202                                sect.gutter = Some(Twips(val));
203                            } else if matches_local_name(key, b"header") {
204                                sect.header_distance = Some(Twips(val));
205                            } else if matches_local_name(key, b"footer") {
206                                sect.footer_distance = Some(Twips(val));
207                            }
208                        }
209                    } else if matches_local_name(name.as_ref(), b"type") {
210                        if let Some(val) = get_val_attr(e)? {
211                            sect.section_type = Some(ST_SectionType::from_str(&val)?);
212                        }
213                    } else if matches_local_name(name.as_ref(), b"cols") {
214                        sect.columns = Some(Self::parse_cols_empty(e)?);
215                    } else if matches_local_name(name.as_ref(), b"headerReference") {
216                        let mut hdr_type = HdrFtrType::Default;
217                        let mut rel_id = String::new();
218                        for attr in e.attributes() {
219                            let attr = attr?;
220                            let key = attr.key.as_ref();
221                            let val = std::str::from_utf8(&attr.value)?;
222                            if matches_local_name(key, b"type") {
223                                hdr_type = HdrFtrType::from_str(val);
224                            } else if matches_local_name(key, b"id") {
225                                rel_id = val.to_string();
226                            }
227                        }
228                        if !rel_id.is_empty() {
229                            sect.header_refs.push(HdrFtrRef {
230                                hdr_ftr_type: hdr_type,
231                                rel_id,
232                            });
233                        }
234                    } else if matches_local_name(name.as_ref(), b"footerReference") {
235                        let mut ftr_type = HdrFtrType::Default;
236                        let mut rel_id = String::new();
237                        for attr in e.attributes() {
238                            let attr = attr?;
239                            let key = attr.key.as_ref();
240                            let val = std::str::from_utf8(&attr.value)?;
241                            if matches_local_name(key, b"type") {
242                                ftr_type = HdrFtrType::from_str(val);
243                            } else if matches_local_name(key, b"id") {
244                                rel_id = val.to_string();
245                            }
246                        }
247                        if !rel_id.is_empty() {
248                            sect.footer_refs.push(HdrFtrRef {
249                                hdr_ftr_type: ftr_type,
250                                rel_id,
251                            });
252                        }
253                    } else if matches_local_name(name.as_ref(), b"titlePg") {
254                        sect.title_pg = Some(true);
255                    } else {
256                        // Capture unknown empty elements
257                        sect.extra_xml.push(capture_empty_element(e)?);
258                    }
259                }
260                Ok(Event::Start(ref e)) => {
261                    let name = e.name();
262                    if matches_local_name(name.as_ref(), b"cols") {
263                        sect.columns = Some(Self::parse_cols_start(reader, e)?);
264                    } else {
265                        // Capture unknown start elements as raw XML
266                        sect.extra_xml.push(capture_element(reader, e)?);
267                    }
268                }
269                Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"sectPr") => {
270                    break;
271                }
272                Ok(Event::Eof) => break,
273                Err(e) => return Err(e.into()),
274                _ => {}
275            }
276            buf.clear();
277        }
278
279        Ok(sect)
280    }
281
282    fn parse_cols_attrs(e: &BytesStart) -> Result<CT_Columns> {
283        let mut cols = CT_Columns::default();
284        for attr in e.attributes() {
285            let attr = attr?;
286            let key = attr.key.as_ref();
287            let val_str = std::str::from_utf8(&attr.value)?;
288            if matches_local_name(key, b"num") {
289                cols.num = Some(val_str.parse()?);
290            } else if matches_local_name(key, b"space") {
291                cols.space = Some(Twips(val_str.parse()?));
292            } else if matches_local_name(key, b"equalWidth") {
293                cols.equal_width = Some(val_str == "1" || val_str == "true");
294            } else if matches_local_name(key, b"sep") {
295                cols.sep = Some(val_str == "1" || val_str == "true");
296            }
297        }
298        Ok(cols)
299    }
300
301    fn parse_cols_empty(e: &BytesStart) -> Result<CT_Columns> {
302        Self::parse_cols_attrs(e)
303    }
304
305    fn parse_cols_start(reader: &mut Reader<&[u8]>, e: &BytesStart) -> Result<CT_Columns> {
306        let mut cols = Self::parse_cols_attrs(e)?;
307        let mut buf = Vec::new();
308
309        loop {
310            match reader.read_event_into(&mut buf) {
311                Ok(Event::Empty(ref e)) if matches_local_name(e.name().as_ref(), b"col") => {
312                    let mut width = None;
313                    let mut space = None;
314                    for attr in e.attributes() {
315                        let attr = attr?;
316                        let key = attr.key.as_ref();
317                        let val: i32 = std::str::from_utf8(&attr.value)?.parse()?;
318                        if matches_local_name(key, b"w") {
319                            width = Some(Twips(val));
320                        } else if matches_local_name(key, b"space") {
321                            space = Some(Twips(val));
322                        }
323                    }
324                    cols.columns.push(CT_Column { width, space });
325                }
326                Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"cols") => {
327                    break;
328                }
329                Ok(Event::Eof) => break,
330                Err(e) => return Err(e.into()),
331                _ => {}
332            }
333            buf.clear();
334        }
335
336        Ok(cols)
337    }
338
339    pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
340        let mut buf = itoa::Buffer::new();
341        writer.write_event(Event::Start(BytesStart::new("w:sectPr")))?;
342
343        // headerReference elements
344        for hdr in &self.header_refs {
345            let mut e = BytesStart::new("w:headerReference");
346            e.push_attribute(("w:type", hdr.hdr_ftr_type.to_str()));
347            e.push_attribute(("r:id", hdr.rel_id.as_str()));
348            writer.write_event(Event::Empty(e))?;
349        }
350
351        // footerReference elements
352        for ftr in &self.footer_refs {
353            let mut e = BytesStart::new("w:footerReference");
354            e.push_attribute(("w:type", ftr.hdr_ftr_type.to_str()));
355            e.push_attribute(("r:id", ftr.rel_id.as_str()));
356            writer.write_event(Event::Empty(e))?;
357        }
358
359        // type (section break type)
360        if let Some(st) = self.section_type {
361            let mut e = BytesStart::new("w:type");
362            e.push_attribute(("w:val", st.to_str()));
363            writer.write_event(Event::Empty(e))?;
364        }
365
366        // pgSz
367        if self.page_width.is_some() || self.page_height.is_some() || self.orientation.is_some() {
368            let mut e = BytesStart::new("w:pgSz");
369            if let Some(w) = self.page_width {
370                e.push_attribute(("w:w", buf.format(w.0)));
371            }
372            if let Some(h) = self.page_height {
373                e.push_attribute(("w:h", buf.format(h.0)));
374            }
375            if let Some(orient) = self.orientation
376                && orient == ST_PageOrientation::Landscape
377            {
378                e.push_attribute(("w:orient", orient.to_str()));
379            }
380            writer.write_event(Event::Empty(e))?;
381        }
382
383        // pgMar
384        if self.margin_top.is_some()
385            || self.margin_right.is_some()
386            || self.margin_bottom.is_some()
387            || self.margin_left.is_some()
388        {
389            let mut e = BytesStart::new("w:pgMar");
390            if let Some(t) = self.margin_top {
391                e.push_attribute(("w:top", buf.format(t.0)));
392            }
393            if let Some(r) = self.margin_right {
394                e.push_attribute(("w:right", buf.format(r.0)));
395            }
396            if let Some(b) = self.margin_bottom {
397                e.push_attribute(("w:bottom", buf.format(b.0)));
398            }
399            if let Some(l) = self.margin_left {
400                e.push_attribute(("w:left", buf.format(l.0)));
401            }
402            if let Some(g) = self.gutter {
403                e.push_attribute(("w:gutter", buf.format(g.0)));
404            }
405            if let Some(h) = self.header_distance {
406                e.push_attribute(("w:header", buf.format(h.0)));
407            }
408            if let Some(f) = self.footer_distance {
409                e.push_attribute(("w:footer", buf.format(f.0)));
410            }
411            writer.write_event(Event::Empty(e))?;
412        }
413
414        // cols
415        if let Some(ref cols) = self.columns {
416            if cols.columns.is_empty() {
417                // Simple equal-width columns
418                let mut e = BytesStart::new("w:cols");
419                if let Some(num) = cols.num {
420                    e.push_attribute(("w:num", buf.format(num)));
421                }
422                if let Some(space) = cols.space {
423                    e.push_attribute(("w:space", buf.format(space.0)));
424                }
425                if let Some(eq) = cols.equal_width
426                    && !eq
427                {
428                    e.push_attribute(("w:equalWidth", "0"));
429                }
430                if let Some(sep) = cols.sep
431                    && sep
432                {
433                    e.push_attribute(("w:sep", "1"));
434                }
435                writer.write_event(Event::Empty(e))?;
436            } else {
437                // Individual column definitions
438                let mut e = BytesStart::new("w:cols");
439                if let Some(num) = cols.num {
440                    e.push_attribute(("w:num", buf.format(num)));
441                }
442                if let Some(eq) = cols.equal_width {
443                    e.push_attribute(("w:equalWidth", if eq { "1" } else { "0" }));
444                }
445                if let Some(sep) = cols.sep
446                    && sep
447                {
448                    e.push_attribute(("w:sep", "1"));
449                }
450                writer.write_event(Event::Start(e))?;
451
452                for col in &cols.columns {
453                    let mut ce = BytesStart::new("w:col");
454                    if let Some(w) = col.width {
455                        ce.push_attribute(("w:w", buf.format(w.0)));
456                    }
457                    if let Some(s) = col.space {
458                        ce.push_attribute(("w:space", buf.format(s.0)));
459                    }
460                    writer.write_event(Event::Empty(ce))?;
461                }
462
463                writer.write_event(Event::End(BytesEnd::new("w:cols")))?;
464            }
465        }
466
467        // titlePg
468        if let Some(true) = self.title_pg {
469            writer.write_event(Event::Empty(BytesStart::new("w:titlePg")))?;
470        }
471
472        // Write captured unknown elements
473        for raw in &self.extra_xml {
474            writer.get_mut().write_all(raw)?;
475        }
476
477        writer.write_event(Event::End(BytesEnd::new("w:sectPr")))?;
478        Ok(())
479    }
480}
481
482/// `CT_Body` — The document body containing paragraphs, tables, and section properties.
483#[derive(Debug, Clone, PartialEq)]
484#[allow(non_snake_case)]
485pub struct CT_Body {
486    /// Mixed content: paragraphs and tables in document order.
487    pub content: Vec<BodyContent>,
488    pub sect_pr: Option<CT_SectPr>,
489}
490
491#[allow(non_snake_case)]
492impl CT_Body {
493    pub fn new() -> Self {
494        CT_Body {
495            content: Vec::new(),
496            sect_pr: Some(CT_SectPr::default_letter()),
497        }
498    }
499
500    /// Get an iterator over only the paragraphs.
501    pub fn paragraphs(&self) -> impl Iterator<Item = &CT_P> {
502        self.content.iter().filter_map(|c| match c {
503            BodyContent::Paragraph(p) => Some(p),
504            _ => None,
505        })
506    }
507
508    /// Get a mutable iterator over only the paragraphs.
509    pub fn paragraphs_mut(&mut self) -> impl Iterator<Item = &mut CT_P> {
510        self.content.iter_mut().filter_map(|c| match c {
511            BodyContent::Paragraph(p) => Some(p),
512            _ => None,
513        })
514    }
515
516    /// Get an iterator over only the tables.
517    pub fn tables(&self) -> impl Iterator<Item = &CT_Tbl> {
518        self.content.iter().filter_map(|c| match c {
519            BodyContent::Table(t) => Some(t),
520            _ => None,
521        })
522    }
523
524    /// Get a mutable iterator over only the tables.
525    pub fn tables_mut(&mut self) -> impl Iterator<Item = &mut CT_Tbl> {
526        self.content.iter_mut().filter_map(|c| match c {
527            BodyContent::Table(t) => Some(t),
528            _ => None,
529        })
530    }
531
532    /// Add a paragraph to the body.
533    pub fn add_paragraph(&mut self, p: CT_P) {
534        self.content.push(BodyContent::Paragraph(p));
535    }
536
537    /// Add a table to the body.
538    pub fn add_table(&mut self, tbl: CT_Tbl) {
539        self.content.push(BodyContent::Table(tbl));
540    }
541
542    /// Get the number of body content elements (paragraphs + tables).
543    pub fn content_count(&self) -> usize {
544        self.content.len()
545    }
546
547    /// Insert a paragraph at the given index.
548    ///
549    /// Panics if `index > content_count()`.
550    pub fn insert_paragraph(&mut self, index: usize, p: CT_P) {
551        self.content.insert(index, BodyContent::Paragraph(p));
552    }
553
554    /// Insert a table at the given index.
555    ///
556    /// Panics if `index > content_count()`.
557    pub fn insert_table(&mut self, index: usize, tbl: CT_Tbl) {
558        self.content.insert(index, BodyContent::Table(tbl));
559    }
560
561    /// Find the index of the first paragraph whose text contains the given substring.
562    pub fn find_paragraph_index(&self, text: &str) -> Option<usize> {
563        self.content.iter().position(|c| match c {
564            BodyContent::Paragraph(p) => p.text().contains(text),
565            _ => false,
566        })
567    }
568
569    /// Remove and return the content at the given index, or `None` if out of bounds.
570    pub fn remove(&mut self, index: usize) -> Option<BodyContent> {
571        if index < self.content.len() {
572            Some(self.content.remove(index))
573        } else {
574            None
575        }
576    }
577
578    /// Get a reference to the content at the given index.
579    pub fn get(&self, index: usize) -> Option<&BodyContent> {
580        self.content.get(index)
581    }
582
583    /// Get a mutable reference to the content at the given index.
584    pub fn get_mut(&mut self, index: usize) -> Option<&mut BodyContent> {
585        self.content.get_mut(index)
586    }
587
588    pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
589        Self::from_xml_with_prefixes(reader, &["w".to_string()])
590    }
591
592    fn from_xml_with_prefixes(
593        reader: &mut Reader<&[u8]>,
594        word_prefixes: &[String],
595    ) -> Result<Self> {
596        let mut content = Vec::new();
597        let mut sect_pr = None;
598        let mut buf = Vec::new();
599
600        loop {
601            match reader.read_event_into(&mut buf) {
602                Ok(Event::Start(ref e)) => {
603                    let name = e.name();
604                    let prefixes = word_prefixes_at(e, word_prefixes)?;
605                    if is_word_element(name.as_ref(), b"p", &prefixes) {
606                        content.push(BodyContent::Paragraph(CT_P::from_xml_with_prefixes(
607                            reader, &prefixes,
608                        )?));
609                    } else if is_word_element(name.as_ref(), b"tbl", &prefixes) {
610                        content.push(BodyContent::Table(CT_Tbl::from_xml_with_prefixes(
611                            reader, &prefixes,
612                        )?));
613                    } else if matches_local_name(name.as_ref(), b"sectPr") {
614                        sect_pr = Some(CT_SectPr::from_xml(reader)?);
615                    } else {
616                        // Capture unknown elements as raw XML
617                        content.push(BodyContent::RawXml(capture_element(reader, e)?));
618                    }
619                }
620                Ok(Event::Empty(ref e)) => {
621                    let name = e.name();
622                    if !matches_local_name(name.as_ref(), b"body") {
623                        content.push(BodyContent::RawXml(capture_empty_element(e)?));
624                    }
625                }
626                Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"body") => {
627                    break;
628                }
629                Ok(Event::Eof) => break,
630                Err(e) => return Err(e.into()),
631                _ => {}
632            }
633            buf.clear();
634        }
635
636        Ok(CT_Body { content, sect_pr })
637    }
638
639    pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
640        writer.write_event(Event::Start(BytesStart::new("w:body")))?;
641
642        for item in &self.content {
643            match item {
644                BodyContent::Paragraph(p) => p.to_xml(writer)?,
645                BodyContent::Table(t) => t.to_xml(writer)?,
646                BodyContent::RawXml(raw) => {
647                    writer.get_mut().write_all(raw)?;
648                }
649            }
650        }
651
652        if let Some(ref sect) = self.sect_pr {
653            sect.to_xml(writer)?;
654        }
655
656        writer.write_event(Event::End(BytesEnd::new("w:body")))?;
657        Ok(())
658    }
659}
660
661impl Default for CT_Body {
662    fn default() -> Self {
663        Self::new()
664    }
665}
666
667/// `CT_Document` — The root document element.
668#[derive(Debug, Clone, PartialEq)]
669#[allow(non_snake_case)]
670pub struct CT_Document {
671    pub body: CT_Body,
672    /// Extra namespace declarations captured from the original document element.
673    /// Each entry is (prefix, uri), e.g. ("xmlns:wp14", "http://...").
674    pub extra_namespaces: Vec<(String, String)>,
675    /// Raw XML for `<w:background>` element if present.
676    pub background_xml: Option<Vec<u8>>,
677}
678
679#[allow(non_snake_case)]
680impl CT_Document {
681    pub fn new() -> Self {
682        CT_Document {
683            body: CT_Body::new(),
684            extra_namespaces: Vec::new(),
685            background_xml: None,
686        }
687    }
688
689    /// Parse from XML bytes (the content of word/document.xml).
690    pub fn from_xml(xml: &[u8]) -> Result<Self> {
691        let mut reader = Reader::from_reader(xml);
692        reader.config_mut().trim_text(true);
693
694        let mut body = None;
695        let mut extra_namespaces = Vec::new();
696        let mut background_xml = None;
697        let mut buf = Vec::new();
698        let mut word_prefixes = Vec::new();
699
700        // Known namespace prefixes that we always emit ourselves
701        let known_ns: &[&[u8]] = &[b"xmlns:w", b"xmlns:r", b"xmlns:mc", b"xmlns"];
702
703        loop {
704            match reader.read_event_into(&mut buf) {
705                Ok(Event::Start(ref e)) => {
706                    let name = e.name();
707                    let prefixes = word_prefixes_at(e, &word_prefixes)?;
708                    if matches_local_name(name.as_ref(), b"body") {
709                        body = Some(CT_Body::from_xml_with_prefixes(&mut reader, &prefixes)?);
710                    } else if matches_local_name(name.as_ref(), b"document") {
711                        // Capture extra namespace declarations from the document element
712                        for attr in e.attributes().flatten() {
713                            let key = attr.key.as_ref();
714                            if (key.starts_with(b"xmlns:") || key == b"xmlns")
715                                && !known_ns.contains(&key)
716                            {
717                                let key_str = std::str::from_utf8(key).unwrap_or("").to_string();
718                                let val_str =
719                                    std::str::from_utf8(&attr.value).unwrap_or("").to_string();
720                                extra_namespaces.push((key_str, val_str));
721                            }
722                        }
723                        // Continue into document element
724                        word_prefixes = prefixes;
725                    } else if matches_local_name(name.as_ref(), b"background") {
726                        background_xml = Some(capture_element(&mut reader, e)?);
727                    } else {
728                        reader.read_to_end_into(name, &mut Vec::new())?;
729                    }
730                }
731                Ok(Event::Empty(ref e)) => {
732                    if matches_local_name(e.name().as_ref(), b"background") {
733                        background_xml = Some(capture_empty_element(e)?);
734                    }
735                }
736                Ok(Event::Eof) => break,
737                Err(e) => return Err(e.into()),
738                _ => {}
739            }
740            buf.clear();
741        }
742
743        Ok(CT_Document {
744            body: body.unwrap_or_default(),
745            extra_namespaces,
746            background_xml,
747        })
748    }
749
750    /// Serialize to XML bytes.
751    pub fn to_xml(&self) -> Result<Vec<u8>> {
752        let mut writer = Writer::new_with_indent(Vec::new(), b' ', 2);
753
754        writer.write_event(Event::Decl(BytesDecl::new(
755            "1.0",
756            Some("UTF-8"),
757            Some("yes"),
758        )))?;
759
760        let mut doc_start = BytesStart::new("w:document");
761        doc_start.push_attribute(("xmlns:w", W_NS));
762        doc_start.push_attribute((
763            "xmlns:r",
764            "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
765        ));
766        doc_start.push_attribute((
767            "xmlns:mc",
768            "http://schemas.openxmlformats.org/markup-compatibility/2006",
769        ));
770
771        // Always emit xmlns:wp for drawing elements
772        let wp_ns = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
773        let mut has_wp = false;
774        for (key, _) in &self.extra_namespaces {
775            if key == "xmlns:wp" {
776                has_wp = true;
777                break;
778            }
779        }
780        if !has_wp {
781            doc_start.push_attribute(("xmlns:wp", wp_ns));
782        }
783
784        // Replay captured extra namespaces
785        for (key, val) in &self.extra_namespaces {
786            doc_start.push_attribute((key.as_str(), val.as_str()));
787        }
788
789        writer.write_event(Event::Start(doc_start))?;
790
791        // Write background element if present
792        if let Some(ref bg) = self.background_xml {
793            writer.get_mut().extend_from_slice(bg);
794        }
795
796        self.body.to_xml(&mut writer)?;
797
798        writer.write_event(Event::End(BytesEnd::new("w:document")))?;
799
800        Ok(writer.into_inner())
801    }
802}
803
804impl Default for CT_Document {
805    fn default() -> Self {
806        Self::new()
807    }
808}
809
810#[cfg(test)]
811mod tests {
812    use super::*;
813
814    #[test]
815    fn round_trip_document() {
816        let mut doc = CT_Document::new();
817        let mut p = CT_P::new();
818        p.add_run("Hello World");
819        doc.body.add_paragraph(p);
820
821        let xml = doc.to_xml().unwrap();
822        let parsed = CT_Document::from_xml(&xml).unwrap();
823
824        let paras: Vec<_> = parsed.body.paragraphs().collect();
825        assert_eq!(paras.len(), 1);
826        assert_eq!(paras[0].text(), "Hello World");
827    }
828
829    #[test]
830    fn default_namespace_document_paragraph_properties_parse_in_scope() {
831        let xml = format!(
832            r#"<document xmlns="{W_NS}" xmlns:q="{W_NS}" xmlns:ext="urn:producer"><body><p><pPr><ext:jc ext:val="right"/><jc q:val="center"/></pPr><r><t>Scoped</t></r></p></body></document>"#
833        );
834        let parsed = CT_Document::from_xml(xml.as_bytes()).unwrap();
835        let paragraph = parsed.body.paragraphs().next().unwrap();
836        assert_eq!(paragraph.text(), "Scoped");
837        assert_eq!(
838            paragraph.properties.as_ref().unwrap().jc,
839            Some(crate::shared::ST_Jc::Center)
840        );
841    }
842
843    #[test]
844    fn round_trip_with_section() {
845        let doc = CT_Document::new();
846        let xml = doc.to_xml().unwrap();
847        let parsed = CT_Document::from_xml(&xml).unwrap();
848        assert!(parsed.body.sect_pr.is_some());
849        let sect = parsed.body.sect_pr.unwrap();
850        assert_eq!(sect.page_width, Some(Twips(12240)));
851    }
852
853    #[test]
854    fn round_trip_landscape() {
855        let mut doc = CT_Document::new();
856        let sect = doc.body.sect_pr.as_mut().unwrap();
857        sect.orientation = Some(ST_PageOrientation::Landscape);
858        sect.page_width = Some(Twips(15840)); // 11"
859        sect.page_height = Some(Twips(12240)); // 8.5"
860
861        let xml = doc.to_xml().unwrap();
862        let parsed = CT_Document::from_xml(&xml).unwrap();
863        let sect = parsed.body.sect_pr.unwrap();
864        assert_eq!(sect.orientation, Some(ST_PageOrientation::Landscape));
865        assert_eq!(sect.page_width, Some(Twips(15840)));
866    }
867
868    #[test]
869    fn round_trip_columns() {
870        let mut doc = CT_Document::new();
871        let sect = doc.body.sect_pr.as_mut().unwrap();
872        sect.columns = Some(CT_Columns {
873            num: Some(2),
874            space: Some(Twips(720)),
875            equal_width: Some(true),
876            sep: Some(true),
877            columns: Vec::new(),
878        });
879
880        let xml = doc.to_xml().unwrap();
881        let parsed = CT_Document::from_xml(&xml).unwrap();
882        let cols = parsed.body.sect_pr.unwrap().columns.unwrap();
883        assert_eq!(cols.num, Some(2));
884        assert_eq!(cols.space, Some(Twips(720)));
885        assert_eq!(cols.sep, Some(true));
886    }
887
888    #[test]
889    fn round_trip_section_type() {
890        let mut doc = CT_Document::new();
891        let sect = doc.body.sect_pr.as_mut().unwrap();
892        sect.section_type = Some(ST_SectionType::Continuous);
893        sect.title_pg = Some(true);
894
895        let xml = doc.to_xml().unwrap();
896        let parsed = CT_Document::from_xml(&xml).unwrap();
897        let sect = parsed.body.sect_pr.unwrap();
898        assert_eq!(sect.section_type, Some(ST_SectionType::Continuous));
899        assert_eq!(sect.title_pg, Some(true));
900    }
901
902    #[test]
903    fn insert_paragraph_at_beginning() {
904        let mut body = CT_Body::new();
905        let mut p1 = CT_P::new();
906        p1.add_run("First");
907        body.add_paragraph(p1);
908
909        let mut p0 = CT_P::new();
910        p0.add_run("Inserted");
911        body.insert_paragraph(0, p0);
912
913        assert_eq!(body.content_count(), 2);
914        match &body.content[0] {
915            BodyContent::Paragraph(p) => assert_eq!(p.text(), "Inserted"),
916            _ => panic!("expected paragraph"),
917        }
918        match &body.content[1] {
919            BodyContent::Paragraph(p) => assert_eq!(p.text(), "First"),
920            _ => panic!("expected paragraph"),
921        }
922    }
923
924    #[test]
925    fn insert_paragraph_in_middle() {
926        let mut body = CT_Body::new();
927        let mut p1 = CT_P::new();
928        p1.add_run("First");
929        body.add_paragraph(p1);
930        let mut p2 = CT_P::new();
931        p2.add_run("Third");
932        body.add_paragraph(p2);
933
934        let mut mid = CT_P::new();
935        mid.add_run("Middle");
936        body.insert_paragraph(1, mid);
937
938        assert_eq!(body.content_count(), 3);
939        let texts: Vec<_> = body.paragraphs().map(|p| p.text()).collect();
940        assert_eq!(texts, vec!["First", "Middle", "Third"]);
941    }
942
943    #[test]
944    fn find_paragraph_index_match() {
945        let mut body = CT_Body::new();
946        let mut p1 = CT_P::new();
947        p1.add_run("Hello World");
948        body.add_paragraph(p1);
949        let mut p2 = CT_P::new();
950        p2.add_run("INSERT_HERE");
951        body.add_paragraph(p2);
952
953        assert_eq!(body.find_paragraph_index("INSERT_HERE"), Some(1));
954        assert_eq!(body.find_paragraph_index("NONEXISTENT"), None);
955    }
956
957    #[test]
958    fn remove_content() {
959        let mut body = CT_Body::new();
960        let mut p1 = CT_P::new();
961        p1.add_run("First");
962        body.add_paragraph(p1);
963        let mut p2 = CT_P::new();
964        p2.add_run("Second");
965        body.add_paragraph(p2);
966
967        let removed = body.remove(0);
968        assert!(removed.is_some());
969        assert_eq!(body.content_count(), 1);
970        match &body.content[0] {
971            BodyContent::Paragraph(p) => assert_eq!(p.text(), "Second"),
972            _ => panic!("expected paragraph"),
973        }
974
975        // Out of bounds
976        assert!(body.remove(5).is_none());
977    }
978
979    #[test]
980    fn get_and_get_mut() {
981        let mut body = CT_Body::new();
982        let mut p = CT_P::new();
983        p.add_run("Test");
984        body.add_paragraph(p);
985
986        assert!(body.get(0).is_some());
987        assert!(body.get(1).is_none());
988
989        if let Some(BodyContent::Paragraph(p)) = body.get_mut(0) {
990            p.add_run(" Modified");
991        }
992        match body.get(0).unwrap() {
993            BodyContent::Paragraph(p) => assert_eq!(p.text(), "Test Modified"),
994            _ => panic!("expected paragraph"),
995        }
996    }
997
998    #[test]
999    fn sect_pr_section_type_and_orientation_round_trip() {
1000        let mut doc = CT_Document::new();
1001        let sect = doc.body.sect_pr.as_mut().unwrap();
1002        sect.section_type = Some(ST_SectionType::NextPage);
1003        sect.orientation = Some(ST_PageOrientation::Landscape);
1004        sect.page_width = Some(Twips(15840));
1005        sect.page_height = Some(Twips(12240));
1006
1007        let xml = doc.to_xml().unwrap();
1008        let parsed = CT_Document::from_xml(&xml).unwrap();
1009        let sect2 = parsed.body.sect_pr.unwrap();
1010        assert_eq!(sect2.section_type, Some(ST_SectionType::NextPage));
1011        assert_eq!(sect2.orientation, Some(ST_PageOrientation::Landscape));
1012        assert_eq!(sect2.page_width, Some(Twips(15840)));
1013        assert_eq!(sect2.page_height, Some(Twips(12240)));
1014    }
1015
1016    #[test]
1017    fn sect_pr_all_section_types() {
1018        for section_type in [
1019            ST_SectionType::NextPage,
1020            ST_SectionType::Continuous,
1021            ST_SectionType::EvenPage,
1022            ST_SectionType::OddPage,
1023        ] {
1024            let mut doc = CT_Document::new();
1025            let sect = doc.body.sect_pr.as_mut().unwrap();
1026            sect.section_type = Some(section_type);
1027
1028            let xml = doc.to_xml().unwrap();
1029            let parsed = CT_Document::from_xml(&xml).unwrap();
1030            let sect2 = parsed.body.sect_pr.unwrap();
1031            assert_eq!(
1032                sect2.section_type,
1033                Some(section_type),
1034                "section type round-trip failed for {section_type:?}"
1035            );
1036        }
1037    }
1038
1039    #[test]
1040    fn sect_pr_in_paragraph_ppr_round_trip() {
1041        // Section breaks inside paragraph properties (pPr/sectPr)
1042        let mut doc = CT_Document::new();
1043        let mut p = CT_P::new();
1044        p.add_run("Section break paragraph");
1045        let mut ppr = crate::properties::CT_PPr::default();
1046        let mut sect = CT_SectPr::default_letter();
1047        sect.section_type = Some(ST_SectionType::NextPage);
1048        sect.orientation = Some(ST_PageOrientation::Landscape);
1049        sect.page_width = Some(Twips(15840));
1050        sect.page_height = Some(Twips(12240));
1051        ppr.sect_pr = Some(sect);
1052        p.properties = Some(ppr);
1053        doc.body.add_paragraph(p);
1054
1055        let xml = doc.to_xml().unwrap();
1056        let parsed = CT_Document::from_xml(&xml).unwrap();
1057
1058        let paras: Vec<_> = parsed.body.paragraphs().collect();
1059        assert_eq!(paras.len(), 1);
1060        let ppr2 = paras[0].properties.as_ref().unwrap();
1061        let sect2 = ppr2.sect_pr.as_ref().unwrap();
1062        assert_eq!(sect2.section_type, Some(ST_SectionType::NextPage));
1063        assert_eq!(sect2.orientation, Some(ST_PageOrientation::Landscape));
1064    }
1065}