use crate::rule_config_serde::RuleConfig;
use crate::types::LineLength;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "kebab-case")]
pub enum ReflowMode {
#[default]
Default,
Normalize,
#[serde(alias = "sentence_per_line")]
SentencePerLine,
#[serde(alias = "semantic_line_breaks")]
SemanticLineBreaks,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "kebab-case")]
pub enum LengthMode {
#[serde(alias = "chars", alias = "characters")]
Chars,
#[default]
#[serde(alias = "display", alias = "visual_width")]
Visual,
Bytes,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub struct MD013Config {
#[serde(default = "default_line_length", alias = "line_length")]
pub line_length: LineLength,
#[serde(default = "default_code_blocks", alias = "code_blocks")]
pub code_blocks: bool,
#[serde(default = "default_tables")]
pub tables: bool,
#[serde(default = "default_headings")]
pub headings: bool,
#[serde(default = "default_paragraphs")]
pub paragraphs: bool,
#[serde(default)]
pub strict: bool,
#[serde(default, alias = "enable_reflow", alias = "enable-reflow")]
pub reflow: bool,
#[serde(default, alias = "reflow_mode")]
pub reflow_mode: ReflowMode,
#[serde(default, alias = "length_mode")]
pub length_mode: LengthMode,
#[serde(default)]
pub abbreviations: Vec<String>,
#[serde(
default = "default_require_sentence_capital",
alias = "require_sentence_capital",
alias = "strict_sentences",
alias = "strict-sentences"
)]
pub require_sentence_capital: bool,
}
fn default_line_length() -> LineLength {
LineLength::from_const(80)
}
fn default_code_blocks() -> bool {
true
}
fn default_tables() -> bool {
false
}
fn default_headings() -> bool {
true
}
fn default_paragraphs() -> bool {
true
}
fn default_require_sentence_capital() -> bool {
true
}
impl Default for MD013Config {
fn default() -> Self {
Self {
line_length: default_line_length(),
code_blocks: default_code_blocks(),
tables: default_tables(),
headings: default_headings(),
paragraphs: default_paragraphs(),
strict: false,
reflow: false,
reflow_mode: ReflowMode::default(),
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: default_require_sentence_capital(),
}
}
}
impl MD013Config {
pub fn abbreviations_for_reflow(&self) -> Option<Vec<String>> {
if self.abbreviations.is_empty() {
None
} else {
Some(self.abbreviations.clone())
}
}
pub fn to_reflow_options(&self) -> crate::utils::text_reflow::ReflowOptions {
let length_mode = match self.length_mode {
LengthMode::Chars => crate::utils::text_reflow::ReflowLengthMode::Chars,
LengthMode::Visual => crate::utils::text_reflow::ReflowLengthMode::Visual,
LengthMode::Bytes => crate::utils::text_reflow::ReflowLengthMode::Bytes,
};
crate::utils::text_reflow::ReflowOptions {
line_length: self.line_length.get(),
break_on_sentences: true,
preserve_breaks: false,
sentence_per_line: self.reflow_mode == ReflowMode::SentencePerLine,
semantic_line_breaks: self.reflow_mode == ReflowMode::SemanticLineBreaks,
abbreviations: self.abbreviations_for_reflow(),
length_mode,
attr_lists: false,
require_sentence_capital: self.require_sentence_capital,
max_list_continuation_indent: None,
}
}
}
impl RuleConfig for MD013Config {
const RULE_NAME: &'static str = "MD013";
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_reflow_mode_deserialization_kebab_case() {
let toml_str = r#"
reflow-mode = "sentence-per-line"
"#;
let config: MD013Config = toml::from_str(toml_str).unwrap();
assert_eq!(config.reflow_mode, ReflowMode::SentencePerLine);
let toml_str = r#"
reflow-mode = "default"
"#;
let config: MD013Config = toml::from_str(toml_str).unwrap();
assert_eq!(config.reflow_mode, ReflowMode::Default);
let toml_str = r#"
reflow-mode = "normalize"
"#;
let config: MD013Config = toml::from_str(toml_str).unwrap();
assert_eq!(config.reflow_mode, ReflowMode::Normalize);
let toml_str = r#"
reflow-mode = "semantic-line-breaks"
"#;
let config: MD013Config = toml::from_str(toml_str).unwrap();
assert_eq!(config.reflow_mode, ReflowMode::SemanticLineBreaks);
}
#[test]
fn test_reflow_mode_deserialization_snake_case_alias() {
let toml_str = r#"
reflow-mode = "sentence_per_line"
"#;
let config: MD013Config = toml::from_str(toml_str).unwrap();
assert_eq!(config.reflow_mode, ReflowMode::SentencePerLine);
let toml_str = r#"
reflow-mode = "semantic_line_breaks"
"#;
let config: MD013Config = toml::from_str(toml_str).unwrap();
assert_eq!(config.reflow_mode, ReflowMode::SemanticLineBreaks);
}
#[test]
fn test_field_name_backwards_compatibility() {
let toml_str = r#"
line_length = 100
code_blocks = false
reflow_mode = "sentence_per_line"
"#;
let config: MD013Config = toml::from_str(toml_str).unwrap();
assert_eq!(config.line_length.get(), 100);
assert!(!config.code_blocks);
assert_eq!(config.reflow_mode, ReflowMode::SentencePerLine);
let toml_str = r#"
line-length = 100
code_blocks = false
reflow-mode = "normalize"
"#;
let config: MD013Config = toml::from_str(toml_str).unwrap();
assert_eq!(config.line_length.get(), 100);
assert!(!config.code_blocks);
assert_eq!(config.reflow_mode, ReflowMode::Normalize);
}
#[test]
fn test_reflow_mode_serialization() {
let config = MD013Config {
line_length: LineLength::from_const(80),
code_blocks: true,
tables: true,
headings: true,
paragraphs: true,
strict: false,
reflow: true,
reflow_mode: ReflowMode::SentencePerLine,
length_mode: LengthMode::default(),
abbreviations: Vec::new(),
require_sentence_capital: true,
};
let toml_str = toml::to_string(&config).unwrap();
assert!(toml_str.contains("sentence-per-line"));
assert!(!toml_str.contains("sentence_per_line"));
let config = MD013Config {
reflow_mode: ReflowMode::SemanticLineBreaks,
..config
};
let toml_str = toml::to_string(&config).unwrap();
assert!(toml_str.contains("semantic-line-breaks"));
assert!(!toml_str.contains("semantic_line_breaks"));
}
#[test]
fn test_reflow_mode_invalid_value() {
let toml_str = r#"
reflow-mode = "invalid_mode"
"#;
let result = toml::from_str::<MD013Config>(toml_str);
assert!(result.is_err());
}
#[test]
fn test_full_config_with_reflow_mode() {
let toml_str = r#"
line-length = 100
code-blocks = false
tables = false
headings = true
strict = true
reflow = true
reflow-mode = "sentence-per-line"
"#;
let config: MD013Config = toml::from_str(toml_str).unwrap();
assert_eq!(config.line_length.get(), 100);
assert!(!config.code_blocks);
assert!(!config.tables);
assert!(config.headings);
assert!(config.strict);
assert!(config.reflow);
assert_eq!(config.reflow_mode, ReflowMode::SentencePerLine);
}
#[test]
fn test_paragraphs_default_true() {
let config = MD013Config::default();
assert!(config.paragraphs, "paragraphs should default to true");
}
#[test]
fn test_paragraphs_deserialization_kebab_case() {
let toml_str = r#"
paragraphs = false
"#;
let config: MD013Config = toml::from_str(toml_str).unwrap();
assert!(!config.paragraphs);
}
#[test]
fn test_paragraphs_full_config() {
let toml_str = r#"
line-length = 80
code-blocks = true
tables = true
headings = false
paragraphs = false
reflow = true
reflow-mode = "sentence-per-line"
"#;
let config: MD013Config = toml::from_str(toml_str).unwrap();
assert_eq!(config.line_length.get(), 80);
assert!(config.code_blocks, "code-blocks should be true");
assert!(config.tables, "tables should be true");
assert!(!config.headings, "headings should be false");
assert!(!config.paragraphs, "paragraphs should be false");
assert!(config.reflow, "reflow should be true");
assert_eq!(config.reflow_mode, ReflowMode::SentencePerLine);
}
#[test]
fn test_abbreviations_for_reflow_empty_vec() {
let config = MD013Config {
abbreviations: Vec::new(),
..Default::default()
};
assert!(
config.abbreviations_for_reflow().is_none(),
"Empty abbreviations should return None for reflow"
);
}
#[test]
fn test_abbreviations_for_reflow_with_custom() {
let config = MD013Config {
abbreviations: vec!["Corp".to_string(), "Inc".to_string()],
..Default::default()
};
let result = config.abbreviations_for_reflow();
assert!(result.is_some(), "Custom abbreviations should return Some");
let abbrevs = result.unwrap();
assert_eq!(abbrevs.len(), 2);
assert!(abbrevs.contains(&"Corp".to_string()));
assert!(abbrevs.contains(&"Inc".to_string()));
}
}