1
//! CSS properties for table layout and styling.
2
//!
3
//! This module contains properties specific to CSS table formatting:
4
//! - `table-layout`: Controls the algorithm used to layout table cells, rows, and columns
5
//! - `border-collapse`: Specifies whether cell borders are collapsed into a single border or
6
//!   separated
7
//! - `border-spacing`: Sets the distance between borders of adjacent cells (separate borders only)
8
//! - `caption-side`: Specifies the placement of a table caption
9
//! - `empty-cells`: Specifies whether or not to display borders on empty cells in a table
10

            
11
use alloc::string::{String, ToString};
12

            
13
use crate::{
14
    format_rust_code::FormatAsRustCode,
15
    props::{
16
        basic::pixel::{CssPixelValueParseError, PixelValue},
17
        formatter::PrintAsCssValue,
18
    },
19
};
20

            
21
// table-layout
22

            
23
/// Controls the algorithm used to lay out table cells, rows, and columns.
24
///
25
/// The `table-layout` property determines whether the browser should use:
26
/// - **auto**: Column widths are determined by the content (slower but flexible)
27
/// - **fixed**: Column widths are determined by the first row (faster and predictable)
28
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
29
#[repr(C)]
30
#[derive(Default)]
31
pub enum LayoutTableLayout {
32
    /// Use automatic table layout algorithm (content-based, default).
33
    /// Column width is set by the widest unbreakable content in the cells.
34
    #[default]
35
    Auto,
36
    /// Use fixed table layout algorithm (first-row-based).
37
    /// Column width is set by the width property of the column or first-row cell.
38
    /// Renders faster than auto.
39
    Fixed,
40
}
41

            
42

            
43
impl PrintAsCssValue for LayoutTableLayout {
44
    fn print_as_css_value(&self) -> String {
45
        match self {
46
            LayoutTableLayout::Auto => "auto".to_string(),
47
            LayoutTableLayout::Fixed => "fixed".to_string(),
48
        }
49
    }
50
}
51

            
52
impl FormatAsRustCode for LayoutTableLayout {
53
    fn format_as_rust_code(&self, _tabs: usize) -> String {
54
        match self {
55
            LayoutTableLayout::Auto => "LayoutTableLayout::Auto".to_string(),
56
            LayoutTableLayout::Fixed => "LayoutTableLayout::Fixed".to_string(),
57
        }
58
    }
59
}
60

            
61
// border-collapse
62

            
63
/// Specifies whether cell borders are collapsed into a single border or separated.
64
///
65
/// The `border-collapse` property determines the border rendering model:
66
/// - **separate**: Each cell has its own border (default, uses border-spacing)
67
/// - **collapse**: Adjacent cells share borders (ignores border-spacing)
68
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
69
#[repr(C)]
70
#[derive(Default)]
71
pub enum StyleBorderCollapse {
72
    /// Borders are separated (default). Each cell has its own border.
73
    /// The `border-spacing` property defines the distance between borders.
74
    #[default]
75
    Separate,
76
    /// Borders are collapsed. Adjacent cells share a single border.
77
    /// Border conflict resolution rules apply when borders differ.
78
    Collapse,
79
}
80

            
81

            
82
impl PrintAsCssValue for StyleBorderCollapse {
83
    fn print_as_css_value(&self) -> String {
84
        match self {
85
            StyleBorderCollapse::Separate => "separate".to_string(),
86
            StyleBorderCollapse::Collapse => "collapse".to_string(),
87
        }
88
    }
89
}
90

            
91
impl FormatAsRustCode for StyleBorderCollapse {
92
    fn format_as_rust_code(&self, _tabs: usize) -> String {
93
        match self {
94
            StyleBorderCollapse::Separate => "StyleBorderCollapse::Separate".to_string(),
95
            StyleBorderCollapse::Collapse => "StyleBorderCollapse::Collapse".to_string(),
96
        }
97
    }
98
}
99

            
100
// border-spacing
101

            
102
/// Sets the distance between the borders of adjacent cells.
103
///
104
/// The `border-spacing` property is only applicable when `border-collapse` is set to `separate`.
105
/// It can have one or two values:
106
/// - One value: Sets both horizontal and vertical spacing
107
/// - Two values: First is horizontal, second is vertical
108
///
109
/// This struct represents a single spacing value (either horizontal or vertical).
110
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
111
#[repr(C)]
112
pub struct LayoutBorderSpacing {
113
    /// Horizontal spacing between cell borders
114
    pub horizontal: PixelValue,
115
    /// Vertical spacing between cell borders
116
    pub vertical: PixelValue,
117
}
118

            
119
impl Default for LayoutBorderSpacing {
120
1155
    fn default() -> Self {
121
        // Default border-spacing is 0 (no spacing)
122
1155
        Self {
123
1155
            horizontal: PixelValue::const_px(0),
124
1155
            vertical: PixelValue::const_px(0),
125
1155
        }
126
1155
    }
127
}
128

            
129
impl LayoutBorderSpacing {
130
    /// Creates a new border spacing with the same value for horizontal and vertical
131
1
    pub const fn new(spacing: PixelValue) -> Self {
132
1
        Self {
133
1
            horizontal: spacing,
134
1
            vertical: spacing,
135
1
        }
136
1
    }
137

            
138
    /// Creates a new border spacing with different horizontal and vertical values
139
1387
    pub const fn new_separate(horizontal: PixelValue, vertical: PixelValue) -> Self {
140
1387
        Self {
141
1387
            horizontal,
142
1387
            vertical,
143
1387
        }
144
1387
    }
145
}
146

            
147
impl PrintAsCssValue for LayoutBorderSpacing {
148
    fn print_as_css_value(&self) -> String {
149
        if self.horizontal == self.vertical {
150
            // Single value: same for both dimensions
151
            self.horizontal.to_string()
152
        } else {
153
            // Two values: horizontal vertical
154
            format!("{} {}", self.horizontal, self.vertical)
155
        }
156
    }
157
}
158

            
159
impl FormatAsRustCode for LayoutBorderSpacing {
160
    fn format_as_rust_code(&self, _tabs: usize) -> String {
161
        use crate::format_rust_code::format_pixel_value;
162
        format!(
163
            "LayoutBorderSpacing {{ horizontal: {}, vertical: {} }}",
164
            format_pixel_value(&self.horizontal),
165
            format_pixel_value(&self.vertical)
166
        )
167
    }
168
}
169

            
170
// caption-side
171

            
172
/// Specifies the placement of a table caption.
173
///
174
/// The `caption-side` property positions the caption either above or below the table.
175
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
176
#[repr(C)]
177
#[derive(Default)]
178
pub enum StyleCaptionSide {
179
    /// Caption is placed above the table (default)
180
    #[default]
181
    Top,
182
    /// Caption is placed below the table
183
    Bottom,
184
}
185

            
186

            
187
impl PrintAsCssValue for StyleCaptionSide {
188
    fn print_as_css_value(&self) -> String {
189
        match self {
190
            StyleCaptionSide::Top => "top".to_string(),
191
            StyleCaptionSide::Bottom => "bottom".to_string(),
192
        }
193
    }
194
}
195

            
196
impl FormatAsRustCode for StyleCaptionSide {
197
    fn format_as_rust_code(&self, _tabs: usize) -> String {
198
        match self {
199
            StyleCaptionSide::Top => "StyleCaptionSide::Top".to_string(),
200
            StyleCaptionSide::Bottom => "StyleCaptionSide::Bottom".to_string(),
201
        }
202
    }
203
}
204

            
205
// empty-cells
206

            
207
/// Specifies whether or not to display borders and background on empty cells.
208
///
209
/// The `empty-cells` property only applies when `border-collapse` is set to `separate`.
210
/// A cell is considered empty if it contains no visible content.
211
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
212
#[repr(C)]
213
#[derive(Default)]
214
pub enum StyleEmptyCells {
215
    /// Show borders and background on empty cells (default)
216
    #[default]
217
    Show,
218
    /// Hide borders and background on empty cells
219
    Hide,
220
}
221

            
222

            
223
impl PrintAsCssValue for StyleEmptyCells {
224
    fn print_as_css_value(&self) -> String {
225
        match self {
226
            StyleEmptyCells::Show => "show".to_string(),
227
            StyleEmptyCells::Hide => "hide".to_string(),
228
        }
229
    }
230
}
231

            
232
impl FormatAsRustCode for StyleEmptyCells {
233
    fn format_as_rust_code(&self, _tabs: usize) -> String {
234
        match self {
235
            StyleEmptyCells::Show => "StyleEmptyCells::Show".to_string(),
236
            StyleEmptyCells::Hide => "StyleEmptyCells::Hide".to_string(),
237
        }
238
    }
239
}
240

            
241
// Parsing Functions
242

            
243
/// Parse errors for table-layout property
244
#[derive(Debug, Clone, PartialEq)]
245
pub(crate) enum LayoutTableLayoutParseError<'a> {
246
    InvalidKeyword(&'a str),
247
}
248

            
249
/// Parse a table-layout value from a string
250
3
pub(crate) fn parse_table_layout<'a>(
251
3
    input: &'a str,
