Skip to main content

rumdl_lib/rules/md060_table_format/
md060_config.rs

1use crate::rule_config_serde::RuleConfig;
2use crate::types::LineLength;
3use serde::ser::Serializer;
4use serde::{Deserialize, Serialize};
5
6/// Controls how cell text is aligned within padded columns.
7#[derive(Debug, Clone, Copy, PartialEq, Default)]
8pub enum ColumnAlign {
9    /// Use alignment indicators from delimiter row (`:---`, `:---:`, `---:`)
10    #[default]
11    Auto,
12    /// Force all columns to left-align text
13    Left,
14    /// Force all columns to center text
15    Center,
16    /// Force all columns to right-align text
17    Right,
18}
19
20impl Serialize for ColumnAlign {
21    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
22    where
23        S: Serializer,
24    {
25        match self {
26            ColumnAlign::Auto => serializer.serialize_str("auto"),
27            ColumnAlign::Left => serializer.serialize_str("left"),
28            ColumnAlign::Center => serializer.serialize_str("center"),
29            ColumnAlign::Right => serializer.serialize_str("right"),
30        }
31    }
32}
33
34impl<'de> Deserialize<'de> for ColumnAlign {
35    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
36    where
37        D: serde::Deserializer<'de>,
38    {
39        let s = String::deserialize(deserializer)?;
40        match s.to_lowercase().as_str() {
41            "auto" => Ok(ColumnAlign::Auto),
42            "left" => Ok(ColumnAlign::Left),
43            "center" => Ok(ColumnAlign::Center),
44            "right" => Ok(ColumnAlign::Right),
45            _ => Err(serde::de::Error::custom(format!(
46                "Invalid column-align value: {s}. Valid options: auto, left, center, right"
47            ))),
48        }
49    }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
53pub struct MD060Config {
54    #[serde(default = "default_enabled")]
55    pub enabled: bool,
56
57    #[serde(
58        default = "default_style",
59        serialize_with = "serialize_style",
60        deserialize_with = "deserialize_style"
61    )]
62    pub style: String,
63
64    /// Maximum table width before auto-switching to compact mode.
65    ///
66    /// - `0` (default): Inherit from MD013's `line-length` setting
67    /// - Non-zero: Explicit max width threshold
68    ///
69    /// When a table's aligned width would exceed this limit, MD060 automatically
70    /// uses compact formatting instead (minimal padding) to prevent excessively
71    /// long lines. This matches the behavior of Prettier's table formatting.
72    ///
73    /// # Examples
74    ///
75    /// ```toml
76    /// [MD013]
77    /// line-length = 100
78    ///
79    /// [MD060]
80    /// style = "aligned"
81    /// max-width = 0  # Uses MD013's line-length (100)
82    /// ```
83    ///
84    /// ```toml
85    /// [MD060]
86    /// style = "aligned"
87    /// max-width = 120  # Explicit threshold, independent of MD013
88    /// ```
89    #[serde(default = "default_max_width", rename = "max-width")]
90    pub max_width: LineLength,
91
92    /// Controls how cell text is aligned within the padded column width.
93    ///
94    /// - `auto` (default): Use alignment indicators from delimiter row (`:---`, `:---:`, `---:`)
95    /// - `left`: Force all columns to left-align text
96    /// - `center`: Force all columns to center text
97    /// - `right`: Force all columns to right-align text
98    ///
99    /// Only applies when `style = "aligned"` or `style = "aligned-no-space"`.
100    ///
101    /// # Examples
102    ///
103    /// ```toml
104    /// [MD060]
105    /// style = "aligned"
106    /// column-align = "center"  # Center all cell text
107    /// ```
108    #[serde(default, rename = "column-align")]
109    pub column_align: ColumnAlign,
110
111    /// Controls alignment specifically for the header row.
112    ///
113    /// When set, overrides `column-align` for the header row only.
114    /// If not set, falls back to `column-align`.
115    ///
116    /// # Examples
117    ///
118    /// ```toml
119    /// [MD060]
120    /// style = "aligned"
121    /// column-align-header = "center"  # Center header text
122    /// column-align-body = "left"      # Left-align body text
123    /// ```
124    #[serde(default, rename = "column-align-header")]
125    pub column_align_header: Option<ColumnAlign>,
126
127    /// Controls alignment specifically for body rows (non-header, non-delimiter).
128    ///
129    /// When set, overrides `column-align` for body rows only.
130    /// If not set, falls back to `column-align`.
131    ///
132    /// # Examples
133    ///
134    /// ```toml
135    /// [MD060]
136    /// style = "aligned"
137    /// column-align-header = "center"  # Center header text
138    /// column-align-body = "left"      # Left-align body text
139    /// ```
140    #[serde(default, rename = "column-align-body")]
141    pub column_align_body: Option<ColumnAlign>,
142
143    /// When enabled, the last column in body rows is not padded to match the header width.
144    ///
145    /// This is useful for tables where the last column contains descriptions or other
146    /// variable-length content. The header and delimiter rows remain fully aligned,
147    /// but body rows can have shorter or longer last columns.
148    ///
149    /// Only applies when `style = "aligned"` or `style = "aligned-no-space"`.
150    ///
151    /// # Examples
152    ///
153    /// ```toml
154    /// [MD060]
155    /// style = "aligned"
156    /// loose-last-column = true
157    /// ```
158    ///
159    /// Result:
160    /// ```markdown
161    /// | Name   | Status   | Description |
162    /// |--------|----------|-------------|
163    /// | Foo    | Enabled  | Short |
164    /// | Bar    | Disabled | A much longer description that would waste space if padded |
165    /// ```
166    #[serde(default, rename = "loose-last-column")]
167    pub loose_last_column: bool,
168}
169
170impl Default for MD060Config {
171    fn default() -> Self {
172        Self {
173            enabled: default_enabled(),
174            style: default_style(),
175            max_width: default_max_width(),
176            column_align: ColumnAlign::Auto,
177            column_align_header: None,
178            column_align_body: None,
179            loose_last_column: false,
180        }
181    }
182}
183
184fn default_enabled() -> bool {
185    false
186}
187
188fn default_style() -> String {
189    "any".to_string()
190}
191
192fn default_max_width() -> LineLength {
193    LineLength::from_const(0) // 0 = inherit from MD013
194}
195
196fn serialize_style<S>(style: &str, serializer: S) -> Result<S::Ok, S::Error>
197where
198    S: Serializer,
199{
200    serializer.serialize_str(style)
201}
202
203fn deserialize_style<'de, D>(deserializer: D) -> Result<String, D::Error>
204where
205    D: serde::Deserializer<'de>,
206{
207    let s = String::deserialize(deserializer)?;
208
209    let valid_styles = ["aligned", "aligned-no-space", "compact", "tight", "any"];
210
211    if valid_styles.contains(&s.as_str()) {
212        Ok(s)
213    } else {
214        Err(serde::de::Error::custom(format!(
215            "Invalid table format style: {s}. Valid options: aligned, aligned-no-space, compact, tight, any"
216        )))
217    }
218}
219
220impl RuleConfig for MD060Config {
221    const RULE_NAME: &'static str = "MD060";
222}