1use crate::Result;
4use crate::style::{CellStyle, RowStyle, TableStyle};
5use tracing::trace;
6
7#[derive(Debug, Clone)]
9pub enum ColumnWidth {
10 Pixels(f32),
12 Percentage(f32),
14 Auto,
16}
17
18#[derive(Debug, Clone)]
20pub struct Table {
21 pub rows: Vec<Row>,
22 pub style: TableStyle,
23 pub column_widths: Option<Vec<ColumnWidth>>,
25 pub total_width: Option<f32>,
27 pub header_rows: usize,
29}
30
31impl Table {
32 pub fn new() -> Self {
34 Self {
35 rows: Vec::new(),
36 style: TableStyle::default(),
37 column_widths: None,
38 total_width: None,
39 header_rows: 0,
40 }
41 }
42
43 pub fn add_row(mut self, row: Row) -> Self {
45 trace!("Adding row with {} cells", row.cells.len());
46 self.rows.push(row);
47 self
48 }
49
50 pub fn with_style(mut self, style: TableStyle) -> Self {
52 self.style = style;
53 self
54 }
55
56 pub fn with_column_widths(mut self, widths: Vec<ColumnWidth>) -> Self {
58 self.column_widths = Some(widths);
59 self
60 }
61
62 pub fn with_total_width(mut self, width: f32) -> Self {
64 self.total_width = Some(width);
65 self
66 }
67
68 pub fn with_pixel_widths(mut self, widths: Vec<f32>) -> Self {
70 self.column_widths = Some(widths.into_iter().map(ColumnWidth::Pixels).collect());
71 self
72 }
73
74 pub fn with_border(mut self, width: f32) -> Self {
76 self.style.border_width = width;
77 self
78 }
79
80 pub fn with_header_rows(mut self, count: usize) -> Self {
82 self.header_rows = count;
83 self
84 }
85
86 pub fn column_count(&self) -> usize {
88 self.rows
89 .first()
90 .map(|r| r.cells.iter().map(|c| c.colspan.max(1)).sum())
91 .unwrap_or(0)
92 }
93
94 pub fn validate(&self) -> Result<()> {
96 if self.rows.is_empty() {
97 return Err(crate::error::TableError::InvalidTable(
98 "Table has no rows".to_string(),
99 ));
100 }
101
102 let expected_cols = self.column_count();
103 for (i, row) in self.rows.iter().enumerate() {
104 let mut total_coverage = 0;
106 for cell in &row.cells {
107 total_coverage += cell.colspan.max(1);
108 }
109
110 if total_coverage != expected_cols {
111 return Err(crate::error::TableError::InvalidTable(format!(
112 "Row {} covers {} columns (with colspan), expected {}",
113 i, total_coverage, expected_cols
114 )));
115 }
116 }
117
118 if let Some(ref widths) = self.column_widths {
119 if widths.len() != expected_cols {
120 return Err(crate::error::TableError::InvalidTable(format!(
121 "Column widths array has {} elements, but table has {} columns",
122 widths.len(),
123 expected_cols
124 )));
125 }
126
127 let total_percentage: f32 = widths
129 .iter()
130 .filter_map(|w| match w {
131 ColumnWidth::Percentage(p) => Some(*p),
132 _ => None,
133 })
134 .sum();
135
136 if total_percentage > 100.0 {
137 return Err(crate::error::TableError::InvalidTable(format!(
138 "Total percentage widths ({:.1}%) exceed 100%",
139 total_percentage
140 )));
141 }
142 }
143
144 Ok(())
145 }
146}
147
148impl Default for Table {
149 fn default() -> Self {
150 Self::new()
151 }
152}
153
154#[derive(Debug, Clone)]
156pub struct Row {
157 pub cells: Vec<Cell>,
158 pub style: Option<RowStyle>,
159 pub height: Option<f32>,
161}
162
163impl Row {
164 pub fn new(cells: Vec<Cell>) -> Self {
166 Self {
167 cells,
168 style: None,
169 height: None,
170 }
171 }
172
173 pub fn with_style(mut self, style: RowStyle) -> Self {
175 self.style = Some(style);
176 self
177 }
178
179 pub fn with_height(mut self, height: f32) -> Self {
181 self.height = Some(height);
182 self
183 }
184}
185
186#[derive(Debug, Clone)]
188pub struct Cell {
189 pub content: String,
190 pub style: Option<CellStyle>,
191 pub colspan: usize,
192 pub rowspan: usize,
193 pub text_wrap: bool,
195}
196
197impl Cell {
198 pub fn new<S: Into<String>>(content: S) -> Self {
200 Self {
201 content: content.into(),
202 style: None,
203 colspan: 1,
204 rowspan: 1,
205 text_wrap: false,
206 }
207 }
208
209 pub fn empty() -> Self {
211 Self::new("")
212 }
213
214 pub fn with_wrap(mut self, wrap: bool) -> Self {
216 self.text_wrap = wrap;
217 self
218 }
219
220 pub fn with_style(mut self, style: CellStyle) -> Self {
222 self.style = Some(style);
223 self
224 }
225
226 pub fn with_colspan(mut self, span: usize) -> Self {
228 self.colspan = span.max(1);
229 self
230 }
231
232 pub fn with_rowspan(mut self, span: usize) -> Self {
234 self.rowspan = span.max(1);
235 self
236 }
237
238 pub fn bold(mut self) -> Self {
240 let mut style = self.style.unwrap_or_default();
241 style.bold = true;
242 self.style = Some(style);
243 self
244 }
245
246 pub fn italic(mut self) -> Self {
248 let mut style = self.style.unwrap_or_default();
249 style.italic = true;
250 self.style = Some(style);
251 self
252 }
253
254 pub fn with_font_size(mut self, size: f32) -> Self {
256 let mut style = self.style.unwrap_or_default();
257 style.font_size = Some(size);
258 self.style = Some(style);
259 self
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn test_table_validation() {
269 let mut table = Table::new();
270 assert!(table.validate().is_err());
271
272 table = table.add_row(Row::new(vec![Cell::new("A"), Cell::new("B")]));
273 assert!(table.validate().is_ok());
274
275 table = table.add_row(Row::new(vec![Cell::new("C")]));
276 assert!(table.validate().is_err());
277 }
278
279 #[test]
280 fn test_cell_builder() {
281 let cell = Cell::new("Test")
282 .bold()
283 .italic()
284 .with_font_size(14.0)
285 .with_colspan(2);
286
287 assert_eq!(cell.content, "Test");
288 assert_eq!(cell.colspan, 2);
289 let style = cell.style.unwrap();
290 assert!(style.bold);
291 assert!(style.italic);
292 assert_eq!(style.font_size, Some(14.0));
293 }
294}