252
3
) -> Result<LayoutTableLayout, LayoutTableLayoutParseError<'a>> {
253
3
    match input.trim() {
254
3
        "auto" => Ok(LayoutTableLayout::Auto),
255
2
        "fixed" => Ok(LayoutTableLayout::Fixed),
256
1
        other => Err(LayoutTableLayoutParseError::InvalidKeyword(other)),
257
    }
258
3
}
259

            
260
/// Parse errors for border-collapse property
261
#[derive(Debug, Clone, PartialEq)]
262
pub(crate) enum StyleBorderCollapseParseError<'a> {
263
    InvalidKeyword(&'a str),
264
}
265

            
266
/// Parse a border-collapse value from a string
267
3
pub(crate) fn parse_border_collapse<'a>(
268
3
    input: &'a str,
269
3
) -> Result<StyleBorderCollapse, StyleBorderCollapseParseError<'a>> {
270
3
    match input.trim() {
271
3
        "separate" => Ok(StyleBorderCollapse::Separate),
272
2
        "collapse" => Ok(StyleBorderCollapse::Collapse),
273
1
        other => Err(StyleBorderCollapseParseError::InvalidKeyword(other)),
274
    }
275
3
}
276

            
277
/// Parse errors for border-spacing property
278
#[derive(Debug, Clone, PartialEq)]
279
pub(crate) enum LayoutBorderSpacingParseError<'a> {
280
    PixelValue(CssPixelValueParseError<'a>),
281
    InvalidFormat,
282
}
283

            
284
/// Parse a border-spacing value from a string
285
/// Accepts: "5px" or "5px 10px"
286
2
pub(crate) fn parse_border_spacing<'a>(
287
2
    input: &'a str,
