1use crate::rule::{LintError, LintResult, LintWarning, Rule, Severity};
2use crate::utils::range_utils::calculate_line_range;
3use crate::utils::regex_cache::BLOCKQUOTE_PREFIX_RE;
4use crate::utils::table_utils::TableUtils;
5use unicode_width::UnicodeWidthStr;
6
7mod md060_config;
8use crate::md013_line_length::MD013Config;
9pub use md060_config::ColumnAlign;
10pub use md060_config::MD060Config;
11
12#[derive(Debug, Clone, Copy, PartialEq)]
14enum RowType {
15 Header,
17 Delimiter,
19 Body,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq)]
24enum ColumnAlignment {
25 Left,
26 Center,
27 Right,
28}
29
30#[derive(Debug, Clone)]
31struct TableFormatResult {
32 lines: Vec<String>,
33 auto_compacted: bool,
34 aligned_width: Option<usize>,
35}
36
37#[derive(Debug, Clone, Copy)]
39struct RowFormatOptions {
40 row_type: RowType,
42 compact_delimiter: bool,
44 column_align: ColumnAlign,
46 column_align_header: Option<ColumnAlign>,
48 column_align_body: Option<ColumnAlign>,
50 loose_last_column: bool,
52}
53
54#[derive(Debug, Clone, Default)]
177pub struct MD060TableFormat {
178 config: MD060Config,
179 md013_config: MD013Config,
180 md013_disabled: bool,
181}
182
183impl MD060TableFormat {
184 pub fn new(enabled: bool, style: String) -> Self {
185 use crate::types::LineLength;
186 Self {
187 config: MD060Config {
188 enabled,
189 style,
190 max_width: LineLength::from_const(0),
191 column_align: ColumnAlign::Auto,
192 column_align_header: None,
193 column_align_body: None,
194 loose_last_column: false,
195 },
196 md013_config: MD013Config::default(),
197 md013_disabled: false,
198 }
199 }
200
201 pub fn from_config_struct(config: MD060Config, md013_config: MD013Config, md013_disabled: bool) -> Self {
202 Self {
203 config,
204 md013_config,
205 md013_disabled,
206 }
207 }
208
209 fn effective_max_width(&self) -> usize {
219 if !self.config.max_width.is_unlimited() {
221 return self.config.max_width.get();
222 }
223
224 if self.md013_disabled || !self.md013_config.tables || self.md013_config.line_length.is_unlimited() {
229 return usize::MAX; }
231
232 self.md013_config.line_length.get()
234 }
235
236 fn contains_problematic_chars(text: &str) -> bool {
247 text.contains('\u{200D}') || text.contains('\u{200B}') || text.contains('\u{200C}') || text.contains('\u{2060}') }
252
253 fn calculate_cell_display_width(cell_content: &str) -> usize {
254 let masked = TableUtils::mask_pipes_in_inline_code(cell_content);
255 masked.trim().width()
256 }
257
258 #[cfg(test)]
261 fn parse_table_row(line: &str) -> Vec<String> {
262 TableUtils::split_table_row(line)
263 }
264
265 fn parse_table_row_with_flavor(line: &str, flavor: crate::config::MarkdownFlavor) -> Vec<String> {
270 TableUtils::split_table_row_with_flavor(line, flavor)
271 }
272
273 fn is_delimiter_row(row: &[String]) -> bool {
274 if row.is_empty() {
275 return false;
276 }
277 row.iter().all(|cell| {
278 let trimmed = cell.trim();
279 !trimmed.is_empty()
282 && trimmed.contains('-')
283 && trimmed.chars().all(|c| c == '-' || c == ':' || c.is_whitespace())
284 })
285 }
286
287 fn extract_blockquote_prefix(line: &str) -> (&str, &str) {
290 if let Some(m) = BLOCKQUOTE_PREFIX_RE.find(line) {
291 (&line[..m.end()], &line[m.end()..])
292 } else {
293 ("", line)
294 }
295 }
296
297 fn parse_column_alignments(delimiter_row: &[String]) -> Vec<ColumnAlignment> {
298 delimiter_row
299 .iter()
300 .map(|cell| {
301 let trimmed = cell.trim();
302 let has_left_colon = trimmed.starts_with(':');
303 let has_right_colon = trimmed.ends_with(':');
304
305 match (has_left_colon, has_right_colon) {
306 (true, true) => ColumnAlignment::Center,
307 (false, true) => ColumnAlignment::Right,
308 _ => ColumnAlignment::Left,
309 }
310 })
311 .collect()
312 }
313
314 fn calculate_column_widths(table_lines: &[&str], flavor: crate::config::MarkdownFlavor) -> Vec<usize> {
315 let mut column_widths = Vec::new();
316 let mut delimiter_cells: Option<Vec<String>> = None;
317
318 for line in table_lines {
319 let cells = Self::parse_table_row_with_flavor(line, flavor);
320
321 if Self::is_delimiter_row(&cells) {
323 delimiter_cells = Some(cells);
324 continue;
325 }
326
327 for (i, cell) in cells.iter().enumerate() {
328 let width = Self::calculate_cell_display_width(cell);
329 if i >= column_widths.len() {
330 column_widths.push(width);
331 } else {
332 column_widths[i] = column_widths[i].max(width);
333 }
334 }
335 }
336
337 let mut final_widths: Vec<usize> = column_widths.iter().map(|&w| w.max(3)).collect();
340
341 if let Some(delimiter_cells) = delimiter_cells {
344 for (i, cell) in delimiter_cells.iter().enumerate() {
345 if i < final_widths.len() {
346 let trimmed = cell.trim();
347 let has_left_colon = trimmed.starts_with(':');
348 let has_right_colon = trimmed.ends_with(':');
349 let colon_count = (has_left_colon as usize) + (has_right_colon as usize);
350
351 let min_width_for_delimiter = 3 + colon_count;
353 final_widths[i] = final_widths[i].max(min_width_for_delimiter);
354 }
355 }
356 }
357
358 final_widths
359 }
360
361 fn format_table_row(
362 cells: &[String],
363 column_widths: &[usize],
364 column_alignments: &[ColumnAlignment],
365 options: &RowFormatOptions,
366 ) -> String {
367 let num_cells = cells.len();
368 let formatted_cells: Vec<String> = cells
369 .iter()
370 .enumerate()
371 .map(|(i, cell)| {
372 let is_last_column = i == num_cells - 1;
373 let target_width = column_widths.get(i).copied().unwrap_or(0);
374
375 match options.row_type {
376 RowType::Delimiter => {
377 let trimmed = cell.trim();
378 let has_left_colon = trimmed.starts_with(':');
379 let has_right_colon = trimmed.ends_with(':');
380
381 let extra_width = if options.compact_delimiter { 2 } else { 0 };
385 let dash_count = if has_left_colon && has_right_colon {
386 (target_width + extra_width).saturating_sub(2)
387 } else if has_left_colon || has_right_colon {
388 (target_width + extra_width).saturating_sub(1)
389 } else {
390 target_width + extra_width
391 };
392
393 let dashes = "-".repeat(dash_count.max(3)); let delimiter_content = if has_left_colon && has_right_colon {
395 format!(":{dashes}:")
396 } else if has_left_colon {
397 format!(":{dashes}")
398 } else if has_right_colon {
399 format!("{dashes}:")
400 } else {
401 dashes
402 };
403
404 if options.compact_delimiter {
406 delimiter_content
407 } else {
408 format!(" {delimiter_content} ")
409 }
410 }
411 RowType::Header | RowType::Body => {
412 let trimmed = cell.trim();
413 let current_width = Self::calculate_cell_display_width(cell);
414
415 let skip_padding =
417 options.loose_last_column && is_last_column && options.row_type == RowType::Body;
418
419 let padding = if skip_padding {
420 0
421 } else {
422 target_width.saturating_sub(current_width)
423 };
424
425 let effective_align = match options.row_type {
427 RowType::Header => options.column_align_header.unwrap_or(options.column_align),
428 RowType::Body => options.column_align_body.unwrap_or(options.column_align),
429 RowType::Delimiter => unreachable!(),
430 };
431
432 let alignment = match effective_align {
434 ColumnAlign::Auto => column_alignments.get(i).copied().unwrap_or(ColumnAlignment::Left),
435 ColumnAlign::Left => ColumnAlignment::Left,
436 ColumnAlign::Center => ColumnAlignment::Center,
437 ColumnAlign::Right => ColumnAlignment::Right,
438 };
439
440 match alignment {
441 ColumnAlignment::Left => {
442 format!(" {trimmed}{} ", " ".repeat(padding))
444 }
445 ColumnAlignment::Center => {
446 let left_padding = padding / 2;
448 let right_padding = padding - left_padding;
449 format!(" {}{trimmed}{} ", " ".repeat(left_padding), " ".repeat(right_padding))
450 }
451 ColumnAlignment::Right => {
452 format!(" {}{trimmed} ", " ".repeat(padding))
454 }
455 }
456 }
457 }
458 })
459 .collect();
460
461 format!("|{}|", formatted_cells.join("|"))
462 }
463
464 fn format_table_compact(cells: &[String]) -> String {
465 let formatted_cells: Vec<String> = cells.iter().map(|cell| format!(" {} ", cell.trim())).collect();
466 format!("|{}|", formatted_cells.join("|"))
467 }
468
469 fn format_table_tight(cells: &[String]) -> String {
470 let formatted_cells: Vec<String> = cells.iter().map(|cell| cell.trim().to_string()).collect();
471 format!("|{}|", formatted_cells.join("|"))
472 }
473
474 fn is_table_already_aligned(
486 table_lines: &[&str],
487 flavor: crate::config::MarkdownFlavor,
488 compact_delimiter: bool,
489 ) -> bool {
490 if table_lines.len() < 2 {
491 return false;
492 }
493
494 let first_len = table_lines[0].len();
496 if !table_lines.iter().all(|line| line.len() == first_len) {
497 return false;
498 }
499
500 let parsed: Vec<Vec<String>> = table_lines
502 .iter()
503 .map(|line| Self::parse_table_row_with_flavor(line, flavor))
504 .collect();
505
506 if parsed.is_empty() {
507 return false;
508 }
509
510 let num_columns = parsed[0].len();
511 if !parsed.iter().all(|row| row.len() == num_columns) {
512 return false;
513 }
514
515 if let Some(delimiter_row) = parsed.get(1) {
518 if !Self::is_delimiter_row(delimiter_row) {
519 return false;
520 }
521 for cell in delimiter_row {
523 let trimmed = cell.trim();
524 let dash_count = trimmed.chars().filter(|&c| c == '-').count();
525 if dash_count < 1 {
526 return false;
527 }
528 }
529
530 let delimiter_has_spaces = delimiter_row
534 .iter()
535 .all(|cell| cell.starts_with(' ') && cell.ends_with(' '));
536
537 if compact_delimiter && delimiter_has_spaces {
540 return false;
541 }
542 if !compact_delimiter && !delimiter_has_spaces {
543 return false;
544 }
545 }
546
547 for col_idx in 0..num_columns {
551 let mut widths = Vec::new();
552 for (row_idx, row) in parsed.iter().enumerate() {
553 if row_idx == 1 {
555 continue;
556 }
557 if let Some(cell) = row.get(col_idx) {
558 widths.push(cell.width());
559 }
560 }
561 if !widths.is_empty() && !widths.iter().all(|&w| w == widths[0]) {
563 return false;
564 }
565 }
566
567 true
568 }
569
570 fn detect_table_style(table_lines: &[&str], flavor: crate::config::MarkdownFlavor) -> Option<String> {
571 if table_lines.is_empty() {
572 return None;
573 }
574
575 let mut is_tight = true;
578 let mut is_compact = true;
579
580 for line in table_lines {
581 let cells = Self::parse_table_row_with_flavor(line, flavor);
582
583 if cells.is_empty() {
584 continue;
585 }
586
587 if Self::is_delimiter_row(&cells) {
589 continue;
590 }
591
592 let row_has_no_padding = cells.iter().all(|cell| !cell.starts_with(' ') && !cell.ends_with(' '));
594
595 let row_has_single_space = cells.iter().all(|cell| {
597 let trimmed = cell.trim();
598 cell == &format!(" {trimmed} ")
599 });
600
601 if !row_has_no_padding {
603 is_tight = false;
604 }
605
606 if !row_has_single_space {
608 is_compact = false;
609 }
610
611 if !is_tight && !is_compact {
613 return Some("aligned".to_string());
614 }
615 }
616
617 if is_tight {
619 Some("tight".to_string())
620 } else if is_compact {
621 Some("compact".to_string())
622 } else {
623 Some("aligned".to_string())
624 }
625 }
626
627 fn fix_table_block(
628 &self,
629 lines: &[&str],
630 table_block: &crate::utils::table_utils::TableBlock,
631 flavor: crate::config::MarkdownFlavor,
632 ) -> TableFormatResult {
633 let mut result = Vec::new();
634 let mut auto_compacted = false;
635 let mut aligned_width = None;
636
637 let table_lines: Vec<&str> = std::iter::once(lines[table_block.header_line])
638 .chain(std::iter::once(lines[table_block.delimiter_line]))
639 .chain(table_block.content_lines.iter().map(|&idx| lines[idx]))
640 .collect();
641
642 if table_lines.iter().any(|line| Self::contains_problematic_chars(line)) {
643 return TableFormatResult {
644 lines: table_lines.iter().map(|s| s.to_string()).collect(),
645 auto_compacted: false,
646 aligned_width: None,
647 };
648 }
649
650 let (blockquote_prefix, _) = Self::extract_blockquote_prefix(table_lines[0]);
653
654 let list_context = &table_block.list_context;
656 let (list_prefix, continuation_indent) = if let Some(ctx) = list_context {
657 (ctx.list_prefix.as_str(), " ".repeat(ctx.content_indent))
658 } else {
659 ("", String::new())
660 };
661
662 let stripped_lines: Vec<&str> = table_lines
664 .iter()
665 .enumerate()
666 .map(|(i, line)| {
667 let after_blockquote = Self::extract_blockquote_prefix(line).1;
668 if list_context.is_some() {
669 if i == 0 {
670 after_blockquote.strip_prefix(list_prefix).unwrap_or_else(|| {
672 crate::utils::table_utils::TableUtils::extract_list_prefix(after_blockquote).1
673 })
674 } else {
675 after_blockquote
677 .strip_prefix(&continuation_indent)
678 .unwrap_or(after_blockquote.trim_start())
679 }
680 } else {
681 after_blockquote
682 }
683 })
684 .collect();
685
686 let style = self.config.style.as_str();
687
688 match style {
689 "any" => {
690 let detected_style = Self::detect_table_style(&stripped_lines, flavor);
691 if detected_style.is_none() {
692 return TableFormatResult {
693 lines: table_lines.iter().map(|s| s.to_string()).collect(),
694 auto_compacted: false,
695 aligned_width: None,
696 };
697 }
698
699 let target_style = detected_style.unwrap();
700
701 let delimiter_cells = Self::parse_table_row_with_flavor(stripped_lines[1], flavor);
703 let column_alignments = Self::parse_column_alignments(&delimiter_cells);
704
705 for (row_idx, line) in stripped_lines.iter().enumerate() {
706 let cells = Self::parse_table_row_with_flavor(line, flavor);
707 match target_style.as_str() {
708 "tight" => result.push(Self::format_table_tight(&cells)),
709 "compact" => result.push(Self::format_table_compact(&cells)),
710 _ => {
711 let column_widths = Self::calculate_column_widths(&stripped_lines, flavor);
712 let row_type = match row_idx {
713 0 => RowType::Header,
714 1 => RowType::Delimiter,
715 _ => RowType::Body,
716 };
717 let options = RowFormatOptions {
718 row_type,
719 compact_delimiter: false,
720 column_align: self.config.column_align,
721 column_align_header: self.config.column_align_header,
722 column_align_body: self.config.column_align_body,
723 loose_last_column: self.config.loose_last_column,
724 };
725 result.push(Self::format_table_row(
726 &cells,
727 &column_widths,
728 &column_alignments,
729 &options,
730 ));
731 }
732 }
733 }
734 }
735 "compact" => {
736 for line in &stripped_lines {
737 let cells = Self::parse_table_row_with_flavor(line, flavor);
738 result.push(Self::format_table_compact(&cells));
739 }
740 }
741 "tight" => {
742 for line in &stripped_lines {
743 let cells = Self::parse_table_row_with_flavor(line, flavor);
744 result.push(Self::format_table_tight(&cells));
745 }
746 }
747 "aligned" | "aligned-no-space" => {
748 let compact_delimiter = style == "aligned-no-space";
749
750 let needs_reformat = self.config.column_align != ColumnAlign::Auto
753 || self.config.column_align_header.is_some()
754 || self.config.column_align_body.is_some()
755 || self.config.loose_last_column;
756
757 if !needs_reformat && Self::is_table_already_aligned(&stripped_lines, flavor, compact_delimiter) {
758 return TableFormatResult {
759 lines: table_lines.iter().map(|s| s.to_string()).collect(),
760 auto_compacted: false,
761 aligned_width: None,
762 };
763 }
764
765 let column_widths = Self::calculate_column_widths(&stripped_lines, flavor);
766
767 let num_columns = column_widths.len();
769 let calc_aligned_width = 1 + (num_columns * 3) + column_widths.iter().sum::<usize>();
770 aligned_width = Some(calc_aligned_width);
771
772 if calc_aligned_width > self.effective_max_width() {
774 auto_compacted = true;
775 for line in &stripped_lines {
776 let cells = Self::parse_table_row_with_flavor(line, flavor);
777 result.push(Self::format_table_compact(&cells));
778 }
779 } else {
780 let delimiter_cells = Self::parse_table_row_with_flavor(stripped_lines[1], flavor);
782 let column_alignments = Self::parse_column_alignments(&delimiter_cells);
783
784 for (row_idx, line) in stripped_lines.iter().enumerate() {
785 let cells = Self::parse_table_row_with_flavor(line, flavor);
786 let row_type = match row_idx {
787 0 => RowType::Header,
788 1 => RowType::Delimiter,
789 _ => RowType::Body,
790 };
791 let options = RowFormatOptions {
792 row_type,
793 compact_delimiter,
794 column_align: self.config.column_align,
795 column_align_header: self.config.column_align_header,
796 column_align_body: self.config.column_align_body,
797 loose_last_column: self.config.loose_last_column,
798 };
799 result.push(Self::format_table_row(
800 &cells,
801 &column_widths,
802 &column_alignments,
803 &options,
804 ));
805 }
806 }
807 }
808 _ => {
809 return TableFormatResult {
810 lines: table_lines.iter().map(|s| s.to_string()).collect(),
811 auto_compacted: false,
812 aligned_width: None,
813 };
814 }
815 }
816
817 let prefixed_result: Vec<String> = result
819 .into_iter()
820 .enumerate()
821 .map(|(i, line)| {
822 if list_context.is_some() {
823 if i == 0 {
824 format!("{blockquote_prefix}{list_prefix}{line}")
826 } else {
827 format!("{blockquote_prefix}{continuation_indent}{line}")
829 }
830 } else {
831 format!("{blockquote_prefix}{line}")
832 }
833 })
834 .collect();
835
836 TableFormatResult {
837 lines: prefixed_result,
838 auto_compacted,
839 aligned_width,
840 }
841 }
842}
843
844impl Rule for MD060TableFormat {
845 fn name(&self) -> &'static str {
846 "MD060"
847 }
848
849 fn description(&self) -> &'static str {
850 "Table columns should be consistently aligned"
851 }
852
853 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
854 !self.config.enabled || !ctx.likely_has_tables()
855 }
856
857 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
858 if !self.config.enabled {
859 return Ok(Vec::new());
860 }
861
862 let line_index = &ctx.line_index;
863 let mut warnings = Vec::new();
864
865 let lines = ctx.raw_lines();
866 let table_blocks = &ctx.table_blocks;
867
868 for table_block in table_blocks {
869 let format_result = self.fix_table_block(lines, table_block, ctx.flavor);
870
871 let table_line_indices: Vec<usize> = std::iter::once(table_block.header_line)
872 .chain(std::iter::once(table_block.delimiter_line))
873 .chain(table_block.content_lines.iter().copied())
874 .collect();
875
876 let table_start_line = table_block.start_line + 1; let table_end_line = table_block.end_line + 1; let mut fixed_table_lines: Vec<String> = Vec::with_capacity(table_line_indices.len());
883 for (i, &line_idx) in table_line_indices.iter().enumerate() {
884 let fixed_line = &format_result.lines[i];
885 if line_idx < lines.len() - 1 {
887 fixed_table_lines.push(format!("{fixed_line}\n"));
888 } else {
889 fixed_table_lines.push(fixed_line.clone());
890 }
891 }
892 let table_replacement = fixed_table_lines.concat();
893 let table_range = line_index.multi_line_range(table_start_line, table_end_line);
894
895 for (i, &line_idx) in table_line_indices.iter().enumerate() {
896 let original = lines[line_idx];
897 let fixed = &format_result.lines[i];
898
899 if original != fixed {
900 let (start_line, start_col, end_line, end_col) = calculate_line_range(line_idx + 1, original);
901
902 let message = if format_result.auto_compacted {
903 if let Some(width) = format_result.aligned_width {
904 format!(
905 "Table too wide for aligned formatting ({} chars > max-width: {})",
906 width,
907 self.effective_max_width()
908 )
909 } else {
910 "Table too wide for aligned formatting".to_string()
911 }
912 } else {
913 "Table columns should be aligned".to_string()
914 };
915
916 warnings.push(LintWarning {
919 rule_name: Some(self.name().to_string()),
920 severity: Severity::Warning,
921 message,
922 line: start_line,
923 column: start_col,
924 end_line,
925 end_column: end_col,
926 fix: Some(crate::rule::Fix {
927 range: table_range.clone(),
928 replacement: table_replacement.clone(),
929 }),
930 });
931 }
932 }
933 }
934
935 Ok(warnings)
936 }
937
938 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
939 if !self.config.enabled {
940 return Ok(ctx.content.to_string());
941 }
942
943 let content = ctx.content;
944 let lines = ctx.raw_lines();
945 let table_blocks = &ctx.table_blocks;
946
947 let mut result_lines: Vec<String> = lines.iter().map(|&s| s.to_string()).collect();
948
949 for table_block in table_blocks {
950 let format_result = self.fix_table_block(lines, table_block, ctx.flavor);
951
952 let table_line_indices: Vec<usize> = std::iter::once(table_block.header_line)
953 .chain(std::iter::once(table_block.delimiter_line))
954 .chain(table_block.content_lines.iter().copied())
955 .collect();
956
957 for (i, &line_idx) in table_line_indices.iter().enumerate() {
958 result_lines[line_idx] = format_result.lines[i].clone();
959 }
960 }
961
962 let mut fixed = result_lines.join("\n");
963 if content.ends_with('\n') && !fixed.ends_with('\n') {
964 fixed.push('\n');
965 }
966 Ok(fixed)
967 }
968
969 fn as_any(&self) -> &dyn std::any::Any {
970 self
971 }
972
973 fn default_config_section(&self) -> Option<(String, toml::Value)> {
974 let mut table = toml::map::Map::new();
977 table.insert("enabled".to_string(), toml::Value::Boolean(self.config.enabled));
978 table.insert("style".to_string(), toml::Value::String(self.config.style.clone()));
979 table.insert(
980 "max-width".to_string(),
981 toml::Value::Integer(self.config.max_width.get() as i64),
982 );
983 table.insert(
984 "column-align".to_string(),
985 toml::Value::String(
986 match self.config.column_align {
987 ColumnAlign::Auto => "auto",
988 ColumnAlign::Left => "left",
989 ColumnAlign::Center => "center",
990 ColumnAlign::Right => "right",
991 }
992 .to_string(),
993 ),
994 );
995 table.insert(
997 "column-align-header".to_string(),
998 toml::Value::String("auto".to_string()),
999 );
1000 table.insert("column-align-body".to_string(), toml::Value::String("auto".to_string()));
1001 table.insert(
1002 "loose-last-column".to_string(),
1003 toml::Value::Boolean(self.config.loose_last_column),
1004 );
1005
1006 Some((self.name().to_string(), toml::Value::Table(table)))
1007 }
1008
1009 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
1010 where
1011 Self: Sized,
1012 {
1013 let rule_config = crate::rule_config_serde::load_rule_config::<MD060Config>(config);
1014 let md013_config = crate::rule_config_serde::load_rule_config::<MD013Config>(config);
1015
1016 let md013_disabled = config.global.disable.iter().any(|r| r == "MD013");
1018
1019 Box::new(Self::from_config_struct(rule_config, md013_config, md013_disabled))
1020 }
1021}
1022
1023#[cfg(test)]
1024mod tests {
1025 use super::*;
1026 use crate::lint_context::LintContext;
1027 use crate::types::LineLength;
1028
1029 fn md013_with_line_length(line_length: usize) -> MD013Config {
1031 MD013Config {
1032 line_length: LineLength::from_const(line_length),
1033 tables: true, ..Default::default()
1035 }
1036 }
1037
1038 #[test]
1039 fn test_md060_disabled_by_default() {
1040 let rule = MD060TableFormat::default();
1041 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
1042 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1043
1044 let warnings = rule.check(&ctx).unwrap();
1045 assert_eq!(warnings.len(), 0);
1046
1047 let fixed = rule.fix(&ctx).unwrap();
1048 assert_eq!(fixed, content);
1049 }
1050
1051 #[test]
1052 fn test_md060_align_simple_ascii_table() {
1053 let rule = MD060TableFormat::new(true, "aligned".to_string());
1054
1055 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
1056 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1057
1058 let fixed = rule.fix(&ctx).unwrap();
1059 let expected = "| Name | Age |\n| ----- | --- |\n| Alice | 30 |";
1060 assert_eq!(fixed, expected);
1061
1062 let lines: Vec<&str> = fixed.lines().collect();
1064 assert_eq!(lines[0].len(), lines[1].len());
1065 assert_eq!(lines[1].len(), lines[2].len());
1066 }
1067
1068 #[test]
1069 fn test_md060_cjk_characters_aligned_correctly() {
1070 let rule = MD060TableFormat::new(true, "aligned".to_string());
1071
1072 let content = "| Name | Age |\n|---|---|\n| δΈζ | 30 |";
1073 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1074
1075 let fixed = rule.fix(&ctx).unwrap();
1076
1077 let lines: Vec<&str> = fixed.lines().collect();
1078 let cells_line1 = MD060TableFormat::parse_table_row(lines[0]);
1079 let cells_line3 = MD060TableFormat::parse_table_row(lines[2]);
1080
1081 let width1 = MD060TableFormat::calculate_cell_display_width(&cells_line1[0]);
1082 let width3 = MD060TableFormat::calculate_cell_display_width(&cells_line3[0]);
1083
1084 assert_eq!(width1, width3);
1085 }
1086
1087 #[test]
1088 fn test_md060_basic_emoji() {
1089 let rule = MD060TableFormat::new(true, "aligned".to_string());
1090
1091 let content = "| Status | Name |\n|---|---|\n| β
| Test |";
1092 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1093
1094 let fixed = rule.fix(&ctx).unwrap();
1095 assert!(fixed.contains("Status"));
1096 }
1097
1098 #[test]
1099 fn test_md060_zwj_emoji_skipped() {
1100 let rule = MD060TableFormat::new(true, "aligned".to_string());
1101
1102 let content = "| Emoji | Name |\n|---|---|\n| π¨βπ©βπ§βπ¦ | Family |";
1103 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1104
1105 let fixed = rule.fix(&ctx).unwrap();
1106 assert_eq!(fixed, content);
1107 }
1108
1109 #[test]
1110 fn test_md060_inline_code_with_escaped_pipes() {
1111 let rule = MD060TableFormat::new(true, "aligned".to_string());
1114
1115 let content = "| Pattern | Regex |\n|---|---|\n| Time | `[0-9]\\|[0-9]` |";
1117 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1118
1119 let fixed = rule.fix(&ctx).unwrap();
1120 assert!(fixed.contains(r"`[0-9]\|[0-9]`"), "Escaped pipes should be preserved");
1121 }
1122
1123 #[test]
1124 fn test_md060_compact_style() {
1125 let rule = MD060TableFormat::new(true, "compact".to_string());
1126
1127 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
1128 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1129
1130 let fixed = rule.fix(&ctx).unwrap();
1131 let expected = "| Name | Age |\n| --- | --- |\n| Alice | 30 |";
1132 assert_eq!(fixed, expected);
1133 }
1134
1135 #[test]
1136 fn test_md060_tight_style() {
1137 let rule = MD060TableFormat::new(true, "tight".to_string());
1138
1139 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
1140 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1141
1142 let fixed = rule.fix(&ctx).unwrap();
1143 let expected = "|Name|Age|\n|---|---|\n|Alice|30|";
1144 assert_eq!(fixed, expected);
1145 }
1146
1147 #[test]
1148 fn test_md060_aligned_no_space_style() {
1149 let rule = MD060TableFormat::new(true, "aligned-no-space".to_string());
1151
1152 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
1153 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1154
1155 let fixed = rule.fix(&ctx).unwrap();
1156
1157 let lines: Vec<&str> = fixed.lines().collect();
1159 assert_eq!(lines[0], "| Name | Age |", "Header should have spaces around content");
1160 assert_eq!(
1161 lines[1], "|-------|-----|",
1162 "Delimiter should have NO spaces around dashes"
1163 );
1164 assert_eq!(lines[2], "| Alice | 30 |", "Content should have spaces around content");
1165
1166 assert_eq!(lines[0].len(), lines[1].len());
1168 assert_eq!(lines[1].len(), lines[2].len());
1169 }
1170
1171 #[test]
1172 fn test_md060_aligned_no_space_preserves_alignment_indicators() {
1173 let rule = MD060TableFormat::new(true, "aligned-no-space".to_string());
1175
1176 let content = "| Left | Center | Right |\n|:---|:---:|---:|\n| A | B | C |";
1177 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1178
1179 let fixed = rule.fix(&ctx).unwrap();
1180 let lines: Vec<&str> = fixed.lines().collect();
1181
1182 assert!(
1184 fixed.contains("|:"),
1185 "Should have left alignment indicator adjacent to pipe"
1186 );
1187 assert!(
1188 fixed.contains(":|"),
1189 "Should have right alignment indicator adjacent to pipe"
1190 );
1191 assert!(
1193 lines[1].contains(":---") && lines[1].contains("---:"),
1194 "Should have center alignment colons"
1195 );
1196 }
1197
1198 #[test]
1199 fn test_md060_aligned_no_space_three_column_table() {
1200 let rule = MD060TableFormat::new(true, "aligned-no-space".to_string());
1202
1203 let content = "| Header 1 | Header 2 | Header 3 |\n|---|---|---|\n| Row 1, Col 1 | Row 1, Col 2 | Row 1, Col 3 |\n| Row 2, Col 1 | Row 2, Col 2 | Row 2, Col 3 |";
1204 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1205
1206 let fixed = rule.fix(&ctx).unwrap();
1207 let lines: Vec<&str> = fixed.lines().collect();
1208
1209 assert!(lines[1].starts_with("|---"), "Delimiter should start with |---");
1211 assert!(lines[1].ends_with("---|"), "Delimiter should end with ---|");
1212 assert!(!lines[1].contains("| -"), "Delimiter should NOT have space after pipe");
1213 assert!(!lines[1].contains("- |"), "Delimiter should NOT have space before pipe");
1214 }
1215
1216 #[test]
1217 fn test_md060_aligned_no_space_auto_compacts_wide_tables() {
1218 let config = MD060Config {
1220 enabled: true,
1221 style: "aligned-no-space".to_string(),
1222 max_width: LineLength::from_const(50),
1223 column_align: ColumnAlign::Auto,
1224 column_align_header: None,
1225 column_align_body: None,
1226 loose_last_column: false,
1227 };
1228 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false);
1229
1230 let content = "| Very Long Column Header A | Very Long Column Header B | Very Long Column Header C |\n|---|---|---|\n| x | y | z |";
1232 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1233
1234 let fixed = rule.fix(&ctx).unwrap();
1235
1236 assert!(
1238 fixed.contains("| --- |"),
1239 "Should be compact format when exceeding max-width"
1240 );
1241 }
1242
1243 #[test]
1244 fn test_md060_aligned_no_space_cjk_characters() {
1245 let rule = MD060TableFormat::new(true, "aligned-no-space".to_string());
1247
1248 let content = "| Name | City |\n|---|---|\n| δΈζ | ζ±δΊ¬ |";
1249 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1250
1251 let fixed = rule.fix(&ctx).unwrap();
1252 let lines: Vec<&str> = fixed.lines().collect();
1253
1254 use unicode_width::UnicodeWidthStr;
1257 assert_eq!(
1258 lines[0].width(),
1259 lines[1].width(),
1260 "Header and delimiter should have same display width"
1261 );
1262 assert_eq!(
1263 lines[1].width(),
1264 lines[2].width(),
1265 "Delimiter and content should have same display width"
1266 );
1267
1268 assert!(!lines[1].contains("| -"), "Delimiter should NOT have space after pipe");
1270 }
1271
1272 #[test]
1273 fn test_md060_aligned_no_space_minimum_width() {
1274 let rule = MD060TableFormat::new(true, "aligned-no-space".to_string());
1276
1277 let content = "| A | B |\n|-|-|\n| 1 | 2 |";
1278 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1279
1280 let fixed = rule.fix(&ctx).unwrap();
1281 let lines: Vec<&str> = fixed.lines().collect();
1282
1283 assert!(lines[1].contains("---"), "Should have minimum 3 dashes");
1285 assert_eq!(lines[0].len(), lines[1].len());
1287 assert_eq!(lines[1].len(), lines[2].len());
1288 }
1289
1290 #[test]
1291 fn test_md060_any_style_consistency() {
1292 let rule = MD060TableFormat::new(true, "any".to_string());
1293
1294 let content = "| Name | Age |\n| --- | --- |\n| Alice | 30 |";
1296 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1297
1298 let fixed = rule.fix(&ctx).unwrap();
1299 assert_eq!(fixed, content);
1300
1301 let content_aligned = "| Name | Age |\n| ----- | --- |\n| Alice | 30 |";
1303 let ctx_aligned = LintContext::new(content_aligned, crate::config::MarkdownFlavor::Standard, None);
1304
1305 let fixed_aligned = rule.fix(&ctx_aligned).unwrap();
1306 assert_eq!(fixed_aligned, content_aligned);
1307 }
1308
1309 #[test]
1310 fn test_md060_empty_cells() {
1311 let rule = MD060TableFormat::new(true, "aligned".to_string());
1312
1313 let content = "| A | B |\n|---|---|\n| | X |";
1314 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1315
1316 let fixed = rule.fix(&ctx).unwrap();
1317 assert!(fixed.contains("|"));
1318 }
1319
1320 #[test]
1321 fn test_md060_mixed_content() {
1322 let rule = MD060TableFormat::new(true, "aligned".to_string());
1323
1324 let content = "| Name | Age | City |\n|---|---|---|\n| δΈζ | 30 | NYC |";
1325 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1326
1327 let fixed = rule.fix(&ctx).unwrap();
1328 assert!(fixed.contains("δΈζ"));
1329 assert!(fixed.contains("NYC"));
1330 }
1331
1332 #[test]
1333 fn test_md060_preserve_alignment_indicators() {
1334 let rule = MD060TableFormat::new(true, "aligned".to_string());
1335
1336 let content = "| Left | Center | Right |\n|:---|:---:|---:|\n| A | B | C |";
1337 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1338
1339 let fixed = rule.fix(&ctx).unwrap();
1340
1341 assert!(fixed.contains(":---"), "Should contain left alignment");
1342 assert!(fixed.contains(":----:"), "Should contain center alignment");
1343 assert!(fixed.contains("----:"), "Should contain right alignment");
1344 }
1345
1346 #[test]
1347 fn test_md060_minimum_column_width() {
1348 let rule = MD060TableFormat::new(true, "aligned".to_string());
1349
1350 let content = "| ID | Name |\n|-|-|\n| 1 | A |";
1353 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1354
1355 let fixed = rule.fix(&ctx).unwrap();
1356
1357 let lines: Vec<&str> = fixed.lines().collect();
1358 assert_eq!(lines[0].len(), lines[1].len());
1359 assert_eq!(lines[1].len(), lines[2].len());
1360
1361 assert!(fixed.contains("ID "), "Short content should be padded");
1363 assert!(fixed.contains("---"), "Delimiter should have at least 3 dashes");
1364 }
1365
1366 #[test]
1367 fn test_md060_auto_compact_exceeds_default_threshold() {
1368 let config = MD060Config {
1370 enabled: true,
1371 style: "aligned".to_string(),
1372 max_width: LineLength::from_const(0),
1373 column_align: ColumnAlign::Auto,
1374 column_align_header: None,
1375 column_align_body: None,
1376 loose_last_column: false,
1377 };
1378 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false);
1379
1380 let content = "| Very Long Column Header | Another Long Header | Third Very Long Header Column |\n|---|---|---|\n| Short | Data | Here |";
1384 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1385
1386 let fixed = rule.fix(&ctx).unwrap();
1387
1388 assert!(fixed.contains("| Very Long Column Header | Another Long Header | Third Very Long Header Column |"));
1390 assert!(fixed.contains("| --- | --- | --- |"));
1391 assert!(fixed.contains("| Short | Data | Here |"));
1392
1393 let lines: Vec<&str> = fixed.lines().collect();
1395 assert!(lines[0].len() != lines[1].len() || lines[1].len() != lines[2].len());
1397 }
1398
1399 #[test]
1400 fn test_md060_auto_compact_exceeds_explicit_threshold() {
1401 let config = MD060Config {
1403 enabled: true,
1404 style: "aligned".to_string(),
1405 max_width: LineLength::from_const(50),
1406 column_align: ColumnAlign::Auto,
1407 column_align_header: None,
1408 column_align_body: None,
1409 loose_last_column: false,
1410 };
1411 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false); let content = "| Very Long Column Header A | Very Long Column Header B | Very Long Column Header C |\n|---|---|---|\n| Data | Data | Data |";
1417 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1418
1419 let fixed = rule.fix(&ctx).unwrap();
1420
1421 assert!(
1423 fixed.contains("| Very Long Column Header A | Very Long Column Header B | Very Long Column Header C |")
1424 );
1425 assert!(fixed.contains("| --- | --- | --- |"));
1426 assert!(fixed.contains("| Data | Data | Data |"));
1427
1428 let lines: Vec<&str> = fixed.lines().collect();
1430 assert!(lines[0].len() != lines[2].len());
1431 }
1432
1433 #[test]
1434 fn test_md060_stays_aligned_under_threshold() {
1435 let config = MD060Config {
1437 enabled: true,
1438 style: "aligned".to_string(),
1439 max_width: LineLength::from_const(100),
1440 column_align: ColumnAlign::Auto,
1441 column_align_header: None,
1442 column_align_body: None,
1443 loose_last_column: false,
1444 };
1445 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false);
1446
1447 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
1449 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1450
1451 let fixed = rule.fix(&ctx).unwrap();
1452
1453 let expected = "| Name | Age |\n| ----- | --- |\n| Alice | 30 |";
1455 assert_eq!(fixed, expected);
1456
1457 let lines: Vec<&str> = fixed.lines().collect();
1458 assert_eq!(lines[0].len(), lines[1].len());
1459 assert_eq!(lines[1].len(), lines[2].len());
1460 }
1461
1462 #[test]
1463 fn test_md060_width_calculation_formula() {
1464 let config = MD060Config {
1466 enabled: true,
1467 style: "aligned".to_string(),
1468 max_width: LineLength::from_const(0),
1469 column_align: ColumnAlign::Auto,
1470 column_align_header: None,
1471 column_align_body: None,
1472 loose_last_column: false,
1473 };
1474 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(30), false);
1475
1476 let content = "| AAAAA | BBBBB | CCCCC |\n|---|---|---|\n| AAAAA | BBBBB | CCCCC |";
1480 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1481
1482 let fixed = rule.fix(&ctx).unwrap();
1483
1484 let lines: Vec<&str> = fixed.lines().collect();
1486 assert_eq!(lines[0].len(), lines[1].len());
1487 assert_eq!(lines[1].len(), lines[2].len());
1488 assert_eq!(lines[0].len(), 25); let config_tight = MD060Config {
1492 enabled: true,
1493 style: "aligned".to_string(),
1494 max_width: LineLength::from_const(24),
1495 column_align: ColumnAlign::Auto,
1496 column_align_header: None,
1497 column_align_body: None,
1498 loose_last_column: false,
1499 };
1500 let rule_tight = MD060TableFormat::from_config_struct(config_tight, md013_with_line_length(80), false);
1501
1502 let fixed_compact = rule_tight.fix(&ctx).unwrap();
1503
1504 assert!(fixed_compact.contains("| AAAAA | BBBBB | CCCCC |"));
1506 assert!(fixed_compact.contains("| --- | --- | --- |"));
1507 }
1508
1509 #[test]
1510 fn test_md060_very_wide_table_auto_compacts() {
1511 let config = MD060Config {
1512 enabled: true,
1513 style: "aligned".to_string(),
1514 max_width: LineLength::from_const(0),
1515 column_align: ColumnAlign::Auto,
1516 column_align_header: None,
1517 column_align_body: None,
1518 loose_last_column: false,
1519 };
1520 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false);
1521
1522 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 |";
1526 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1527
1528 let fixed = rule.fix(&ctx).unwrap();
1529
1530 assert!(fixed.contains("| Column One A | Column Two B | Column Three | Column Four D | Column Five E | Column Six FG | Column Seven | Column Eight |"));
1532 assert!(fixed.contains("| --- | --- | --- | --- | --- | --- | --- | --- |"));
1533 }
1534
1535 #[test]
1536 fn test_md060_inherit_from_md013_line_length() {
1537 let config = MD060Config {
1539 enabled: true,
1540 style: "aligned".to_string(),
1541 max_width: LineLength::from_const(0), column_align: ColumnAlign::Auto,
1543 column_align_header: None,
1544 column_align_body: None,
1545 loose_last_column: false,
1546 };
1547
1548 let rule_80 = MD060TableFormat::from_config_struct(config.clone(), md013_with_line_length(80), false);
1550 let rule_120 = MD060TableFormat::from_config_struct(config.clone(), md013_with_line_length(120), false);
1551
1552 let content = "| Column Header A | Column Header B | Column Header C |\n|---|---|---|\n| Some Data | More Data | Even More |";
1554 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1555
1556 let _fixed_80 = rule_80.fix(&ctx).unwrap();
1558
1559 let fixed_120 = rule_120.fix(&ctx).unwrap();
1561
1562 let lines_120: Vec<&str> = fixed_120.lines().collect();
1564 assert_eq!(lines_120[0].len(), lines_120[1].len());
1565 assert_eq!(lines_120[1].len(), lines_120[2].len());
1566 }
1567
1568 #[test]
1569 fn test_md060_edge_case_exactly_at_threshold() {
1570 let config = MD060Config {
1574 enabled: true,
1575 style: "aligned".to_string(),
1576 max_width: LineLength::from_const(17),
1577 column_align: ColumnAlign::Auto,
1578 column_align_header: None,
1579 column_align_body: None,
1580 loose_last_column: false,
1581 };
1582 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false);
1583
1584 let content = "| AAAAA | BBBBB |\n|---|---|\n| AAAAA | BBBBB |";
1585 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1586
1587 let fixed = rule.fix(&ctx).unwrap();
1588
1589 let lines: Vec<&str> = fixed.lines().collect();
1591 assert_eq!(lines[0].len(), 17);
1592 assert_eq!(lines[0].len(), lines[1].len());
1593 assert_eq!(lines[1].len(), lines[2].len());
1594
1595 let config_under = MD060Config {
1597 enabled: true,
1598 style: "aligned".to_string(),
1599 max_width: LineLength::from_const(16),
1600 column_align: ColumnAlign::Auto,
1601 column_align_header: None,
1602 column_align_body: None,
1603 loose_last_column: false,
1604 };
1605 let rule_under = MD060TableFormat::from_config_struct(config_under, md013_with_line_length(80), false);
1606
1607 let fixed_compact = rule_under.fix(&ctx).unwrap();
1608
1609 assert!(fixed_compact.contains("| AAAAA | BBBBB |"));
1611 assert!(fixed_compact.contains("| --- | --- |"));
1612 }
1613
1614 #[test]
1615 fn test_md060_auto_compact_warning_message() {
1616 let config = MD060Config {
1618 enabled: true,
1619 style: "aligned".to_string(),
1620 max_width: LineLength::from_const(50),
1621 column_align: ColumnAlign::Auto,
1622 column_align_header: None,
1623 column_align_body: None,
1624 loose_last_column: false,
1625 };
1626 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false);
1627
1628 let content = "| Very Long Column Header A | Very Long Column Header B | Very Long Column Header C |\n|---|---|---|\n| Data | Data | Data |";
1630 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1631
1632 let warnings = rule.check(&ctx).unwrap();
1633
1634 assert!(!warnings.is_empty(), "Should generate warnings");
1636
1637 let auto_compact_warnings: Vec<_> = warnings
1638 .iter()
1639 .filter(|w| w.message.contains("too wide for aligned formatting"))
1640 .collect();
1641
1642 assert!(!auto_compact_warnings.is_empty(), "Should have auto-compact warning");
1643
1644 let first_warning = auto_compact_warnings[0];
1646 assert!(first_warning.message.contains("85 chars > max-width: 50"));
1647 assert!(first_warning.message.contains("Table too wide for aligned formatting"));
1648 }
1649
1650 #[test]
1651 fn test_md060_issue_129_detect_style_from_all_rows() {
1652 let rule = MD060TableFormat::new(true, "any".to_string());
1656
1657 let content = "| a long heading | another long heading |\n\
1659 | -------------- | -------------------- |\n\
1660 | a | 1 |\n\
1661 | b b | 2 |\n\
1662 | c c c | 3 |";
1663 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1664
1665 let fixed = rule.fix(&ctx).unwrap();
1666
1667 assert!(
1669 fixed.contains("| a | 1 |"),
1670 "Should preserve aligned padding in first content row"
1671 );
1672 assert!(
1673 fixed.contains("| b b | 2 |"),
1674 "Should preserve aligned padding in second content row"
1675 );
1676 assert!(
1677 fixed.contains("| c c c | 3 |"),
1678 "Should preserve aligned padding in third content row"
1679 );
1680
1681 assert_eq!(fixed, content, "Table should be detected as aligned and preserved");
1683 }
1684
1685 #[test]
1686 fn test_md060_regular_alignment_warning_message() {
1687 let config = MD060Config {
1689 enabled: true,
1690 style: "aligned".to_string(),
1691 max_width: LineLength::from_const(100), column_align: ColumnAlign::Auto,
1693 column_align_header: None,
1694 column_align_body: None,
1695 loose_last_column: false,
1696 };
1697 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false);
1698
1699 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
1701 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1702
1703 let warnings = rule.check(&ctx).unwrap();
1704
1705 assert!(!warnings.is_empty(), "Should generate warnings");
1707
1708 assert!(warnings[0].message.contains("Table columns should be aligned"));
1710 assert!(!warnings[0].message.contains("too wide"));
1711 assert!(!warnings[0].message.contains("max-width"));
1712 }
1713
1714 #[test]
1717 fn test_md060_unlimited_when_md013_disabled() {
1718 let config = MD060Config {
1720 enabled: true,
1721 style: "aligned".to_string(),
1722 max_width: LineLength::from_const(0), column_align: ColumnAlign::Auto,
1724 column_align_header: None,
1725 column_align_body: None,
1726 loose_last_column: false,
1727 };
1728 let md013_config = MD013Config::default();
1729 let rule = MD060TableFormat::from_config_struct(config, md013_config, true );
1730
1731 let content = "| Very Long Column Header A | Very Long Column Header B | Very Long Column Header C |\n|---|---|---|\n| data | data | data |";
1733 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1734 let fixed = rule.fix(&ctx).unwrap();
1735
1736 let lines: Vec<&str> = fixed.lines().collect();
1738 assert_eq!(
1740 lines[0].len(),
1741 lines[1].len(),
1742 "Table should be aligned when MD013 is disabled"
1743 );
1744 }
1745
1746 #[test]
1747 fn test_md060_unlimited_when_md013_tables_false() {
1748 let config = MD060Config {
1750 enabled: true,
1751 style: "aligned".to_string(),
1752 max_width: LineLength::from_const(0),
1753 column_align: ColumnAlign::Auto,
1754 column_align_header: None,
1755 column_align_body: None,
1756 loose_last_column: false,
1757 };
1758 let md013_config = MD013Config {
1759 tables: false, line_length: LineLength::from_const(80),
1761 ..Default::default()
1762 };
1763 let rule = MD060TableFormat::from_config_struct(config, md013_config, false);
1764
1765 let content = "| Very Long Header A | Very Long Header B | Very Long Header C |\n|---|---|---|\n| x | y | z |";
1767 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1768 let fixed = rule.fix(&ctx).unwrap();
1769
1770 let lines: Vec<&str> = fixed.lines().collect();
1772 assert_eq!(
1773 lines[0].len(),
1774 lines[1].len(),
1775 "Table should be aligned when MD013.tables=false"
1776 );
1777 }
1778
1779 #[test]
1780 fn test_md060_unlimited_when_md013_line_length_zero() {
1781 let config = MD060Config {
1783 enabled: true,
1784 style: "aligned".to_string(),
1785 max_width: LineLength::from_const(0),
1786 column_align: ColumnAlign::Auto,
1787 column_align_header: None,
1788 column_align_body: None,
1789 loose_last_column: false,
1790 };
1791 let md013_config = MD013Config {
1792 tables: true,
1793 line_length: LineLength::from_const(0), ..Default::default()
1795 };
1796 let rule = MD060TableFormat::from_config_struct(config, md013_config, false);
1797
1798 let content = "| Very Long Header | Another Long Header | Third Long Header |\n|---|---|---|\n| x | y | z |";
1800 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1801 let fixed = rule.fix(&ctx).unwrap();
1802
1803 let lines: Vec<&str> = fixed.lines().collect();
1805 assert_eq!(
1806 lines[0].len(),
1807 lines[1].len(),
1808 "Table should be aligned when MD013.line_length=0"
1809 );
1810 }
1811
1812 #[test]
1813 fn test_md060_explicit_max_width_overrides_md013_settings() {
1814 let config = MD060Config {
1816 enabled: true,
1817 style: "aligned".to_string(),
1818 max_width: LineLength::from_const(50), column_align: ColumnAlign::Auto,
1820 column_align_header: None,
1821 column_align_body: None,
1822 loose_last_column: false,
1823 };
1824 let md013_config = MD013Config {
1825 tables: false, line_length: LineLength::from_const(0), ..Default::default()
1828 };
1829 let rule = MD060TableFormat::from_config_struct(config, md013_config, false);
1830
1831 let content = "| Very Long Column Header A | Very Long Column Header B | Very Long Column Header C |\n|---|---|---|\n| x | y | z |";
1833 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1834 let fixed = rule.fix(&ctx).unwrap();
1835
1836 assert!(
1838 fixed.contains("| --- |"),
1839 "Should be compact format due to explicit max_width"
1840 );
1841 }
1842
1843 #[test]
1844 fn test_md060_inherits_md013_line_length_when_tables_enabled() {
1845 let config = MD060Config {
1847 enabled: true,
1848 style: "aligned".to_string(),
1849 max_width: LineLength::from_const(0), column_align: ColumnAlign::Auto,
1851 column_align_header: None,
1852 column_align_body: None,
1853 loose_last_column: false,
1854 };
1855 let md013_config = MD013Config {
1856 tables: true,
1857 line_length: LineLength::from_const(50), ..Default::default()
1859 };
1860 let rule = MD060TableFormat::from_config_struct(config, md013_config, false);
1861
1862 let content = "| Very Long Column Header A | Very Long Column Header B | Very Long Column Header C |\n|---|---|---|\n| x | y | z |";
1864 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1865 let fixed = rule.fix(&ctx).unwrap();
1866
1867 assert!(
1869 fixed.contains("| --- |"),
1870 "Should be compact format when inheriting MD013 limit"
1871 );
1872 }
1873
1874 #[test]
1877 fn test_aligned_no_space_reformats_spaced_delimiter() {
1878 let config = MD060Config {
1881 enabled: true,
1882 style: "aligned-no-space".to_string(),
1883 max_width: LineLength::from_const(0),
1884 column_align: ColumnAlign::Auto,
1885 column_align_header: None,
1886 column_align_body: None,
1887 loose_last_column: false,
1888 };
1889 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
1890
1891 let content = "| Header 1 | Header 2 |\n| -------- | -------- |\n| Cell 1 | Cell 2 |";
1893 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1894 let fixed = rule.fix(&ctx).unwrap();
1895
1896 assert!(
1899 !fixed.contains("| ----"),
1900 "Delimiter should NOT have spaces after pipe. Got:\n{fixed}"
1901 );
1902 assert!(
1903 !fixed.contains("---- |"),
1904 "Delimiter should NOT have spaces before pipe. Got:\n{fixed}"
1905 );
1906 assert!(
1908 fixed.contains("|----"),
1909 "Delimiter should have dashes touching the leading pipe. Got:\n{fixed}"
1910 );
1911 }
1912
1913 #[test]
1914 fn test_aligned_reformats_compact_delimiter() {
1915 let config = MD060Config {
1918 enabled: true,
1919 style: "aligned".to_string(),
1920 max_width: LineLength::from_const(0),
1921 column_align: ColumnAlign::Auto,
1922 column_align_header: None,
1923 column_align_body: None,
1924 loose_last_column: false,
1925 };
1926 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
1927
1928 let content = "| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |";
1930 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1931 let fixed = rule.fix(&ctx).unwrap();
1932
1933 assert!(
1935 fixed.contains("| -------- | -------- |") || fixed.contains("| ---------- | ---------- |"),
1936 "Delimiter should have spaces around dashes. Got:\n{fixed}"
1937 );
1938 }
1939
1940 #[test]
1941 fn test_aligned_no_space_preserves_matching_table() {
1942 let config = MD060Config {
1944 enabled: true,
1945 style: "aligned-no-space".to_string(),
1946 max_width: LineLength::from_const(0),
1947 column_align: ColumnAlign::Auto,
1948 column_align_header: None,
1949 column_align_body: None,
1950 loose_last_column: false,
1951 };
1952 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
1953
1954 let content = "| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |";
1956 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1957 let fixed = rule.fix(&ctx).unwrap();
1958
1959 assert_eq!(
1961 fixed, content,
1962 "Table already in aligned-no-space style should be preserved"
1963 );
1964 }
1965
1966 #[test]
1967 fn test_aligned_preserves_matching_table() {
1968 let config = MD060Config {
1970 enabled: true,
1971 style: "aligned".to_string(),
1972 max_width: LineLength::from_const(0),
1973 column_align: ColumnAlign::Auto,
1974 column_align_header: None,
1975 column_align_body: None,
1976 loose_last_column: false,
1977 };
1978 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
1979
1980 let content = "| Header 1 | Header 2 |\n| -------- | -------- |\n| Cell 1 | Cell 2 |";
1982 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1983 let fixed = rule.fix(&ctx).unwrap();
1984
1985 assert_eq!(fixed, content, "Table already in aligned style should be preserved");
1987 }
1988
1989 #[test]
1990 fn test_cjk_table_display_width_consistency() {
1991 let table_lines = vec!["| εε | Age |", "|------|-----|", "| η°δΈ | 25 |"];
1997
1998 let is_aligned =
2000 MD060TableFormat::is_table_already_aligned(&table_lines, crate::config::MarkdownFlavor::Standard, false);
2001 assert!(
2002 !is_aligned,
2003 "Table with uneven raw line lengths should NOT be considered aligned"
2004 );
2005 }
2006
2007 #[test]
2008 fn test_cjk_width_calculation_in_aligned_check() {
2009 let cjk_width = MD060TableFormat::calculate_cell_display_width("εε");
2012 assert_eq!(cjk_width, 4, "Two CJK characters should have display width 4");
2013
2014 let ascii_width = MD060TableFormat::calculate_cell_display_width("Age");
2015 assert_eq!(ascii_width, 3, "Three ASCII characters should have display width 3");
2016
2017 let padded_cjk = MD060TableFormat::calculate_cell_display_width(" εε ");
2019 assert_eq!(padded_cjk, 4, "Padded CJK should have same width after trim");
2020
2021 let mixed = MD060TableFormat::calculate_cell_display_width(" ζ₯ζ¬θͺABC ");
2023 assert_eq!(mixed, 9, "Mixed CJK/ASCII content");
2025 }
2026
2027 #[test]
2030 fn test_md060_column_align_left() {
2031 let config = MD060Config {
2033 enabled: true,
2034 style: "aligned".to_string(),
2035 max_width: LineLength::from_const(0),
2036 column_align: ColumnAlign::Left,
2037 column_align_header: None,
2038 column_align_body: None,
2039 loose_last_column: false,
2040 };
2041 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2042
2043 let content = "| Name | Age | City |\n|---|---|---|\n| Alice | 30 | Seattle |\n| Bob | 25 | Portland |";
2044 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2045
2046 let fixed = rule.fix(&ctx).unwrap();
2047 let lines: Vec<&str> = fixed.lines().collect();
2048
2049 assert!(
2051 lines[2].contains("| Alice "),
2052 "Content should be left-aligned (Alice should have trailing padding)"
2053 );
2054 assert!(
2055 lines[3].contains("| Bob "),
2056 "Content should be left-aligned (Bob should have trailing padding)"
2057 );
2058 }
2059
2060 #[test]
2061 fn test_md060_column_align_center() {
2062 let config = MD060Config {
2064 enabled: true,
2065 style: "aligned".to_string(),
2066 max_width: LineLength::from_const(0),
2067 column_align: ColumnAlign::Center,
2068 column_align_header: None,
2069 column_align_body: None,
2070 loose_last_column: false,
2071 };
2072 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2073
2074 let content = "| Name | Age | City |\n|---|---|---|\n| Alice | 30 | Seattle |\n| Bob | 25 | Portland |";
2075 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2076
2077 let fixed = rule.fix(&ctx).unwrap();
2078 let lines: Vec<&str> = fixed.lines().collect();
2079
2080 assert!(
2083 lines[3].contains("| Bob |"),
2084 "Bob should be centered with padding on both sides. Got: {}",
2085 lines[3]
2086 );
2087 }
2088
2089 #[test]
2090 fn test_md060_column_align_right() {
2091 let config = MD060Config {
2093 enabled: true,
2094 style: "aligned".to_string(),
2095 max_width: LineLength::from_const(0),
2096 column_align: ColumnAlign::Right,
2097 column_align_header: None,
2098 column_align_body: None,
2099 loose_last_column: false,
2100 };
2101 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2102
2103 let content = "| Name | Age | City |\n|---|---|---|\n| Alice | 30 | Seattle |\n| Bob | 25 | Portland |";
2104 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2105
2106 let fixed = rule.fix(&ctx).unwrap();
2107 let lines: Vec<&str> = fixed.lines().collect();
2108
2109 assert!(
2111 lines[3].contains("| Bob |"),
2112 "Bob should be right-aligned with padding on left. Got: {}",
2113 lines[3]
2114 );
2115 }
2116
2117 #[test]
2118 fn test_md060_column_align_auto_respects_delimiter() {
2119 let config = MD060Config {
2121 enabled: true,
2122 style: "aligned".to_string(),
2123 max_width: LineLength::from_const(0),
2124 column_align: ColumnAlign::Auto,
2125 column_align_header: None,
2126 column_align_body: None,
2127 loose_last_column: false,
2128 };
2129 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2130
2131 let content = "| Left | Center | Right |\n|:---|:---:|---:|\n| A | B | C |";
2133 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2134
2135 let fixed = rule.fix(&ctx).unwrap();
2136
2137 assert!(fixed.contains("| A "), "Left column should be left-aligned");
2139 let lines: Vec<&str> = fixed.lines().collect();
2141 assert!(
2145 lines[2].contains(" C |"),
2146 "Right column should be right-aligned. Got: {}",
2147 lines[2]
2148 );
2149 }
2150
2151 #[test]
2152 fn test_md060_column_align_overrides_delimiter_indicators() {
2153 let config = MD060Config {
2155 enabled: true,
2156 style: "aligned".to_string(),
2157 max_width: LineLength::from_const(0),
2158 column_align: ColumnAlign::Right, column_align_header: None,
2160 column_align_body: None,
2161 loose_last_column: false,
2162 };
2163 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2164
2165 let content = "| Left | Center | Right |\n|:---|:---:|---:|\n| A | B | C |";
2167 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2168
2169 let fixed = rule.fix(&ctx).unwrap();
2170 let lines: Vec<&str> = fixed.lines().collect();
2171
2172 assert!(
2175 lines[2].contains(" A |") || lines[2].contains(" A |"),
2176 "Even left-indicated column should be right-aligned. Got: {}",
2177 lines[2]
2178 );
2179 }
2180
2181 #[test]
2182 fn test_md060_column_align_with_aligned_no_space() {
2183 let config = MD060Config {
2185 enabled: true,
2186 style: "aligned-no-space".to_string(),
2187 max_width: LineLength::from_const(0),
2188 column_align: ColumnAlign::Center,
2189 column_align_header: None,
2190 column_align_body: None,
2191 loose_last_column: false,
2192 };
2193 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2194
2195 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |\n| Bob | 25 |";
2196 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2197
2198 let fixed = rule.fix(&ctx).unwrap();
2199 let lines: Vec<&str> = fixed.lines().collect();
2200
2201 assert!(
2203 lines[1].contains("|---"),
2204 "Delimiter should have no spaces in aligned-no-space style. Got: {}",
2205 lines[1]
2206 );
2207 assert!(
2209 lines[3].contains("| Bob |"),
2210 "Content should be centered. Got: {}",
2211 lines[3]
2212 );
2213 }
2214
2215 #[test]
2216 fn test_md060_column_align_config_parsing() {
2217 let toml_str = r#"
2219enabled = true
2220style = "aligned"
2221column-align = "center"
2222"#;
2223 let config: MD060Config = toml::from_str(toml_str).expect("Should parse config");
2224 assert_eq!(config.column_align, ColumnAlign::Center);
2225
2226 let toml_str = r#"
2227enabled = true
2228style = "aligned"
2229column-align = "right"
2230"#;
2231 let config: MD060Config = toml::from_str(toml_str).expect("Should parse config");
2232 assert_eq!(config.column_align, ColumnAlign::Right);
2233
2234 let toml_str = r#"
2235enabled = true
2236style = "aligned"
2237column-align = "left"
2238"#;
2239 let config: MD060Config = toml::from_str(toml_str).expect("Should parse config");
2240 assert_eq!(config.column_align, ColumnAlign::Left);
2241
2242 let toml_str = r#"
2243enabled = true
2244style = "aligned"
2245column-align = "auto"
2246"#;
2247 let config: MD060Config = toml::from_str(toml_str).expect("Should parse config");
2248 assert_eq!(config.column_align, ColumnAlign::Auto);
2249 }
2250
2251 #[test]
2252 fn test_md060_column_align_default_is_auto() {
2253 let toml_str = r#"
2255enabled = true
2256style = "aligned"
2257"#;
2258 let config: MD060Config = toml::from_str(toml_str).expect("Should parse config");
2259 assert_eq!(config.column_align, ColumnAlign::Auto);
2260 }
2261
2262 #[test]
2263 fn test_md060_column_align_reformats_already_aligned_table() {
2264 let config = MD060Config {
2266 enabled: true,
2267 style: "aligned".to_string(),
2268 max_width: LineLength::from_const(0),
2269 column_align: ColumnAlign::Right,
2270 column_align_header: None,
2271 column_align_body: None,
2272 loose_last_column: false,
2273 };
2274 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2275
2276 let content = "| Name | Age |\n| ----- | --- |\n| Alice | 30 |\n| Bob | 25 |";
2278 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2279
2280 let fixed = rule.fix(&ctx).unwrap();
2281 let lines: Vec<&str> = fixed.lines().collect();
2282
2283 assert!(
2285 lines[2].contains("| Alice |") && lines[2].contains("| 30 |"),
2286 "Already aligned table should be reformatted with right alignment. Got: {}",
2287 lines[2]
2288 );
2289 assert!(
2290 lines[3].contains("| Bob |") || lines[3].contains("| Bob |"),
2291 "Bob should be right-aligned. Got: {}",
2292 lines[3]
2293 );
2294 }
2295
2296 #[test]
2297 fn test_md060_column_align_with_cjk_characters() {
2298 let config = MD060Config {
2300 enabled: true,
2301 style: "aligned".to_string(),
2302 max_width: LineLength::from_const(0),
2303 column_align: ColumnAlign::Center,
2304 column_align_header: None,
2305 column_align_body: None,
2306 loose_last_column: false,
2307 };
2308 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2309
2310 let content = "| Name | City |\n|---|---|\n| Alice | ζ±δΊ¬ |\n| Bob | LA |";
2311 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2312
2313 let fixed = rule.fix(&ctx).unwrap();
2314
2315 assert!(fixed.contains("Bob"), "Table should contain Bob");
2318 assert!(fixed.contains("ζ±δΊ¬"), "Table should contain ζ±δΊ¬");
2319 }
2320
2321 #[test]
2322 fn test_md060_column_align_ignored_for_compact_style() {
2323 let config = MD060Config {
2325 enabled: true,
2326 style: "compact".to_string(),
2327 max_width: LineLength::from_const(0),
2328 column_align: ColumnAlign::Right, column_align_header: None,
2330 column_align_body: None,
2331 loose_last_column: false,
2332 };
2333 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2334
2335 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |\n| Bob | 25 |";
2336 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2337
2338 let fixed = rule.fix(&ctx).unwrap();
2339
2340 assert!(
2342 fixed.contains("| Alice |"),
2343 "Compact style should have single space padding, not alignment. Got: {fixed}"
2344 );
2345 }
2346
2347 #[test]
2348 fn test_md060_column_align_ignored_for_tight_style() {
2349 let config = MD060Config {
2351 enabled: true,
2352 style: "tight".to_string(),
2353 max_width: LineLength::from_const(0),
2354 column_align: ColumnAlign::Center, column_align_header: None,
2356 column_align_body: None,
2357 loose_last_column: false,
2358 };
2359 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2360
2361 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |\n| Bob | 25 |";
2362 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2363
2364 let fixed = rule.fix(&ctx).unwrap();
2365
2366 assert!(
2368 fixed.contains("|Alice|"),
2369 "Tight style should have no spaces. Got: {fixed}"
2370 );
2371 }
2372
2373 #[test]
2374 fn test_md060_column_align_with_empty_cells() {
2375 let config = MD060Config {
2377 enabled: true,
2378 style: "aligned".to_string(),
2379 max_width: LineLength::from_const(0),
2380 column_align: ColumnAlign::Center,
2381 column_align_header: None,
2382 column_align_body: None,
2383 loose_last_column: false,
2384 };
2385 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2386
2387 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |\n| | 25 |";
2388 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2389
2390 let fixed = rule.fix(&ctx).unwrap();
2391 let lines: Vec<&str> = fixed.lines().collect();
2392
2393 assert!(
2395 lines[3].contains("| |") || lines[3].contains("| |"),
2396 "Empty cell should be padded correctly. Got: {}",
2397 lines[3]
2398 );
2399 }
2400
2401 #[test]
2402 fn test_md060_column_align_auto_preserves_already_aligned() {
2403 let config = MD060Config {
2405 enabled: true,
2406 style: "aligned".to_string(),
2407 max_width: LineLength::from_const(0),
2408 column_align: ColumnAlign::Auto,
2409 column_align_header: None,
2410 column_align_body: None,
2411 loose_last_column: false,
2412 };
2413 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2414
2415 let content = "| Name | Age |\n| ----- | --- |\n| Alice | 30 |\n| Bob | 25 |";
2417 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2418
2419 let fixed = rule.fix(&ctx).unwrap();
2420
2421 assert_eq!(
2423 fixed, content,
2424 "Already aligned table should be preserved with column-align=auto"
2425 );
2426 }
2427}