rumdl_lib/rules/md060_table_format/
md060_config.rs1use crate::rule_config_serde::RuleConfig;
2use crate::types::LineLength;
3use serde::ser::Serializer;
4use serde::{Deserialize, Serialize};
5#[derive(Debug, Clone, Copy, PartialEq, Default)]
7pub enum ColumnAlign {
8 #[default]
10 Auto,
11 Left,
13 Center,
15 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 #[serde(default = "default_max_width", rename = "max-width")]
89 pub max_width: LineLength,
90
91 #[serde(default, rename = "column-align")]
108 pub column_align: ColumnAlign,
109
110 #[serde(default, rename = "column-align-header")]
124 pub column_align_header: Option<ColumnAlign>,
125
126 #[serde(default, rename = "column-align-body")]
140 pub column_align_body: Option<ColumnAlign>,
141
142 #[serde(default, rename = "loose-last-column")]
159 pub loose_last_column: bool,
160
161 #[serde(default, rename = "aligned-delimiter", alias = "aligned_delimiter")]
174 pub aligned_delimiter: bool,
175}
176
177impl Default for MD060Config {
178 fn default() -> Self {
179 Self {
180 enabled: default_enabled(),
181 style: default_style(),
182 max_width: default_max_width(),
183 column_align: ColumnAlign::Auto,
184 column_align_header: None,
185 column_align_body: None,
186 loose_last_column: false,
187 aligned_delimiter: false,
188 }
189 }
190}
191
192fn default_enabled() -> bool {
193 false
194}
195
196fn default_style() -> String {
197 "any".to_string()
198}
199
200fn default_max_width() -> LineLength {
201 LineLength::from_const(0) }
203
204fn serialize_style<S>(style: &str, serializer: S) -> Result<S::Ok, S::Error>
205where
206 S: Serializer,
207{
208 serializer.serialize_str(style)
209}
210
211fn deserialize_style<'de, D>(deserializer: D) -> Result<String, D::Error>
212where
213 D: serde::Deserializer<'de>,
214{
215 let s = String::deserialize(deserializer)?;
216 let normalized = s.trim().to_ascii_lowercase().replace('_', "-");
217
218 let valid_styles = ["aligned", "aligned-no-space", "compact", "tight", "any"];
219
220 if valid_styles.contains(&normalized.as_str()) {
221 Ok(normalized)
222 } else {
223 Err(serde::de::Error::custom(format!(
224 "Invalid table format style: {s}. Valid options: aligned, aligned-no-space, compact, tight, any"
225 )))
226 }
227}
228
229impl RuleConfig for MD060Config {
230 const RULE_NAME: &'static str = "MD060";
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn test_style_accepts_hyphen_and_underscore_variants() {
239 let kebab_case: MD060Config = toml::from_str("style = \"aligned-no-space\"").unwrap();
240 assert_eq!(kebab_case.style, "aligned-no-space");
241
242 let snake_case: MD060Config = toml::from_str("style = \"aligned_no_space\"").unwrap();
243 assert_eq!(snake_case.style, "aligned-no-space");
244 }
245
246 #[test]
247 fn test_style_normalizes_case_for_compatibility() {
248 let uppercase: MD060Config = toml::from_str("style = \"ALIGNED_NO_SPACE\"").unwrap();
249 assert_eq!(uppercase.style, "aligned-no-space");
250 }
251
252 #[test]
253 fn test_aligned_delimiter_default_is_false() {
254 let cfg: MD060Config = toml::from_str("").unwrap();
255 assert!(!cfg.aligned_delimiter, "aligned-delimiter defaults to false");
256 }
257
258 #[test]
259 fn test_aligned_delimiter_kebab_case_key() {
260 let cfg: MD060Config = toml::from_str("aligned-delimiter = true").unwrap();
261 assert!(cfg.aligned_delimiter, "kebab-case aligned-delimiter is accepted");
262 }
263
264 #[test]
265 fn test_aligned_delimiter_snake_case_alias_for_markdownlint_parity() {
266 let cfg: MD060Config = toml::from_str("aligned_delimiter = true").unwrap();
268 assert!(cfg.aligned_delimiter, "snake_case aligned_delimiter alias is accepted");
269 }
270}