use crate::rule_config_serde::RuleConfig;
use crate::types::LineLength;
use serde::ser::Serializer;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum ColumnAlign {
#[default]
Auto,
Left,
Center,
Right,
}
impl Serialize for ColumnAlign {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
ColumnAlign::Auto => serializer.serialize_str("auto"),
ColumnAlign::Left => serializer.serialize_str("left"),
ColumnAlign::Center => serializer.serialize_str("center"),
ColumnAlign::Right => serializer.serialize_str("right"),
}
}
}
impl<'de> Deserialize<'de> for ColumnAlign {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.to_lowercase().as_str() {
"auto" => Ok(ColumnAlign::Auto),
"left" => Ok(ColumnAlign::Left),
"center" => Ok(ColumnAlign::Center),
"right" => Ok(ColumnAlign::Right),
_ => Err(serde::de::Error::custom(format!(
"Invalid column-align value: {s}. Valid options: auto, left, center, right"
))),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MD060Config {
#[serde(default = "default_enabled")]
pub enabled: bool,
#[serde(
default = "default_style",
serialize_with = "serialize_style",
deserialize_with = "deserialize_style"
)]
pub style: String,
#[serde(default = "default_max_width", rename = "max-width")]
pub max_width: LineLength,
#[serde(default, rename = "column-align")]
pub column_align: ColumnAlign,
#[serde(default, rename = "column-align-header")]
pub column_align_header: Option<ColumnAlign>,
#[serde(default, rename = "column-align-body")]
pub column_align_body: Option<ColumnAlign>,
#[serde(default, rename = "loose-last-column")]
pub loose_last_column: bool,
}
impl Default for MD060Config {
fn default() -> Self {
Self {
enabled: default_enabled(),
style: default_style(),
max_width: default_max_width(),
column_align: ColumnAlign::Auto,
column_align_header: None,
column_align_body: None,
loose_last_column: false,
}
}
}
fn default_enabled() -> bool {
false
}
fn default_style() -> String {
"any".to_string()
}
fn default_max_width() -> LineLength {
LineLength::from_const(0) }
fn serialize_style<S>(style: &str, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(style)
}
fn deserialize_style<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let normalized = s.trim().to_ascii_lowercase().replace('_', "-");
let valid_styles = ["aligned", "aligned-no-space", "compact", "tight", "any"];
if valid_styles.contains(&normalized.as_str()) {
Ok(normalized)
} else {
Err(serde::de::Error::custom(format!(
"Invalid table format style: {s}. Valid options: aligned, aligned-no-space, compact, tight, any"
)))
}
}
impl RuleConfig for MD060Config {
const RULE_NAME: &'static str = "MD060";
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_style_accepts_hyphen_and_underscore_variants() {
let kebab_case: MD060Config = toml::from_str("style = \"aligned-no-space\"").unwrap();
assert_eq!(kebab_case.style, "aligned-no-space");
let snake_case: MD060Config = toml::from_str("style = \"aligned_no_space\"").unwrap();
assert_eq!(snake_case.style, "aligned-no-space");
}
#[test]
fn test_style_normalizes_case_for_compatibility() {
let uppercase: MD060Config = toml::from_str("style = \"ALIGNED_NO_SPACE\"").unwrap();
assert_eq!(uppercase.style, "aligned-no-space");
}
}