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