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 !ctx.likely_has_tables()
855 }
856
857 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
858 let line_index = &ctx.line_index;
859 let mut warnings = Vec::new();
860
861 let lines = ctx.raw_lines();
862 let table_blocks = &ctx.table_blocks;
863
864 for table_block in table_blocks {
865 let format_result = self.fix_table_block(lines, table_block, ctx.flavor);
866
867 let table_line_indices: Vec<usize> = std::iter::once(table_block.header_line)
868 .chain(std::iter::once(table_block.delimiter_line))
869 .chain(table_block.content_lines.iter().copied())
870 .collect();
871
872 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());
879 for (i, &line_idx) in table_line_indices.iter().enumerate() {
880 let fixed_line = &format_result.lines[i];
881 if line_idx < lines.len() - 1 {
883 fixed_table_lines.push(format!("{fixed_line}\n"));
884 } else {
885 fixed_table_lines.push(fixed_line.clone());
886 }
887 }
888 let table_replacement = fixed_table_lines.concat();
889 let table_range = line_index.multi_line_range(table_start_line, table_end_line);
890
891 for (i, &line_idx) in table_line_indices.iter().enumerate() {
892 let original = lines[line_idx];
893 let fixed = &format_result.lines[i];
894
895 if original != fixed {
896 let (start_line, start_col, end_line, end_col) = calculate_line_range(line_idx + 1, original);
897
898 let message = if format_result.auto_compacted {
899 if let Some(width) = format_result.aligned_width {
900 format!(
901 "Table too wide for aligned formatting ({} chars > max-width: {})",
902 width,
903 self.effective_max_width()
904 )
905 } else {
906 "Table too wide for aligned formatting".to_string()
907 }
908 } else {
909 "Table columns should be aligned".to_string()
910 };
911
912 warnings.push(LintWarning {
915 rule_name: Some(self.name().to_string()),
916 severity: Severity::Warning,
917 message,
918 line: start_line,
919 column: start_col,
920 end_line,
921 end_column: end_col,
922 fix: Some(crate::rule::Fix {
923 range: table_range.clone(),
924 replacement: table_replacement.clone(),
925 }),
926 });
927 }
928 }
929 }
930
931 Ok(warnings)
932 }
933
934 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
935 let content = ctx.content;
936 let lines = ctx.raw_lines();
937 let table_blocks = &ctx.table_blocks;
938
939 let mut result_lines: Vec<String> = lines.iter().map(|&s| s.to_string()).collect();
940
941 for table_block in table_blocks {
942 let format_result = self.fix_table_block(lines, table_block, ctx.flavor);
943
944 let table_line_indices: Vec<usize> = std::iter::once(table_block.header_line)
945 .chain(std::iter::once(table_block.delimiter_line))
946 .chain(table_block.content_lines.iter().copied())
947 .collect();
948
949 for (i, &line_idx) in table_line_indices.iter().enumerate() {
950 result_lines[line_idx] = format_result.lines[i].clone();
951 }
952 }
953
954 let mut fixed = result_lines.join("\n");
955 if content.ends_with('\n') && !fixed.ends_with('\n') {
956 fixed.push('\n');
957 }
958 Ok(fixed)
959 }
960
961 fn as_any(&self) -> &dyn std::any::Any {
962 self
963 }
964
965 fn default_config_section(&self) -> Option<(String, toml::Value)> {
966 let mut table = toml::map::Map::new();
969 table.insert("enabled".to_string(), toml::Value::Boolean(self.config.enabled));
970 table.insert("style".to_string(), toml::Value::String(self.config.style.clone()));
971 table.insert(
972 "max-width".to_string(),
973 toml::Value::Integer(self.config.max_width.get() as i64),
974 );
975 table.insert(
976 "column-align".to_string(),
977 toml::Value::String(
978 match self.config.column_align {
979 ColumnAlign::Auto => "auto",
980 ColumnAlign::Left => "left",
981 ColumnAlign::Center => "center",
982 ColumnAlign::Right => "right",
983 }
984 .to_string(),
985 ),
986 );
987 table.insert(
989 "column-align-header".to_string(),
990 toml::Value::String("auto".to_string()),
991 );
992 table.insert("column-align-body".to_string(), toml::Value::String("auto".to_string()));
993 table.insert(
994 "loose-last-column".to_string(),
995 toml::Value::Boolean(self.config.loose_last_column),
996 );
997
998 Some((self.name().to_string(), toml::Value::Table(table)))
999 }
1000
1001 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
1002 where
1003 Self: Sized,
1004 {
1005 let rule_config = crate::rule_config_serde::load_rule_config::<MD060Config>(config);
1006 let md013_config = crate::rule_config_serde::load_rule_config::<MD013Config>(config);
1007
1008 let md013_disabled = config.global.disable.iter().any(|r| r == "MD013");
1010
1011 Box::new(Self::from_config_struct(rule_config, md013_config, md013_disabled))
1012 }
1013}
1014
1015#[cfg(test)]
1016mod tests {
1017 use super::*;
1018 use crate::lint_context::LintContext;
1019 use crate::types::LineLength;
1020
1021 fn md013_with_line_length(line_length: usize) -> MD013Config {
1023 MD013Config {
1024 line_length: LineLength::from_const(line_length),
1025 tables: true, ..Default::default()
1027 }
1028 }
1029
1030 #[test]
1031 fn test_md060_align_simple_ascii_table() {
1032 let rule = MD060TableFormat::new(true, "aligned".to_string());
1033
1034 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
1035 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1036
1037 let fixed = rule.fix(&ctx).unwrap();
1038 let expected = "| Name | Age |\n| ----- | --- |\n| Alice | 30 |";
1039 assert_eq!(fixed, expected);
1040
1041 let lines: Vec<&str> = fixed.lines().collect();
1043 assert_eq!(lines[0].len(), lines[1].len());
1044 assert_eq!(lines[1].len(), lines[2].len());
1045 }
1046
1047 #[test]
1048 fn test_md060_cjk_characters_aligned_correctly() {
1049 let rule = MD060TableFormat::new(true, "aligned".to_string());
1050
1051 let content = "| Name | Age |\n|---|---|\n| δΈζ | 30 |";
1052 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1053
1054 let fixed = rule.fix(&ctx).unwrap();
1055
1056 let lines: Vec<&str> = fixed.lines().collect();
1057 let cells_line1 = MD060TableFormat::parse_table_row(lines[0]);
1058 let cells_line3 = MD060TableFormat::parse_table_row(lines[2]);
1059
1060 let width1 = MD060TableFormat::calculate_cell_display_width(&cells_line1[0]);
1061 let width3 = MD060TableFormat::calculate_cell_display_width(&cells_line3[0]);
1062
1063 assert_eq!(width1, width3);
1064 }
1065
1066 #[test]
1067 fn test_md060_basic_emoji() {
1068 let rule = MD060TableFormat::new(true, "aligned".to_string());
1069
1070 let content = "| Status | Name |\n|---|---|\n| β
| Test |";
1071 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1072
1073 let fixed = rule.fix(&ctx).unwrap();
1074 assert!(fixed.contains("Status"));
1075 }
1076
1077 #[test]
1078 fn test_md060_zwj_emoji_skipped() {
1079 let rule = MD060TableFormat::new(true, "aligned".to_string());
1080
1081 let content = "| Emoji | Name |\n|---|---|\n| π¨βπ©βπ§βπ¦ | Family |";
1082 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1083
1084 let fixed = rule.fix(&ctx).unwrap();
1085 assert_eq!(fixed, content);
1086 }
1087
1088 #[test]
1089 fn test_md060_inline_code_with_escaped_pipes() {
1090 let rule = MD060TableFormat::new(true, "aligned".to_string());
1093
1094 let content = "| Pattern | Regex |\n|---|---|\n| Time | `[0-9]\\|[0-9]` |";
1096 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1097
1098 let fixed = rule.fix(&ctx).unwrap();
1099 assert!(fixed.contains(r"`[0-9]\|[0-9]`"), "Escaped pipes should be preserved");
1100 }
1101
1102 #[test]
1103 fn test_md060_compact_style() {
1104 let rule = MD060TableFormat::new(true, "compact".to_string());
1105
1106 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
1107 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1108
1109 let fixed = rule.fix(&ctx).unwrap();
1110 let expected = "| Name | Age |\n| --- | --- |\n| Alice | 30 |";
1111 assert_eq!(fixed, expected);
1112 }
1113
1114 #[test]
1115 fn test_md060_tight_style() {
1116 let rule = MD060TableFormat::new(true, "tight".to_string());
1117
1118 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
1119 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1120
1121 let fixed = rule.fix(&ctx).unwrap();
1122 let expected = "|Name|Age|\n|---|---|\n|Alice|30|";
1123 assert_eq!(fixed, expected);
1124 }
1125
1126 #[test]
1127 fn test_md060_aligned_no_space_style() {
1128 let rule = MD060TableFormat::new(true, "aligned-no-space".to_string());
1130
1131 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
1132 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1133
1134 let fixed = rule.fix(&ctx).unwrap();
1135
1136 let lines: Vec<&str> = fixed.lines().collect();
1138 assert_eq!(lines[0], "| Name | Age |", "Header should have spaces around content");
1139 assert_eq!(
1140 lines[1], "|-------|-----|",
1141 "Delimiter should have NO spaces around dashes"
1142 );
1143 assert_eq!(lines[2], "| Alice | 30 |", "Content should have spaces around content");
1144
1145 assert_eq!(lines[0].len(), lines[1].len());
1147 assert_eq!(lines[1].len(), lines[2].len());
1148 }
1149
1150 #[test]
1151 fn test_md060_aligned_no_space_preserves_alignment_indicators() {
1152 let rule = MD060TableFormat::new(true, "aligned-no-space".to_string());
1154
1155 let content = "| Left | Center | Right |\n|:---|:---:|---:|\n| A | B | C |";
1156 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1157
1158 let fixed = rule.fix(&ctx).unwrap();
1159 let lines: Vec<&str> = fixed.lines().collect();
1160
1161 assert!(
1163 fixed.contains("|:"),
1164 "Should have left alignment indicator adjacent to pipe"
1165 );
1166 assert!(
1167 fixed.contains(":|"),
1168 "Should have right alignment indicator adjacent to pipe"
1169 );
1170 assert!(
1172 lines[1].contains(":---") && lines[1].contains("---:"),
1173 "Should have center alignment colons"
1174 );
1175 }
1176
1177 #[test]
1178 fn test_md060_aligned_no_space_three_column_table() {
1179 let rule = MD060TableFormat::new(true, "aligned-no-space".to_string());
1181
1182 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 |";
1183 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1184
1185 let fixed = rule.fix(&ctx).unwrap();
1186 let lines: Vec<&str> = fixed.lines().collect();
1187
1188 assert!(lines[1].starts_with("|---"), "Delimiter should start with |---");
1190 assert!(lines[1].ends_with("---|"), "Delimiter should end with ---|");
1191 assert!(!lines[1].contains("| -"), "Delimiter should NOT have space after pipe");
1192 assert!(!lines[1].contains("- |"), "Delimiter should NOT have space before pipe");
1193 }
1194
1195 #[test]
1196 fn test_md060_aligned_no_space_auto_compacts_wide_tables() {
1197 let config = MD060Config {
1199 enabled: true,
1200 style: "aligned-no-space".to_string(),
1201 max_width: LineLength::from_const(50),
1202 column_align: ColumnAlign::Auto,
1203 column_align_header: None,
1204 column_align_body: None,
1205 loose_last_column: false,
1206 };
1207 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false);
1208
1209 let content = "| Very Long Column Header A | Very Long Column Header B | Very Long Column Header C |\n|---|---|---|\n| x | y | z |";
1211 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1212
1213 let fixed = rule.fix(&ctx).unwrap();
1214
1215 assert!(
1217 fixed.contains("| --- |"),
1218 "Should be compact format when exceeding max-width"
1219 );
1220 }
1221
1222 #[test]
1223 fn test_md060_aligned_no_space_cjk_characters() {
1224 let rule = MD060TableFormat::new(true, "aligned-no-space".to_string());
1226
1227 let content = "| Name | City |\n|---|---|\n| δΈζ | ζ±δΊ¬ |";
1228 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1229
1230 let fixed = rule.fix(&ctx).unwrap();
1231 let lines: Vec<&str> = fixed.lines().collect();
1232
1233 use unicode_width::UnicodeWidthStr;
1236 assert_eq!(
1237 lines[0].width(),
1238 lines[1].width(),
1239 "Header and delimiter should have same display width"
1240 );
1241 assert_eq!(
1242 lines[1].width(),
1243 lines[2].width(),
1244 "Delimiter and content should have same display width"
1245 );
1246
1247 assert!(!lines[1].contains("| -"), "Delimiter should NOT have space after pipe");
1249 }
1250
1251 #[test]
1252 fn test_md060_aligned_no_space_minimum_width() {
1253 let rule = MD060TableFormat::new(true, "aligned-no-space".to_string());
1255
1256 let content = "| A | B |\n|-|-|\n| 1 | 2 |";
1257 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1258
1259 let fixed = rule.fix(&ctx).unwrap();
1260 let lines: Vec<&str> = fixed.lines().collect();
1261
1262 assert!(lines[1].contains("---"), "Should have minimum 3 dashes");
1264 assert_eq!(lines[0].len(), lines[1].len());
1266 assert_eq!(lines[1].len(), lines[2].len());
1267 }
1268
1269 #[test]
1270 fn test_md060_any_style_consistency() {
1271 let rule = MD060TableFormat::new(true, "any".to_string());
1272
1273 let content = "| Name | Age |\n| --- | --- |\n| Alice | 30 |";
1275 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1276
1277 let fixed = rule.fix(&ctx).unwrap();
1278 assert_eq!(fixed, content);
1279
1280 let content_aligned = "| Name | Age |\n| ----- | --- |\n| Alice | 30 |";
1282 let ctx_aligned = LintContext::new(content_aligned, crate::config::MarkdownFlavor::Standard, None);
1283
1284 let fixed_aligned = rule.fix(&ctx_aligned).unwrap();
1285 assert_eq!(fixed_aligned, content_aligned);
1286 }
1287
1288 #[test]
1289 fn test_md060_empty_cells() {
1290 let rule = MD060TableFormat::new(true, "aligned".to_string());
1291
1292 let content = "| A | B |\n|---|---|\n| | X |";
1293 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1294
1295 let fixed = rule.fix(&ctx).unwrap();
1296 assert!(fixed.contains("|"));
1297 }
1298
1299 #[test]
1300 fn test_md060_mixed_content() {
1301 let rule = MD060TableFormat::new(true, "aligned".to_string());
1302
1303 let content = "| Name | Age | City |\n|---|---|---|\n| δΈζ | 30 | NYC |";
1304 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1305
1306 let fixed = rule.fix(&ctx).unwrap();
1307 assert!(fixed.contains("δΈζ"));
1308 assert!(fixed.contains("NYC"));
1309 }
1310
1311 #[test]
1312 fn test_md060_preserve_alignment_indicators() {
1313 let rule = MD060TableFormat::new(true, "aligned".to_string());
1314
1315 let content = "| Left | Center | Right |\n|:---|:---:|---:|\n| A | B | C |";
1316 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1317
1318 let fixed = rule.fix(&ctx).unwrap();
1319
1320 assert!(fixed.contains(":---"), "Should contain left alignment");
1321 assert!(fixed.contains(":----:"), "Should contain center alignment");
1322 assert!(fixed.contains("----:"), "Should contain right alignment");
1323 }
1324
1325 #[test]
1326 fn test_md060_minimum_column_width() {
1327 let rule = MD060TableFormat::new(true, "aligned".to_string());
1328
1329 let content = "| ID | Name |\n|-|-|\n| 1 | A |";
1332 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1333
1334 let fixed = rule.fix(&ctx).unwrap();
1335
1336 let lines: Vec<&str> = fixed.lines().collect();
1337 assert_eq!(lines[0].len(), lines[1].len());
1338 assert_eq!(lines[1].len(), lines[2].len());
1339
1340 assert!(fixed.contains("ID "), "Short content should be padded");
1342 assert!(fixed.contains("---"), "Delimiter should have at least 3 dashes");
1343 }
1344
1345 #[test]
1346 fn test_md060_auto_compact_exceeds_default_threshold() {
1347 let config = MD060Config {
1349 enabled: true,
1350 style: "aligned".to_string(),
1351 max_width: LineLength::from_const(0),
1352 column_align: ColumnAlign::Auto,
1353 column_align_header: None,
1354 column_align_body: None,
1355 loose_last_column: false,
1356 };
1357 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false);
1358
1359 let content = "| Very Long Column Header | Another Long Header | Third Very Long Header Column |\n|---|---|---|\n| Short | Data | Here |";
1363 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1364
1365 let fixed = rule.fix(&ctx).unwrap();
1366
1367 assert!(fixed.contains("| Very Long Column Header | Another Long Header | Third Very Long Header Column |"));
1369 assert!(fixed.contains("| --- | --- | --- |"));
1370 assert!(fixed.contains("| Short | Data | Here |"));
1371
1372 let lines: Vec<&str> = fixed.lines().collect();
1374 assert!(lines[0].len() != lines[1].len() || lines[1].len() != lines[2].len());
1376 }
1377
1378 #[test]
1379 fn test_md060_auto_compact_exceeds_explicit_threshold() {
1380 let config = MD060Config {
1382 enabled: true,
1383 style: "aligned".to_string(),
1384 max_width: LineLength::from_const(50),
1385 column_align: ColumnAlign::Auto,
1386 column_align_header: None,
1387 column_align_body: None,
1388 loose_last_column: false,
1389 };
1390 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 |";
1396 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1397
1398 let fixed = rule.fix(&ctx).unwrap();
1399
1400 assert!(
1402 fixed.contains("| Very Long Column Header A | Very Long Column Header B | Very Long Column Header C |")
1403 );
1404 assert!(fixed.contains("| --- | --- | --- |"));
1405 assert!(fixed.contains("| Data | Data | Data |"));
1406
1407 let lines: Vec<&str> = fixed.lines().collect();
1409 assert!(lines[0].len() != lines[2].len());
1410 }
1411
1412 #[test]
1413 fn test_md060_stays_aligned_under_threshold() {
1414 let config = MD060Config {
1416 enabled: true,
1417 style: "aligned".to_string(),
1418 max_width: LineLength::from_const(100),
1419 column_align: ColumnAlign::Auto,
1420 column_align_header: None,
1421 column_align_body: None,
1422 loose_last_column: false,
1423 };
1424 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false);
1425
1426 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
1428 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1429
1430 let fixed = rule.fix(&ctx).unwrap();
1431
1432 let expected = "| Name | Age |\n| ----- | --- |\n| Alice | 30 |";
1434 assert_eq!(fixed, expected);
1435
1436 let lines: Vec<&str> = fixed.lines().collect();
1437 assert_eq!(lines[0].len(), lines[1].len());
1438 assert_eq!(lines[1].len(), lines[2].len());
1439 }
1440
1441 #[test]
1442 fn test_md060_width_calculation_formula() {
1443 let config = MD060Config {
1445 enabled: true,
1446 style: "aligned".to_string(),
1447 max_width: LineLength::from_const(0),
1448 column_align: ColumnAlign::Auto,
1449 column_align_header: None,
1450 column_align_body: None,
1451 loose_last_column: false,
1452 };
1453 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(30), false);
1454
1455 let content = "| AAAAA | BBBBB | CCCCC |\n|---|---|---|\n| AAAAA | BBBBB | CCCCC |";
1459 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1460
1461 let fixed = rule.fix(&ctx).unwrap();
1462
1463 let lines: Vec<&str> = fixed.lines().collect();
1465 assert_eq!(lines[0].len(), lines[1].len());
1466 assert_eq!(lines[1].len(), lines[2].len());
1467 assert_eq!(lines[0].len(), 25); let config_tight = MD060Config {
1471 enabled: true,
1472 style: "aligned".to_string(),
1473 max_width: LineLength::from_const(24),
1474 column_align: ColumnAlign::Auto,
1475 column_align_header: None,
1476 column_align_body: None,
1477 loose_last_column: false,
1478 };
1479 let rule_tight = MD060TableFormat::from_config_struct(config_tight, md013_with_line_length(80), false);
1480
1481 let fixed_compact = rule_tight.fix(&ctx).unwrap();
1482
1483 assert!(fixed_compact.contains("| AAAAA | BBBBB | CCCCC |"));
1485 assert!(fixed_compact.contains("| --- | --- | --- |"));
1486 }
1487
1488 #[test]
1489 fn test_md060_very_wide_table_auto_compacts() {
1490 let config = MD060Config {
1491 enabled: true,
1492 style: "aligned".to_string(),
1493 max_width: LineLength::from_const(0),
1494 column_align: ColumnAlign::Auto,
1495 column_align_header: None,
1496 column_align_body: None,
1497 loose_last_column: false,
1498 };
1499 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false);
1500
1501 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 |";
1505 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1506
1507 let fixed = rule.fix(&ctx).unwrap();
1508
1509 assert!(fixed.contains("| Column One A | Column Two B | Column Three | Column Four D | Column Five E | Column Six FG | Column Seven | Column Eight |"));
1511 assert!(fixed.contains("| --- | --- | --- | --- | --- | --- | --- | --- |"));
1512 }
1513
1514 #[test]
1515 fn test_md060_inherit_from_md013_line_length() {
1516 let config = MD060Config {
1518 enabled: true,
1519 style: "aligned".to_string(),
1520 max_width: LineLength::from_const(0), column_align: ColumnAlign::Auto,
1522 column_align_header: None,
1523 column_align_body: None,
1524 loose_last_column: false,
1525 };
1526
1527 let rule_80 = MD060TableFormat::from_config_struct(config.clone(), md013_with_line_length(80), false);
1529 let rule_120 = MD060TableFormat::from_config_struct(config.clone(), md013_with_line_length(120), false);
1530
1531 let content = "| Column Header A | Column Header B | Column Header C |\n|---|---|---|\n| Some Data | More Data | Even More |";
1533 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1534
1535 let _fixed_80 = rule_80.fix(&ctx).unwrap();
1537
1538 let fixed_120 = rule_120.fix(&ctx).unwrap();
1540
1541 let lines_120: Vec<&str> = fixed_120.lines().collect();
1543 assert_eq!(lines_120[0].len(), lines_120[1].len());
1544 assert_eq!(lines_120[1].len(), lines_120[2].len());
1545 }
1546
1547 #[test]
1548 fn test_md060_edge_case_exactly_at_threshold() {
1549 let config = MD060Config {
1553 enabled: true,
1554 style: "aligned".to_string(),
1555 max_width: LineLength::from_const(17),
1556 column_align: ColumnAlign::Auto,
1557 column_align_header: None,
1558 column_align_body: None,
1559 loose_last_column: false,
1560 };
1561 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false);
1562
1563 let content = "| AAAAA | BBBBB |\n|---|---|\n| AAAAA | BBBBB |";
1564 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1565
1566 let fixed = rule.fix(&ctx).unwrap();
1567
1568 let lines: Vec<&str> = fixed.lines().collect();
1570 assert_eq!(lines[0].len(), 17);
1571 assert_eq!(lines[0].len(), lines[1].len());
1572 assert_eq!(lines[1].len(), lines[2].len());
1573
1574 let config_under = MD060Config {
1576 enabled: true,
1577 style: "aligned".to_string(),
1578 max_width: LineLength::from_const(16),
1579 column_align: ColumnAlign::Auto,
1580 column_align_header: None,
1581 column_align_body: None,
1582 loose_last_column: false,
1583 };
1584 let rule_under = MD060TableFormat::from_config_struct(config_under, md013_with_line_length(80), false);
1585
1586 let fixed_compact = rule_under.fix(&ctx).unwrap();
1587
1588 assert!(fixed_compact.contains("| AAAAA | BBBBB |"));
1590 assert!(fixed_compact.contains("| --- | --- |"));
1591 }
1592
1593 #[test]
1594 fn test_md060_auto_compact_warning_message() {
1595 let config = MD060Config {
1597 enabled: true,
1598 style: "aligned".to_string(),
1599 max_width: LineLength::from_const(50),
1600 column_align: ColumnAlign::Auto,
1601 column_align_header: None,
1602 column_align_body: None,
1603 loose_last_column: false,
1604 };
1605 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false);
1606
1607 let content = "| Very Long Column Header A | Very Long Column Header B | Very Long Column Header C |\n|---|---|---|\n| Data | Data | Data |";
1609 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1610
1611 let warnings = rule.check(&ctx).unwrap();
1612
1613 assert!(!warnings.is_empty(), "Should generate warnings");
1615
1616 let auto_compact_warnings: Vec<_> = warnings
1617 .iter()
1618 .filter(|w| w.message.contains("too wide for aligned formatting"))
1619 .collect();
1620
1621 assert!(!auto_compact_warnings.is_empty(), "Should have auto-compact warning");
1622
1623 let first_warning = auto_compact_warnings[0];
1625 assert!(first_warning.message.contains("85 chars > max-width: 50"));
1626 assert!(first_warning.message.contains("Table too wide for aligned formatting"));
1627 }
1628
1629 #[test]
1630 fn test_md060_issue_129_detect_style_from_all_rows() {
1631 let rule = MD060TableFormat::new(true, "any".to_string());
1635
1636 let content = "| a long heading | another long heading |\n\
1638 | -------------- | -------------------- |\n\
1639 | a | 1 |\n\
1640 | b b | 2 |\n\
1641 | c c c | 3 |";
1642 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1643
1644 let fixed = rule.fix(&ctx).unwrap();
1645
1646 assert!(
1648 fixed.contains("| a | 1 |"),
1649 "Should preserve aligned padding in first content row"
1650 );
1651 assert!(
1652 fixed.contains("| b b | 2 |"),
1653 "Should preserve aligned padding in second content row"
1654 );
1655 assert!(
1656 fixed.contains("| c c c | 3 |"),
1657 "Should preserve aligned padding in third content row"
1658 );
1659
1660 assert_eq!(fixed, content, "Table should be detected as aligned and preserved");
1662 }
1663
1664 #[test]
1665 fn test_md060_regular_alignment_warning_message() {
1666 let config = MD060Config {
1668 enabled: true,
1669 style: "aligned".to_string(),
1670 max_width: LineLength::from_const(100), column_align: ColumnAlign::Auto,
1672 column_align_header: None,
1673 column_align_body: None,
1674 loose_last_column: false,
1675 };
1676 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false);
1677
1678 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
1680 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1681
1682 let warnings = rule.check(&ctx).unwrap();
1683
1684 assert!(!warnings.is_empty(), "Should generate warnings");
1686
1687 assert!(warnings[0].message.contains("Table columns should be aligned"));
1689 assert!(!warnings[0].message.contains("too wide"));
1690 assert!(!warnings[0].message.contains("max-width"));
1691 }
1692
1693 #[test]
1696 fn test_md060_unlimited_when_md013_disabled() {
1697 let config = MD060Config {
1699 enabled: true,
1700 style: "aligned".to_string(),
1701 max_width: LineLength::from_const(0), column_align: ColumnAlign::Auto,
1703 column_align_header: None,
1704 column_align_body: None,
1705 loose_last_column: false,
1706 };
1707 let md013_config = MD013Config::default();
1708 let rule = MD060TableFormat::from_config_struct(config, md013_config, true );
1709
1710 let content = "| Very Long Column Header A | Very Long Column Header B | Very Long Column Header C |\n|---|---|---|\n| data | data | data |";
1712 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1713 let fixed = rule.fix(&ctx).unwrap();
1714
1715 let lines: Vec<&str> = fixed.lines().collect();
1717 assert_eq!(
1719 lines[0].len(),
1720 lines[1].len(),
1721 "Table should be aligned when MD013 is disabled"
1722 );
1723 }
1724
1725 #[test]
1726 fn test_md060_unlimited_when_md013_tables_false() {
1727 let config = MD060Config {
1729 enabled: true,
1730 style: "aligned".to_string(),
1731 max_width: LineLength::from_const(0),
1732 column_align: ColumnAlign::Auto,
1733 column_align_header: None,
1734 column_align_body: None,
1735 loose_last_column: false,
1736 };
1737 let md013_config = MD013Config {
1738 tables: false, line_length: LineLength::from_const(80),
1740 ..Default::default()
1741 };
1742 let rule = MD060TableFormat::from_config_struct(config, md013_config, false);
1743
1744 let content = "| Very Long Header A | Very Long Header B | Very Long Header C |\n|---|---|---|\n| x | y | z |";
1746 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1747 let fixed = rule.fix(&ctx).unwrap();
1748
1749 let lines: Vec<&str> = fixed.lines().collect();
1751 assert_eq!(
1752 lines[0].len(),
1753 lines[1].len(),
1754 "Table should be aligned when MD013.tables=false"
1755 );
1756 }
1757
1758 #[test]
1759 fn test_md060_unlimited_when_md013_line_length_zero() {
1760 let config = MD060Config {
1762 enabled: true,
1763 style: "aligned".to_string(),
1764 max_width: LineLength::from_const(0),
1765 column_align: ColumnAlign::Auto,
1766 column_align_header: None,
1767 column_align_body: None,
1768 loose_last_column: false,
1769 };
1770 let md013_config = MD013Config {
1771 tables: true,
1772 line_length: LineLength::from_const(0), ..Default::default()
1774 };
1775 let rule = MD060TableFormat::from_config_struct(config, md013_config, false);
1776
1777 let content = "| Very Long Header | Another Long Header | Third Long Header |\n|---|---|---|\n| x | y | z |";
1779 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1780 let fixed = rule.fix(&ctx).unwrap();
1781
1782 let lines: Vec<&str> = fixed.lines().collect();
1784 assert_eq!(
1785 lines[0].len(),
1786 lines[1].len(),
1787 "Table should be aligned when MD013.line_length=0"
1788 );
1789 }
1790
1791 #[test]
1792 fn test_md060_explicit_max_width_overrides_md013_settings() {
1793 let config = MD060Config {
1795 enabled: true,
1796 style: "aligned".to_string(),
1797 max_width: LineLength::from_const(50), column_align: ColumnAlign::Auto,
1799 column_align_header: None,
1800 column_align_body: None,
1801 loose_last_column: false,
1802 };
1803 let md013_config = MD013Config {
1804 tables: false, line_length: LineLength::from_const(0), ..Default::default()
1807 };
1808 let rule = MD060TableFormat::from_config_struct(config, md013_config, false);
1809
1810 let content = "| Very Long Column Header A | Very Long Column Header B | Very Long Column Header C |\n|---|---|---|\n| x | y | z |";
1812 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1813 let fixed = rule.fix(&ctx).unwrap();
1814
1815 assert!(
1817 fixed.contains("| --- |"),
1818 "Should be compact format due to explicit max_width"
1819 );
1820 }
1821
1822 #[test]
1823 fn test_md060_inherits_md013_line_length_when_tables_enabled() {
1824 let config = MD060Config {
1826 enabled: true,
1827 style: "aligned".to_string(),
1828 max_width: LineLength::from_const(0), column_align: ColumnAlign::Auto,
1830 column_align_header: None,
1831 column_align_body: None,
1832 loose_last_column: false,
1833 };
1834 let md013_config = MD013Config {
1835 tables: true,
1836 line_length: LineLength::from_const(50), ..Default::default()
1838 };
1839 let rule = MD060TableFormat::from_config_struct(config, md013_config, false);
1840
1841 let content = "| Very Long Column Header A | Very Long Column Header B | Very Long Column Header C |\n|---|---|---|\n| x | y | z |";
1843 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1844 let fixed = rule.fix(&ctx).unwrap();
1845
1846 assert!(
1848 fixed.contains("| --- |"),
1849 "Should be compact format when inheriting MD013 limit"
1850 );
1851 }
1852
1853 #[test]
1856 fn test_aligned_no_space_reformats_spaced_delimiter() {
1857 let config = MD060Config {
1860 enabled: true,
1861 style: "aligned-no-space".to_string(),
1862 max_width: LineLength::from_const(0),
1863 column_align: ColumnAlign::Auto,
1864 column_align_header: None,
1865 column_align_body: None,
1866 loose_last_column: false,
1867 };
1868 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
1869
1870 let content = "| Header 1 | Header 2 |\n| -------- | -------- |\n| Cell 1 | Cell 2 |";
1872 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1873 let fixed = rule.fix(&ctx).unwrap();
1874
1875 assert!(
1878 !fixed.contains("| ----"),
1879 "Delimiter should NOT have spaces after pipe. Got:\n{fixed}"
1880 );
1881 assert!(
1882 !fixed.contains("---- |"),
1883 "Delimiter should NOT have spaces before pipe. Got:\n{fixed}"
1884 );
1885 assert!(
1887 fixed.contains("|----"),
1888 "Delimiter should have dashes touching the leading pipe. Got:\n{fixed}"
1889 );
1890 }
1891
1892 #[test]
1893 fn test_aligned_reformats_compact_delimiter() {
1894 let config = MD060Config {
1897 enabled: true,
1898 style: "aligned".to_string(),
1899 max_width: LineLength::from_const(0),
1900 column_align: ColumnAlign::Auto,
1901 column_align_header: None,
1902 column_align_body: None,
1903 loose_last_column: false,
1904 };
1905 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
1906
1907 let content = "| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |";
1909 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1910 let fixed = rule.fix(&ctx).unwrap();
1911
1912 assert!(
1914 fixed.contains("| -------- | -------- |") || fixed.contains("| ---------- | ---------- |"),
1915 "Delimiter should have spaces around dashes. Got:\n{fixed}"
1916 );
1917 }
1918
1919 #[test]
1920 fn test_aligned_no_space_preserves_matching_table() {
1921 let config = MD060Config {
1923 enabled: true,
1924 style: "aligned-no-space".to_string(),
1925 max_width: LineLength::from_const(0),
1926 column_align: ColumnAlign::Auto,
1927 column_align_header: None,
1928 column_align_body: None,
1929 loose_last_column: false,
1930 };
1931 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
1932
1933 let content = "| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |";
1935 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1936 let fixed = rule.fix(&ctx).unwrap();
1937
1938 assert_eq!(
1940 fixed, content,
1941 "Table already in aligned-no-space style should be preserved"
1942 );
1943 }
1944
1945 #[test]
1946 fn test_aligned_preserves_matching_table() {
1947 let config = MD060Config {
1949 enabled: true,
1950 style: "aligned".to_string(),
1951 max_width: LineLength::from_const(0),
1952 column_align: ColumnAlign::Auto,
1953 column_align_header: None,
1954 column_align_body: None,
1955 loose_last_column: false,
1956 };
1957 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
1958
1959 let content = "| Header 1 | Header 2 |\n| -------- | -------- |\n| Cell 1 | Cell 2 |";
1961 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1962 let fixed = rule.fix(&ctx).unwrap();
1963
1964 assert_eq!(fixed, content, "Table already in aligned style should be preserved");
1966 }
1967
1968 #[test]
1969 fn test_cjk_table_display_width_consistency() {
1970 let table_lines = vec!["| εε | Age |", "|------|-----|", "| η°δΈ | 25 |"];
1976
1977 let is_aligned =
1979 MD060TableFormat::is_table_already_aligned(&table_lines, crate::config::MarkdownFlavor::Standard, false);
1980 assert!(
1981 !is_aligned,
1982 "Table with uneven raw line lengths should NOT be considered aligned"
1983 );
1984 }
1985
1986 #[test]
1987 fn test_cjk_width_calculation_in_aligned_check() {
1988 let cjk_width = MD060TableFormat::calculate_cell_display_width("εε");
1991 assert_eq!(cjk_width, 4, "Two CJK characters should have display width 4");
1992
1993 let ascii_width = MD060TableFormat::calculate_cell_display_width("Age");
1994 assert_eq!(ascii_width, 3, "Three ASCII characters should have display width 3");
1995
1996 let padded_cjk = MD060TableFormat::calculate_cell_display_width(" εε ");
1998 assert_eq!(padded_cjk, 4, "Padded CJK should have same width after trim");
1999
2000 let mixed = MD060TableFormat::calculate_cell_display_width(" ζ₯ζ¬θͺABC ");
2002 assert_eq!(mixed, 9, "Mixed CJK/ASCII content");
2004 }
2005
2006 #[test]
2009 fn test_md060_column_align_left() {
2010 let config = MD060Config {
2012 enabled: true,
2013 style: "aligned".to_string(),
2014 max_width: LineLength::from_const(0),
2015 column_align: ColumnAlign::Left,
2016 column_align_header: None,
2017 column_align_body: None,
2018 loose_last_column: false,
2019 };
2020 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2021
2022 let content = "| Name | Age | City |\n|---|---|---|\n| Alice | 30 | Seattle |\n| Bob | 25 | Portland |";
2023 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2024
2025 let fixed = rule.fix(&ctx).unwrap();
2026 let lines: Vec<&str> = fixed.lines().collect();
2027
2028 assert!(
2030 lines[2].contains("| Alice "),
2031 "Content should be left-aligned (Alice should have trailing padding)"
2032 );
2033 assert!(
2034 lines[3].contains("| Bob "),
2035 "Content should be left-aligned (Bob should have trailing padding)"
2036 );
2037 }
2038
2039 #[test]
2040 fn test_md060_column_align_center() {
2041 let config = MD060Config {
2043 enabled: true,
2044 style: "aligned".to_string(),
2045 max_width: LineLength::from_const(0),
2046 column_align: ColumnAlign::Center,
2047 column_align_header: None,
2048 column_align_body: None,
2049 loose_last_column: false,
2050 };
2051 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2052
2053 let content = "| Name | Age | City |\n|---|---|---|\n| Alice | 30 | Seattle |\n| Bob | 25 | Portland |";
2054 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2055
2056 let fixed = rule.fix(&ctx).unwrap();
2057 let lines: Vec<&str> = fixed.lines().collect();
2058
2059 assert!(
2062 lines[3].contains("| Bob |"),
2063 "Bob should be centered with padding on both sides. Got: {}",
2064 lines[3]
2065 );
2066 }
2067
2068 #[test]
2069 fn test_md060_column_align_right() {
2070 let config = MD060Config {
2072 enabled: true,
2073 style: "aligned".to_string(),
2074 max_width: LineLength::from_const(0),
2075 column_align: ColumnAlign::Right,
2076 column_align_header: None,
2077 column_align_body: None,
2078 loose_last_column: false,
2079 };
2080 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2081
2082 let content = "| Name | Age | City |\n|---|---|---|\n| Alice | 30 | Seattle |\n| Bob | 25 | Portland |";
2083 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2084
2085 let fixed = rule.fix(&ctx).unwrap();
2086 let lines: Vec<&str> = fixed.lines().collect();
2087
2088 assert!(
2090 lines[3].contains("| Bob |"),
2091 "Bob should be right-aligned with padding on left. Got: {}",
2092 lines[3]
2093 );
2094 }
2095
2096 #[test]
2097 fn test_md060_column_align_auto_respects_delimiter() {
2098 let config = MD060Config {
2100 enabled: true,
2101 style: "aligned".to_string(),
2102 max_width: LineLength::from_const(0),
2103 column_align: ColumnAlign::Auto,
2104 column_align_header: None,
2105 column_align_body: None,
2106 loose_last_column: false,
2107 };
2108 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2109
2110 let content = "| Left | Center | Right |\n|:---|:---:|---:|\n| A | B | C |";
2112 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2113
2114 let fixed = rule.fix(&ctx).unwrap();
2115
2116 assert!(fixed.contains("| A "), "Left column should be left-aligned");
2118 let lines: Vec<&str> = fixed.lines().collect();
2120 assert!(
2124 lines[2].contains(" C |"),
2125 "Right column should be right-aligned. Got: {}",
2126 lines[2]
2127 );
2128 }
2129
2130 #[test]
2131 fn test_md060_column_align_overrides_delimiter_indicators() {
2132 let config = MD060Config {
2134 enabled: true,
2135 style: "aligned".to_string(),
2136 max_width: LineLength::from_const(0),
2137 column_align: ColumnAlign::Right, column_align_header: None,
2139 column_align_body: None,
2140 loose_last_column: false,
2141 };
2142 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2143
2144 let content = "| Left | Center | Right |\n|:---|:---:|---:|\n| A | B | C |";
2146 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2147
2148 let fixed = rule.fix(&ctx).unwrap();
2149 let lines: Vec<&str> = fixed.lines().collect();
2150
2151 assert!(
2154 lines[2].contains(" A |") || lines[2].contains(" A |"),
2155 "Even left-indicated column should be right-aligned. Got: {}",
2156 lines[2]
2157 );
2158 }
2159
2160 #[test]
2161 fn test_md060_column_align_with_aligned_no_space() {
2162 let config = MD060Config {
2164 enabled: true,
2165 style: "aligned-no-space".to_string(),
2166 max_width: LineLength::from_const(0),
2167 column_align: ColumnAlign::Center,
2168 column_align_header: None,
2169 column_align_body: None,
2170 loose_last_column: false,
2171 };
2172 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2173
2174 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |\n| Bob | 25 |";
2175 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2176
2177 let fixed = rule.fix(&ctx).unwrap();
2178 let lines: Vec<&str> = fixed.lines().collect();
2179
2180 assert!(
2182 lines[1].contains("|---"),
2183 "Delimiter should have no spaces in aligned-no-space style. Got: {}",
2184 lines[1]
2185 );
2186 assert!(
2188 lines[3].contains("| Bob |"),
2189 "Content should be centered. Got: {}",
2190 lines[3]
2191 );
2192 }
2193
2194 #[test]
2195 fn test_md060_column_align_config_parsing() {
2196 let toml_str = r#"
2198enabled = true
2199style = "aligned"
2200column-align = "center"
2201"#;
2202 let config: MD060Config = toml::from_str(toml_str).expect("Should parse config");
2203 assert_eq!(config.column_align, ColumnAlign::Center);
2204
2205 let toml_str = r#"
2206enabled = true
2207style = "aligned"
2208column-align = "right"
2209"#;
2210 let config: MD060Config = toml::from_str(toml_str).expect("Should parse config");
2211 assert_eq!(config.column_align, ColumnAlign::Right);
2212
2213 let toml_str = r#"
2214enabled = true
2215style = "aligned"
2216column-align = "left"
2217"#;
2218 let config: MD060Config = toml::from_str(toml_str).expect("Should parse config");
2219 assert_eq!(config.column_align, ColumnAlign::Left);
2220
2221 let toml_str = r#"
2222enabled = true
2223style = "aligned"
2224column-align = "auto"
2225"#;
2226 let config: MD060Config = toml::from_str(toml_str).expect("Should parse config");
2227 assert_eq!(config.column_align, ColumnAlign::Auto);
2228 }
2229
2230 #[test]
2231 fn test_md060_column_align_default_is_auto() {
2232 let toml_str = r#"
2234enabled = true
2235style = "aligned"
2236"#;
2237 let config: MD060Config = toml::from_str(toml_str).expect("Should parse config");
2238 assert_eq!(config.column_align, ColumnAlign::Auto);
2239 }
2240
2241 #[test]
2242 fn test_md060_column_align_reformats_already_aligned_table() {
2243 let config = MD060Config {
2245 enabled: true,
2246 style: "aligned".to_string(),
2247 max_width: LineLength::from_const(0),
2248 column_align: ColumnAlign::Right,
2249 column_align_header: None,
2250 column_align_body: None,
2251 loose_last_column: false,
2252 };
2253 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2254
2255 let content = "| Name | Age |\n| ----- | --- |\n| Alice | 30 |\n| Bob | 25 |";
2257 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2258
2259 let fixed = rule.fix(&ctx).unwrap();
2260 let lines: Vec<&str> = fixed.lines().collect();
2261
2262 assert!(
2264 lines[2].contains("| Alice |") && lines[2].contains("| 30 |"),
2265 "Already aligned table should be reformatted with right alignment. Got: {}",
2266 lines[2]
2267 );
2268 assert!(
2269 lines[3].contains("| Bob |") || lines[3].contains("| Bob |"),
2270 "Bob should be right-aligned. Got: {}",
2271 lines[3]
2272 );
2273 }
2274
2275 #[test]
2276 fn test_md060_column_align_with_cjk_characters() {
2277 let config = MD060Config {
2279 enabled: true,
2280 style: "aligned".to_string(),
2281 max_width: LineLength::from_const(0),
2282 column_align: ColumnAlign::Center,
2283 column_align_header: None,
2284 column_align_body: None,
2285 loose_last_column: false,
2286 };
2287 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2288
2289 let content = "| Name | City |\n|---|---|\n| Alice | ζ±δΊ¬ |\n| Bob | LA |";
2290 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2291
2292 let fixed = rule.fix(&ctx).unwrap();
2293
2294 assert!(fixed.contains("Bob"), "Table should contain Bob");
2297 assert!(fixed.contains("ζ±δΊ¬"), "Table should contain ζ±δΊ¬");
2298 }
2299
2300 #[test]
2301 fn test_md060_column_align_ignored_for_compact_style() {
2302 let config = MD060Config {
2304 enabled: true,
2305 style: "compact".to_string(),
2306 max_width: LineLength::from_const(0),
2307 column_align: ColumnAlign::Right, column_align_header: None,
2309 column_align_body: None,
2310 loose_last_column: false,
2311 };
2312 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2313
2314 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |\n| Bob | 25 |";
2315 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2316
2317 let fixed = rule.fix(&ctx).unwrap();
2318
2319 assert!(
2321 fixed.contains("| Alice |"),
2322 "Compact style should have single space padding, not alignment. Got: {fixed}"
2323 );
2324 }
2325
2326 #[test]
2327 fn test_md060_column_align_ignored_for_tight_style() {
2328 let config = MD060Config {
2330 enabled: true,
2331 style: "tight".to_string(),
2332 max_width: LineLength::from_const(0),
2333 column_align: ColumnAlign::Center, column_align_header: None,
2335 column_align_body: None,
2336 loose_last_column: false,
2337 };
2338 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2339
2340 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |\n| Bob | 25 |";
2341 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2342
2343 let fixed = rule.fix(&ctx).unwrap();
2344
2345 assert!(
2347 fixed.contains("|Alice|"),
2348 "Tight style should have no spaces. Got: {fixed}"
2349 );
2350 }
2351
2352 #[test]
2353 fn test_md060_column_align_with_empty_cells() {
2354 let config = MD060Config {
2356 enabled: true,
2357 style: "aligned".to_string(),
2358 max_width: LineLength::from_const(0),
2359 column_align: ColumnAlign::Center,
2360 column_align_header: None,
2361 column_align_body: None,
2362 loose_last_column: false,
2363 };
2364 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2365
2366 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |\n| | 25 |";
2367 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2368
2369 let fixed = rule.fix(&ctx).unwrap();
2370 let lines: Vec<&str> = fixed.lines().collect();
2371
2372 assert!(
2374 lines[3].contains("| |") || lines[3].contains("| |"),
2375 "Empty cell should be padded correctly. Got: {}",
2376 lines[3]
2377 );
2378 }
2379
2380 #[test]
2381 fn test_md060_column_align_auto_preserves_already_aligned() {
2382 let config = MD060Config {
2384 enabled: true,
2385 style: "aligned".to_string(),
2386 max_width: LineLength::from_const(0),
2387 column_align: ColumnAlign::Auto,
2388 column_align_header: None,
2389 column_align_body: None,
2390 loose_last_column: false,
2391 };
2392 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2393
2394 let content = "| Name | Age |\n| ----- | --- |\n| Alice | 30 |\n| Bob | 25 |";
2396 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2397
2398 let fixed = rule.fix(&ctx).unwrap();
2399
2400 assert_eq!(
2402 fixed, content,
2403 "Already aligned table should be preserved with column-align=auto"
2404 );
2405 }
2406}