1use 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
18fn 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#[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#[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#[derive(Debug, Clone, PartialEq)]
221pub struct CT_TblWidth {
222 pub w: i32,
224 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#[derive(Debug, Clone, PartialEq)]
282pub struct CT_TblGridCol {
283 pub width: Twips,
285}
286
287#[derive(Debug, Clone, Default, PartialEq)]
291pub struct CT_TblPr {
292 pub style_id: Option<String>,
294 pub width: Option<CT_TblWidth>,
296 pub jc: Option<ST_Jc>,
298 pub borders: Option<CT_TblBorders>,
300 pub cell_margin: Option<CT_TblCellMar>,
302 pub layout: Option<String>,
304 pub indent: Option<CT_TblWidth>,
306 pub shading: Option<CT_Shd>,
308 pub look: Option<CT_TblLook>,
310}
311
312#[derive(Debug, Clone, PartialEq, Default)]
324#[allow(non_snake_case)]
325pub struct CT_TblLook {
326 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
336fn 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
562pub enum VMerge {
563 Restart,
565 Continue,
567}
568
569#[derive(Debug, Clone, Default, PartialEq)]
571pub struct CT_TrPr {
572 pub height: Option<Twips>,
574 pub height_rule: Option<String>,
576 pub header: Option<bool>,
578 pub jc: Option<ST_Jc>,
580 pub cant_split: Option<bool>,
582 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 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#[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#[derive(Debug, Clone, Default, PartialEq)]
718pub struct CT_TcPr {
719 pub width: Option<CT_TblWidth>,
721 pub grid_span: Option<u32>,
723 pub v_merge: Option<VMerge>,
725 pub borders: Option<CT_TblBorders>,
727 pub shading: Option<CT_Shd>,
729 pub v_align: Option<ST_VerticalJc>,
731 pub no_wrap: Option<bool>,
733 pub text_direction: Option<String>,
735 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 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 => {} }
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#[derive(Debug, Clone, PartialEq)]
885pub enum CellContent {
886 Paragraph(CT_P),
888 Table(CT_Tbl),
890}
891
892#[derive(Debug, Clone, PartialEq)]
894pub struct CT_Tc {
895 pub properties: Option<CT_TcPr>,
896 pub content: Vec<CellContent>,
898 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 content: vec![CellContent::Paragraph(CT_P::new())],
911 extra_xml: Vec::new(),
912 }
913 }
914
915 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 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 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#[derive(Debug, Clone, PartialEq)]
1035pub struct CT_Row {
1036 pub properties: Option<CT_TrPr>,
1037 pub cells: Vec<CT_Tc>,
1038 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 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#[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 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 extra_xml.push((rows.len(), capture_element(reader, e)?));
1181 }
1182 }
1183 Ok(Event::Empty(ref e)) => {
1184 let name = e.name();
1185 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 assert_eq!(
1373 tbl.rows[0].cells[0].properties.as_ref().unwrap().grid_span,
1374 Some(2)
1375 );
1376
1377 assert_eq!(
1379 tbl.rows[1].cells[0].properties.as_ref().unwrap().v_merge,
1380 Some(VMerge::Restart)
1381 );
1382
1383 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 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 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 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 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 assert!(xml.contains("<w:tbl>"));
1552 assert!(xml.contains("Nested content"));
1553
1554 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 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 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 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 assert_eq!(cell.paragraphs().len(), 1);
1598
1599 cell.paragraphs_mut()[0].add_run("First");
1601
1602 let nested = CT_Tbl::new();
1604 cell.content.push(CellContent::Table(nested));
1605
1606 let mut p = CT_P::new();
1608 p.add_run("Second");
1609 cell.content.push(CellContent::Paragraph(p));
1610
1611 assert_eq!(cell.paragraphs().len(), 2);
1613 assert_eq!(cell.paragraphs()[0].text(), "First");
1614 assert_eq!(cell.paragraphs()[1].text(), "Second");
1615
1616 assert_eq!(cell.text(), "First\nSecond");
1618 }
1619
1620 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 #[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 #[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 #[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 #[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 #[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}