use crate::oxml::color::Color;
use crate::oxml::txbody::TextBody;
use crate::units::Emu;
#[derive(Clone, Debug, Default)]
pub struct Col {
pub width: Emu,
}
#[derive(Clone, Debug, Default)]
pub struct Row {
pub height: Emu,
pub cells: Vec<Cell>,
pub header: bool,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub enum VerticalAnchor {
Top,
#[default]
Middle,
Bottom,
}
impl VerticalAnchor {
pub fn as_str(&self) -> &'static str {
match self {
VerticalAnchor::Top => "t",
VerticalAnchor::Middle => "ctr",
VerticalAnchor::Bottom => "b",
}
}
}
#[derive(Clone, Debug, Default)]
pub struct CellBorder {
pub color: Color,
pub width: Emu,
pub no_fill: bool,
}
#[derive(Clone, Debug, Default)]
pub struct Cell {
pub text: TextBody,
pub fill: Color,
pub margin: (Option<Emu>, Option<Emu>, Option<Emu>, Option<Emu>), pub row_span: u32,
pub grid_span: u32,
pub h_merge: bool,
pub v_merge: bool,
pub anchor: VerticalAnchor,
pub border_left: Option<CellBorder>,
pub border_right: Option<CellBorder>,
pub border_top: Option<CellBorder>,
pub border_bottom: Option<CellBorder>,
}
#[derive(Clone, Debug)]
pub struct TableLook {
pub val: String,
pub first_row: bool,
pub last_row: bool,
pub first_column: bool,
pub last_column: bool,
pub no_h_band: bool,
pub no_v_band: bool,
}
impl Default for TableLook {
fn default() -> Self {
Self {
val: "04A0".to_string(),
first_row: true,
last_row: false,
first_column: true,
last_column: false,
no_h_band: false,
no_v_band: true,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TableStyle {
style_id: String,
style_name: Option<String>,
}
impl TableStyle {
pub fn new(style_id: impl Into<String>) -> Self {
Self {
style_id: style_id.into(),
style_name: None,
}
}
pub fn with_name(style_id: impl Into<String>, style_name: impl Into<String>) -> Self {
Self {
style_id: style_id.into(),
style_name: Some(style_name.into()),
}
}
pub fn from_name(name: &str) -> Option<Self> {
builtin_table_style_guid(name).map(|guid| Self::with_name(guid, name))
}
pub fn style_id(&self) -> &str {
&self.style_id
}
pub fn style_name(&self) -> Option<&str> {
self.style_name.as_deref()
}
}
fn builtin_table_style_guid(name: &str) -> Option<&'static str> {
match name {
"No Style, Table Grid" => Some("{5940675A-B579-460E-94D1-54222C63F5DA}"),
"No Style, No Grid" => Some("{2D5ABB26-0587-4C30-8999-92F81FD0307C}"),
"Medium Style 2 - Accent 1" => Some("{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}"),
"Themed Style 1 - Accent 1" => Some("{3C2FFA5D-87B4-456A-9821-1D502468CF0F}"),
_ => None,
}
}
#[derive(Clone, Debug, Default)]
pub struct Table {
pub cols: Vec<Col>,
pub rows: Vec<Row>,
pub tbl_look: TableLook,
pub table_style: Option<TableStyle>,
}
impl Table {
pub fn write_xml(&self, w: &mut super::writer::XmlWriter) {
w.open("a:tbl");
w.open("a:tblPr");
w.empty_with("a:tblW", &[("w", "0"), ("type", "auto")]);
let lk = &self.tbl_look;
w.empty_with(
"a:tblLook",
&[
("val", lk.val.as_str()),
("firstRow", if lk.first_row { "1" } else { "0" }),
("lastRow", if lk.last_row { "1" } else { "0" }),
("firstColumn", if lk.first_column { "1" } else { "0" }),
("lastColumn", if lk.last_column { "1" } else { "0" }),
("noHBand", if lk.no_h_band { "1" } else { "0" }),
("noVBand", if lk.no_v_band { "1" } else { "0" }),
],
);
if let Some(style) = &self.table_style {
w.leaf("a:tableStyleId", style.style_id());
}
w.close("a:tblPr");
w.open("a:tblGrid");
for c in &self.cols {
w.empty_with("a:gridCol", &[("w", &c.width.value().to_string())]);
}
w.close("a:tblGrid");
for r in &self.rows {
w.open_with("a:tr", &[("h", &r.height.value().to_string())]);
for c in &r.cells {
let mut tc_attrs: Vec<(&str, String)> = Vec::new();
if c.grid_span > 1 {
tc_attrs.push(("gridSpan", c.grid_span.to_string()));
}
if c.row_span > 1 {
tc_attrs.push(("rowSpan", c.row_span.to_string()));
}
if c.h_merge {
tc_attrs.push(("hMerge", "1".to_string()));
}
if c.v_merge {
tc_attrs.push(("vMerge", "1".to_string()));
}
let attr_refs: Vec<(&str, &str)> =
tc_attrs.iter().map(|(k, v)| (*k, v.as_str())).collect();
if attr_refs.is_empty() {
w.open("a:tc");
} else {
w.open_with("a:tc", &attr_refs);
}
if c.text.paragraphs.is_empty() {
let tb = TextBody::new();
tb.write_xml(w, "a:txBody");
} else {
c.text.write_xml(w, "a:txBody");
}
let has_margins = c.margin.0.is_some()
|| c.margin.1.is_some()
|| c.margin.2.is_some()
|| c.margin.3.is_some();
let has_borders = c.border_left.is_some()
|| c.border_right.is_some()
|| c.border_top.is_some()
|| c.border_bottom.is_some();
let has_fill = !matches!(c.fill, Color::None);
let has_anchor = c.anchor != VerticalAnchor::default();
if has_margins || has_borders || has_fill || has_anchor {
let mart_s = c.margin.0.map(|m| m.value().to_string());
let marl_s = c.margin.1.map(|m| m.value().to_string());
let marb_s = c.margin.2.map(|m| m.value().to_string());
let marr_s = c.margin.3.map(|m| m.value().to_string());
let mut tcpr_attrs: Vec<(&str, &str)> = Vec::new();
if let Some(s) = &mart_s {
tcpr_attrs.push(("marT", s));
}
if let Some(s) = &marl_s {
tcpr_attrs.push(("marL", s));
}
if let Some(s) = &marb_s {
tcpr_attrs.push(("marB", s));
}
if let Some(s) = &marr_s {
tcpr_attrs.push(("marR", s));
}
if has_anchor {
tcpr_attrs.push(("anchor", c.anchor.as_str()));
}
if tcpr_attrs.is_empty() {
w.open("a:tcPr");
} else {
w.open_with("a:tcPr", &tcpr_attrs);
}
if has_fill {
c.fill.write_solid_fill(w);
}
write_cell_border(w, "a:lnL", c.border_left.as_ref());
write_cell_border(w, "a:lnR", c.border_right.as_ref());
write_cell_border(w, "a:lnT", c.border_top.as_ref());
write_cell_border(w, "a:lnB", c.border_bottom.as_ref());
w.close("a:tcPr");
} else if has_fill {
w.open("a:tcPr");
c.fill.write_solid_fill(w);
w.close("a:tcPr");
}
w.close("a:tc");
}
w.close("a:tr");
}
w.close("a:tbl");
}
pub fn set_style(&mut self, name: &str) -> bool {
if let Some(style) = TableStyle::from_name(name) {
self.table_style = Some(style);
true
} else {
false
}
}
pub fn set_style_id(&mut self, guid: impl Into<String>) {
self.table_style = Some(TableStyle::new(guid));
}
pub fn clear_style(&mut self) {
self.table_style = None;
}
}
fn write_cell_border(w: &mut super::writer::XmlWriter, tag: &str, border: Option<&CellBorder>) {
if let Some(b) = border {
let w_s = b.width.value().to_string();
if b.no_fill {
w.open_with(tag, &[("w", w_s.as_str())]);
w.empty("a:noFill");
w.close(tag);
} else if !matches!(b.color, Color::None) {
w.open_with(tag, &[("w", w_s.as_str())]);
b.color.write_solid_fill(w);
w.close(tag);
} else {
w.empty_with(tag, &[("w", w_s.as_str())]);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn table_style_from_name_finds_builtin() {
let style = TableStyle::from_name("Medium Style 2 - Accent 1")
.expect("Medium Style 2 - Accent 1 应在注册表中");
assert_eq!(style.style_id(), "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}");
assert_eq!(style.style_name(), Some("Medium Style 2 - Accent 1"));
let style2 = TableStyle::from_name("No Style, Table Grid")
.expect("No Style, Table Grid 应在注册表中");
assert_eq!(style2.style_id(), "{5940675A-B579-460E-94D1-54222C63F5DA}");
}
#[test]
fn table_style_from_name_unknown_returns_none() {
assert!(TableStyle::from_name("Nonexistent Style").is_none());
}
#[test]
fn table_style_new_has_no_name() {
let style = TableStyle::new("{5940675A-B579-460E-94D1-54222C63F5DA}");
assert_eq!(style.style_id(), "{5940675A-B579-460E-94D1-54222C63F5DA}");
assert_eq!(style.style_name(), None);
}
#[test]
fn table_set_style_builtin() {
let mut t = Table::default();
assert!(t.set_style("Medium Style 2 - Accent 1"));
let style = t.table_style.as_ref().expect("style 应已设置");
assert_eq!(style.style_id(), "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}");
assert_eq!(style.style_name(), Some("Medium Style 2 - Accent 1"));
}
#[test]
fn table_set_style_unknown_returns_false() {
let mut t = Table::default();
t.set_style("Medium Style 2 - Accent 1");
assert!(!t.set_style("Unknown Style"));
assert_eq!(
t.table_style.as_ref().unwrap().style_id(),
"{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}"
);
}
#[test]
fn table_set_style_id_raw_guid() {
let mut t = Table::default();
t.set_style_id("{5940675A-B579-460E-94D1-54222C63F5DA}");
let style = t.table_style.as_ref().expect("style 应已设置");
assert_eq!(style.style_id(), "{5940675A-B579-460E-94D1-54222C63F5DA}");
assert_eq!(style.style_name(), None);
}
#[test]
fn table_clear_style() {
let mut t = Table::default();
t.set_style("Medium Style 2 - Accent 1");
assert!(t.table_style.is_some());
t.clear_style();
assert!(t.table_style.is_none());
}
#[test]
fn table_write_xml_with_style() {
let mut t = Table::default();
t.set_style("Medium Style 2 - Accent 1");
let mut w = crate::oxml::writer::XmlWriter::new();
t.write_xml(&mut w);
let xml = &w.buf;
assert!(xml.contains("<a:tableStyleId>"));
assert!(xml.contains("{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}"));
assert!(xml.contains("</a:tableStyleId>"));
let pr_start = xml.find("<a:tblPr>").unwrap();
let pr_end = xml.find("</a:tblPr>").unwrap();
let style_pos = xml.find("<a:tableStyleId>").unwrap();
let style_end = xml.find("</a:tableStyleId>").unwrap();
assert!(style_pos > pr_start && style_end < pr_end);
}
#[test]
fn table_write_xml_without_style() {
let t = Table::default();
let mut w = crate::oxml::writer::XmlWriter::new();
t.write_xml(&mut w);
assert!(!w.buf.contains("tableStyleId"));
}
}