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_code_spans", alias = "code_spans")]
63 pub code_spans: bool,
64
65 #[serde(default = "default_tables")]
71 pub tables: bool,
72
73 #[serde(default = "default_headings")]
75 pub headings: bool,
76
77 #[serde(default = "default_paragraphs")]
81 pub paragraphs: bool,
82
83 #[serde(default = "default_blockquotes")]
88 pub blockquotes: bool,
89
90 #[serde(default)]
92 pub strict: bool,
93
94 #[serde(default)]
99 pub stern: bool,
100
101 #[serde(default, alias = "heading_line_length")]
106 pub heading_line_length: Option<LineLength>,
107
108 #[serde(default, alias = "code_block_line_length")]
113 pub code_block_line_length: Option<LineLength>,
114
115 #[serde(default, alias = "enable_reflow", alias = "enable-reflow")]
117 pub reflow: bool,
118
119 #[serde(default, alias = "reflow_mode")]
121 pub reflow_mode: ReflowMode,
122
123 #[serde(default, alias = "length_mode")]
128 pub length_mode: LengthMode,
129
130 #[serde(default)]
135 pub abbreviations: Vec<String>,
136
137 #[serde(
142 default = "default_require_sentence_capital",
143 alias = "require_sentence_capital",
144 alias = "strict_sentences",
145 alias = "strict-sentences"
146 )]
147 pub require_sentence_capital: bool,
148}
149
150fn default_line_length() -> LineLength {
151 LineLength::from_const(80)
152}
153
154fn default_code_blocks() -> bool {
155 true
156}
157
158fn default_code_spans() -> bool {
159 true
160}
161
162fn default_tables() -> bool {
163 false
164}
165
166fn default_headings() -> bool {
167 true
168}
169
170fn default_paragraphs() -> bool {
171 true
172}
173
174fn default_blockquotes() -> bool {
175 true
176}
177
178fn default_require_sentence_capital() -> bool {
179 true
180}
181
182impl Default for MD013Config {
183 fn default() -> Self {
184 Self {
185 line_length: default_line_length(),
186 code_blocks: default_code_blocks(),
187 code_spans: default_code_spans(),
188 tables: default_tables(),
189 headings: default_headings(),
190 paragraphs: default_paragraphs(),
191 blockquotes: default_blockquotes(),
192 strict: false,
193 stern: false,
194 heading_line_length: None,
195 code_block_line_length: None,
196 reflow: false,
197 reflow_mode: ReflowMode::default(),
198 length_mode: LengthMode::default(),
199 abbreviations: Vec::new(),
200 require_sentence_capital: default_require_sentence_capital(),
201 }
202 }
203}
204
205impl MD013Config {
206 pub fn effective_heading_line_length(&self) -> LineLength {
209 self.heading_line_length.unwrap_or(self.line_length)
210 }
211
212 pub fn effective_code_block_line_length(&self) -> LineLength {
215 self.code_block_line_length.unwrap_or(self.line_length)
216 }
217
218 pub fn min_effective_line_length(&self) -> LineLength {
222 let mut limits: Vec<LineLength> = vec![self.line_length];
223 if let Some(h) = self.heading_line_length {
224 limits.push(h);
225 }
226 if let Some(c) = self.code_block_line_length {
227 limits.push(c);
228 }
229 let bounded: Vec<LineLength> = limits.iter().copied().filter(|l| !l.is_unlimited()).collect();
232 if bounded.is_empty() {
233 LineLength::from_const(0)
234 } else {
235 bounded.into_iter().min_by_key(|l| l.get()).unwrap()
236 }
237 }
238
239 pub fn abbreviations_for_reflow(&self) -> Option<Vec<String>> {
242 if self.abbreviations.is_empty() {
243 None
244 } else {
245 Some(self.abbreviations.clone())
246 }
247 }
248
249 pub fn to_reflow_options(&self) -> crate::utils::text_reflow::ReflowOptions {
254 let length_mode = match self.length_mode {
255 LengthMode::Chars => crate::utils::text_reflow::ReflowLengthMode::Chars,
256 LengthMode::Visual => crate::utils::text_reflow::ReflowLengthMode::Visual,
257 LengthMode::Bytes => crate::utils::text_reflow::ReflowLengthMode::Bytes,
258 };
259 crate::utils::text_reflow::ReflowOptions {
260 line_length: self.line_length.get(),
261 break_on_sentences: true,
262 preserve_breaks: false,
263 sentence_per_line: self.reflow_mode == ReflowMode::SentencePerLine,
264 semantic_line_breaks: self.reflow_mode == ReflowMode::SemanticLineBreaks,
265 abbreviations: self.abbreviations_for_reflow(),
266 length_mode,
267 attr_lists: false,
268 myst_roles: false,
269 require_sentence_capital: self.require_sentence_capital,
270 max_list_continuation_indent: None,
271 }
272 }
273}
274
275impl RuleConfig for MD013Config {
276 const RULE_NAME: &'static str = "MD013";
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 #[test]
284 fn test_reflow_mode_deserialization_kebab_case() {
285 let toml_str = r#"
288 reflow-mode = "sentence-per-line"
289 "#;
290 let config: MD013Config = toml::from_str(toml_str).unwrap();
291 assert_eq!(config.reflow_mode, ReflowMode::SentencePerLine);
292
293 let toml_str = r#"
294 reflow-mode = "default"
295 "#;
296 let config: MD013Config = toml::from_str(toml_str).unwrap();
297 assert_eq!(config.reflow_mode, ReflowMode::Default);
298
299 let toml_str = r#"
300 reflow-mode = "normalize"
301 "#;
302 let config: MD013Config = toml::from_str(toml_str).unwrap();
303 assert_eq!(config.reflow_mode, ReflowMode::Normalize);
304
305 let toml_str = r#"
306 reflow-mode = "semantic-line-breaks"
307 "#;
308 let config: MD013Config = toml::from_str(toml_str).unwrap();
309 assert_eq!(config.reflow_mode, ReflowMode::SemanticLineBreaks);
310 }
311
312 #[test]
313 fn test_reflow_mode_deserialization_snake_case_alias() {
314 let toml_str = r#"
317 reflow-mode = "sentence_per_line"
318 "#;
319 let config: MD013Config = toml::from_str(toml_str).unwrap();
320 assert_eq!(config.reflow_mode, ReflowMode::SentencePerLine);
321
322 let toml_str = r#"
323 reflow-mode = "semantic_line_breaks"
324 "#;
325 let config: MD013Config = toml::from_str(toml_str).unwrap();
326 assert_eq!(config.reflow_mode, ReflowMode::SemanticLineBreaks);
327 }
328
329 #[test]
330 fn test_field_name_backwards_compatibility() {
331 let toml_str = r#"
334 line_length = 100
335 code_blocks = false
336 reflow_mode = "sentence_per_line"
337 "#;
338 let config: MD013Config = toml::from_str(toml_str).unwrap();
339 assert_eq!(config.line_length.get(), 100);
340 assert!(!config.code_blocks);
341 assert_eq!(config.reflow_mode, ReflowMode::SentencePerLine);
342
343 let toml_str = r#"
345 line-length = 100
346 code_blocks = false
347 reflow-mode = "normalize"
348 "#;
349 let config: MD013Config = toml::from_str(toml_str).unwrap();
350 assert_eq!(config.line_length.get(), 100);
351 assert!(!config.code_blocks);
352 assert_eq!(config.reflow_mode, ReflowMode::Normalize);
353 }
354
355 #[test]
356 fn test_reflow_mode_serialization() {
357 let config = MD013Config {
359 line_length: LineLength::from_const(80),
360 code_blocks: true,
361 code_spans: true,
362 tables: true,
363 headings: true,
364 paragraphs: true,
365 blockquotes: true,
366 strict: false,
367 stern: false,
368 heading_line_length: None,
369 code_block_line_length: None,
370 reflow: true,
371 reflow_mode: ReflowMode::SentencePerLine,
372 length_mode: LengthMode::default(),
373 abbreviations: Vec::new(),
374 require_sentence_capital: true,
375 };
376
377 let toml_str = toml::to_string(&config).unwrap();
378 assert!(toml_str.contains("sentence-per-line"));
379 assert!(!toml_str.contains("sentence_per_line"));
380
381 let config = MD013Config {
383 reflow_mode: ReflowMode::SemanticLineBreaks,
384 ..config
385 };
386 let toml_str = toml::to_string(&config).unwrap();
387 assert!(toml_str.contains("semantic-line-breaks"));
388 assert!(!toml_str.contains("semantic_line_breaks"));
389 }
390
391 #[test]
392 fn test_reflow_mode_invalid_value() {
393 let toml_str = r#"
395 reflow-mode = "invalid_mode"
396 "#;
397 let result = toml::from_str::<MD013Config>(toml_str);
398 assert!(result.is_err());
399 }
400
401 #[test]
402 fn test_full_config_with_reflow_mode() {
403 let toml_str = r#"
404 line-length = 100
405 code-blocks = false
406 tables = false
407 headings = true
408 strict = true
409 reflow = true
410 reflow-mode = "sentence-per-line"
411 "#;
412 let config: MD013Config = toml::from_str(toml_str).unwrap();
413 assert_eq!(config.line_length.get(), 100);
414 assert!(!config.code_blocks);
415 assert!(!config.tables);
416 assert!(config.headings);
417 assert!(config.strict);
418 assert!(config.reflow);
419 assert_eq!(config.reflow_mode, ReflowMode::SentencePerLine);
420 }
421
422 #[test]
423 fn test_code_spans_default_true_and_parses() {
424 assert!(MD013Config::default().code_spans, "code-spans defaults to true");
425
426 let config: MD013Config = toml::from_str("code-spans = false").unwrap();
427 assert!(!config.code_spans);
428 let config: MD013Config = toml::from_str("code_spans = false").unwrap();
430 assert!(!config.code_spans);
431 }
432
433 #[test]
434 fn test_paragraphs_default_true() {
435 let config = MD013Config::default();
437 assert!(config.paragraphs, "paragraphs should default to true");
438 }
439
440 #[test]
441 fn test_paragraphs_deserialization_kebab_case() {
442 let toml_str = r#"
444 paragraphs = false
445 "#;
446 let config: MD013Config = toml::from_str(toml_str).unwrap();
447 assert!(!config.paragraphs);
448 }
449
450 #[test]
451 fn test_paragraphs_full_config() {
452 let toml_str = r#"
454 line-length = 80
455 code-blocks = true
456 tables = true
457 headings = false
458 paragraphs = false
459 reflow = true
460 reflow-mode = "sentence-per-line"
461 "#;
462 let config: MD013Config = toml::from_str(toml_str).unwrap();
463 assert_eq!(config.line_length.get(), 80);
464 assert!(config.code_blocks, "code-blocks should be true");
465 assert!(config.tables, "tables should be true");
466 assert!(!config.headings, "headings should be false");
467 assert!(!config.paragraphs, "paragraphs should be false");
468 assert!(config.reflow, "reflow should be true");
469 assert_eq!(config.reflow_mode, ReflowMode::SentencePerLine);
470 }
471
472 #[test]
473 fn test_abbreviations_for_reflow_empty_vec() {
474 let config = MD013Config {
476 abbreviations: Vec::new(),
477 ..Default::default()
478 };
479 assert!(
480 config.abbreviations_for_reflow().is_none(),
481 "Empty abbreviations should return None for reflow"
482 );
483 }
484
485 #[test]
486 fn test_abbreviations_for_reflow_with_custom() {
487 let config = MD013Config {
489 abbreviations: vec!["Corp".to_string(), "Inc".to_string()],
490 ..Default::default()
491 };
492 let result = config.abbreviations_for_reflow();
493 assert!(result.is_some(), "Custom abbreviations should return Some");
494 let abbrevs = result.unwrap();
495 assert_eq!(abbrevs.len(), 2);
496 assert!(abbrevs.contains(&"Corp".to_string()));
497 assert!(abbrevs.contains(&"Inc".to_string()));
498 }
499}