Skip to main content

acdc_parser/model/
tables.rs

1//! Table types for `AsciiDoc` documents.
2
3use serde::Serialize;
4
5use super::Block;
6use super::location::Location;
7
8/// Horizontal alignment for table cells
9#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize)]
10#[serde(rename_all = "lowercase")]
11pub enum HorizontalAlignment {
12    #[default]
13    Left,
14    Center,
15    Right,
16}
17
18/// Vertical alignment for table cells
19#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize)]
20#[serde(rename_all = "lowercase")]
21pub enum VerticalAlignment {
22    #[default]
23    Top,
24    Middle,
25    Bottom,
26}
27
28/// Column width specification
29#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
30#[serde(rename_all = "lowercase")]
31#[non_exhaustive]
32pub enum ColumnWidth {
33    /// Proportional width (e.g., 1, 2, 3 - relative to other columns)
34    Proportional(u32),
35    /// Percentage width (e.g., 15%, 30%, 55%)
36    Percentage(u32),
37    /// Auto-width - content determines width (~)
38    Auto,
39}
40
41impl Default for ColumnWidth {
42    fn default() -> Self {
43        ColumnWidth::Proportional(1)
44    }
45}
46
47/// Column content style
48#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize)]
49#[serde(rename_all = "lowercase")]
50#[non_exhaustive]
51pub enum ColumnStyle {
52    /// `AsciiDoc` block content (a) - supports lists, blocks, macros
53    #[serde(rename = "asciidoc")]
54    AsciiDoc,
55    /// Default paragraph-level markup (d)
56    #[default]
57    Default,
58    /// Emphasis/italic (e)
59    Emphasis,
60    /// Header styling (h)
61    Header,
62    /// Literal block text (l)
63    Literal,
64    /// Monospace font (m)
65    Monospace,
66    /// Strong/bold (s)
67    Strong,
68}
69
70/// Column format specification for table formatting
71#[derive(Clone, Debug, Default, PartialEq, Serialize)]
72#[non_exhaustive]
73pub struct ColumnFormat {
74    #[serde(default, skip_serializing_if = "is_default_halign")]
75    pub halign: HorizontalAlignment,
76    #[serde(default, skip_serializing_if = "is_default_valign")]
77    pub valign: VerticalAlignment,
78    #[serde(default, skip_serializing_if = "is_default_width")]
79    pub width: ColumnWidth,
80    #[serde(default, skip_serializing_if = "is_default_style")]
81    pub style: ColumnStyle,
82}
83
84impl ColumnFormat {
85    /// Create a new column format with default values.
86    #[must_use]
87    pub fn new() -> Self {
88        Self::default()
89    }
90
91    /// Set the horizontal alignment.
92    #[must_use]
93    pub fn with_halign(mut self, halign: HorizontalAlignment) -> Self {
94        self.halign = halign;
95        self
96    }
97
98    /// Set the vertical alignment.
99    #[must_use]
100    pub fn with_valign(mut self, valign: VerticalAlignment) -> Self {
101        self.valign = valign;
102        self
103    }
104
105    /// Set the column width.
106    #[must_use]
107    pub fn with_width(mut self, width: ColumnWidth) -> Self {
108        self.width = width;
109        self
110    }
111
112    /// Set the column style.
113    #[must_use]
114    pub fn with_style(mut self, style: ColumnStyle) -> Self {
115        self.style = style;
116        self
117    }
118}
119
120#[allow(clippy::trivially_copy_pass_by_ref)]
121fn is_default_halign(h: &HorizontalAlignment) -> bool {
122    *h == HorizontalAlignment::default()
123}
124
125#[allow(clippy::trivially_copy_pass_by_ref)]
126fn is_default_valign(v: &VerticalAlignment) -> bool {
127    *v == VerticalAlignment::default()
128}
129
130#[allow(clippy::trivially_copy_pass_by_ref)]
131fn is_default_width(w: &ColumnWidth) -> bool {
132    *w == ColumnWidth::default()
133}
134
135#[allow(clippy::trivially_copy_pass_by_ref)]
136fn is_default_style(s: &ColumnStyle) -> bool {
137    *s == ColumnStyle::default()
138}
139
140pub(crate) fn are_all_columns_default(specs: &[ColumnFormat]) -> bool {
141    specs.iter().all(|s| *s == ColumnFormat::default())
142}
143
144/// A `Table` represents a table in a document.
145#[derive(Clone, Debug, PartialEq, Serialize)]
146#[non_exhaustive]
147pub struct Table {
148    pub header: Option<TableRow>,
149    pub footer: Option<TableRow>,
150    pub rows: Vec<TableRow>,
151    /// Column format specification for each column (alignment, width, style)
152    /// Skipped if all columns have default format
153    #[serde(default, skip_serializing_if = "are_all_columns_default")]
154    pub columns: Vec<ColumnFormat>,
155    pub location: Location,
156}
157
158impl Table {
159    /// Create a new table with the given rows and location.
160    #[must_use]
161    pub fn new(rows: Vec<TableRow>, location: Location) -> Self {
162        Self {
163            header: None,
164            footer: None,
165            rows,
166            columns: Vec::new(),
167            location,
168        }
169    }
170
171    /// Set the header row.
172    #[must_use]
173    pub fn with_header(mut self, header: Option<TableRow>) -> Self {
174        self.header = header;
175        self
176    }
177
178    /// Set the footer row.
179    #[must_use]
180    pub fn with_footer(mut self, footer: Option<TableRow>) -> Self {
181        self.footer = footer;
182        self
183    }
184
185    /// Set the column format specifications.
186    #[must_use]
187    pub fn with_columns(mut self, columns: Vec<ColumnFormat>) -> Self {
188        self.columns = columns;
189        self
190    }
191}
192
193/// A row in a table, containing one or more columns (cells).
194///
195/// # Note on Field Name
196///
197/// The field is named `columns` (not `cells`) to align with the column-oriented
198/// table model. Each `TableColumn` represents one cell in this row.
199///
200/// ```
201/// # use acdc_parser::{TableRow, TableColumn};
202/// fn count_cells(row: &TableRow) -> usize {
203///     row.columns.len()  // Access cells via .columns
204/// }
205/// ```
206#[derive(Clone, Debug, PartialEq, Serialize)]
207#[non_exhaustive]
208pub struct TableRow {
209    /// The cells in this row (one per table column).
210    pub columns: Vec<TableColumn>,
211}
212
213impl TableRow {
214    /// Create a new table row with the given columns.
215    #[must_use]
216    pub fn new(columns: Vec<TableColumn>) -> Self {
217        Self { columns }
218    }
219}
220
221/// A `TableColumn` represents a column/cell in a table row.
222#[derive(Clone, Debug, PartialEq, Serialize)]
223#[non_exhaustive]
224pub struct TableColumn {
225    pub content: Vec<Block>,
226    /// Number of columns this cell spans (default 1).
227    /// Specified in `AsciiDoc` with `n+|` syntax (e.g., `2+|` for colspan=2).
228    #[serde(skip_serializing_if = "is_default_span")]
229    pub colspan: usize,
230    /// Number of rows this cell spans (default 1).
231    /// Specified in `AsciiDoc` with `.n+|` syntax (e.g., `.2+|` for rowspan=2).
232    #[serde(skip_serializing_if = "is_default_span")]
233    pub rowspan: usize,
234    /// Cell-level horizontal alignment override.
235    /// Specified with `<`, `^`, or `>` in cell specifier (e.g., `^|` for center).
236    #[serde(default, skip_serializing_if = "Option::is_none")]
237    pub halign: Option<HorizontalAlignment>,
238    /// Cell-level vertical alignment override.
239    /// Specified with `.<`, `.^`, or `.>` in cell specifier (e.g., `.>|` for bottom).
240    #[serde(default, skip_serializing_if = "Option::is_none")]
241    pub valign: Option<VerticalAlignment>,
242    /// Cell-level style override.
243    /// Specified with style letter after operator (e.g., `s|` for strong/bold).
244    #[serde(default, skip_serializing_if = "Option::is_none")]
245    pub style: Option<ColumnStyle>,
246}
247
248#[allow(clippy::trivially_copy_pass_by_ref)]
249const fn is_default_span(span: &usize) -> bool {
250    *span == 1
251}
252
253impl TableColumn {
254    /// Create a new table column with full cell specifier options.
255    #[must_use]
256    pub(crate) fn with_format(
257        content: Vec<Block>,
258        colspan: usize,
259        rowspan: usize,
260        halign: Option<HorizontalAlignment>,
261        valign: Option<VerticalAlignment>,
262        style: Option<ColumnStyle>,
263    ) -> Self {
264        Self {
265            content,
266            colspan,
267            rowspan,
268            halign,
269            valign,
270            style,
271        }
272    }
273}