288
2
) -> Result<LayoutBorderSpacing, LayoutBorderSpacingParseError<'a>> {
289
    use crate::props::basic::parse_pixel_value;
290

            
291
2
    let parts: Vec<&str> = input.split_whitespace().collect();
292

            
293
2
    match parts.len() {
294
        1 => {
295
            // Single value: use for both horizontal and vertical
296
1
            let value =
297
1
                parse_pixel_value(parts[0]).map_err(LayoutBorderSpacingParseError::PixelValue)?;
298
1
            Ok(LayoutBorderSpacing::new(value))
299
        }
300
        2 => {
301
            // Two values: horizontal vertical
302
1
            let horizontal =
303
1
                parse_pixel_value(parts[0]).map_err(LayoutBorderSpacingParseError::PixelValue)?;
304
1
            let vertical =
305
1
                parse_pixel_value(parts[1]).map_err(LayoutBorderSpacingParseError::PixelValue)?;
306
1
            Ok(LayoutBorderSpacing::new_separate(horizontal, vertical))
307
        }
308
        _ => Err(LayoutBorderSpacingParseError::InvalidFormat),
309
    }
310
2
}
311

            
312
/// Parse errors for caption-side property
313
#[derive(Debug, Clone, PartialEq)]
314
pub(crate) enum StyleCaptionSideParseError<'a> {
315
    InvalidKeyword(&'a str),
316
}
317

            
318
/// Parse a caption-side value from a string
319
3
pub(crate) fn parse_caption_side<'a>(
320
3
    input: &'a str,
