rumdl_lib/rules/md013_line_length/
md013_config.rs1use crate::rule_config_serde::RuleConfig;
2use crate::types::LineLength;
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Default)]
7#[serde(rename_all = "kebab-case")]
8pub enum ReflowMode {
9 #[default]
11 Default,
12 Normalize,
14 #[serde(alias = "sentence_per_line")]
16 SentencePerLine,
17 #[serde(alias = "semantic_line_breaks")]
23 SemanticLineBreaks,
24}
25
26#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Default)]
28#[serde(rename_all = "kebab-case")]
29pub enum LengthMode {
30 #[serde(alias = "chars", alias = "characters")]
33 Chars,
34 #[default]
37 #[serde(alias = "display", alias = "visual_width")]
38 Visual,
39 Bytes,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
45#[serde(rename_all = "kebab-case")]
46pub struct MD013Config {
47 #[serde(default = "default_line_length", alias = "line_length")]
49 pub line_length: LineLength,
50
51 #[serde(default = "default_code_blocks", alias = "code_blocks")]
53 pub code_blocks: bool,
54
55 #[serde(default = "default_tables")]
61 pub tables: bool,
62
63 #[serde(default = "default_headings")]
65 pub headings: bool,
66
67 #[serde(default = "default_paragraphs")]
71 pub paragraphs: bool,
72
73 #[serde(default = "default_blockquotes")]
78 pub blockquotes: bool,
79
80 #[serde(default)]
82 pub strict: bool,
83
84 #[serde(default)]
89 pub stern: bool,
90
91 #[serde(default, alias = "heading_line_length")]
96 pub heading_line_length: Option<LineLength>,
97
98 #[serde(default, alias = "code_block_line_length")]
103 pub code_block_line_length: Option<LineLength>,
104
105 #[serde(default, alias = "enable_reflow", alias = "enable-reflow")]
107 pub reflow: bool,
108
109 #[serde(default, alias = "reflow_mode")]
111 pub reflow_mode: ReflowMode,
112
113 #[serde(default, alias = "length_mode")]
118 pub length_mode: LengthMode,
119
120 #[serde(default)]
125 pub abbreviations: Vec<String>,
126
127 #[serde(
132 default = "default_require_sentence_capital",
133 alias = "require_sentence_capital",
134 alias = "strict_sentences",
135 alias = "strict-sentences"
136 )]
137 pub require_sentence_capital: bool,
138}
139
140fn default_line_length() -> LineLength {
141 LineLength::from_const(80)
142}
143
144fn default_code_blocks() -> bool {
145 true
146}
147
148fn default_tables() -> bool {
149 false
150}
151
152fn default_headings() -> bool {
153 true
154}
155
156fn default_paragraphs() -> bool {
157 true
158}
159
160fn default_blockquotes() -> bool {
161 true
162}
163
164fn default_require_sentence_capital() -> bool {
165 true
166}
167
168impl Default for MD013Config {
169 fn default() -> Self {
170 Self {
171 line_length: default_line_length(),
172 code_blocks: default_code_blocks(),
173 tables: default_tables(),
174 headings: default_headings(),
175 paragraphs: default_paragraphs(),
176 blockquotes: default_blockquotes(),
177 strict: false,
178 stern: false,
179 heading_line_length: None,
180 code_block_line_length: None,
181 reflow: false,
182 reflow_mode: ReflowMode::default(),
183 length_mode: LengthMode::default(),
184 abbreviations: Vec::new(),
185 require_sentence_capital: default_require_sentence_capital(),
186 }
187 }
188}
189
190impl MD013Config {
191 pub fn effective_heading_line_length(&self) -> LineLength {
194 self.heading_line_length.unwrap_or(self.line_length)
195 }
196
197 pub fn effective_code_block_line_length(&self) -> LineLength {
200 self.code_block_line_length.unwrap_or(self.line_length)
201 }
202
203 pub fn min_effective_line_length(&self) -> LineLength {
207 let mut limits: Vec<LineLength> = vec![self.line_length];
208 if let Some(h) = self.heading_line_length {
209 limits.push(h);
210 }
211 if let Some(c) = self.code_block_line_length {
212 limits.push(c);
213 }
214 let bounded: Vec<LineLength> = limits.iter().copied().filter(|l| !l.is_unlimited()).collect();
217 if bounded.is_empty() {
218 LineLength::from_const(0)
219 } else {
220 bounded.into_iter().min_by_key(|l| l.get()).unwrap()
221 }
222 }
223
224 pub fn abbreviations_for_reflow(&self) -> Option<Vec<String>> {
227 if self.abbreviations.is_empty() {
228 None
229 } else {
230 Some(self.abbreviations.clone())
231 }
232 }
233
234 pub fn to_reflow_options(&self) -> crate::utils::text_reflow::ReflowOptions {
239 let length_mode = match self.length_mode {
240 LengthMode::Chars => crate::utils::text_reflow::ReflowLengthMode::Chars,
241 LengthMode::Visual => crate::utils::text_reflow::ReflowLengthMode::Visual,
242 LengthMode::Bytes => crate::utils::text_reflow::ReflowLengthMode::Bytes,
243 };
244 crate::utils::text_reflow::ReflowOptions {
245 line_length: self.line_length.get(),
246 break_on_sentences: true,
247 preserve_breaks: false,
248 sentence_per_line: self.reflow_mode == ReflowMode::SentencePerLine,
249 semantic_line_breaks: self.reflow_mode == ReflowMode::SemanticLineBreaks,
250 abbreviations: self.abbreviations_for_reflow(),
251 length_mode,
252 attr_lists: false,
253 require_sentence_capital: self.require_sentence_capital,
254 max_list_continuation_indent: None,
255 }
256 }
257}
258
259impl RuleConfig for MD013Config {
260 const RULE_NAME: &'static str = "MD013";
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn test_reflow_mode_deserialization_kebab_case() {
269 let toml_str = r#"
272 reflow-mode = "sentence-per-line"
273 "#;
274 let config: MD013Config = toml::from_str(toml_str).unwrap();
275 assert_eq!(config.reflow_mode, ReflowMode::SentencePerLine);
276
277 let toml_str = r#"
278 reflow-mode = "default"
279 "#;
280 let config: MD013Config = toml::from_str(toml_str).unwrap();
281 assert_eq!(config.reflow_mode, ReflowMode::Default);
282
283 let toml_str = r#"
284 reflow-mode = "normalize"
285 "#;
286 let config: MD013Config = toml::from_str(toml_str).unwrap();
287 assert_eq!(config.reflow_mode, ReflowMode::Normalize);
288
289 let toml_str = r#"
290 reflow-mode = "semantic-line-breaks"
291 "#;
292 let config: MD013Config = toml::from_str(toml_str).unwrap();
293 assert_eq!(config.reflow_mode, ReflowMode::SemanticLineBreaks);
294 }
295
296 #[test]
297 fn test_reflow_mode_deserialization_snake_case_alias() {
298 let toml_str = r#"
301 reflow-mode = "sentence_per_line"
302 "#;
303 let config: MD013Config = toml::from_str(toml_str).unwrap();
304 assert_eq!(config.reflow_mode, ReflowMode::SentencePerLine);
305
306 let toml_str = r#"
307 reflow-mode = "semantic_line_breaks"
308 "#;
309 let config: MD013Config = toml::from_str(toml_str).unwrap();
310 assert_eq!(config.reflow_mode, ReflowMode::SemanticLineBreaks);
311 }
312
313 #[test]
314 fn test_field_name_backwards_compatibility() {
315 let toml_str = r#"
318 line_length = 100
319 code_blocks = false
320 reflow_mode = "sentence_per_line"
321 "#;
322 let config: MD013Config = toml::from_str(toml_str).unwrap();
323 assert_eq!(config.line_length.get(), 100);
324 assert!(!config.code_blocks);
325 assert_eq!(config.reflow_mode, ReflowMode::SentencePerLine);
326
327 let toml_str = r#"
329 line-length = 100
330 code_blocks = false
331 reflow-mode = "normalize"
332 "#;
333 let config: MD013Config = toml::from_str(toml_str).unwrap();
334 assert_eq!(config.line_length.get(), 100);
335 assert!(!config.code_blocks);
336 assert_eq!(config.reflow_mode, ReflowMode::Normalize);
337 }
338
339 #[test]
340 fn test_reflow_mode_serialization() {
341 let config = MD013Config {
343 line_length: LineLength::from_const(80),
344 code_blocks: true,
345 tables: true,
346 headings: true,
347 paragraphs: true,
348 blockquotes: true,
349 strict: false,
350 stern: false,
351 heading_line_length: None,
352 code_block_line_length: None,
353 reflow: true,
354 reflow_mode: ReflowMode::SentencePerLine,
355 length_mode: LengthMode::default(),
356 abbreviations: Vec::new(),
357 require_sentence_capital: true,
358 };
359
360 let toml_str = toml::to_string(&config).unwrap();
361 assert!(toml_str.contains("sentence-per-line"));
362 assert!(!toml_str.contains("sentence_per_line"));
363
364 let config = MD013Config {
366 reflow_mode: ReflowMode::SemanticLineBreaks,
367 ..config
368 };
369 let toml_str = toml::to_string(&config).unwrap();
370 assert!(toml_str.contains("semantic-line-breaks"));
371 assert!(!toml_str.contains("semantic_line_breaks"));
372 }
373
374 #[test]
375 fn test_reflow_mode_invalid_value() {
376 let toml_str = r#"
378 reflow-mode = "invalid_mode"
379 "#;
380 let result = toml::from_str::<MD013Config>(toml_str);
381 assert!(result.is_err());
382 }
383
384 #[test]
385 fn test_full_config_with_reflow_mode() {
386 let toml_str = r#"
387 line-length = 100
388 code-blocks = false
389 tables = false
390 headings = true
391 strict = true
392 reflow = true
393 reflow-mode = "sentence-per-line"
394 "#;
395 let config: MD013Config = toml::from_str(toml_str).unwrap();
396 assert_eq!(config.line_length.get(), 100);
397 assert!(!config.code_blocks);
398 assert!(!config.tables);
399 assert!(config.headings);
400 assert!(config.strict);
401 assert!(config.reflow);
402 assert_eq!(config.reflow_mode, ReflowMode::SentencePerLine);
403 }
404
405 #[test]
406 fn test_paragraphs_default_true() {
407 let config = MD013Config::default();
409 assert!(config.paragraphs, "paragraphs should default to true");
410 }
411
412 #[test]
413 fn test_paragraphs_deserialization_kebab_case() {
414 let toml_str = r#"
416 paragraphs = false
417 "#;
418 let config: MD013Config = toml::from_str(toml_str).unwrap();
419 assert!(!config.paragraphs);
420 }
421
422 #[test]
423 fn test_paragraphs_full_config() {
424 let toml_str = r#"
426 line-length = 80
427 code-blocks = true
428 tables = true
429 headings = false
430 paragraphs = false
431 reflow = true
432 reflow-mode = "sentence-per-line"
433 "#;
434 let config: MD013Config = toml::from_str(toml_str).unwrap();
435 assert_eq!(config.line_length.get(), 80);
436 assert!(config.code_blocks, "code-blocks should be true");
437 assert!(config.tables, "tables should be true");
438 assert!(!config.headings, "headings should be false");
439 assert!(!config.paragraphs, "paragraphs should be false");
440 assert!(config.reflow, "reflow should be true");
441 assert_eq!(config.reflow_mode, ReflowMode::SentencePerLine);
442 }
443
444 #[test]
445 fn test_abbreviations_for_reflow_empty_vec() {
446 let config = MD013Config {
448 abbreviations: Vec::new(),
449 ..Default::default()
450 };
451 assert!(
452 config.abbreviations_for_reflow().is_none(),
453 "Empty abbreviations should return None for reflow"
454 );
455 }
456
457 #[test]
458 fn test_abbreviations_for_reflow_with_custom() {
459 let config = MD013Config {
461 abbreviations: vec!["Corp".to_string(), "Inc".to_string()],
462 ..Default::default()
463 };
464 let result = config.abbreviations_for_reflow();
465 assert!(result.is_some(), "Custom abbreviations should return Some");
466 let abbrevs = result.unwrap();
467 assert_eq!(abbrevs.len(), 2);
468 assert!(abbrevs.contains(&"Corp".to_string()));
469 assert!(abbrevs.contains(&"Inc".to_string()));
470 }
471}