1use crate::rule::{LintError, LintResult, LintWarning, Rule, Severity};
2use crate::utils::range_utils::calculate_line_range;
3use crate::utils::table_utils::TableUtils;
4use unicode_width::UnicodeWidthStr;
5
6mod md060_config;
7use crate::md013_line_length::MD013Config;
8use md060_config::MD060Config;
9
10#[derive(Debug, Clone, Copy, PartialEq)]
11enum ColumnAlignment {
12 Left,
13 Center,
14 Right,
15}
16
17#[derive(Debug, Clone)]
18struct TableFormatResult {
19 lines: Vec<String>,
20 auto_compacted: bool,
21 aligned_width: Option<usize>,
22}
23
24#[derive(Debug, Clone)]
143pub struct MD060TableFormat {
144 config: MD060Config,
145 md013_line_length: usize,
146}
147
148impl Default for MD060TableFormat {
149 fn default() -> Self {
150 Self {
151 config: MD060Config::default(),
152 md013_line_length: 80,
153 }
154 }
155}
156
157impl MD060TableFormat {
158 pub fn new(enabled: bool, style: String) -> Self {
159 Self {
160 config: MD060Config {
161 enabled,
162 style,
163 max_width: 0,
164 },
165 md013_line_length: 80, }
167 }
168
169 pub fn from_config_struct(config: MD060Config, md013_line_length: usize) -> Self {
170 Self {
171 config,
172 md013_line_length,
173 }
174 }
175
176 fn effective_max_width(&self) -> usize {
181 if self.config.max_width == 0 {
182 self.md013_line_length
183 } else {
184 self.config.max_width
185 }
186 }
187
188 fn contains_problematic_chars(text: &str) -> bool {
199 text.contains('\u{200D}') || text.contains('\u{200B}') || text.contains('\u{200C}') || text.contains('\u{2060}') }
204
205 fn calculate_cell_display_width(cell_content: &str) -> usize {
206 let masked = TableUtils::mask_pipes_in_inline_code(cell_content);
207 masked.trim().width()
208 }
209
210 fn parse_table_row(line: &str) -> Vec<String> {
211 let trimmed = line.trim();
212 let masked = TableUtils::mask_pipes_for_table_parsing(trimmed);
213
214 let has_leading = masked.starts_with('|');
215 let has_trailing = masked.ends_with('|');
216
217 let mut masked_content = masked.as_str();
218 let mut orig_content = trimmed;
219
220 if has_leading {
221 masked_content = &masked_content[1..];
222 orig_content = &orig_content[1..];
223 }
224 if has_trailing && !masked_content.is_empty() {
225 masked_content = &masked_content[..masked_content.len() - 1];
226 orig_content = &orig_content[..orig_content.len() - 1];
227 }
228
229 let masked_parts: Vec<&str> = masked_content.split('|').collect();
230 let mut cells = Vec::new();
231 let mut pos = 0;
232
233 for masked_cell in masked_parts {
234 let cell_len = masked_cell.len();
235 let orig_cell = if pos + cell_len <= orig_content.len() {
236 &orig_content[pos..pos + cell_len]
237 } else {
238 masked_cell
239 };
240 cells.push(orig_cell.to_string());
241 pos += cell_len + 1;
242 }
243
244 cells
245 }
246
247 fn is_delimiter_row(row: &[String]) -> bool {
248 if row.is_empty() {
249 return false;
250 }
251 row.iter().all(|cell| {
252 let trimmed = cell.trim();
253 !trimmed.is_empty()
256 && trimmed.contains('-')
257 && trimmed.chars().all(|c| c == '-' || c == ':' || c.is_whitespace())
258 })
259 }
260
261 fn parse_column_alignments(delimiter_row: &[String]) -> Vec<ColumnAlignment> {
262 delimiter_row
263 .iter()
264 .map(|cell| {
265 let trimmed = cell.trim();
266 let has_left_colon = trimmed.starts_with(':');
267 let has_right_colon = trimmed.ends_with(':');
268
269 match (has_left_colon, has_right_colon) {
270 (true, true) => ColumnAlignment::Center,
271 (false, true) => ColumnAlignment::Right,
272 _ => ColumnAlignment::Left,
273 }
274 })
275 .collect()
276 }
277
278 fn calculate_column_widths(table_lines: &[&str]) -> Vec<usize> {
279 let mut column_widths = Vec::new();
280 let mut delimiter_cells: Option<Vec<String>> = None;
281
282 for line in table_lines {
283 let cells = Self::parse_table_row(line);
284
285 if Self::is_delimiter_row(&cells) {
287 delimiter_cells = Some(cells);
288 continue;
289 }
290
291 for (i, cell) in cells.iter().enumerate() {
292 let width = Self::calculate_cell_display_width(cell);
293 if i >= column_widths.len() {
294 column_widths.push(width);
295 } else {
296 column_widths[i] = column_widths[i].max(width);
297 }
298 }
299 }
300
301 let mut final_widths: Vec<usize> = column_widths.iter().map(|&w| w.max(3)).collect();
304
305 if let Some(delimiter_cells) = delimiter_cells {
308 for (i, cell) in delimiter_cells.iter().enumerate() {
309 if i < final_widths.len() {
310 let trimmed = cell.trim();
311 let has_left_colon = trimmed.starts_with(':');
312 let has_right_colon = trimmed.ends_with(':');
313 let colon_count = (has_left_colon as usize) + (has_right_colon as usize);
314
315 let min_width_for_delimiter = 3 + colon_count;
317 final_widths[i] = final_widths[i].max(min_width_for_delimiter);
318 }
319 }
320 }
321
322 final_widths
323 }
324
325 fn format_table_row(
326 cells: &[String],
327 column_widths: &[usize],
328 column_alignments: &[ColumnAlignment],
329 is_delimiter: bool,
330 ) -> String {
331 let formatted_cells: Vec<String> = cells
332 .iter()
333 .enumerate()
334 .map(|(i, cell)| {
335 let target_width = column_widths.get(i).copied().unwrap_or(0);
336 if is_delimiter {
337 let trimmed = cell.trim();
338 let has_left_colon = trimmed.starts_with(':');
339 let has_right_colon = trimmed.ends_with(':');
340
341 let dash_count = if has_left_colon && has_right_colon {
344 target_width.saturating_sub(2)
345 } else if has_left_colon || has_right_colon {
346 target_width.saturating_sub(1)
347 } else {
348 target_width
349 };
350
351 let dashes = "-".repeat(dash_count.max(3)); let delimiter_content = if has_left_colon && has_right_colon {
353 format!(":{dashes}:")
354 } else if has_left_colon {
355 format!(":{dashes}")
356 } else if has_right_colon {
357 format!("{dashes}:")
358 } else {
359 dashes
360 };
361
362 format!(" {delimiter_content} ")
364 } else {
365 let trimmed = cell.trim();
366 let current_width = Self::calculate_cell_display_width(cell);
367 let padding = target_width.saturating_sub(current_width);
368
369 let alignment = column_alignments.get(i).copied().unwrap_or(ColumnAlignment::Left);
371 match alignment {
372 ColumnAlignment::Left => {
373 format!(" {trimmed}{} ", " ".repeat(padding))
375 }
376 ColumnAlignment::Center => {
377 let left_padding = padding / 2;
379 let right_padding = padding - left_padding;
380 format!(" {}{trimmed}{} ", " ".repeat(left_padding), " ".repeat(right_padding))
381 }
382 ColumnAlignment::Right => {
383 format!(" {}{trimmed} ", " ".repeat(padding))
385 }
386 }
387 }
388 })
389 .collect();
390
391 format!("|{}|", formatted_cells.join("|"))
392 }
393
394 fn format_table_compact(cells: &[String]) -> String {
395 let formatted_cells: Vec<String> = cells.iter().map(|cell| format!(" {} ", cell.trim())).collect();
396 format!("|{}|", formatted_cells.join("|"))
397 }
398
399 fn format_table_tight(cells: &[String]) -> String {
400 let formatted_cells: Vec<String> = cells.iter().map(|cell| cell.trim().to_string()).collect();
401 format!("|{}|", formatted_cells.join("|"))
402 }
403
404 fn detect_table_style(table_lines: &[&str]) -> Option<String> {
405 if table_lines.is_empty() {
406 return None;
407 }
408
409 let first_line = table_lines[0];
410 let cells = Self::parse_table_row(first_line);
411
412 if cells.is_empty() {
413 return None;
414 }
415
416 let has_no_padding = cells.iter().all(|cell| !cell.starts_with(' ') && !cell.ends_with(' '));
417
418 let has_single_space = cells.iter().all(|cell| {
419 let trimmed = cell.trim();
420 cell == &format!(" {trimmed} ")
421 });
422
423 if has_no_padding {
424 Some("tight".to_string())
425 } else if has_single_space {
426 Some("compact".to_string())
427 } else {
428 Some("aligned".to_string())
429 }
430 }
431
432 fn fix_table_block(
433 &self,
434 lines: &[&str],
435 table_block: &crate::utils::table_utils::TableBlock,
436 ) -> TableFormatResult {
437 let mut result = Vec::new();
438 let mut auto_compacted = false;
439 let mut aligned_width = None;
440
441 let table_lines: Vec<&str> = std::iter::once(lines[table_block.header_line])
442 .chain(std::iter::once(lines[table_block.delimiter_line]))
443 .chain(table_block.content_lines.iter().map(|&idx| lines[idx]))
444 .collect();
445
446 if table_lines.iter().any(|line| Self::contains_problematic_chars(line)) {
447 return TableFormatResult {
448 lines: table_lines.iter().map(|s| s.to_string()).collect(),
449 auto_compacted: false,
450 aligned_width: None,
451 };
452 }
453
454 let style = self.config.style.as_str();
455
456 match style {
457 "any" => {
458 let detected_style = Self::detect_table_style(&table_lines);
459 if detected_style.is_none() {
460 return TableFormatResult {
461 lines: table_lines.iter().map(|s| s.to_string()).collect(),
462 auto_compacted: false,
463 aligned_width: None,
464 };
465 }
466
467 let target_style = detected_style.unwrap();
468
469 let delimiter_cells = Self::parse_table_row(table_lines[1]);
471 let column_alignments = Self::parse_column_alignments(&delimiter_cells);
472
473 for line in &table_lines {
474 let cells = Self::parse_table_row(line);
475 match target_style.as_str() {
476 "tight" => result.push(Self::format_table_tight(&cells)),
477 "compact" => result.push(Self::format_table_compact(&cells)),
478 _ => {
479 let column_widths = Self::calculate_column_widths(&table_lines);
480 let is_delimiter = Self::is_delimiter_row(&cells);
481 result.push(Self::format_table_row(
482 &cells,
483 &column_widths,
484 &column_alignments,
485 is_delimiter,
486 ));
487 }
488 }
489 }
490 }
491 "compact" => {
492 for line in table_lines {
493 let cells = Self::parse_table_row(line);
494 result.push(Self::format_table_compact(&cells));
495 }
496 }
497 "tight" => {
498 for line in table_lines {
499 let cells = Self::parse_table_row(line);
500 result.push(Self::format_table_tight(&cells));
501 }
502 }
503 "aligned" => {
504 let column_widths = Self::calculate_column_widths(&table_lines);
505
506 let num_columns = column_widths.len();
508 let calc_aligned_width = 1 + (num_columns * 3) + column_widths.iter().sum::<usize>();
509 aligned_width = Some(calc_aligned_width);
510
511 if calc_aligned_width > self.effective_max_width() {
513 auto_compacted = true;
514 for line in table_lines {
515 let cells = Self::parse_table_row(line);
516 result.push(Self::format_table_compact(&cells));
517 }
518 } else {
519 let delimiter_cells = Self::parse_table_row(table_lines[1]);
521 let column_alignments = Self::parse_column_alignments(&delimiter_cells);
522
523 for line in table_lines {
524 let cells = Self::parse_table_row(line);
525 let is_delimiter = Self::is_delimiter_row(&cells);
526 result.push(Self::format_table_row(
527 &cells,
528 &column_widths,
529 &column_alignments,
530 is_delimiter,
531 ));
532 }
533 }
534 }
535 _ => {
536 return TableFormatResult {
537 lines: table_lines.iter().map(|s| s.to_string()).collect(),
538 auto_compacted: false,
539 aligned_width: None,
540 };
541 }
542 }
543
544 TableFormatResult {
545 lines: result,
546 auto_compacted,
547 aligned_width,
548 }
549 }
550}
551
552impl Rule for MD060TableFormat {
553 fn name(&self) -> &'static str {
554 "MD060"
555 }
556
557 fn description(&self) -> &'static str {
558 "Table columns should be consistently aligned"
559 }
560
561 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
562 !self.config.enabled || !ctx.likely_has_tables()
563 }
564
565 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
566 if !self.config.enabled {
567 return Ok(Vec::new());
568 }
569
570 let content = ctx.content;
571 let line_index = &ctx.line_index;
572 let mut warnings = Vec::new();
573
574 let lines: Vec<&str> = content.lines().collect();
575 let table_blocks = &ctx.table_blocks;
576
577 for table_block in table_blocks {
578 let format_result = self.fix_table_block(&lines, table_block);
579
580 let table_line_indices: Vec<usize> = std::iter::once(table_block.header_line)
581 .chain(std::iter::once(table_block.delimiter_line))
582 .chain(table_block.content_lines.iter().copied())
583 .collect();
584
585 for (i, &line_idx) in table_line_indices.iter().enumerate() {
586 let original = lines[line_idx];
587 let fixed = &format_result.lines[i];
588
589 if original != fixed {
590 let (start_line, start_col, end_line, end_col) = calculate_line_range(line_idx + 1, original);
591
592 let message = if format_result.auto_compacted {
593 if let Some(width) = format_result.aligned_width {
594 format!(
595 "Table too wide for aligned formatting ({} chars > max-width: {})",
596 width,
597 self.effective_max_width()
598 )
599 } else {
600 "Table too wide for aligned formatting".to_string()
601 }
602 } else {
603 "Table columns should be aligned".to_string()
604 };
605
606 warnings.push(LintWarning {
607 rule_name: Some(self.name().to_string()),
608 severity: Severity::Warning,
609 message,
610 line: start_line,
611 column: start_col,
612 end_line,
613 end_column: end_col,
614 fix: Some(crate::rule::Fix {
615 range: line_index.whole_line_range(line_idx + 1),
616 replacement: if line_idx < lines.len() - 1 {
617 format!("{fixed}\n")
618 } else {
619 fixed.clone()
620 },
621 }),
622 });
623 }
624 }
625 }
626
627 Ok(warnings)
628 }
629
630 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
631 if !self.config.enabled {
632 return Ok(ctx.content.to_string());
633 }
634
635 let content = ctx.content;
636 let lines: Vec<&str> = content.lines().collect();
637 let table_blocks = &ctx.table_blocks;
638
639 let mut result_lines: Vec<String> = lines.iter().map(|&s| s.to_string()).collect();
640
641 for table_block in table_blocks {
642 let format_result = self.fix_table_block(&lines, table_block);
643
644 let table_line_indices: Vec<usize> = std::iter::once(table_block.header_line)
645 .chain(std::iter::once(table_block.delimiter_line))
646 .chain(table_block.content_lines.iter().copied())
647 .collect();
648
649 for (i, &line_idx) in table_line_indices.iter().enumerate() {
650 result_lines[line_idx] = format_result.lines[i].clone();
651 }
652 }
653
654 let mut fixed = result_lines.join("\n");
655 if content.ends_with('\n') && !fixed.ends_with('\n') {
656 fixed.push('\n');
657 }
658 Ok(fixed)
659 }
660
661 fn as_any(&self) -> &dyn std::any::Any {
662 self
663 }
664
665 fn default_config_section(&self) -> Option<(String, toml::Value)> {
666 let json_value = serde_json::to_value(&self.config).ok()?;
667 Some((
668 self.name().to_string(),
669 crate::rule_config_serde::json_to_toml_value(&json_value)?,
670 ))
671 }
672
673 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
674 where
675 Self: Sized,
676 {
677 let rule_config = crate::rule_config_serde::load_rule_config::<MD060Config>(config);
678 let md013_config = crate::rule_config_serde::load_rule_config::<MD013Config>(config);
679 Box::new(Self::from_config_struct(rule_config, md013_config.line_length))
680 }
681}
682
683#[cfg(test)]
684mod tests {
685 use super::*;
686 use crate::lint_context::LintContext;
687
688 #[test]
689 fn test_md060_disabled_by_default() {
690 let rule = MD060TableFormat::default();
691 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
692 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
693
694 let warnings = rule.check(&ctx).unwrap();
695 assert_eq!(warnings.len(), 0);
696
697 let fixed = rule.fix(&ctx).unwrap();
698 assert_eq!(fixed, content);
699 }
700
701 #[test]
702 fn test_md060_align_simple_ascii_table() {
703 let rule = MD060TableFormat::new(true, "aligned".to_string());
704
705 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
706 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
707
708 let fixed = rule.fix(&ctx).unwrap();
709 let expected = "| Name | Age |\n| ----- | --- |\n| Alice | 30 |";
710 assert_eq!(fixed, expected);
711
712 let lines: Vec<&str> = fixed.lines().collect();
714 assert_eq!(lines[0].len(), lines[1].len());
715 assert_eq!(lines[1].len(), lines[2].len());
716 }
717
718 #[test]
719 fn test_md060_cjk_characters_aligned_correctly() {
720 let rule = MD060TableFormat::new(true, "aligned".to_string());
721
722 let content = "| Name | Age |\n|---|---|\n| δΈζ | 30 |";
723 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
724
725 let fixed = rule.fix(&ctx).unwrap();
726
727 let lines: Vec<&str> = fixed.lines().collect();
728 let cells_line1 = MD060TableFormat::parse_table_row(lines[0]);
729 let cells_line3 = MD060TableFormat::parse_table_row(lines[2]);
730
731 let width1 = MD060TableFormat::calculate_cell_display_width(&cells_line1[0]);
732 let width3 = MD060TableFormat::calculate_cell_display_width(&cells_line3[0]);
733
734 assert_eq!(width1, width3);
735 }
736
737 #[test]
738 fn test_md060_basic_emoji() {
739 let rule = MD060TableFormat::new(true, "aligned".to_string());
740
741 let content = "| Status | Name |\n|---|---|\n| β
| Test |";
742 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
743
744 let fixed = rule.fix(&ctx).unwrap();
745 assert!(fixed.contains("Status"));
746 }
747
748 #[test]
749 fn test_md060_zwj_emoji_skipped() {
750 let rule = MD060TableFormat::new(true, "aligned".to_string());
751
752 let content = "| Emoji | Name |\n|---|---|\n| π¨βπ©βπ§βπ¦ | Family |";
753 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
754
755 let fixed = rule.fix(&ctx).unwrap();
756 assert_eq!(fixed, content);
757 }
758
759 #[test]
760 fn test_md060_inline_code_with_pipes() {
761 let rule = MD060TableFormat::new(true, "aligned".to_string());
762
763 let content = "| Pattern | Regex |\n|---|---|\n| Time | `[0-9]|[0-9]` |";
764 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
765
766 let fixed = rule.fix(&ctx).unwrap();
767 assert!(fixed.contains("`[0-9]|[0-9]`"));
768 }
769
770 #[test]
771 fn test_md060_compact_style() {
772 let rule = MD060TableFormat::new(true, "compact".to_string());
773
774 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
775 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
776
777 let fixed = rule.fix(&ctx).unwrap();
778 let expected = "| Name | Age |\n| --- | --- |\n| Alice | 30 |";
779 assert_eq!(fixed, expected);
780 }
781
782 #[test]
783 fn test_md060_tight_style() {
784 let rule = MD060TableFormat::new(true, "tight".to_string());
785
786 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
787 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
788
789 let fixed = rule.fix(&ctx).unwrap();
790 let expected = "|Name|Age|\n|---|---|\n|Alice|30|";
791 assert_eq!(fixed, expected);
792 }
793
794 #[test]
795 fn test_md060_any_style_consistency() {
796 let rule = MD060TableFormat::new(true, "any".to_string());
797
798 let content = "| Name | Age |\n| --- | --- |\n| Alice | 30 |";
800 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
801
802 let fixed = rule.fix(&ctx).unwrap();
803 assert_eq!(fixed, content);
804
805 let content_aligned = "| Name | Age |\n| ----- | --- |\n| Alice | 30 |";
807 let ctx_aligned = LintContext::new(content_aligned, crate::config::MarkdownFlavor::Standard);
808
809 let fixed_aligned = rule.fix(&ctx_aligned).unwrap();
810 assert_eq!(fixed_aligned, content_aligned);
811 }
812
813 #[test]
814 fn test_md060_empty_cells() {
815 let rule = MD060TableFormat::new(true, "aligned".to_string());
816
817 let content = "| A | B |\n|---|---|\n| | X |";
818 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
819
820 let fixed = rule.fix(&ctx).unwrap();
821 assert!(fixed.contains("|"));
822 }
823
824 #[test]
825 fn test_md060_mixed_content() {
826 let rule = MD060TableFormat::new(true, "aligned".to_string());
827
828 let content = "| Name | Age | City |\n|---|---|---|\n| δΈζ | 30 | NYC |";
829 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
830
831 let fixed = rule.fix(&ctx).unwrap();
832 assert!(fixed.contains("δΈζ"));
833 assert!(fixed.contains("NYC"));
834 }
835
836 #[test]
837 fn test_md060_preserve_alignment_indicators() {
838 let rule = MD060TableFormat::new(true, "aligned".to_string());
839
840 let content = "| Left | Center | Right |\n|:---|:---:|---:|\n| A | B | C |";
841 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
842
843 let fixed = rule.fix(&ctx).unwrap();
844
845 assert!(fixed.contains(":---"), "Should contain left alignment");
846 assert!(fixed.contains(":----:"), "Should contain center alignment");
847 assert!(fixed.contains("----:"), "Should contain right alignment");
848 }
849
850 #[test]
851 fn test_md060_minimum_column_width() {
852 let rule = MD060TableFormat::new(true, "aligned".to_string());
853
854 let content = "| ID | Name |\n|-|-|\n| 1 | A |";
857 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
858
859 let fixed = rule.fix(&ctx).unwrap();
860
861 let lines: Vec<&str> = fixed.lines().collect();
862 assert_eq!(lines[0].len(), lines[1].len());
863 assert_eq!(lines[1].len(), lines[2].len());
864
865 assert!(fixed.contains("ID "), "Short content should be padded");
867 assert!(fixed.contains("---"), "Delimiter should have at least 3 dashes");
868 }
869
870 #[test]
871 fn test_md060_auto_compact_exceeds_default_threshold() {
872 let config = MD060Config {
874 enabled: true,
875 style: "aligned".to_string(),
876 max_width: 0,
877 };
878 let rule = MD060TableFormat::from_config_struct(config, 80);
879
880 let content = "| Very Long Column Header | Another Long Header | Third Very Long Header Column |\n|---|---|---|\n| Short | Data | Here |";
884 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
885
886 let fixed = rule.fix(&ctx).unwrap();
887
888 assert!(fixed.contains("| Very Long Column Header | Another Long Header | Third Very Long Header Column |"));
890 assert!(fixed.contains("| --- | --- | --- |"));
891 assert!(fixed.contains("| Short | Data | Here |"));
892
893 let lines: Vec<&str> = fixed.lines().collect();
895 assert!(lines[0].len() != lines[1].len() || lines[1].len() != lines[2].len());
897 }
898
899 #[test]
900 fn test_md060_auto_compact_exceeds_explicit_threshold() {
901 let config = MD060Config {
903 enabled: true,
904 style: "aligned".to_string(),
905 max_width: 50,
906 };
907 let rule = MD060TableFormat::from_config_struct(config, 80); let content = "| Very Long Column Header A | Very Long Column Header B | Very Long Column Header C |\n|---|---|---|\n| Data | Data | Data |";
913 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
914
915 let fixed = rule.fix(&ctx).unwrap();
916
917 assert!(
919 fixed.contains("| Very Long Column Header A | Very Long Column Header B | Very Long Column Header C |")
920 );
921 assert!(fixed.contains("| --- | --- | --- |"));
922 assert!(fixed.contains("| Data | Data | Data |"));
923
924 let lines: Vec<&str> = fixed.lines().collect();
926 assert!(lines[0].len() != lines[2].len());
927 }
928
929 #[test]
930 fn test_md060_stays_aligned_under_threshold() {
931 let config = MD060Config {
933 enabled: true,
934 style: "aligned".to_string(),
935 max_width: 100,
936 };
937 let rule = MD060TableFormat::from_config_struct(config, 80);
938
939 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
941 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
942
943 let fixed = rule.fix(&ctx).unwrap();
944
945 let expected = "| Name | Age |\n| ----- | --- |\n| Alice | 30 |";
947 assert_eq!(fixed, expected);
948
949 let lines: Vec<&str> = fixed.lines().collect();
950 assert_eq!(lines[0].len(), lines[1].len());
951 assert_eq!(lines[1].len(), lines[2].len());
952 }
953
954 #[test]
955 fn test_md060_width_calculation_formula() {
956 let config = MD060Config {
958 enabled: true,
959 style: "aligned".to_string(),
960 max_width: 0,
961 };
962 let rule = MD060TableFormat::from_config_struct(config, 30);
963
964 let content = "| AAAAA | BBBBB | CCCCC |\n|---|---|---|\n| AAAAA | BBBBB | CCCCC |";
968 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
969
970 let fixed = rule.fix(&ctx).unwrap();
971
972 let lines: Vec<&str> = fixed.lines().collect();
974 assert_eq!(lines[0].len(), lines[1].len());
975 assert_eq!(lines[1].len(), lines[2].len());
976 assert_eq!(lines[0].len(), 25); let config_tight = MD060Config {
980 enabled: true,
981 style: "aligned".to_string(),
982 max_width: 24,
983 };
984 let rule_tight = MD060TableFormat::from_config_struct(config_tight, 80);
985
986 let fixed_compact = rule_tight.fix(&ctx).unwrap();
987
988 assert!(fixed_compact.contains("| AAAAA | BBBBB | CCCCC |"));
990 assert!(fixed_compact.contains("| --- | --- | --- |"));
991 }
992
993 #[test]
994 fn test_md060_very_wide_table_auto_compacts() {
995 let config = MD060Config {
996 enabled: true,
997 style: "aligned".to_string(),
998 max_width: 0,
999 };
1000 let rule = MD060TableFormat::from_config_struct(config, 80);
1001
1002 let content = "| Column One A | Column Two B | Column Three | Column Four D | Column Five E | Column Six FG | Column Seven | Column Eight |\n|---|---|---|---|---|---|---|---|\n| A | B | C | D | E | F | G | H |";
1006 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
1007
1008 let fixed = rule.fix(&ctx).unwrap();
1009
1010 assert!(fixed.contains("| Column One A | Column Two B | Column Three | Column Four D | Column Five E | Column Six FG | Column Seven | Column Eight |"));
1012 assert!(fixed.contains("| --- | --- | --- | --- | --- | --- | --- | --- |"));
1013 }
1014
1015 #[test]
1016 fn test_md060_inherit_from_md013_line_length() {
1017 let config = MD060Config {
1019 enabled: true,
1020 style: "aligned".to_string(),
1021 max_width: 0, };
1023
1024 let rule_80 = MD060TableFormat::from_config_struct(config.clone(), 80);
1026 let rule_120 = MD060TableFormat::from_config_struct(config.clone(), 120);
1027
1028 let content = "| Column Header A | Column Header B | Column Header C |\n|---|---|---|\n| Some Data | More Data | Even More |";
1030 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
1031
1032 let _fixed_80 = rule_80.fix(&ctx).unwrap();
1034
1035 let fixed_120 = rule_120.fix(&ctx).unwrap();
1037
1038 let lines_120: Vec<&str> = fixed_120.lines().collect();
1040 assert_eq!(lines_120[0].len(), lines_120[1].len());
1041 assert_eq!(lines_120[1].len(), lines_120[2].len());
1042 }
1043
1044 #[test]
1045 fn test_md060_edge_case_exactly_at_threshold() {
1046 let config = MD060Config {
1050 enabled: true,
1051 style: "aligned".to_string(),
1052 max_width: 17,
1053 };
1054 let rule = MD060TableFormat::from_config_struct(config, 80);
1055
1056 let content = "| AAAAA | BBBBB |\n|---|---|\n| AAAAA | BBBBB |";
1057 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
1058
1059 let fixed = rule.fix(&ctx).unwrap();
1060
1061 let lines: Vec<&str> = fixed.lines().collect();
1063 assert_eq!(lines[0].len(), 17);
1064 assert_eq!(lines[0].len(), lines[1].len());
1065 assert_eq!(lines[1].len(), lines[2].len());
1066
1067 let config_under = MD060Config {
1069 enabled: true,
1070 style: "aligned".to_string(),
1071 max_width: 16,
1072 };
1073 let rule_under = MD060TableFormat::from_config_struct(config_under, 80);
1074
1075 let fixed_compact = rule_under.fix(&ctx).unwrap();
1076
1077 assert!(fixed_compact.contains("| AAAAA | BBBBB |"));
1079 assert!(fixed_compact.contains("| --- | --- |"));
1080 }
1081
1082 #[test]
1083 fn test_md060_auto_compact_warning_message() {
1084 let config = MD060Config {
1086 enabled: true,
1087 style: "aligned".to_string(),
1088 max_width: 50,
1089 };
1090 let rule = MD060TableFormat::from_config_struct(config, 80);
1091
1092 let content = "| Very Long Column Header A | Very Long Column Header B | Very Long Column Header C |\n|---|---|---|\n| Data | Data | Data |";
1094 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
1095
1096 let warnings = rule.check(&ctx).unwrap();
1097
1098 assert!(!warnings.is_empty(), "Should generate warnings");
1100
1101 let auto_compact_warnings: Vec<_> = warnings
1102 .iter()
1103 .filter(|w| w.message.contains("too wide for aligned formatting"))
1104 .collect();
1105
1106 assert!(!auto_compact_warnings.is_empty(), "Should have auto-compact warning");
1107
1108 let first_warning = auto_compact_warnings[0];
1110 assert!(first_warning.message.contains("85 chars > max-width: 50"));
1111 assert!(first_warning.message.contains("Table too wide for aligned formatting"));
1112 }
1113
1114 #[test]
1115 fn test_md060_regular_alignment_warning_message() {
1116 let config = MD060Config {
1118 enabled: true,
1119 style: "aligned".to_string(),
1120 max_width: 100, };
1122 let rule = MD060TableFormat::from_config_struct(config, 80);
1123
1124 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
1126 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
1127
1128 let warnings = rule.check(&ctx).unwrap();
1129
1130 assert!(!warnings.is_empty(), "Should generate warnings");
1132
1133 assert!(warnings[0].message.contains("Table columns should be aligned"));
1135 assert!(!warnings[0].message.contains("too wide"));
1136 assert!(!warnings[0].message.contains("max-width"));
1137 }
1138}