321
3
) -> Result<StyleCaptionSide, StyleCaptionSideParseError<'a>> {
322
3
    match input.trim() {
323
3
        "top" => Ok(StyleCaptionSide::Top),
324
2
        "bottom" => Ok(StyleCaptionSide::Bottom),
325
1
        other => Err(StyleCaptionSideParseError::InvalidKeyword(other)),
326
    }
327
3
}
328

            
329
/// Parse errors for empty-cells property
330
#[derive(Debug, Clone, PartialEq)]
331
pub(crate) enum StyleEmptyCellsParseError<'a> {
332
    InvalidKeyword(&'a str),
333
}
334

            
335
/// Parse an empty-cells value from a string
336
3
pub(crate) fn parse_empty_cells<'a>(
337
3
    input: &'a str,
338
3
) -> Result<StyleEmptyCells, StyleEmptyCellsParseError<'a>> {
339
3
    match input.trim() {
340
3
        "show" => Ok(StyleEmptyCells::Show),
341
2
        "hide" => Ok(StyleEmptyCells::Hide),
342
1
        other => Err(StyleEmptyCellsParseError::InvalidKeyword(other)),
343
    }
344
3
}
345

            
346
#[cfg(test)]
347
mod tests {
348
    use super::*;
349

            
350
    #[test]
351
1
    fn test_parse_table_layout() {
352
1
        assert_eq!(parse_table_layout("auto").unwrap(), LayoutTableLayout::Auto);
353
1
        assert_eq!(
354
1
            parse_table_layout("fixed").unwrap(),
355
            LayoutTableLayout::Fixed
356
        );
357
1
        assert!(parse_table_layout("invalid").is_err());
358
1
    }
359

            
360
    #[test]
361
1
    fn test_parse_border_collapse() {
362
1
        assert_eq!(
363
1
            parse_border_collapse("separate").unwrap(),
364
            StyleBorderCollapse::Separate
365
        );
366
1
        assert_eq!(
367
1
            parse_border_collapse("collapse").unwrap(),
368
            StyleBorderCollapse::Collapse
369
        );
370
1
        assert!(parse_border_collapse("invalid").is_err());
371
1
    }
372

            
373
    #[test]
374
1
    fn test_parse_border_spacing() {
375
1
        let spacing1 = parse_border_spacing("5px").unwrap();
376
1
        assert_eq!(spacing1.horizontal, PixelValue::const_px(5));
377
1
        assert_eq!(spacing1.vertical, PixelValue::const_px(5));
378

            
379
1
        let spacing2 = parse_border_spacing("5px 10px").unwrap();
380
1
        assert_eq!(spacing2.horizontal, PixelValue::const_px(5));
381
1
        assert_eq!(spacing2.vertical, PixelValue::const_px(10));
382
1
    }
383

            
384
    #[test]
385
1
    fn test_parse_caption_side() {
386
1
        assert_eq!(parse_caption_side("top").unwrap(), StyleCaptionSide::Top);
387
1
        assert_eq!(
388
1
            parse_caption_side("bottom").unwrap(),
389
            StyleCaptionSide::Bottom
390
        );
391
1
        assert!(parse_caption_side("invalid").is_err());
392
1
    }
393

            
394
    #[test]
395
1
    fn test_parse_empty_cells() {
396
1
        assert_eq!(parse_empty_cells("show").unwrap(), StyleEmptyCells::Show);
397
1
        assert_eq!(parse_empty_cells("hide").unwrap(), StyleEmptyCells::Hide);
398
1
        assert!(parse_empty_cells("invalid").is_err());
399
1
    }
400
}