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(
114 default = "default_ignore_link_urls",
115 alias = "ignore_link_urls",
116 alias = "semantic-link-understanding",
117 alias = "semantic_link_understanding"
118 )]
119 pub ignore_link_urls: bool,
120
121 #[serde(default, alias = "heading_line_length")]
126 pub heading_line_length: Option<LineLength>,
127
128 #[serde(default, alias = "code_block_line_length")]
133 pub code_block_line_length: Option<LineLength>,
134
135 #[serde(default, alias = "enable_reflow", alias = "enable-reflow")]
137 pub reflow: bool,
138
139 #[serde(default, alias = "reflow_mode")]
141 pub reflow_mode: ReflowMode,
142
143 #[serde(default, alias = "length_mode")]
148 pub length_mode: LengthMode,
149
150 #[serde(default)]
155 pub abbreviations: Vec<String>,
156
157 #[serde(
162 default = "default_require_sentence_capital",
163 alias = "require_sentence_capital",
164 alias = "strict_sentences",
165 alias = "strict-sentences"
166 )]
167 pub require_sentence_capital: bool,
168
169 #[serde(default = "default_atomic_spans", alias = "atomic_spans")]
173 pub atomic_spans: bool,
174}
175
176fn default_line_length() -> LineLength {
177 LineLength::from_const(80)
178}
179
180fn default_code_blocks() -> bool {
181 true
182}
183
184fn default_code_spans() -> bool {
185 true
186}
187
188fn default_tables() -> bool {
189 false
190}
191
192fn default_headings() -> bool {
193 true
194}
195
196fn default_paragraphs() -> bool {
197 true
198}
199
200fn default_blockquotes() -> bool {
201 true
202}
203
204fn default_require_sentence_capital() -> bool {
205 true
206}
207
208fn default_ignore_link_urls() -> bool {
209 true
210}
211
212fn default_atomic_spans() -> bool {
213 true
214}
215
216impl Default for MD013Config {
217 fn default() -> Self {
218 Self {
219 line_length: default_line_length(),
220 code_blocks: default_code_blocks(),
221 code_spans: default_code_spans(),
222 tables: default_tables(),
223 headings: default_headings(),
224 paragraphs: default_paragraphs(),
225 blockquotes: default_blockquotes(),
226 strict: false,
227 stern: false,
228 ignore_link_urls: default_ignore_link_urls(),
229 heading_line_length: None,
230 code_block_line_length: None,
231 reflow: false,
232 reflow_mode: ReflowMode::default(),
233 length_mode: LengthMode::default(),
234 abbreviations: Vec::new(),
235 require_sentence_capital: default_require_sentence_capital(),
236 atomic_spans: default_atomic_spans(),
237 }
238 }
239}
240
241impl MD013Config {
242 pub fn effective_heading_line_length(&self) -> LineLength {
245 self.heading_line_length.unwrap_or(self.line_length)
246 }
247
248 pub fn effective_code_block_line_length(&self) -> LineLength {
251 self.code_block_line_length.unwrap_or(self.line_length)
252 }
253
254 pub fn min_effective_line_length(&self) -> LineLength {
258 [
259 Some(self.line_length),
260 self.heading_line_length,
261 self.code_block_line_length,
262 ]
263 .into_iter()
264 .flatten()
265 .filter(|l| !l.is_unlimited())
266 .min_by_key(|l| l.get())
267 .unwrap_or(LineLength::from_const(0))
268 }
269
270 pub fn abbreviations_for_reflow(&self) -> Option<Vec<String>> {
273 if self.abbreviations.is_empty() {
274 None
275 } else {
276 Some(self.abbreviations.clone())
277 }
278 }
279
280 pub(crate) fn reflow_length_mode(&self) -> crate::utils::text_reflow::ReflowLengthMode {
282 match self.length_mode {
283 LengthMode::Chars => crate::utils::text_reflow::ReflowLengthMode::Chars,
284 LengthMode::Visual => crate::utils::text_reflow::ReflowLengthMode::Visual,
285 LengthMode::Bytes => crate::utils::text_reflow::ReflowLengthMode::Bytes,
286 }
287 }
288
289 pub fn to_reflow_options(&self) -> crate::utils::text_reflow::ReflowOptions {
294 crate::utils::text_reflow::ReflowOptions {
295 line_length: self.line_length.get(),
296 break_on_sentences: true,
297 preserve_breaks: false,
298 sentence_per_line: self.reflow_mode == ReflowMode::SentencePerLine,
299 semantic_line_breaks: self.reflow_mode == ReflowMode::SemanticLineBreaks,
300 abbreviations: self.abbreviations_for_reflow(),
301 length_mode: self.reflow_length_mode(),
302 attr_lists: false,
303 myst_roles: false,
304 require_sentence_capital: self.require_sentence_capital,
305 max_list_continuation_indent: None,
306 defined_references: None,
309 atomic_spans: self.atomic_spans,
310 }
311 }
312}
313
314impl RuleConfig for MD013Config {
315 const RULE_NAME: &'static str = "MD013";
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn test_reflow_mode_deserialization_kebab_case() {
324 let toml_str = r#"
327 reflow-mode = "sentence-per-line"
328 "#;
329 let config: MD013Config = toml::from_str(toml_str).unwrap();
330 assert_eq!(config.reflow_mode, ReflowMode::SentencePerLine);
331
332 let toml_str = r#"
333 reflow-mode = "default"
334 "#;
335 let config: MD013Config = toml::from_str(toml_str).unwrap();
336 assert_eq!(config.reflow_mode, ReflowMode::Default);
337
338 let toml_str = r#"
339 reflow-mode = "normalize"
340 "#;
341 let config: MD013Config = toml::from_str(toml_str).unwrap();
342 assert_eq!(config.reflow_mode, ReflowMode::Normalize);
343
344 let toml_str = r#"
345 reflow-mode = "semantic-line-breaks"
346 "#;
347 let config: MD013Config = toml::from_str(toml_str).unwrap();
348 assert_eq!(config.reflow_mode, ReflowMode::SemanticLineBreaks);
349 }
350
351 #[test]
352 fn test_reflow_mode_deserialization_snake_case_alias() {
353 let toml_str = r#"
356 reflow-mode = "sentence_per_line"
357 "#;
358 let config: MD013Config = toml::from_str(toml_str).unwrap();
359 assert_eq!(config.reflow_mode, ReflowMode::SentencePerLine);
360
361 let toml_str = r#"
362 reflow-mode = "semantic_line_breaks"
363 "#;
364 let config: MD013Config = toml::from_str(toml_str).unwrap();
365 assert_eq!(config.reflow_mode, ReflowMode::SemanticLineBreaks);
366 }
367
368 #[test]
369 fn test_field_name_backwards_compatibility() {
370 let toml_str = r#"
373 line_length = 100
374 code_blocks = false
375 reflow_mode = "sentence_per_line"
376 "#;
377 let config: MD013Config = toml::from_str(toml_str).unwrap();
378 assert_eq!(config.line_length.get(), 100);
379 assert!(!config.code_blocks);
380 assert_eq!(config.reflow_mode, ReflowMode::SentencePerLine);
381
382 let toml_str = r#"
384 line-length = 100
385 code_blocks = false
386 reflow-mode = "normalize"
387 "#;
388 let config: MD013Config = toml::from_str(toml_str).unwrap();
389 assert_eq!(config.line_length.get(), 100);
390 assert!(!config.code_blocks);
391 assert_eq!(config.reflow_mode, ReflowMode::Normalize);
392 }
393
394 #[test]
395 fn test_reflow_mode_serialization() {
396 let config = MD013Config {
398 line_length: LineLength::from_const(80),
399 code_blocks: true,
400 code_spans: true,
401 tables: true,
402 headings: true,
403 paragraphs: true,
404 blockquotes: true,
405 strict: false,
406 stern: false,
407 heading_line_length: None,
408 code_block_line_length: None,
409 reflow: true,
410 reflow_mode: ReflowMode::SentencePerLine,
411 length_mode: LengthMode::default(),
412 abbreviations: Vec::new(),
413 require_sentence_capital: true,
414 ignore_link_urls: true,
415 atomic_spans: true,
416 };
417
418 let toml_str = toml::to_string(&config).unwrap();
419 assert!(toml_str.contains("sentence-per-line"));
420 assert!(!toml_str.contains("sentence_per_line"));
421
422 let config = MD013Config {
424 reflow_mode: ReflowMode::SemanticLineBreaks,
425 ..config
426 };
427 let toml_str = toml::to_string(&config).unwrap();
428 assert!(toml_str.contains("semantic-line-breaks"));
429 assert!(!toml_str.contains("semantic_line_breaks"));
430 }
431
432 #[test]
433 fn test_reflow_mode_invalid_value() {
434 let toml_str = r#"
436 reflow-mode = "invalid_mode"
437 "#;
438 let result = toml::from_str::<MD013Config>(toml_str);
439 assert!(result.is_err());
440 }
441
442 #[test]
443 fn test_full_config_with_reflow_mode() {
444 let toml_str = r#"
445 line-length = 100
446 code-blocks = false
447 tables = false
448 headings = true
449 strict = true
450 reflow = true
451 reflow-mode = "sentence-per-line"
452 "#;
453 let config: MD013Config = toml::from_str(toml_str).unwrap();
454 assert_eq!(config.line_length.get(), 100);
455 assert!(!config.code_blocks);
456 assert!(!config.tables);
457 assert!(config.headings);
458 assert!(config.strict);
459 assert!(config.reflow);
460 assert_eq!(config.reflow_mode, ReflowMode::SentencePerLine);
461 }
462
463 #[test]
464 fn test_code_spans_default_true_and_parses() {
465 assert!(MD013Config::default().code_spans, "code-spans defaults to true");
466
467 let config: MD013Config = toml::from_str("code-spans = false").unwrap();
468 assert!(!config.code_spans);
469 let config: MD013Config = toml::from_str("code_spans = false").unwrap();
471 assert!(!config.code_spans);
472 }
473
474 #[test]
475 fn test_paragraphs_default_true() {
476 let config = MD013Config::default();
478 assert!(config.paragraphs, "paragraphs should default to true");
479 }
480
481 #[test]
482 fn test_paragraphs_deserialization_kebab_case() {
483 let toml_str = r#"
485 paragraphs = false
486 "#;
487 let config: MD013Config = toml::from_str(toml_str).unwrap();
488 assert!(!config.paragraphs);
489 }
490
491 #[test]
492 fn test_paragraphs_full_config() {
493 let toml_str = r#"
495 line-length = 80
496 code-blocks = true
497 tables = true
498 headings = false
499 paragraphs = false
500 reflow = true
501 reflow-mode = "sentence-per-line"
502 "#;
503 let config: MD013Config = toml::from_str(toml_str).unwrap();
504 assert_eq!(config.line_length.get(), 80);
505 assert!(config.code_blocks, "code-blocks should be true");
506 assert!(config.tables, "tables should be true");
507 assert!(!config.headings, "headings should be false");
508 assert!(!config.paragraphs, "paragraphs should be false");
509 assert!(config.reflow, "reflow should be true");
510 assert_eq!(config.reflow_mode, ReflowMode::SentencePerLine);
511 }
512
513 #[test]
514 fn test_abbreviations_for_reflow_empty_vec() {
515 let config = MD013Config {
517 abbreviations: Vec::new(),
518 ..Default::default()
519 };
520 assert!(
521 config.abbreviations_for_reflow().is_none(),
522 "Empty abbreviations should return None for reflow"
523 );
524 }
525
526 #[test]
527 fn test_abbreviations_for_reflow_with_custom() {
528 let config = MD013Config {
530 abbreviations: vec!["Corp".to_string(), "Inc".to_string()],
531 ..Default::default()
532 };
533 let result = config.abbreviations_for_reflow();
534 assert!(result.is_some(), "Custom abbreviations should return Some");
535 let abbrevs = result.unwrap();
536 assert_eq!(abbrevs.len(), 2);
537 assert!(abbrevs.contains(&"Corp".to_string()));
538 assert!(abbrevs.contains(&"Inc".to_string()));
539 }
540}