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_math_blocks")]
89 pub math_blocks: bool,
90
91 #[serde(default = "default_paragraphs")]
95 pub paragraphs: bool,
96
97 #[serde(default = "default_blockquotes")]
102 pub blockquotes: bool,
103
104 #[serde(default)]
106 pub strict: bool,
107
108 #[serde(default)]
113 pub stern: bool,
114
115 #[serde(
128 default = "default_ignore_link_urls",
129 alias = "ignore_link_urls",
130 alias = "semantic-link-understanding",
131 alias = "semantic_link_understanding"
132 )]
133 pub ignore_link_urls: bool,
134
135 #[serde(default, alias = "heading_line_length")]
140 pub heading_line_length: Option<LineLength>,
141
142 #[serde(default, alias = "code_block_line_length")]
147 pub code_block_line_length: Option<LineLength>,
148
149 #[serde(default, alias = "enable_reflow", alias = "enable-reflow")]
151 pub reflow: bool,
152
153 #[serde(default, alias = "reflow_mode")]
155 pub reflow_mode: ReflowMode,
156
157 #[serde(default, alias = "length_mode")]
162 pub length_mode: LengthMode,
163
164 #[serde(default)]
169 pub abbreviations: Vec<String>,
170
171 #[serde(
176 default = "default_require_sentence_capital",
177 alias = "require_sentence_capital",
178 alias = "strict_sentences",
179 alias = "strict-sentences"
180 )]
181 pub require_sentence_capital: bool,
182
183 #[serde(default = "default_atomic_spans", alias = "atomic_spans")]
187 pub atomic_spans: bool,
188
189 #[serde(default)]
200 pub reflow_length_exemptions: bool,
201}
202
203fn default_line_length() -> LineLength {
204 LineLength::from_const(80)
205}
206
207fn default_code_blocks() -> bool {
208 true
209}
210
211fn default_code_spans() -> bool {
212 true
213}
214
215fn default_tables() -> bool {
216 false
217}
218
219fn default_headings() -> bool {
220 true
221}
222
223fn default_math_blocks() -> bool {
224 true
225}
226
227fn default_paragraphs() -> bool {
228 true
229}
230
231fn default_blockquotes() -> bool {
232 true
233}
234
235fn default_require_sentence_capital() -> bool {
236 true
237}
238
239fn default_ignore_link_urls() -> bool {
240 true
241}
242
243fn default_atomic_spans() -> bool {
244 true
245}
246
247impl Default for MD013Config {
248 fn default() -> Self {
249 Self {
250 line_length: default_line_length(),
251 code_blocks: default_code_blocks(),
252 code_spans: default_code_spans(),
253 tables: default_tables(),
254 headings: default_headings(),
255 math_blocks: default_math_blocks(),
256 paragraphs: default_paragraphs(),
257 blockquotes: default_blockquotes(),
258 strict: false,
259 stern: false,
260 ignore_link_urls: default_ignore_link_urls(),
261 heading_line_length: None,
262 code_block_line_length: None,
263 reflow: false,
264 reflow_mode: ReflowMode::default(),
265 length_mode: LengthMode::default(),
266 abbreviations: Vec::new(),
267 require_sentence_capital: default_require_sentence_capital(),
268 atomic_spans: default_atomic_spans(),
269 reflow_length_exemptions: false,
270 }
271 }
272}
273
274impl MD013Config {
275 pub fn effective_heading_line_length(&self) -> LineLength {
278 self.heading_line_length.unwrap_or(self.line_length)
279 }
280
281 pub fn effective_code_block_line_length(&self) -> LineLength {
284 self.code_block_line_length.unwrap_or(self.line_length)
285 }
286
287 pub fn min_effective_line_length(&self) -> LineLength {
291 [
292 Some(self.line_length),
293 self.heading_line_length,
294 self.code_block_line_length,
295 ]
296 .into_iter()
297 .flatten()
298 .filter(|l| !l.is_unlimited())
299 .min_by_key(|l| l.get())
300 .unwrap_or(LineLength::from_const(0))
301 }
302
303 pub fn abbreviations_for_reflow(&self) -> Option<Vec<String>> {
306 if self.abbreviations.is_empty() {
307 None
308 } else {
309 Some(self.abbreviations.clone())
310 }
311 }
312
313 pub(crate) fn length_exemptions_for_reflow(&self) -> crate::utils::text_reflow::LengthExemptions {
318 if !self.reflow_length_exemptions {
319 return crate::utils::text_reflow::LengthExemptions::default();
320 }
321 crate::utils::text_reflow::LengthExemptions {
322 link_urls: !self.strict && self.ignore_link_urls,
323 code_spans: !self.code_spans,
324 }
325 }
326
327 pub(crate) fn reflow_length_mode(&self) -> crate::utils::text_reflow::ReflowLengthMode {
329 match self.length_mode {
330 LengthMode::Chars => crate::utils::text_reflow::ReflowLengthMode::Chars,
331 LengthMode::Visual => crate::utils::text_reflow::ReflowLengthMode::Visual,
332 LengthMode::Bytes => crate::utils::text_reflow::ReflowLengthMode::Bytes,
333 }
334 }
335
336 pub fn to_reflow_options(&self) -> crate::utils::text_reflow::ReflowOptions {
341 crate::utils::text_reflow::ReflowOptions {
342 line_length: self.line_length.get(),
343 break_on_sentences: true,
344 preserve_breaks: false,
345 sentence_per_line: self.reflow_mode == ReflowMode::SentencePerLine,
346 semantic_line_breaks: self.reflow_mode == ReflowMode::SemanticLineBreaks,
347 abbreviations: self.abbreviations_for_reflow(),
348 length_mode: self.reflow_length_mode(),
349 attr_lists: false,
350 myst_roles: false,
351 require_sentence_capital: self.require_sentence_capital,
352 max_list_continuation_indent: None,
353 defined_references: None,
356 atomic_spans: self.atomic_spans,
357 length_exemptions: self.length_exemptions_for_reflow(),
358 }
359 }
360}
361
362impl RuleConfig for MD013Config {
363 const RULE_NAME: &'static str = "MD013";
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369
370 #[test]
371 fn test_reflow_mode_deserialization_kebab_case() {
372 let toml_str = r#"
375 reflow-mode = "sentence-per-line"
376 "#;
377 let config: MD013Config = toml::from_str(toml_str).unwrap();
378 assert_eq!(config.reflow_mode, ReflowMode::SentencePerLine);
379
380 let toml_str = r#"
381 reflow-mode = "default"
382 "#;
383 let config: MD013Config = toml::from_str(toml_str).unwrap();
384 assert_eq!(config.reflow_mode, ReflowMode::Default);
385
386 let toml_str = r#"
387 reflow-mode = "normalize"
388 "#;
389 let config: MD013Config = toml::from_str(toml_str).unwrap();
390 assert_eq!(config.reflow_mode, ReflowMode::Normalize);
391
392 let toml_str = r#"
393 reflow-mode = "semantic-line-breaks"
394 "#;
395 let config: MD013Config = toml::from_str(toml_str).unwrap();
396 assert_eq!(config.reflow_mode, ReflowMode::SemanticLineBreaks);
397 }
398
399 #[test]
400 fn test_reflow_mode_deserialization_snake_case_alias() {
401 let toml_str = r#"
404 reflow-mode = "sentence_per_line"
405 "#;
406 let config: MD013Config = toml::from_str(toml_str).unwrap();
407 assert_eq!(config.reflow_mode, ReflowMode::SentencePerLine);
408
409 let toml_str = r#"
410 reflow-mode = "semantic_line_breaks"
411 "#;
412 let config: MD013Config = toml::from_str(toml_str).unwrap();
413 assert_eq!(config.reflow_mode, ReflowMode::SemanticLineBreaks);
414 }
415
416 #[test]
417 fn test_field_name_backwards_compatibility() {
418 let toml_str = r#"
421 line_length = 100
422 code_blocks = false
423 reflow_mode = "sentence_per_line"
424 "#;
425 let config: MD013Config = toml::from_str(toml_str).unwrap();
426 assert_eq!(config.line_length.get(), 100);
427 assert!(!config.code_blocks);
428 assert_eq!(config.reflow_mode, ReflowMode::SentencePerLine);
429
430 let toml_str = r#"
432 line-length = 100
433 code_blocks = false
434 reflow-mode = "normalize"
435 "#;
436 let config: MD013Config = toml::from_str(toml_str).unwrap();
437 assert_eq!(config.line_length.get(), 100);
438 assert!(!config.code_blocks);
439 assert_eq!(config.reflow_mode, ReflowMode::Normalize);
440 }
441
442 #[test]
443 fn test_reflow_mode_serialization() {
444 let config = MD013Config {
446 line_length: LineLength::from_const(80),
447 code_blocks: true,
448 code_spans: true,
449 tables: true,
450 headings: true,
451 paragraphs: true,
452 blockquotes: true,
453 strict: false,
454 stern: false,
455 heading_line_length: None,
456 code_block_line_length: None,
457 reflow: true,
458 reflow_mode: ReflowMode::SentencePerLine,
459 length_mode: LengthMode::default(),
460 abbreviations: Vec::new(),
461 require_sentence_capital: true,
462 ignore_link_urls: true,
463 atomic_spans: true,
464 ..Default::default()
465 };
466
467 let toml_str = toml::to_string(&config).unwrap();
468 assert!(toml_str.contains("sentence-per-line"));
469 assert!(!toml_str.contains("sentence_per_line"));
470
471 let config = MD013Config {
473 reflow_mode: ReflowMode::SemanticLineBreaks,
474 ..config
475 };
476 let toml_str = toml::to_string(&config).unwrap();
477 assert!(toml_str.contains("semantic-line-breaks"));
478 assert!(!toml_str.contains("semantic_line_breaks"));
479 }
480
481 #[test]
482 fn test_reflow_mode_invalid_value() {
483 let toml_str = r#"
485 reflow-mode = "invalid_mode"
486 "#;
487 let result = toml::from_str::<MD013Config>(toml_str);
488 assert!(result.is_err());
489 }
490
491 #[test]
492 fn test_full_config_with_reflow_mode() {
493 let toml_str = r#"
494 line-length = 100
495 code-blocks = false
496 tables = false
497 headings = true
498 strict = true
499 reflow = true
500 reflow-mode = "sentence-per-line"
501 "#;
502 let config: MD013Config = toml::from_str(toml_str).unwrap();
503 assert_eq!(config.line_length.get(), 100);
504 assert!(!config.code_blocks);
505 assert!(!config.tables);
506 assert!(config.headings);
507 assert!(config.strict);
508 assert!(config.reflow);
509 assert_eq!(config.reflow_mode, ReflowMode::SentencePerLine);
510 }
511
512 #[test]
513 fn test_code_spans_default_true_and_parses() {
514 assert!(MD013Config::default().code_spans, "code-spans defaults to true");
515
516 let config: MD013Config = toml::from_str("code-spans = false").unwrap();
517 assert!(!config.code_spans);
518 let config: MD013Config = toml::from_str("code_spans = false").unwrap();
520 assert!(!config.code_spans);
521 }
522
523 #[test]
524 fn test_paragraphs_default_true() {
525 let config = MD013Config::default();
527 assert!(config.paragraphs, "paragraphs should default to true");
528 }
529
530 #[test]
531 fn test_paragraphs_deserialization_kebab_case() {
532 let toml_str = r#"
534 paragraphs = false
535 "#;
536 let config: MD013Config = toml::from_str(toml_str).unwrap();
537 assert!(!config.paragraphs);
538 }
539
540 #[test]
541 fn test_paragraphs_full_config() {
542 let toml_str = r#"
544 line-length = 80
545 code-blocks = true
546 tables = true
547 headings = false
548 paragraphs = false
549 reflow = true
550 reflow-mode = "sentence-per-line"
551 "#;
552 let config: MD013Config = toml::from_str(toml_str).unwrap();
553 assert_eq!(config.line_length.get(), 80);
554 assert!(config.code_blocks, "code-blocks should be true");
555 assert!(config.tables, "tables should be true");
556 assert!(!config.headings, "headings should be false");
557 assert!(!config.paragraphs, "paragraphs should be false");
558 assert!(config.reflow, "reflow should be true");
559 assert_eq!(config.reflow_mode, ReflowMode::SentencePerLine);
560 }
561
562 #[test]
563 fn test_abbreviations_for_reflow_empty_vec() {
564 let config = MD013Config {
566 abbreviations: Vec::new(),
567 ..Default::default()
568 };
569 assert!(
570 config.abbreviations_for_reflow().is_none(),
571 "Empty abbreviations should return None for reflow"
572 );
573 }
574
575 #[test]
576 fn test_abbreviations_for_reflow_with_custom() {
577 let config = MD013Config {
579 abbreviations: vec!["Corp".to_string(), "Inc".to_string()],
580 ..Default::default()
581 };
582 let result = config.abbreviations_for_reflow();
583 assert!(result.is_some(), "Custom abbreviations should return Some");
584 let abbrevs = result.unwrap();
585 assert_eq!(abbrevs.len(), 2);
586 assert!(abbrevs.contains(&"Corp".to_string()));
587 assert!(abbrevs.contains(&"Inc".to_string()));
588 }
589}