Skip to main content

rdocx_oxml/
table.rs

1//! Table elements: `CT_Tbl`, `CT_Row`, `CT_Tc` and related types.
2
3use quick_xml::events::{BytesEnd, BytesStart, Event};
4use quick_xml::{Reader, Writer};
5
6use crate::borders::CT_BorderEdge;
7use crate::error::Result;
8use crate::namespace::matches_local_name;
9use crate::numbering::word_prefixes_at;
10use crate::properties::{CT_Shd, get_val_attr, is_word_element};
11use crate::raw_xml::{capture_element, capture_empty_element};
12#[cfg(test)]
13use crate::shared::ST_Border;
14use crate::shared::ST_Jc;
15use crate::text::CT_P;
16use crate::units::Twips;
17
18/// Write any captured raw XML that belongs immediately before position `pos`.
19///
20/// Table children we do not model are stored as `(position, raw)` pairs so
21/// they can be put back where they were found, the same way `CT_P` handles
22/// its own unknown children.
23fn write_extras_at<W: std::io::Write>(
24    writer: &mut Writer<W>,
25    extra_xml: &[(usize, Vec<u8>)],
26    pos: usize,
27) -> Result<()> {
28    for (at, raw) in extra_xml {
29        if *at == pos {
30            writer.get_mut().write_all(raw)?;
31        }
32    }
33    Ok(())
34}
35
36// ---- Table border types ----
37
38/// `CT_TblBorders` — Table-level borders.
39#[derive(Debug, Clone, Default, PartialEq)]
40pub struct CT_TblBorders {
41    pub top: Option<CT_BorderEdge>,
42    pub bottom: Option<CT_BorderEdge>,
43    pub left: Option<CT_BorderEdge>,
44    pub right: Option<CT_BorderEdge>,
45    pub inside_h: Option<CT_BorderEdge>,
46    pub inside_v: Option<CT_BorderEdge>,
47}
48
49impl CT_TblBorders {
50    pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
51        let mut borders = CT_TblBorders::default();
52        let mut buf = Vec::new();
53
54        loop {
55            match reader.read_event_into(&mut buf) {
56                Ok(Event::Empty(ref e)) => {
57                    let name = e.name();
58                    let edge = CT_BorderEdge::from_xml_attrs(e)?;
59                    if matches_local_name(name.as_ref(), b"top") {
60                        borders.top = Some(edge);
61                    } else if matches_local_name(name.as_ref(), b"bottom") {
62                        borders.bottom = Some(edge);
63                    } else if matches_local_name(name.as_ref(), b"left")
64                        || matches_local_name(name.as_ref(), b"start")
65                    {
66                        borders.left = Some(edge);
67                    } else if matches_local_name(name.as_ref(), b"right")
68                        || matches_local_name(name.as_ref(), b"end")
69                    {
70                        borders.right = Some(edge);
71                    } else if matches_local_name(name.as_ref(), b"insideH") {
72                        borders.inside_h = Some(edge);
73                    } else if matches_local_name(name.as_ref(), b"insideV") {
74                        borders.inside_v = Some(edge);
75                    }
76                }
77                Ok(Event::End(ref e))
78                    if matches_local_name(e.name().as_ref(), b"tblBorders")
79                        || matches_local_name(e.name().as_ref(), b"tcBorders") =>
80                {
81                    break;
82                }
83                Ok(Event::Eof) => break,
84                Err(e) => return Err(e.into()),
85                _ => {}
86            }
87            buf.clear();
88        }
89
90        Ok(borders)
91    }
92
93    pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
94        writer.write_event(Event::Start(BytesStart::new(tag)))?;
95        if let Some(ref e) = self.top {
96            e.to_xml(writer, "w:top")?;
97        }
98        if let Some(ref e) = self.left {
99            e.to_xml(writer, "w:left")?;
100        }
101        if let Some(ref e) = self.bottom {
102            e.to_xml(writer, "w:bottom")?;
103        }
104        if let Some(ref e) = self.right {
105            e.to_xml(writer, "w:right")?;
106        }
107        if let Some(ref e) = self.inside_h {
108            e.to_xml(writer, "w:insideH")?;
109        }
110        if let Some(ref e) = self.inside_v {
111            e.to_xml(writer, "w:insideV")?;
112        }
113        writer.write_event(Event::End(BytesEnd::new(tag)))?;
114        Ok(())
115    }
116
117    pub fn is_empty(&self) -> bool {
118        self.top.is_none()
119            && self.bottom.is_none()
120            && self.left.is_none()
121            && self.right.is_none()
122            && self.inside_h.is_none()
123            && self.inside_v.is_none()
124    }
125}
126
127/// Table cell margin (a single edge width).
128#[derive(Debug, Clone, Default, PartialEq)]
129pub struct CT_TblCellMar {
130    pub top: Option<Twips>,
131    pub bottom: Option<Twips>,
132    pub left: Option<Twips>,
133    pub right: Option<Twips>,
134}
135
136impl CT_TblCellMar {
137    fn parse_edge(e: &BytesStart) -> Result<Option<Twips>> {
138        for attr in e.attributes() {
139            let attr = attr?;
140            if matches_local_name(attr.key.as_ref(), b"w") {
141                let val: i32 = std::str::from_utf8(&attr.value)?.parse()?;
142                return Ok(Some(Twips(val)));
143            }
144        }
145        Ok(None)
146    }
147
148    pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
149        let mut mar = CT_TblCellMar::default();
150        let mut buf = Vec::new();
151
152        loop {
153            match reader.read_event_into(&mut buf) {
154                Ok(Event::Empty(ref e)) => {
155                    let name = e.name();
156                    if matches_local_name(name.as_ref(), b"top") {
157                        mar.top = Self::parse_edge(e)?;
158                    } else if matches_local_name(name.as_ref(), b"bottom") {
159                        mar.bottom = Self::parse_edge(e)?;
160                    } else if matches_local_name(name.as_ref(), b"left")
161                        || matches_local_name(name.as_ref(), b"start")
162                    {
163                        mar.left = Self::parse_edge(e)?;
164                    } else if matches_local_name(name.as_ref(), b"right")
165                        || matches_local_name(name.as_ref(), b"end")
166                    {
167                        mar.right = Self::parse_edge(e)?;
168                    }
169                }
170                Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"tblCellMar") => {
171                    break;
172                }
173                Ok(Event::Eof) => break,
174                Err(e) => return Err(e.into()),
175                _ => {}
176            }
177            buf.clear();
178        }
179
180        Ok(mar)
181    }
182
183    pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
184        writer.write_event(Event::Start(BytesStart::new("w:tblCellMar")))?;
185
186        fn write_edge<W: std::io::Write>(
187            writer: &mut Writer<W>,
188            tag: &str,
189            val: Twips,
190        ) -> Result<()> {
191            let mut buf = itoa::Buffer::new();
192            let mut e = BytesStart::new(tag);
193            e.push_attribute(("w:w", buf.format(val.0)));
194            e.push_attribute(("w:type", "dxa"));
195            writer.write_event(Event::Empty(e))?;
196            Ok(())
197        }
198
199        if let Some(t) = self.top {
200            write_edge(writer, "w:top", t)?;
201        }
202        if let Some(l) = self.left {
203            write_edge(writer, "w:left", l)?;
204        }
205        if let Some(b) = self.bottom {
206            write_edge(writer, "w:bottom", b)?;
207        }
208        if let Some(r) = self.right {
209            write_edge(writer, "w:right", r)?;
210        }
211
212        writer.write_event(Event::End(BytesEnd::new("w:tblCellMar")))?;
213        Ok(())
214    }
215}
216
217// ---- Table width ----
218
219/// Table width specification.
220#[derive(Debug, Clone, PartialEq)]
221pub struct CT_TblWidth {
222    /// Width value
223    pub w: i32,
224    /// Width type: "dxa" (twips), "pct" (50ths of a percent), "auto", "nil"
225    pub width_type: String,
226}
227
228impl CT_TblWidth {
229    pub fn dxa(twips: i32) -> Self {
230        CT_TblWidth {
231            w: twips,
232            width_type: "dxa".to_string(),
233        }
234    }
235
236    pub fn pct(fiftieths: i32) -> Self {
237        CT_TblWidth {
238            w: fiftieths,
239            width_type: "pct".to_string(),
240        }
241    }
242
243    pub fn auto() -> Self {
244        CT_TblWidth {
245            w: 0,
246            width_type: "auto".to_string(),
247        }
248    }
249
250    pub fn from_xml_attrs(e: &BytesStart) -> Result<Self> {
251        let mut w = 0;
252        let mut width_type = "dxa".to_string();
253
254        for attr in e.attributes() {
255            let attr = attr?;
256            let key = attr.key.as_ref();
257            let val = std::str::from_utf8(&attr.value)?;
258            if matches_local_name(key, b"w") {
259                w = val.parse().unwrap_or(0);
260            } else if matches_local_name(key, b"type") {
261                width_type = val.to_string();
262            }
263        }
264
265        Ok(CT_TblWidth { w, width_type })
266    }
267
268    pub fn write_xml<W: std::io::Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
269        let mut buf = itoa::Buffer::new();
270        let mut e = BytesStart::new(tag);
271        e.push_attribute(("w:w", buf.format(self.w)));
272        e.push_attribute(("w:type", self.width_type.as_str()));
273        writer.write_event(Event::Empty(e))?;
274        Ok(())
275    }
276}
277
278// ---- Table grid column ----
279
280/// `CT_TblGridCol` — A column definition in the table grid.
281#[derive(Debug, Clone, PartialEq)]
282pub struct CT_TblGridCol {
283    /// Column width in twips
284    pub width: Twips,
285}
286
287// ---- Table properties ----
288
289/// `CT_TblPr` — Table properties.
290#[derive(Debug, Clone, Default, PartialEq)]
291pub struct CT_TblPr {
292    /// Table style ID
293    pub style_id: Option<String>,
294    /// Table width
295    pub width: Option<CT_TblWidth>,
296    /// Table alignment
297    pub jc: Option<ST_Jc>,
298    /// Table borders
299    pub borders: Option<CT_TblBorders>,
300    /// Default cell margins
301    pub cell_margin: Option<CT_TblCellMar>,
302    /// Table layout: "fixed" or "autofit"
303    pub layout: Option<String>,
304    /// Table indent from left margin
305    pub indent: Option<CT_TblWidth>,
306    /// Table shading/background
307    pub shading: Option<CT_Shd>,
308    /// Which parts of the table style's conditional formatting apply.
309    pub look: Option<CT_TblLook>,
310}
311
312/// `w:tblLook` — which parts of a table style's conditional formatting apply.
313///
314/// The style reference in `w:tblStyle` says *which* style to use. This says
315/// which of its conditional parts to turn on: header row emphasis, banding,
316/// first-column formatting. Dropping it leaves the style name intact and the
317/// table rendered with base formatting only, which reads as the style having
318/// been lost.
319///
320/// `w:val` is a legacy bitmask carrying the same information. Both are kept,
321/// because writers disagree about which one to emit and readers disagree about
322/// which one to trust.
323#[derive(Debug, Clone, PartialEq, Default)]
324#[allow(non_snake_case)]
325pub struct CT_TblLook {
326    /// Legacy bitmask form, e.g. "04A0".
327    pub val: Option<String>,
328    pub first_row: Option<bool>,
329    pub last_row: Option<bool>,
330    pub first_column: Option<bool>,
331    pub last_column: Option<bool>,
332    pub no_h_band: Option<bool>,
333    pub no_v_band: Option<bool>,
334}
335
336/// Read an OOXML boolean attribute, which may be written as 1/0 or true/false.
337fn parse_ooxml_bool(value: &str) -> Option<bool> {
338    match value {
339        "1" | "true" | "on" => Some(true),
340        "0" | "false" | "off" => Some(false),
341        _ => None,
342    }
343}
344
345fn ooxml_bool_str(value: bool) -> &'static str {
346    if value { "1" } else { "0" }
347}
348
349#[allow(non_snake_case)]
350impl CT_TblLook {
351    pub fn from_xml_attrs(e: &BytesStart) -> Result<Self> {
352        let mut look = CT_TblLook::default();
353        for attr in e.attributes().flatten() {
354            let value = std::str::from_utf8(&attr.value)?;
355            let key = attr.key.as_ref();
356            if matches_local_name(key, b"val") {
357                look.val = Some(value.to_string());
358            } else if matches_local_name(key, b"firstRow") {
359                look.first_row = parse_ooxml_bool(value);
360            } else if matches_local_name(key, b"lastRow") {
361                look.last_row = parse_ooxml_bool(value);
362            } else if matches_local_name(key, b"firstColumn") {
363                look.first_column = parse_ooxml_bool(value);
364            } else if matches_local_name(key, b"lastColumn") {
365                look.last_column = parse_ooxml_bool(value);
366            } else if matches_local_name(key, b"noHBand") {
367                look.no_h_band = parse_ooxml_bool(value);
368            } else if matches_local_name(key, b"noVBand") {
369                look.no_v_band = parse_ooxml_bool(value);
370            }
371        }
372        Ok(look)
373    }
374
375    pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
376        let mut e = BytesStart::new("w:tblLook");
377        if let Some(ref val) = self.val {
378            e.push_attribute(("w:val", val.as_str()));
379        }
380        for (name, value) in [
381            ("w:firstRow", self.first_row),
382            ("w:lastRow", self.last_row),
383            ("w:firstColumn", self.first_column),
384            ("w:lastColumn", self.last_column),
385            ("w:noHBand", self.no_h_band),
386            ("w:noVBand", self.no_v_band),
387        ] {
388            if let Some(value) = value {
389                e.push_attribute((name, ooxml_bool_str(value)));
390            }
391        }
392        writer.write_event(Event::Empty(e))?;
393        Ok(())
394    }
395}
396
397#[allow(non_snake_case)]
398impl CT_TblPr {
399    pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
400        let mut pr = CT_TblPr::default();
401        let mut buf = Vec::new();
402
403        loop {
404            match reader.read_event_into(&mut buf) {
405                Ok(Event::Empty(ref e)) => {
406                    let name = e.name();
407                    if matches_local_name(name.as_ref(), b"tblStyle") {
408                        pr.style_id = get_val_attr(e)?;
409                    } else if matches_local_name(name.as_ref(), b"tblW") {
410                        pr.width = Some(CT_TblWidth::from_xml_attrs(e)?);
411                    } else if matches_local_name(name.as_ref(), b"jc") {
412                        if let Some(val) = get_val_attr(e)? {
413                            pr.jc = Some(ST_Jc::from_str(&val)?);
414                        }
415                    } else if matches_local_name(name.as_ref(), b"tblLayout") {
416                        if let Some(val) = get_val_attr(e)? {
417                            pr.layout = Some(val);
418                        }
419                    } else if matches_local_name(name.as_ref(), b"tblInd") {
420                        pr.indent = Some(CT_TblWidth::from_xml_attrs(e)?);
421                    } else if matches_local_name(name.as_ref(), b"shd") {
422                        pr.shading = Some(CT_Shd::from_xml_attrs(e)?);
423                    } else if matches_local_name(name.as_ref(), b"tblLook") {
424                        pr.look = Some(CT_TblLook::from_xml_attrs(e)?);
425                    }
426                }
427                Ok(Event::Start(ref e)) => {
428                    let name = e.name();
429                    if matches_local_name(name.as_ref(), b"tblBorders") {
430                        pr.borders = Some(CT_TblBorders::from_xml(reader)?);
431                    } else if matches_local_name(name.as_ref(), b"tblCellMar") {
432                        pr.cell_margin = Some(CT_TblCellMar::from_xml(reader)?);
433                    } else {
434                        reader.read_to_end_into(name, &mut Vec::new())?;
435                    }
436                }
437                Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"tblPr") => {
438                    break;
439                }
440                Ok(Event::Eof) => break,
441                Err(e) => return Err(e.into()),
442                _ => {}
443            }
444            buf.clear();
445        }
446
447        Ok(pr)
448    }
449
450    pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
451        writer.write_event(Event::Start(BytesStart::new("w:tblPr")))?;
452
453        if let Some(ref style_id) = self.style_id {
454            let mut e = BytesStart::new("w:tblStyle");
455            e.push_attribute(("w:val", style_id.as_str()));
456            writer.write_event(Event::Empty(e))?;
457        }
458
459        if let Some(ref width) = self.width {
460            width.write_xml(writer, "w:tblW")?;
461        }
462
463        if let Some(jc) = self.jc {
464            let mut e = BytesStart::new("w:jc");
465            e.push_attribute(("w:val", jc.to_str()));
466            writer.write_event(Event::Empty(e))?;
467        }
468
469        if let Some(ref indent) = self.indent {
470            indent.write_xml(writer, "w:tblInd")?;
471        }
472
473        if let Some(ref borders) = self.borders
474            && !borders.is_empty()
475        {
476            borders.to_xml(writer, "w:tblBorders")?;
477        }
478
479        if let Some(ref shd) = self.shading {
480            shd.write_xml(writer, "w:shd")?;
481        }
482
483        if let Some(ref layout) = self.layout {
484            let mut e = BytesStart::new("w:tblLayout");
485            e.push_attribute(("w:type", layout.as_str()));
486            writer.write_event(Event::Empty(e))?;
487        }
488
489        if let Some(ref cell_margin) = self.cell_margin {
490            cell_margin.to_xml(writer)?;
491        }
492
493        if let Some(ref look) = self.look {
494            look.to_xml(writer)?;
495        }
496
497        writer.write_event(Event::End(BytesEnd::new("w:tblPr")))?;
498        Ok(())
499    }
500}
501
502// ---- Table grid ----
503
504/// `CT_TblGrid` — Defines the column structure of a table.
505#[derive(Debug, Clone, Default, PartialEq)]
506pub struct CT_TblGrid {
507    pub columns: Vec<CT_TblGridCol>,
508}
509
510#[allow(non_snake_case)]
511impl CT_TblGrid {
512    pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
513        let mut columns = Vec::new();
514        let mut buf = Vec::new();
515
516        loop {
517            match reader.read_event_into(&mut buf) {
518                Ok(Event::Empty(ref e)) => {
519                    if matches_local_name(e.name().as_ref(), b"gridCol") {
520                        let mut width = Twips(0);
521                        for attr in e.attributes() {
522                            let attr = attr?;
523                            if matches_local_name(attr.key.as_ref(), b"w") {
524                                width = Twips(std::str::from_utf8(&attr.value)?.parse()?);
525                            }
526                        }
527                        columns.push(CT_TblGridCol { width });
528                    }
529                }
530                Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"tblGrid") => {
531                    break;
532                }
533                Ok(Event::Eof) => break,
534                Err(e) => return Err(e.into()),
535                _ => {}
536            }
537            buf.clear();
538        }
539
540        Ok(CT_TblGrid { columns })
541    }
542
543    pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
544        let mut buf = itoa::Buffer::new();
545        writer.write_event(Event::Start(BytesStart::new("w:tblGrid")))?;
546
547        for col in &self.columns {
548            let mut e = BytesStart::new("w:gridCol");
549            e.push_attribute(("w:w", buf.format(col.width.0)));
550            writer.write_event(Event::Empty(e))?;
551        }
552
553        writer.write_event(Event::End(BytesEnd::new("w:tblGrid")))?;
554        Ok(())
555    }
556}
557
558// ---- Row properties ----
559
560/// Vertical merge state for a cell.
561#[derive(Debug, Clone, Copy, PartialEq, Eq)]
562pub enum VMerge {
563    /// Start of a vertical merge group
564    Restart,
565    /// Continuation of the merge group above
566    Continue,
567}
568
569/// `CT_TrPr` — Table row properties.
570#[derive(Debug, Clone, Default, PartialEq)]
571pub struct CT_TrPr {
572    /// Row height in twips
573    pub height: Option<Twips>,
574    /// Row height rule: "exact" or "atLeast"
575    pub height_rule: Option<String>,
576    /// Repeat as header row on each page
577    pub header: Option<bool>,
578    /// Row alignment
579    pub jc: Option<ST_Jc>,
580    /// Allow row to break across pages
581    pub cant_split: Option<bool>,
582    /// `w:cnfStyle` — which conditional parts of the table style this row is.
583    ///
584    /// Word writes this alongside `w:tblLook` and needs both to reproduce a
585    /// styled table. Dropping it loses the header-row and banding emphasis.
586    pub cnf_style: Option<String>,
587}
588
589#[allow(non_snake_case)]
590impl CT_TrPr {
591    pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
592        let mut pr = CT_TrPr::default();
593        let mut buf = Vec::new();
594
595        loop {
596            match reader.read_event_into(&mut buf) {
597                Ok(Event::Empty(ref e)) => {
598                    let name = e.name();
599                    if matches_local_name(name.as_ref(), b"trHeight") {
600                        for attr in e.attributes() {
601                            let attr = attr?;
602                            let key = attr.key.as_ref();
603                            let val = std::str::from_utf8(&attr.value)?;
604                            if matches_local_name(key, b"val") {
605                                pr.height = Some(Twips(val.parse()?));
606                            } else if matches_local_name(key, b"hRule") {
607                                pr.height_rule = Some(val.to_string());
608                            }
609                        }
610                    } else if matches_local_name(name.as_ref(), b"tblHeader") {
611                        pr.header = Some(true);
612                    } else if matches_local_name(name.as_ref(), b"jc") {
613                        if let Some(val) = get_val_attr(e)? {
614                            pr.jc = Some(ST_Jc::from_str(&val)?);
615                        }
616                    } else if matches_local_name(name.as_ref(), b"cnfStyle") {
617                        pr.cnf_style = get_val_attr(e)?;
618                    } else if matches_local_name(name.as_ref(), b"cantSplit") {
619                        pr.cant_split = Some(true);
620                    }
621                }
622                Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"trPr") => {
623                    break;
624                }
625                Ok(Event::Eof) => break,
626                Err(e) => return Err(e.into()),
627                _ => {}
628            }
629            buf.clear();
630        }
631
632        Ok(pr)
633    }
634
635    pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
636        if self.is_empty() {
637            return Ok(());
638        }
639
640        writer.write_event(Event::Start(BytesStart::new("w:trPr")))?;
641
642        // cnfStyle comes first in the schema sequence for both trPr and tcPr.
643        if let Some(ref cnf) = self.cnf_style {
644            let mut e = BytesStart::new("w:cnfStyle");
645            e.push_attribute(("w:val", cnf.as_str()));
646            writer.write_event(Event::Empty(e))?;
647        }
648
649        if let Some(ref cant_split) = self.cant_split
650            && *cant_split
651        {
652            writer.write_event(Event::Empty(BytesStart::new("w:cantSplit")))?;
653        }
654
655        if let Some(height) = self.height {
656            let mut buf = itoa::Buffer::new();
657            let mut e = BytesStart::new("w:trHeight");
658            e.push_attribute(("w:val", buf.format(height.0)));
659            if let Some(ref rule) = self.height_rule {
660                e.push_attribute(("w:hRule", rule.as_str()));
661            }
662            writer.write_event(Event::Empty(e))?;
663        }
664
665        if let Some(true) = self.header {
666            writer.write_event(Event::Empty(BytesStart::new("w:tblHeader")))?;
667        }
668
669        if let Some(jc) = self.jc {
670            let mut e = BytesStart::new("w:jc");
671            e.push_attribute(("w:val", jc.to_str()));
672            writer.write_event(Event::Empty(e))?;
673        }
674
675        writer.write_event(Event::End(BytesEnd::new("w:trPr")))?;
676        Ok(())
677    }
678
679    fn is_empty(&self) -> bool {
680        self.height.is_none()
681            && self.header.is_none()
682            && self.jc.is_none()
683            && self.cant_split.is_none()
684            && self.cnf_style.is_none()
685    }
686}
687
688// ---- Cell properties ----
689
690/// Vertical alignment within a cell.
691#[derive(Debug, Clone, Copy, PartialEq, Eq)]
692pub enum ST_VerticalJc {
693    Top,
694    Center,
695    Bottom,
696}
697
698impl ST_VerticalJc {
699    pub fn from_str(s: &str) -> Self {
700        match s {
701            "center" => Self::Center,
702            "bottom" => Self::Bottom,
703            _ => Self::Top,
704        }
705    }
706
707    pub fn to_str(self) -> &'static str {
708        match self {
709            Self::Top => "top",
710            Self::Center => "center",
711            Self::Bottom => "bottom",
712        }
713    }
714}
715
716/// `CT_TcPr` — Table cell properties.
717#[derive(Debug, Clone, Default, PartialEq)]
718pub struct CT_TcPr {
719    /// Cell width
720    pub width: Option<CT_TblWidth>,
721    /// Horizontal merge (number of grid columns spanned)
722    pub grid_span: Option<u32>,
723    /// Vertical merge
724    pub v_merge: Option<VMerge>,
725    /// Cell borders
726    pub borders: Option<CT_TblBorders>,
727    /// Cell shading
728    pub shading: Option<CT_Shd>,
729    /// Vertical alignment
730    pub v_align: Option<ST_VerticalJc>,
731    /// No-wrap text
732    pub no_wrap: Option<bool>,
733    /// Text direction
734    pub text_direction: Option<String>,
735    /// `w:cnfStyle` — which conditional parts of the table style this cell is.
736    pub cnf_style: Option<String>,
737}
738
739#[allow(non_snake_case)]
740impl CT_TcPr {
741    pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
742        let mut pr = CT_TcPr::default();
743        let mut buf = Vec::new();
744
745        loop {
746            match reader.read_event_into(&mut buf) {
747                Ok(Event::Empty(ref e)) => {
748                    let name = e.name();
749                    if matches_local_name(name.as_ref(), b"tcW") {
750                        pr.width = Some(CT_TblWidth::from_xml_attrs(e)?);
751                    } else if matches_local_name(name.as_ref(), b"gridSpan") {
752                        if let Some(val) = get_val_attr(e)? {
753                            pr.grid_span = Some(val.parse()?);
754                        }
755                    } else if matches_local_name(name.as_ref(), b"vMerge") {
756                        if let Some(val) = get_val_attr(e)? {
757                            pr.v_merge = Some(if val == "restart" {
758                                VMerge::Restart
759                            } else {
760                                VMerge::Continue
761                            });
762                        } else {
763                            // Empty vMerge means "continue"
764                            pr.v_merge = Some(VMerge::Continue);
765                        }
766                    } else if matches_local_name(name.as_ref(), b"vAlign") {
767                        if let Some(val) = get_val_attr(e)? {
768                            pr.v_align = Some(ST_VerticalJc::from_str(&val));
769                        }
770                    } else if matches_local_name(name.as_ref(), b"shd") {
771                        pr.shading = Some(CT_Shd::from_xml_attrs(e)?);
772                    } else if matches_local_name(name.as_ref(), b"cnfStyle") {
773                        pr.cnf_style = get_val_attr(e)?;
774                    } else if matches_local_name(name.as_ref(), b"noWrap") {
775                        pr.no_wrap = Some(true);
776                    } else if matches_local_name(name.as_ref(), b"textDirection")
777                        && let Some(val) = get_val_attr(e)?
778                    {
779                        pr.text_direction = Some(val);
780                    }
781                }
782                Ok(Event::Start(ref e)) => {
783                    let name = e.name();
784                    if matches_local_name(name.as_ref(), b"tcBorders") {
785                        pr.borders = Some(CT_TblBorders::from_xml(reader)?);
786                    } else {
787                        reader.read_to_end_into(name, &mut Vec::new())?;
788                    }
789                }
790                Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"tcPr") => {
791                    break;
792                }
793                Ok(Event::Eof) => break,
794                Err(e) => return Err(e.into()),
795                _ => {}
796            }
797            buf.clear();
798        }
799
800        Ok(pr)
801    }
802
803    pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
804        if self.is_empty() {
805            return Ok(());
806        }
807
808        writer.write_event(Event::Start(BytesStart::new("w:tcPr")))?;
809
810        if let Some(ref cnf) = self.cnf_style {
811            let mut e = BytesStart::new("w:cnfStyle");
812            e.push_attribute(("w:val", cnf.as_str()));
813            writer.write_event(Event::Empty(e))?;
814        }
815
816        if let Some(ref width) = self.width {
817            width.write_xml(writer, "w:tcW")?;
818        }
819
820        if let Some(grid_span) = self.grid_span
821            && grid_span > 1
822        {
823            let mut buf = itoa::Buffer::new();
824            let mut e = BytesStart::new("w:gridSpan");
825            e.push_attribute(("w:val", buf.format(grid_span)));
826            writer.write_event(Event::Empty(e))?;
827        }
828
829        if let Some(ref vm) = self.v_merge {
830            let mut e = BytesStart::new("w:vMerge");
831            match vm {
832                VMerge::Restart => e.push_attribute(("w:val", "restart")),
833                VMerge::Continue => {} // empty element
834            }
835            writer.write_event(Event::Empty(e))?;
836        }
837
838        if let Some(ref borders) = self.borders
839            && !borders.is_empty()
840        {
841            borders.to_xml(writer, "w:tcBorders")?;
842        }
843
844        if let Some(ref shd) = self.shading {
845            shd.write_xml(writer, "w:shd")?;
846        }
847
848        if let Some(true) = self.no_wrap {
849            writer.write_event(Event::Empty(BytesStart::new("w:noWrap")))?;
850        }
851
852        if let Some(ref va) = self.v_align {
853            let mut e = BytesStart::new("w:vAlign");
854            e.push_attribute(("w:val", va.to_str()));
855            writer.write_event(Event::Empty(e))?;
856        }
857
858        if let Some(ref td) = self.text_direction {
859            let mut e = BytesStart::new("w:textDirection");
860            e.push_attribute(("w:val", td.as_str()));
861            writer.write_event(Event::Empty(e))?;
862        }
863
864        writer.write_event(Event::End(BytesEnd::new("w:tcPr")))?;
865        Ok(())
866    }
867
868    fn is_empty(&self) -> bool {
869        self.width.is_none()
870            && self.grid_span.is_none()
871            && self.v_merge.is_none()
872            && self.borders.is_none()
873            && self.shading.is_none()
874            && self.v_align.is_none()
875            && self.no_wrap.is_none()
876            && self.text_direction.is_none()
877            && self.cnf_style.is_none()
878    }
879}
880
881// ---- Table cell ----
882
883/// Content that can appear inside a table cell.
884#[derive(Debug, Clone, PartialEq)]
885pub enum CellContent {
886    /// A paragraph.
887    Paragraph(CT_P),
888    /// A nested table.
889    Table(CT_Tbl),
890}
891
892/// `CT_Tc` — A table cell containing paragraphs and possibly nested tables.
893#[derive(Debug, Clone, PartialEq)]
894pub struct CT_Tc {
895    pub properties: Option<CT_TcPr>,
896    /// Cell content (paragraphs and nested tables).
897    pub content: Vec<CellContent>,
898    /// Raw XML for children we do not model (content controls, bookmarks,
899    /// revision marks), tagged with the content index they appeared before so
900    /// they can be written back in place.
901    pub extra_xml: Vec<(usize, Vec<u8>)>,
902}
903
904#[allow(non_snake_case)]
905impl CT_Tc {
906    pub fn new() -> Self {
907        CT_Tc {
908            properties: None,
909            // OOXML requires at least one paragraph per cell
910            content: vec![CellContent::Paragraph(CT_P::new())],
911            extra_xml: Vec::new(),
912        }
913    }
914
915    /// Get all paragraphs in this cell (excludes nested tables).
916    pub fn paragraphs(&self) -> Vec<&CT_P> {
917        self.content
918            .iter()
919            .filter_map(|c| match c {
920                CellContent::Paragraph(p) => Some(p),
921                CellContent::Table(_) => None,
922            })
923            .collect()
924    }
925
926    /// Get mutable reference to paragraphs (backward compatibility).
927    pub fn paragraphs_mut(&mut self) -> Vec<&mut CT_P> {
928        self.content
929            .iter_mut()
930            .filter_map(|c| match c {
931                CellContent::Paragraph(p) => Some(p),
932                CellContent::Table(_) => None,
933            })
934            .collect()
935    }
936
937    pub fn text(&self) -> String {
938        self.paragraphs()
939            .iter()
940            .map(|p| p.text())
941            .collect::<Vec<_>>()
942            .join("\n")
943    }
944
945    pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
946        Self::from_xml_with_prefixes(reader, &["w".to_string()])
947    }
948
949    fn from_xml_with_prefixes(
950        reader: &mut Reader<&[u8]>,
951        word_prefixes: &[String],
952    ) -> Result<Self> {
953        let mut properties = None;
954        let mut content = Vec::new();
955        let mut extra_xml = Vec::new();
956        let mut buf = Vec::new();
957
958        loop {
959            match reader.read_event_into(&mut buf) {
960                Ok(Event::Start(ref e)) => {
961                    let name = e.name();
962                    let prefixes = word_prefixes_at(e, word_prefixes)?;
963                    if matches_local_name(name.as_ref(), b"tcPr") {
964                        properties = Some(CT_TcPr::from_xml(reader)?);
965                    } else if is_word_element(name.as_ref(), b"p", &prefixes) {
966                        content.push(CellContent::Paragraph(CT_P::from_xml_with_prefixes(
967                            reader, &prefixes,
968                        )?));
969                    } else if is_word_element(name.as_ref(), b"tbl", &prefixes) {
970                        content.push(CellContent::Table(CT_Tbl::from_xml_with_prefixes(
971                            reader, &prefixes,
972                        )?));
973                    } else {
974                        // Content controls (w:sdt), bookmarks and revision
975                        // marks live here. Keep them verbatim rather than
976                        // dropping the subtree, which used to delete every
977                        // paragraph wrapped in a content control.
978                        extra_xml.push((content.len(), capture_element(reader, e)?));
979                    }
980                }
981                Ok(Event::Empty(ref e)) => {
982                    let name = e.name();
983                    if !matches_local_name(name.as_ref(), b"tcPr") {
984                        extra_xml.push((content.len(), capture_empty_element(e)?));
985                    }
986                }
987                Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"tc") => {
988                    break;
989                }
990                Ok(Event::Eof) => break,
991                Err(e) => return Err(e.into()),
992                _ => {}
993            }
994            buf.clear();
995        }
996
997        Ok(CT_Tc {
998            properties,
999            content,
1000            extra_xml,
1001        })
1002    }
1003
1004    pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1005        writer.write_event(Event::Start(BytesStart::new("w:tc")))?;
1006
1007        if let Some(ref props) = self.properties {
1008            props.to_xml(writer)?;
1009        }
1010
1011        for (idx, item) in self.content.iter().enumerate() {
1012            write_extras_at(writer, &self.extra_xml, idx)?;
1013            match item {
1014                CellContent::Paragraph(p) => p.to_xml(writer)?,
1015                CellContent::Table(tbl) => tbl.to_xml(writer)?,
1016            }
1017        }
1018        write_extras_at(writer, &self.extra_xml, self.content.len())?;
1019
1020        writer.write_event(Event::End(BytesEnd::new("w:tc")))?;
1021        Ok(())
1022    }
1023}
1024
1025impl Default for CT_Tc {
1026    fn default() -> Self {
1027        Self::new()
1028    }
1029}
1030
1031// ---- Table row ----
1032
1033/// `CT_Row` — A table row containing cells.
1034#[derive(Debug, Clone, PartialEq)]
1035pub struct CT_Row {
1036    pub properties: Option<CT_TrPr>,
1037    pub cells: Vec<CT_Tc>,
1038    /// Raw XML for children we do not model, tagged with the cell index they
1039    /// appeared before so they can be written back in place.
1040    pub extra_xml: Vec<(usize, Vec<u8>)>,
1041}
1042
1043#[allow(non_snake_case)]
1044impl CT_Row {
1045    pub fn new() -> Self {
1046        CT_Row {
1047            properties: None,
1048            cells: Vec::new(),
1049            extra_xml: Vec::new(),
1050        }
1051    }
1052
1053    pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
1054        Self::from_xml_with_prefixes(reader, &["w".to_string()])
1055    }
1056
1057    fn from_xml_with_prefixes(
1058        reader: &mut Reader<&[u8]>,
1059        word_prefixes: &[String],
1060    ) -> Result<Self> {
1061        let mut properties = None;
1062        let mut cells = Vec::new();
1063        let mut extra_xml = Vec::new();
1064        let mut buf = Vec::new();
1065
1066        loop {
1067            match reader.read_event_into(&mut buf) {
1068                Ok(Event::Start(ref e)) => {
1069                    let name = e.name();
1070                    let prefixes = word_prefixes_at(e, word_prefixes)?;
1071                    if matches_local_name(name.as_ref(), b"trPr") {
1072                        properties = Some(CT_TrPr::from_xml(reader)?);
1073                    } else if is_word_element(name.as_ref(), b"tc", &prefixes) {
1074                        cells.push(CT_Tc::from_xml_with_prefixes(reader, &prefixes)?);
1075                    } else {
1076                        // A cell wrapped in a content control used to be
1077                        // dropped here, leaving a row with no cells at all.
1078                        extra_xml.push((cells.len(), capture_element(reader, e)?));
1079                    }
1080                }
1081                Ok(Event::Empty(ref e)) => {
1082                    let name = e.name();
1083                    if !matches_local_name(name.as_ref(), b"trPr") {
1084                        extra_xml.push((cells.len(), capture_empty_element(e)?));
1085                    }
1086                }
1087                Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"tr") => {
1088                    break;
1089                }
1090                Ok(Event::Eof) => break,
1091                Err(e) => return Err(e.into()),
1092                _ => {}
1093            }
1094            buf.clear();
1095        }
1096
1097        Ok(CT_Row {
1098            properties,
1099            cells,
1100            extra_xml,
1101        })
1102    }
1103
1104    pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1105        writer.write_event(Event::Start(BytesStart::new("w:tr")))?;
1106
1107        if let Some(ref props) = self.properties {
1108            props.to_xml(writer)?;
1109        }
1110
1111        for (idx, cell) in self.cells.iter().enumerate() {
1112            write_extras_at(writer, &self.extra_xml, idx)?;
1113            cell.to_xml(writer)?;
1114        }
1115        write_extras_at(writer, &self.extra_xml, self.cells.len())?;
1116
1117        writer.write_event(Event::End(BytesEnd::new("w:tr")))?;
1118        Ok(())
1119    }
1120}
1121
1122impl Default for CT_Row {
1123    fn default() -> Self {
1124        Self::new()
1125    }
1126}
1127
1128// ---- Table ----
1129
1130/// `CT_Tbl` — A table element containing rows.
1131#[derive(Debug, Clone, PartialEq)]
1132pub struct CT_Tbl {
1133    pub properties: Option<CT_TblPr>,
1134    pub grid: Option<CT_TblGrid>,
1135    pub rows: Vec<CT_Row>,
1136    /// Raw XML for children we do not model, tagged with the row index they
1137    /// appeared before so they can be written back in place.
1138    pub extra_xml: Vec<(usize, Vec<u8>)>,
1139}
1140
1141#[allow(non_snake_case)]
1142impl CT_Tbl {
1143    pub fn new() -> Self {
1144        CT_Tbl {
1145            properties: None,
1146            grid: None,
1147            rows: Vec::new(),
1148            extra_xml: Vec::new(),
1149        }
1150    }
1151
1152    pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
1153        Self::from_xml_with_prefixes(reader, &["w".to_string()])
1154    }
1155
1156    pub(crate) fn from_xml_with_prefixes(
1157        reader: &mut Reader<&[u8]>,
1158        word_prefixes: &[String],
1159    ) -> Result<Self> {
1160        let mut properties = None;
1161        let mut grid = None;
1162        let mut rows = Vec::new();
1163        let mut extra_xml = Vec::new();
1164        let mut buf = Vec::new();
1165
1166        loop {
1167            match reader.read_event_into(&mut buf) {
1168                Ok(Event::Start(ref e)) => {
1169                    let name = e.name();
1170                    let prefixes = word_prefixes_at(e, word_prefixes)?;
1171                    if matches_local_name(name.as_ref(), b"tblPr") {
1172                        properties = Some(CT_TblPr::from_xml(reader)?);
1173                    } else if matches_local_name(name.as_ref(), b"tblGrid") {
1174                        grid = Some(CT_TblGrid::from_xml(reader)?);
1175                    } else if is_word_element(name.as_ref(), b"tr", &prefixes) {
1176                        rows.push(CT_Row::from_xml_with_prefixes(reader, &prefixes)?);
1177                    } else {
1178                        // Rows wrapped in a content control used to be dropped
1179                        // here, which silently deleted whole tables.
1180                        extra_xml.push((rows.len(), capture_element(reader, e)?));
1181                    }
1182                }
1183                Ok(Event::Empty(ref e)) => {
1184                    let name = e.name();
1185                    // tblPr and tblGrid have fixed positions ahead of the rows,
1186                    // so a self-closing one must not be re-emitted from here.
1187                    if !matches_local_name(name.as_ref(), b"tblPr")
1188                        && !matches_local_name(name.as_ref(), b"tblGrid")
1189                    {
1190                        extra_xml.push((rows.len(), capture_empty_element(e)?));
1191                    }
1192                }
1193                Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"tbl") => {
1194                    break;
1195                }
1196                Ok(Event::Eof) => break,
1197                Err(e) => return Err(e.into()),
1198                _ => {}
1199            }
1200            buf.clear();
1201        }
1202
1203        Ok(CT_Tbl {
1204            properties,
1205            grid,
1206            rows,
1207            extra_xml,
1208        })
1209    }
1210
1211    pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1212        writer.write_event(Event::Start(BytesStart::new("w:tbl")))?;
1213
1214        if let Some(ref props) = self.properties {
1215            props.to_xml(writer)?;
1216        }
1217
1218        if let Some(ref grid) = self.grid {
1219            grid.to_xml(writer)?;
1220        }
1221
1222        for (idx, row) in self.rows.iter().enumerate() {
1223            write_extras_at(writer, &self.extra_xml, idx)?;
1224            row.to_xml(writer)?;
1225        }
1226        write_extras_at(writer, &self.extra_xml, self.rows.len())?;
1227
1228        writer.write_event(Event::End(BytesEnd::new("w:tbl")))?;
1229        Ok(())
1230    }
1231}
1232
1233impl Default for CT_Tbl {
1234    fn default() -> Self {
1235        Self::new()
1236    }
1237}
1238
1239#[cfg(test)]
1240mod tests {
1241    use super::*;
1242
1243    fn parse_table(xml: &str) -> CT_Tbl {
1244        let full = format!("<w:tbl>{xml}</w:tbl>");
1245        let mut reader = Reader::from_str(&full);
1246        reader.config_mut().trim_text(true);
1247        let mut buf = Vec::new();
1248        loop {
1249            match reader.read_event_into(&mut buf) {
1250                Ok(Event::Start(ref e)) if matches_local_name(e.name().as_ref(), b"tbl") => break,
1251                _ => {}
1252            }
1253            buf.clear();
1254        }
1255        CT_Tbl::from_xml(&mut reader).unwrap()
1256    }
1257
1258    #[test]
1259    fn parse_simple_table() {
1260        let tbl = parse_table(
1261            r#"<w:tblPr><w:tblW w:w="5000" w:type="dxa"/></w:tblPr>
1262               <w:tblGrid><w:gridCol w:w="2500"/><w:gridCol w:w="2500"/></w:tblGrid>
1263               <w:tr>
1264                 <w:tc><w:p><w:r><w:t>A1</w:t></w:r></w:p></w:tc>
1265                 <w:tc><w:p><w:r><w:t>B1</w:t></w:r></w:p></w:tc>
1266               </w:tr>
1267               <w:tr>
1268                 <w:tc><w:p><w:r><w:t>A2</w:t></w:r></w:p></w:tc>
1269                 <w:tc><w:p><w:r><w:t>B2</w:t></w:r></w:p></w:tc>
1270               </w:tr>"#,
1271        );
1272        assert_eq!(tbl.rows.len(), 2);
1273        assert_eq!(tbl.rows[0].cells.len(), 2);
1274        assert_eq!(tbl.rows[0].cells[0].text(), "A1");
1275        assert_eq!(tbl.rows[1].cells[1].text(), "B2");
1276
1277        let grid = tbl.grid.unwrap();
1278        assert_eq!(grid.columns.len(), 2);
1279        assert_eq!(grid.columns[0].width, Twips(2500));
1280
1281        let pr = tbl.properties.unwrap();
1282        assert_eq!(pr.width.as_ref().unwrap().w, 5000);
1283    }
1284
1285    #[test]
1286    fn aliased_table_cell_paragraph_properties_keep_root_scope() {
1287        let xml = format!(
1288            r#"<q:tbl xmlns:q="{}" xmlns:ext="urn:producer"><q:tr><q:tc><ext:p><ext:pPr><ext:jc ext:val="right"/></ext:pPr></ext:p><q:p><q:pPr><ext:jc ext:val="right"/><q:jc q:val="center"/></q:pPr><q:r><q:t>Cell</q:t></q:r></q:p></q:tc></q:tr></q:tbl>"#,
1289            crate::namespace::W_NS
1290        );
1291        let mut reader = Reader::from_str(&xml);
1292        let mut buf = Vec::new();
1293        let table = loop {
1294            match reader.read_event_into(&mut buf) {
1295                Ok(Event::Start(ref element)) if element.local_name().as_ref() == b"tbl" => {
1296                    let prefixes = word_prefixes_at(element, &[]).unwrap();
1297                    break CT_Tbl::from_xml_with_prefixes(&mut reader, &prefixes).unwrap();
1298                }
1299                Ok(Event::Eof) => panic!("missing table"),
1300                event => {
1301                    event.unwrap();
1302                }
1303            }
1304            buf.clear();
1305        };
1306        let paragraphs = table.rows[0].cells[0].paragraphs();
1307        assert_eq!(paragraphs.len(), 1);
1308        assert_eq!(paragraphs[0].text(), "Cell");
1309        assert_eq!(
1310            paragraphs[0].properties.as_ref().unwrap().jc,
1311            Some(ST_Jc::Center)
1312        );
1313    }
1314
1315    #[test]
1316    fn default_namespace_table_cell_properties_keep_root_scope() {
1317        let xml = format!(
1318            r#"<tbl xmlns="{0}" xmlns:w="{0}" xmlns:ext="urn:producer"><tr><tc><ext:p><ext:pPr><ext:jc ext:val="right"/></ext:pPr></ext:p><p><pPr><ext:jc ext:val="right"/><jc w:val="center"/></pPr><r><t>Cell</t></r></p></tc></tr></tbl>"#,
1319            crate::namespace::W_NS
1320        );
1321        let mut reader = Reader::from_str(&xml);
1322        let mut buf = Vec::new();
1323        let table = loop {
1324            match reader.read_event_into(&mut buf) {
1325                Ok(Event::Start(ref element)) if element.local_name().as_ref() == b"tbl" => {
1326                    let prefixes = word_prefixes_at(element, &[]).unwrap();
1327                    break CT_Tbl::from_xml_with_prefixes(&mut reader, &prefixes).unwrap();
1328                }
1329                Ok(Event::Eof) => panic!("missing table"),
1330                event => {
1331                    event.unwrap();
1332                }
1333            }
1334            buf.clear();
1335        };
1336        let paragraphs = table.rows[0].cells[0].paragraphs();
1337        assert_eq!(paragraphs.len(), 1);
1338        assert_eq!(paragraphs[0].text(), "Cell");
1339        assert_eq!(
1340            paragraphs[0].properties.as_ref().unwrap().jc,
1341            Some(ST_Jc::Center)
1342        );
1343    }
1344
1345    #[test]
1346    fn parse_cell_merge() {
1347        let tbl = parse_table(
1348            r#"<w:tblGrid><w:gridCol w:w="2500"/><w:gridCol w:w="2500"/></w:tblGrid>
1349               <w:tr>
1350                 <w:tc>
1351                   <w:tcPr><w:gridSpan w:val="2"/></w:tcPr>
1352                   <w:p><w:r><w:t>Merged</w:t></w:r></w:p>
1353                 </w:tc>
1354               </w:tr>
1355               <w:tr>
1356                 <w:tc>
1357                   <w:tcPr><w:vMerge w:val="restart"/></w:tcPr>
1358                   <w:p><w:r><w:t>VM Start</w:t></w:r></w:p>
1359                 </w:tc>
1360                 <w:tc><w:p/></w:tc>
1361               </w:tr>
1362               <w:tr>
1363                 <w:tc>
1364                   <w:tcPr><w:vMerge/></w:tcPr>
1365                   <w:p/>
1366                 </w:tc>
1367                 <w:tc><w:p/></w:tc>
1368               </w:tr>"#,
1369        );
1370
1371        // First row: horizontal merge
1372        assert_eq!(
1373            tbl.rows[0].cells[0].properties.as_ref().unwrap().grid_span,
1374            Some(2)
1375        );
1376
1377        // Second row: vertical merge start
1378        assert_eq!(
1379            tbl.rows[1].cells[0].properties.as_ref().unwrap().v_merge,
1380            Some(VMerge::Restart)
1381        );
1382
1383        // Third row: vertical merge continue
1384        assert_eq!(
1385            tbl.rows[2].cells[0].properties.as_ref().unwrap().v_merge,
1386            Some(VMerge::Continue)
1387        );
1388    }
1389
1390    #[test]
1391    fn parse_table_borders() {
1392        let tbl = parse_table(
1393            r#"<w:tblPr>
1394                 <w:tblBorders>
1395                   <w:top w:val="single" w:sz="4" w:color="000000"/>
1396                   <w:bottom w:val="single" w:sz="4" w:color="000000"/>
1397                   <w:left w:val="single" w:sz="4" w:color="000000"/>
1398                   <w:right w:val="single" w:sz="4" w:color="000000"/>
1399                   <w:insideH w:val="single" w:sz="4" w:color="000000"/>
1400                   <w:insideV w:val="single" w:sz="4" w:color="000000"/>
1401                 </w:tblBorders>
1402               </w:tblPr>
1403               <w:tblGrid><w:gridCol w:w="5000"/></w:tblGrid>
1404               <w:tr><w:tc><w:p/></w:tc></w:tr>"#,
1405        );
1406
1407        let borders = tbl.properties.unwrap().borders.unwrap();
1408        assert_eq!(borders.top.unwrap().val, ST_Border::Single);
1409        assert_eq!(borders.inside_h.unwrap().val, ST_Border::Single);
1410        assert_eq!(borders.inside_v.unwrap().val, ST_Border::Single);
1411    }
1412
1413    #[test]
1414    fn parse_cell_shading() {
1415        let tbl = parse_table(
1416            r#"<w:tblGrid><w:gridCol w:w="5000"/></w:tblGrid>
1417               <w:tr>
1418                 <w:tc>
1419                   <w:tcPr><w:shd w:val="clear" w:fill="FFFF00"/></w:tcPr>
1420                   <w:p/>
1421                 </w:tc>
1422               </w:tr>"#,
1423        );
1424
1425        let shd = tbl.rows[0].cells[0]
1426            .properties
1427            .as_ref()
1428            .unwrap()
1429            .shading
1430            .as_ref()
1431            .unwrap();
1432        assert_eq!(shd.fill, Some("FFFF00".to_string()));
1433    }
1434
1435    #[test]
1436    fn parse_row_properties() {
1437        let tbl = parse_table(
1438            r#"<w:tblGrid><w:gridCol w:w="5000"/></w:tblGrid>
1439               <w:tr>
1440                 <w:trPr>
1441                   <w:trHeight w:val="720" w:hRule="exact"/>
1442                   <w:tblHeader/>
1443                 </w:trPr>
1444                 <w:tc><w:p/></w:tc>
1445               </w:tr>"#,
1446        );
1447
1448        let tr_pr = tbl.rows[0].properties.as_ref().unwrap();
1449        assert_eq!(tr_pr.height, Some(Twips(720)));
1450        assert_eq!(tr_pr.height_rule, Some("exact".to_string()));
1451        assert_eq!(tr_pr.header, Some(true));
1452    }
1453
1454    #[test]
1455    fn round_trip_table() {
1456        let mut tbl = CT_Tbl::new();
1457        tbl.properties = Some(CT_TblPr {
1458            width: Some(CT_TblWidth::dxa(9000)),
1459            borders: Some(CT_TblBorders {
1460                top: Some(CT_BorderEdge {
1461                    val: ST_Border::Single,
1462                    sz: Some(4),
1463                    space: Some(0),
1464                    color: Some("000000".to_string()),
1465                }),
1466                bottom: Some(CT_BorderEdge {
1467                    val: ST_Border::Single,
1468                    sz: Some(4),
1469                    space: Some(0),
1470                    color: Some("000000".to_string()),
1471                }),
1472                ..Default::default()
1473            }),
1474            ..Default::default()
1475        });
1476        tbl.grid = Some(CT_TblGrid {
1477            columns: vec![
1478                CT_TblGridCol { width: Twips(4500) },
1479                CT_TblGridCol { width: Twips(4500) },
1480            ],
1481        });
1482
1483        let mut row = CT_Row::new();
1484        let mut cell1 = CT_Tc::new();
1485        cell1.paragraphs_mut()[0].add_run("Hello");
1486        let mut cell2 = CT_Tc::new();
1487        cell2.paragraphs_mut()[0].add_run("World");
1488        row.cells.push(cell1);
1489        row.cells.push(cell2);
1490        tbl.rows.push(row);
1491
1492        // Serialize
1493        let mut output = Vec::new();
1494        let mut writer = Writer::new(&mut output);
1495        tbl.to_xml(&mut writer).unwrap();
1496        let xml = String::from_utf8(output).unwrap();
1497
1498        // Parse back
1499        let parsed = parse_table(
1500            xml.strip_prefix("<w:tbl>")
1501                .unwrap()
1502                .strip_suffix("</w:tbl>")
1503                .unwrap(),
1504        );
1505
1506        assert_eq!(parsed.rows.len(), 1);
1507        assert_eq!(parsed.rows[0].cells.len(), 2);
1508        assert_eq!(parsed.rows[0].cells[0].text(), "Hello");
1509        assert_eq!(parsed.rows[0].cells[1].text(), "World");
1510
1511        let grid = parsed.grid.unwrap();
1512        assert_eq!(grid.columns.len(), 2);
1513        assert_eq!(grid.columns[0].width, Twips(4500));
1514
1515        let borders = parsed.properties.unwrap().borders.unwrap();
1516        assert!(borders.top.is_some());
1517        assert!(borders.bottom.is_some());
1518    }
1519
1520    #[test]
1521    fn nested_table_xml_round_trip() {
1522        use crate::text::CT_P;
1523
1524        // Build a cell containing a paragraph + a nested table
1525        let mut outer_cell = CT_Tc::new();
1526        outer_cell.paragraphs_mut()[0].add_run("Before table");
1527
1528        let mut nested_tbl = CT_Tbl::new();
1529        nested_tbl.grid = Some(CT_TblGrid {
1530            columns: vec![CT_TblGridCol { width: Twips(2000) }],
1531        });
1532        let mut nested_row = CT_Row::new();
1533        let mut nested_cell = CT_Tc::new();
1534        nested_cell.paragraphs_mut()[0].add_run("Nested content");
1535        nested_row.cells.push(nested_cell);
1536        nested_tbl.rows.push(nested_row);
1537
1538        outer_cell.content.push(CellContent::Table(nested_tbl));
1539
1540        let mut after = CT_P::new();
1541        after.add_run("After table");
1542        outer_cell.content.push(CellContent::Paragraph(after));
1543
1544        // Serialize
1545        let mut output = Vec::new();
1546        let mut writer = Writer::new(&mut output);
1547        outer_cell.to_xml(&mut writer).unwrap();
1548        let xml = String::from_utf8(output).unwrap();
1549
1550        // Should contain nested <w:tbl>
1551        assert!(xml.contains("<w:tbl>"));
1552        assert!(xml.contains("Nested content"));
1553
1554        // Parse back
1555        let inner_xml = xml
1556            .strip_prefix("<w:tc>")
1557            .unwrap()
1558            .strip_suffix("</w:tc>")
1559            .unwrap();
1560        let full_xml = format!(
1561            "<w:tc xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\">{inner_xml}</w:tc>"
1562        );
1563        let mut reader = Reader::from_str(&full_xml);
1564        reader.config_mut().trim_text(true);
1565        // Skip start tag
1566        loop {
1567            match reader.read_event() {
1568                Ok(Event::Start(e)) if e.local_name().as_ref() == b"tc" => break,
1569                _ => {}
1570            }
1571        }
1572        let parsed = CT_Tc::from_xml(&mut reader).unwrap();
1573
1574        // Check structure: 2 paragraphs + 1 nested table
1575        assert_eq!(parsed.paragraphs().len(), 2);
1576        assert_eq!(parsed.paragraphs()[0].text(), "Before table");
1577        assert_eq!(parsed.paragraphs()[1].text(), "After table");
1578
1579        // Check nested table
1580        let tables: Vec<_> = parsed
1581            .content
1582            .iter()
1583            .filter_map(|c| match c {
1584                CellContent::Table(t) => Some(t),
1585                _ => None,
1586            })
1587            .collect();
1588        assert_eq!(tables.len(), 1);
1589        assert_eq!(tables[0].rows.len(), 1);
1590        assert_eq!(tables[0].rows[0].cells[0].text(), "Nested content");
1591    }
1592
1593    #[test]
1594    fn paragraphs_method_backward_compat() {
1595        let mut cell = CT_Tc::new();
1596        // Cell starts with one empty paragraph
1597        assert_eq!(cell.paragraphs().len(), 1);
1598
1599        // Add a run to existing paragraph
1600        cell.paragraphs_mut()[0].add_run("First");
1601
1602        // Add a nested table (should not appear in paragraphs())
1603        let nested = CT_Tbl::new();
1604        cell.content.push(CellContent::Table(nested));
1605
1606        // Add another paragraph
1607        let mut p = CT_P::new();
1608        p.add_run("Second");
1609        cell.content.push(CellContent::Paragraph(p));
1610
1611        // paragraphs() should return only the 2 CT_P items
1612        assert_eq!(cell.paragraphs().len(), 2);
1613        assert_eq!(cell.paragraphs()[0].text(), "First");
1614        assert_eq!(cell.paragraphs()[1].text(), "Second");
1615
1616        // text() should concat paragraph text with newline separator
1617        assert_eq!(cell.text(), "First\nSecond");
1618    }
1619
1620    /// Serialize a table and return the XML, for the fidelity tests below.
1621    fn table_to_xml(tbl: &CT_Tbl) -> String {
1622        let mut output = Vec::new();
1623        let mut writer = Writer::new(&mut output);
1624        tbl.to_xml(&mut writer).unwrap();
1625        String::from_utf8(output).unwrap()
1626    }
1627
1628    /// Table children we do not model must survive a read and write cycle.
1629    ///
1630    /// These used to be dropped, which silently deleted whole rows, cells and
1631    /// paragraphs whenever they were wrapped in a content control, and lost
1632    /// the bookmarks that cross references and a table of figures rely on.
1633    #[test]
1634    fn unknown_table_children_round_trip() {
1635        const GRID: &str = r#"<w:tblGrid><w:gridCol w:w="4675"/></w:tblGrid>"#;
1636
1637        for (label, inner) in [
1638            (
1639                "row wrapped in a content control",
1640                format!(
1641                    r#"{GRID}<w:sdt><w:sdtContent><w:tr><w:tc><w:p><w:r><w:t>x</w:t></w:r></w:p></w:tc></w:tr></w:sdtContent></w:sdt>"#
1642                ),
1643            ),
1644            (
1645                "cell wrapped in a content control",
1646                format!(
1647                    r#"{GRID}<w:tr><w:sdt><w:sdtContent><w:tc><w:p><w:r><w:t>x</w:t></w:r></w:p></w:tc></w:sdtContent></w:sdt></w:tr>"#
1648                ),
1649            ),
1650            (
1651                "paragraph wrapped in a content control",
1652                format!(
1653                    r#"{GRID}<w:tr><w:tc><w:sdt><w:sdtContent><w:p><w:r><w:t>x</w:t></w:r></w:p></w:sdtContent></w:sdt></w:tc></w:tr>"#
1654                ),
1655            ),
1656            (
1657                "bookmark at table level",
1658                format!(
1659                    r#"{GRID}<w:bookmarkStart w:id="1" w:name="b"/><w:tr><w:tc><w:p><w:r><w:t>x</w:t></w:r></w:p></w:tc></w:tr>"#
1660                ),
1661            ),
1662            (
1663                "bookmark at row level",
1664                format!(
1665                    r#"{GRID}<w:tr><w:bookmarkStart w:id="1" w:name="b"/><w:tc><w:p><w:r><w:t>x</w:t></w:r></w:p></w:tc></w:tr>"#
1666                ),
1667            ),
1668        ] {
1669            let tbl = parse_table(&inner);
1670            let xml = table_to_xml(&tbl);
1671            assert_eq!(
1672                xml,
1673                format!("<w:tbl>{inner}</w:tbl>"),
1674                "{label} was not preserved"
1675            );
1676        }
1677    }
1678
1679    /// A styled table must keep the markup that says which conditional parts
1680    /// of its style apply.
1681    ///
1682    /// `w:tblStyle` alone is not enough. `w:tblLook` and `w:cnfStyle` are what
1683    /// turn on the header row, banding and first column formatting, so losing
1684    /// them leaves the style name intact and the table drawn with base
1685    /// formatting only, which reads as the style having been lost.
1686    #[test]
1687    fn table_style_conditional_formatting_round_trips() {
1688        let inner = concat!(
1689            r#"<w:tblPr><w:tblStyle w:val="GridTable4-Accent1"/>"#,
1690            r#"<w:tblLook w:val="04A0" w:firstRow="1" w:lastRow="0" w:firstColumn="1" w:lastColumn="0" w:noHBand="0" w:noVBand="1"/>"#,
1691            r#"</w:tblPr><w:tblGrid><w:gridCol w:w="4675"/></w:tblGrid>"#,
1692            r#"<w:tr><w:trPr><w:cnfStyle w:val="100000000000"/></w:trPr>"#,
1693            r#"<w:tc><w:tcPr><w:cnfStyle w:val="001000000000"/></w:tcPr><w:p/></w:tc>"#,
1694            r#"</w:tr>"#,
1695        );
1696        let tbl = parse_table(inner);
1697
1698        let look = tbl
1699            .properties
1700            .as_ref()
1701            .and_then(|p| p.look.as_ref())
1702            .expect("tblLook should be parsed");
1703        assert_eq!(look.val.as_deref(), Some("04A0"));
1704        assert_eq!(look.first_row, Some(true));
1705        assert_eq!(look.last_row, Some(false));
1706        assert_eq!(look.first_column, Some(true));
1707        assert_eq!(look.no_v_band, Some(true));
1708
1709        assert_eq!(
1710            tbl.rows[0]
1711                .properties
1712                .as_ref()
1713                .and_then(|p| p.cnf_style.as_deref()),
1714            Some("100000000000")
1715        );
1716        assert_eq!(
1717            tbl.rows[0].cells[0]
1718                .properties
1719                .as_ref()
1720                .and_then(|p| p.cnf_style.as_deref()),
1721            Some("001000000000")
1722        );
1723
1724        assert_eq!(
1725            table_to_xml(&tbl),
1726            format!("<w:tbl>{inner}</w:tbl>"),
1727            "the whole thing must survive a write"
1728        );
1729    }
1730
1731    /// A row or cell carrying only cnfStyle is not empty.
1732    ///
1733    /// Both types skip writing their properties when every field is unset, so
1734    /// a new field that is not in that check is parsed and then silently
1735    /// dropped on the way out.
1736    #[test]
1737    fn properties_holding_only_cnf_style_are_still_written() {
1738        let tbl = parse_table(concat!(
1739            r#"<w:tblGrid><w:gridCol w:w="100"/></w:tblGrid>"#,
1740            r#"<w:tr><w:trPr><w:cnfStyle w:val="100000000000"/></w:trPr>"#,
1741            r#"<w:tc><w:tcPr><w:cnfStyle w:val="001000000000"/></w:tcPr><w:p/></w:tc></w:tr>"#,
1742        ));
1743        let xml = table_to_xml(&tbl);
1744        assert!(
1745            xml.contains(r#"<w:trPr><w:cnfStyle w:val="100000000000"/></w:trPr>"#),
1746            "{xml}"
1747        );
1748        assert!(
1749            xml.contains(r#"<w:tcPr><w:cnfStyle w:val="001000000000"/></w:tcPr>"#),
1750            "{xml}"
1751        );
1752    }
1753
1754    /// OOXML booleans come in both spellings.
1755    #[test]
1756    fn tbl_look_accepts_either_boolean_spelling() {
1757        let tbl = parse_table(concat!(
1758            r#"<w:tblPr><w:tblLook w:firstRow="true" w:lastRow="false" w:noVBand="1"/></w:tblPr>"#,
1759            r#"<w:tblGrid><w:gridCol w:w="100"/></w:tblGrid>"#,
1760        ));
1761        let look = tbl
1762            .properties
1763            .as_ref()
1764            .and_then(|p| p.look.as_ref())
1765            .unwrap();
1766        assert_eq!(look.first_row, Some(true));
1767        assert_eq!(look.last_row, Some(false));
1768        assert_eq!(look.no_v_band, Some(true));
1769    }
1770
1771    /// A self-closing tblPr or tblGrid must not be captured as extra XML.
1772    /// Both have a fixed position ahead of the rows, and extras are written
1773    /// from the row positions, so capturing them would reorder the children.
1774    #[test]
1775    fn self_closing_table_properties_are_not_reordered() {
1776        let tbl = parse_table(
1777            r#"<w:tblPr/><w:tblGrid><w:gridCol w:w="100"/></w:tblGrid><w:tr><w:tc><w:p/></w:tc></w:tr>"#,
1778        );
1779        assert!(tbl.extra_xml.is_empty(), "tblPr must not be captured");
1780        let xml = table_to_xml(&tbl);
1781        assert!(
1782            !xml.contains("</w:tr><w:tblPr/>"),
1783            "tblPr must never follow the rows: {xml}"
1784        );
1785    }
1786}