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}