Skip to main content

rumdl_lib/rules/md007_ul_indent/
md007_config.rs

1use crate::rule_config_serde::RuleConfig;
2use crate::types::IndentSize;
3use serde::{Deserialize, Serialize};
4
5/// Indentation style for unordered lists
6#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Default)]
7#[serde(rename_all = "kebab-case")]
8pub enum IndentStyle {
9    /// Text-aligned: Nested items align with parent's text content (rumdl default)
10    #[default]
11    #[serde(rename = "text-aligned", alias = "text_aligned")]
12    TextAligned,
13    /// Fixed: Use fixed multiples of indent size (markdownlint compatible)
14    #[serde(rename = "fixed")]
15    Fixed,
16}
17
18/// Configuration for MD007 (Unordered list indentation)
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
20#[serde(rename_all = "kebab-case")]
21pub struct MD007Config {
22    /// Indentation size for nested unordered lists (default: 2)
23    #[serde(default = "default_indent")]
24    pub indent: IndentSize,
25
26    /// Allow first level lists to start indented (default: false)
27    #[serde(default, alias = "start_indented")]
28    pub start_indented: bool,
29
30    /// Number of spaces for first level indent when start_indented is true (default: 2)
31    #[serde(default = "default_start_indent", alias = "start_indent")]
32    pub start_indent: IndentSize,
33
34    /// Indentation style: text-aligned (default) or fixed (markdownlint compatible)
35    #[serde(default)]
36    pub style: IndentStyle,
37
38    /// Whether style was explicitly set in config (used for smart auto-detection)
39    /// When false and indent != 2, we auto-select style based on document content:
40    /// - Pure unordered lists → fixed style (markdownlint compatible)
41    /// - Mixed ordered/unordered → text-aligned (avoids oscillation)
42    #[serde(skip)]
43    pub style_explicit: bool,
44
45    /// Whether indent was explicitly set in config (used for "Do What I Mean" behavior)
46    /// When indent is explicitly set but style is not, automatically use fixed style
47    #[serde(skip)]
48    pub indent_explicit: bool,
49}
50
51fn default_indent() -> IndentSize {
52    IndentSize::from_const(2)
53}
54
55fn default_start_indent() -> IndentSize {
56    IndentSize::from_const(2)
57}
58
59impl Default for MD007Config {
60    fn default() -> Self {
61        Self {
62            indent: default_indent(),
63            start_indented: false,
64            start_indent: default_start_indent(),
65            style: IndentStyle::default(),
66            style_explicit: false,
67            indent_explicit: false,
68        }
69    }
70}
71
72impl RuleConfig for MD007Config {
73    const RULE_NAME: &'static str = "MD007";
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn test_snake_case_backwards_compatibility() {
82        // Test that snake_case field names work for backwards compatibility
83        let toml_str = r#"
84            start_indented = true
85            start_indent = 4
86        "#;
87        let config: MD007Config = toml::from_str(toml_str).unwrap();
88        assert!(config.start_indented);
89        assert_eq!(config.start_indent.get(), 4);
90    }
91
92    #[test]
93    fn test_kebab_case_canonical_format() {
94        // Test that kebab-case (canonical format) works
95        let toml_str = r#"
96            start-indented = true
97            start-indent = 4
98        "#;
99        let config: MD007Config = toml::from_str(toml_str).unwrap();
100        assert!(config.start_indented);
101        assert_eq!(config.start_indent.get(), 4);
102    }
103
104    #[test]
105    fn test_indent_validation() {
106        // Test that invalid indent values are rejected
107        let toml_str = r#"
108            indent = 0
109        "#;
110        let result: Result<MD007Config, _> = toml::from_str(toml_str);
111        assert!(result.is_err());
112        let err = result.unwrap_err().to_string();
113        assert!(err.contains("must be between 1 and 8") || err.contains("got 0"));
114
115        // Test that indent value above 8 is rejected
116        let toml_str = r#"
117            indent = 9
118        "#;
119        let result: Result<MD007Config, _> = toml::from_str(toml_str);
120        assert!(result.is_err());
121        let err = result.unwrap_err().to_string();
122        assert!(err.contains("must be between 1 and 8") || err.contains("got 9"));
123    }
124}