1use crate::oxml::color::Color;
35use crate::oxml::txbody::TextBody;
36use crate::units::Emu;
37
38#[derive(Clone, Debug, Default)]
40pub struct Col {
41 pub width: Emu,
43}
44
45#[derive(Clone, Debug, Default)]
47pub struct Row {
48 pub height: Emu,
50 pub cells: Vec<Cell>,
52 pub header: bool,
54}
55
56#[derive(Clone, Debug, Default, PartialEq)]
60pub enum VerticalAnchor {
61 Top,
63 #[default]
65 Middle,
66 Bottom,
68}
69
70impl VerticalAnchor {
71 pub fn as_str(&self) -> &'static str {
73 match self {
74 VerticalAnchor::Top => "t",
75 VerticalAnchor::Middle => "ctr",
76 VerticalAnchor::Bottom => "b",
77 }
78 }
79}
80
81#[derive(Clone, Debug, Default)]
85pub struct CellBorder {
86 pub color: Color,
88 pub width: Emu,
90 pub no_fill: bool,
92}
93
94#[derive(Clone, Debug, Default)]
96pub struct Cell {
97 pub text: TextBody,
99 pub fill: Color,
101 pub margin: (Option<Emu>, Option<Emu>, Option<Emu>, Option<Emu>), pub row_span: u32,
105 pub grid_span: u32,
107 pub h_merge: bool,
109 pub v_merge: bool,
111 pub anchor: VerticalAnchor,
113 pub border_left: Option<CellBorder>,
115 pub border_right: Option<CellBorder>,
117 pub border_top: Option<CellBorder>,
119 pub border_bottom: Option<CellBorder>,
121}
122
123#[derive(Clone, Debug)]
127pub struct TableLook {
128 pub val: String,
130 pub first_row: bool,
132 pub last_row: bool,
134 pub first_column: bool,
136 pub last_column: bool,
138 pub no_h_band: bool,
140 pub no_v_band: bool,
142}
143
144impl Default for TableLook {
145 fn default() -> Self {
146 Self {
148 val: "04A0".to_string(),
149 first_row: true,
150 last_row: false,
151 first_column: true,
152 last_column: false,
153 no_h_band: false,
154 no_v_band: true,
155 }
156 }
157}
158
159#[derive(Clone, Debug, PartialEq, Eq)]
188pub struct TableStyle {
189 style_id: String,
191 style_name: Option<String>,
193}
194
195impl TableStyle {
196 pub fn new(style_id: impl Into<String>) -> Self {
201 Self {
202 style_id: style_id.into(),
203 style_name: None,
204 }
205 }
206
207 pub fn with_name(style_id: impl Into<String>, style_name: impl Into<String>) -> Self {
213 Self {
214 style_id: style_id.into(),
215 style_name: Some(style_name.into()),
216 }
217 }
218
219 pub fn from_name(name: &str) -> Option<Self> {
236 builtin_table_style_guid(name).map(|guid| Self::with_name(guid, name))
237 }
238
239 pub fn style_id(&self) -> &str {
241 &self.style_id
242 }
243
244 pub fn style_name(&self) -> Option<&str> {
246 self.style_name.as_deref()
247 }
248}
249
250fn builtin_table_style_guid(name: &str) -> Option<&'static str> {
255 match name {
259 "No Style, Table Grid" => Some("{5940675A-B579-460E-94D1-54222C63F5DA}"),
261 "No Style, No Grid" => Some("{2D5ABB26-0587-4C30-8999-92F81FD0307C}"),
262 "Medium Style 2 - Accent 1" => Some("{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}"),
264 "Themed Style 1 - Accent 1" => Some("{3C2FFA5D-87B4-456A-9821-1D502468CF0F}"),
266 _ => None,
267 }
268}
269
270#[derive(Clone, Debug, Default)]
272pub struct Table {
273 pub cols: Vec<Col>,
274 pub rows: Vec<Row>,
275 pub tbl_look: TableLook,
277 pub table_style: Option<TableStyle>,
281}
282
283impl Table {
284 pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
305 w.open("a:tbl");
306 w.open("a:tblPr");
308 w.empty_with("a:tblW", &[("w", "0"), ("type", "auto")]);
309 let lk = &self.tbl_look;
311 w.empty_with(
312 "a:tblLook",
313 &[
314 ("val", lk.val.as_str()),
315 ("firstRow", if lk.first_row { "1" } else { "0" }),
316 ("lastRow", if lk.last_row { "1" } else { "0" }),
317 ("firstColumn", if lk.first_column { "1" } else { "0" }),
318 ("lastColumn", if lk.last_column { "1" } else { "0" }),
319 ("noHBand", if lk.no_h_band { "1" } else { "0" }),
320 ("noVBand", if lk.no_v_band { "1" } else { "0" }),
321 ],
322 );
323 if let Some(style) = &self.table_style {
325 w.leaf("a:tableStyleId", style.style_id());
326 }
327 w.close("a:tblPr");
328 w.open("a:tblGrid");
330 for c in &self.cols {
331 w.empty_with("a:gridCol", &[("w", &c.width.value().to_string())]);
332 }
333 w.close("a:tblGrid");
334 for r in &self.rows {
336 w.open_with("a:tr", &[("h", &r.height.value().to_string())]);
337 for c in &r.cells {
338 let mut tc_attrs: Vec<(&str, String)> = Vec::new();
340 if c.grid_span > 1 {
341 tc_attrs.push(("gridSpan", c.grid_span.to_string()));
342 }
343 if c.row_span > 1 {
344 tc_attrs.push(("rowSpan", c.row_span.to_string()));
345 }
346 if c.h_merge {
347 tc_attrs.push(("hMerge", "1".to_string()));
348 }
349 if c.v_merge {
350 tc_attrs.push(("vMerge", "1".to_string()));
351 }
352 let attr_refs: Vec<(&str, &str)> =
353 tc_attrs.iter().map(|(k, v)| (*k, v.as_str())).collect();
354 if attr_refs.is_empty() {
355 w.open("a:tc");
356 } else {
357 w.open_with("a:tc", &attr_refs);
358 }
359
360 if c.text.paragraphs.is_empty() {
362 let tb = TextBody::new();
363 tb.write_xml(w, "a:txBody");
364 } else {
365 c.text.write_xml(w, "a:txBody");
366 }
367
368 let has_margins = c.margin.0.is_some()
370 || c.margin.1.is_some()
371 || c.margin.2.is_some()
372 || c.margin.3.is_some();
373 let has_borders = c.border_left.is_some()
374 || c.border_right.is_some()
375 || c.border_top.is_some()
376 || c.border_bottom.is_some();
377 let has_fill = !matches!(c.fill, Color::None);
378 let has_anchor = c.anchor != VerticalAnchor::default();
379
380 if has_margins || has_borders || has_fill || has_anchor {
381 let mart_s = c.margin.0.map(|m| m.value().to_string());
383 let marl_s = c.margin.1.map(|m| m.value().to_string());
384 let marb_s = c.margin.2.map(|m| m.value().to_string());
385 let marr_s = c.margin.3.map(|m| m.value().to_string());
386 let mut tcpr_attrs: Vec<(&str, &str)> = Vec::new();
387 if let Some(s) = &mart_s {
388 tcpr_attrs.push(("marT", s));
389 }
390 if let Some(s) = &marl_s {
391 tcpr_attrs.push(("marL", s));
392 }
393 if let Some(s) = &marb_s {
394 tcpr_attrs.push(("marB", s));
395 }
396 if let Some(s) = &marr_s {
397 tcpr_attrs.push(("marR", s));
398 }
399 if has_anchor {
400 tcpr_attrs.push(("anchor", c.anchor.as_str()));
401 }
402 if tcpr_attrs.is_empty() {
403 w.open("a:tcPr");
404 } else {
405 w.open_with("a:tcPr", &tcpr_attrs);
406 }
407 if has_fill {
409 c.fill.write_solid_fill(w);
410 }
411 write_cell_border(w, "a:lnL", c.border_left.as_ref());
413 write_cell_border(w, "a:lnR", c.border_right.as_ref());
414 write_cell_border(w, "a:lnT", c.border_top.as_ref());
415 write_cell_border(w, "a:lnB", c.border_bottom.as_ref());
416 w.close("a:tcPr");
417 } else if has_fill {
418 w.open("a:tcPr");
420 c.fill.write_solid_fill(w);
421 w.close("a:tcPr");
422 }
423 w.close("a:tc");
424 }
425 w.close("a:tr");
426 }
427 w.close("a:tbl");
428 }
429
430 pub fn set_style(&mut self, name: &str) -> bool {
454 if let Some(style) = TableStyle::from_name(name) {
455 self.table_style = Some(style);
456 true
457 } else {
458 false
459 }
460 }
461
462 pub fn set_style_id(&mut self, guid: impl Into<String>) {
469 self.table_style = Some(TableStyle::new(guid));
470 }
471
472 pub fn clear_style(&mut self) {
474 self.table_style = None;
475 }
476}
477
478fn write_cell_border(w: &mut super::writer::XmlWriter, tag: &str, border: Option<&CellBorder>) {
482 if let Some(b) = border {
483 let w_s = b.width.value().to_string();
485 if b.no_fill {
486 w.open_with(tag, &[("w", w_s.as_str())]);
487 w.empty("a:noFill");
488 w.close(tag);
489 } else if !matches!(b.color, Color::None) {
490 w.open_with(tag, &[("w", w_s.as_str())]);
491 b.color.write_solid_fill(w);
492 w.close(tag);
493 } else {
494 w.empty_with(tag, &[("w", w_s.as_str())]);
496 }
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503
504 #[test]
506 fn table_style_from_name_finds_builtin() {
507 let style = TableStyle::from_name("Medium Style 2 - Accent 1")
508 .expect("Medium Style 2 - Accent 1 应在注册表中");
509 assert_eq!(style.style_id(), "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}");
510 assert_eq!(style.style_name(), Some("Medium Style 2 - Accent 1"));
511
512 let style2 = TableStyle::from_name("No Style, Table Grid")
513 .expect("No Style, Table Grid 应在注册表中");
514 assert_eq!(style2.style_id(), "{5940675A-B579-460E-94D1-54222C63F5DA}");
515 }
516
517 #[test]
519 fn table_style_from_name_unknown_returns_none() {
520 assert!(TableStyle::from_name("Nonexistent Style").is_none());
521 }
522
523 #[test]
525 fn table_style_new_has_no_name() {
526 let style = TableStyle::new("{5940675A-B579-460E-94D1-54222C63F5DA}");
527 assert_eq!(style.style_id(), "{5940675A-B579-460E-94D1-54222C63F5DA}");
528 assert_eq!(style.style_name(), None);
529 }
530
531 #[test]
533 fn table_set_style_builtin() {
534 let mut t = Table::default();
535 assert!(t.set_style("Medium Style 2 - Accent 1"));
536 let style = t.table_style.as_ref().expect("style 应已设置");
537 assert_eq!(style.style_id(), "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}");
538 assert_eq!(style.style_name(), Some("Medium Style 2 - Accent 1"));
539 }
540
541 #[test]
543 fn table_set_style_unknown_returns_false() {
544 let mut t = Table::default();
545 t.set_style("Medium Style 2 - Accent 1");
546 assert!(!t.set_style("Unknown Style"));
547 assert_eq!(
549 t.table_style.as_ref().unwrap().style_id(),
550 "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}"
551 );
552 }
553
554 #[test]
556 fn table_set_style_id_raw_guid() {
557 let mut t = Table::default();
558 t.set_style_id("{5940675A-B579-460E-94D1-54222C63F5DA}");
559 let style = t.table_style.as_ref().expect("style 应已设置");
560 assert_eq!(style.style_id(), "{5940675A-B579-460E-94D1-54222C63F5DA}");
561 assert_eq!(style.style_name(), None);
562 }
563
564 #[test]
566 fn table_clear_style() {
567 let mut t = Table::default();
568 t.set_style("Medium Style 2 - Accent 1");
569 assert!(t.table_style.is_some());
570 t.clear_style();
571 assert!(t.table_style.is_none());
572 }
573
574 #[test]
576 fn table_write_xml_with_style() {
577 let mut t = Table::default();
578 t.set_style("Medium Style 2 - Accent 1");
579 let mut w = crate::oxml::writer::XmlWriter::new();
580 t.write_xml(&mut w);
581 let xml = &w.buf;
582 assert!(xml.contains("<a:tableStyleId>"));
583 assert!(xml.contains("{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}"));
584 assert!(xml.contains("</a:tableStyleId>"));
585 let pr_start = xml.find("<a:tblPr>").unwrap();
587 let pr_end = xml.find("</a:tblPr>").unwrap();
588 let style_pos = xml.find("<a:tableStyleId>").unwrap();
589 let style_end = xml.find("</a:tableStyleId>").unwrap();
590 assert!(style_pos > pr_start && style_end < pr_end);
591 }
592
593 #[test]
595 fn table_write_xml_without_style() {
596 let t = Table::default();
597 let mut w = crate::oxml::writer::XmlWriter::new();
598 t.write_xml(&mut w);
599 assert!(!w.buf.contains("tableStyleId"));
600 }
601}