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 crate::utils::table_utils::TableUtils::extract_list_prefix(after_blockquote).1
672 } else {
673 after_blockquote
675 .strip_prefix(&continuation_indent)
676 .unwrap_or(after_blockquote.trim_start())
677 }
678 } else {
679 after_blockquote
680 }
681 })
682 .collect();
683
684 let style = self.config.style.as_str();
685
686 match style {
687 "any" => {
688 let detected_style = Self::detect_table_style(&stripped_lines, flavor);
689 if detected_style.is_none() {
690 return TableFormatResult {
691 lines: table_lines.iter().map(|s| s.to_string()).collect(),
692 auto_compacted: false,
693 aligned_width: None,
694 };
695 }
696
697 let target_style = detected_style.unwrap();
698
699 let delimiter_cells = Self::parse_table_row_with_flavor(stripped_lines[1], flavor);
701 let column_alignments = Self::parse_column_alignments(&delimiter_cells);
702
703 for (row_idx, line) in stripped_lines.iter().enumerate() {
704 let cells = Self::parse_table_row_with_flavor(line, flavor);
705 match target_style.as_str() {
706 "tight" => result.push(Self::format_table_tight(&cells)),
707 "compact" => result.push(Self::format_table_compact(&cells)),
708 _ => {
709 let column_widths = Self::calculate_column_widths(&stripped_lines, flavor);
710 let row_type = match row_idx {
711 0 => RowType::Header,
712 1 => RowType::Delimiter,
713 _ => RowType::Body,
714 };
715 let options = RowFormatOptions {
716 row_type,
717 compact_delimiter: false,
718 column_align: self.config.column_align,
719 column_align_header: self.config.column_align_header,
720 column_align_body: self.config.column_align_body,
721 loose_last_column: self.config.loose_last_column,
722 };
723 result.push(Self::format_table_row(
724 &cells,
725 &column_widths,
726 &column_alignments,
727 &options,
728 ));
729 }
730 }
731 }
732 }
733 "compact" => {
734 for line in &stripped_lines {
735 let cells = Self::parse_table_row_with_flavor(line, flavor);
736 result.push(Self::format_table_compact(&cells));
737 }
738 }
739 "tight" => {
740 for line in &stripped_lines {
741 let cells = Self::parse_table_row_with_flavor(line, flavor);
742 result.push(Self::format_table_tight(&cells));
743 }
744 }
745 "aligned" | "aligned-no-space" => {
746 let compact_delimiter = style == "aligned-no-space";
747
748 let needs_reformat = self.config.column_align != ColumnAlign::Auto
751 || self.config.column_align_header.is_some()
752 || self.config.column_align_body.is_some()
753 || self.config.loose_last_column;
754
755 if !needs_reformat && Self::is_table_already_aligned(&stripped_lines, flavor, compact_delimiter) {
756 return TableFormatResult {
757 lines: table_lines.iter().map(|s| s.to_string()).collect(),
758 auto_compacted: false,
759 aligned_width: None,
760 };
761 }
762
763 let column_widths = Self::calculate_column_widths(&stripped_lines, flavor);
764
765 let num_columns = column_widths.len();
767 let calc_aligned_width = 1 + (num_columns * 3) + column_widths.iter().sum::<usize>();
768 aligned_width = Some(calc_aligned_width);
769
770 if calc_aligned_width > self.effective_max_width() {
772 auto_compacted = true;
773 for line in &stripped_lines {
774 let cells = Self::parse_table_row_with_flavor(line, flavor);
775 result.push(Self::format_table_compact(&cells));
776 }
777 } else {
778 let delimiter_cells = Self::parse_table_row_with_flavor(stripped_lines[1], flavor);
780 let column_alignments = Self::parse_column_alignments(&delimiter_cells);
781
782 for (row_idx, line) in stripped_lines.iter().enumerate() {
783 let cells = Self::parse_table_row_with_flavor(line, flavor);
784 let row_type = match row_idx {
785 0 => RowType::Header,
786 1 => RowType::Delimiter,
787 _ => RowType::Body,
788 };
789 let options = RowFormatOptions {
790 row_type,
791 compact_delimiter,
792 column_align: self.config.column_align,
793 column_align_header: self.config.column_align_header,
794 column_align_body: self.config.column_align_body,
795 loose_last_column: self.config.loose_last_column,
796 };
797 result.push(Self::format_table_row(
798 &cells,
799 &column_widths,
800 &column_alignments,
801 &options,
802 ));
803 }
804 }
805 }
806 _ => {
807 return TableFormatResult {
808 lines: table_lines.iter().map(|s| s.to_string()).collect(),
809 auto_compacted: false,
810 aligned_width: None,
811 };
812 }
813 }
814
815 let prefixed_result: Vec<String> = result
817 .into_iter()
818 .enumerate()
819 .map(|(i, line)| {
820 if list_context.is_some() {
821 if i == 0 {
822 format!("{blockquote_prefix}{list_prefix}{line}")
824 } else {
825 format!("{blockquote_prefix}{continuation_indent}{line}")
827 }
828 } else {
829 format!("{blockquote_prefix}{line}")
830 }
831 })
832 .collect();
833
834 TableFormatResult {
835 lines: prefixed_result,
836 auto_compacted,
837 aligned_width,
838 }
839 }
840}
841
842impl Rule for MD060TableFormat {
843 fn name(&self) -> &'static str {
844 "MD060"
845 }
846
847 fn description(&self) -> &'static str {
848 "Table columns should be consistently aligned"
849 }
850
851 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
852 !self.config.enabled || !ctx.likely_has_tables()
853 }
854
855 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
856 if !self.config.enabled {
857 return Ok(Vec::new());
858 }
859
860 let content = ctx.content;
861 let line_index = &ctx.line_index;
862 let mut warnings = Vec::new();
863
864 let lines: Vec<&str> = content.lines().collect();
865 let table_blocks = &ctx.table_blocks;
866
867 for table_block in table_blocks {
868 let format_result = self.fix_table_block(&lines, table_block, ctx.flavor);
869
870 let table_line_indices: Vec<usize> = std::iter::once(table_block.header_line)
871 .chain(std::iter::once(table_block.delimiter_line))
872 .chain(table_block.content_lines.iter().copied())
873 .collect();
874
875 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());
882 for (i, &line_idx) in table_line_indices.iter().enumerate() {
883 let fixed_line = &format_result.lines[i];
884 if line_idx < lines.len() - 1 {
886 fixed_table_lines.push(format!("{fixed_line}\n"));
887 } else {
888 fixed_table_lines.push(fixed_line.clone());
889 }
890 }
891 let table_replacement = fixed_table_lines.concat();
892 let table_range = line_index.multi_line_range(table_start_line, table_end_line);
893
894 for (i, &line_idx) in table_line_indices.iter().enumerate() {
895 let original = lines[line_idx];
896 let fixed = &format_result.lines[i];
897
898 if original != fixed {
899 let (start_line, start_col, end_line, end_col) = calculate_line_range(line_idx + 1, original);
900
901 let message = if format_result.auto_compacted {
902 if let Some(width) = format_result.aligned_width {
903 format!(
904 "Table too wide for aligned formatting ({} chars > max-width: {})",
905 width,
906 self.effective_max_width()
907 )
908 } else {
909 "Table too wide for aligned formatting".to_string()
910 }
911 } else {
912 "Table columns should be aligned".to_string()
913 };
914
915 warnings.push(LintWarning {
918 rule_name: Some(self.name().to_string()),
919 severity: Severity::Warning,
920 message,
921 line: start_line,
922 column: start_col,
923 end_line,
924 end_column: end_col,
925 fix: Some(crate::rule::Fix {
926 range: table_range.clone(),
927 replacement: table_replacement.clone(),
928 }),
929 });
930 }
931 }
932 }
933
934 Ok(warnings)
935 }
936
937 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
938 if !self.config.enabled {
939 return Ok(ctx.content.to_string());
940 }
941
942 let content = ctx.content;
943 let lines: Vec<&str> = content.lines().collect();
944 let table_blocks = &ctx.table_blocks;
945
946 let mut result_lines: Vec<String> = lines.iter().map(|&s| s.to_string()).collect();
947
948 for table_block in table_blocks {
949 let format_result = self.fix_table_block(&lines, table_block, ctx.flavor);
950
951 let table_line_indices: Vec<usize> = std::iter::once(table_block.header_line)
952 .chain(std::iter::once(table_block.delimiter_line))
953 .chain(table_block.content_lines.iter().copied())
954 .collect();
955
956 for (i, &line_idx) in table_line_indices.iter().enumerate() {
957 result_lines[line_idx] = format_result.lines[i].clone();
958 }
959 }
960
961 let mut fixed = result_lines.join("\n");
962 if content.ends_with('\n') && !fixed.ends_with('\n') {
963 fixed.push('\n');
964 }
965 Ok(fixed)
966 }
967
968 fn as_any(&self) -> &dyn std::any::Any {
969 self
970 }
971
972 fn default_config_section(&self) -> Option<(String, toml::Value)> {
973 let mut table = toml::map::Map::new();
976 table.insert("enabled".to_string(), toml::Value::Boolean(self.config.enabled));
977 table.insert("style".to_string(), toml::Value::String(self.config.style.clone()));
978 table.insert(
979 "max-width".to_string(),
980 toml::Value::Integer(self.config.max_width.get() as i64),
981 );
982 table.insert(
983 "column-align".to_string(),
984 toml::Value::String(
985 match self.config.column_align {
986 ColumnAlign::Auto => "auto",
987 ColumnAlign::Left => "left",
988 ColumnAlign::Center => "center",
989 ColumnAlign::Right => "right",
990 }
991 .to_string(),
992 ),
993 );
994 table.insert(
996 "column-align-header".to_string(),
997 toml::Value::String("auto".to_string()),
998 );
999 table.insert("column-align-body".to_string(), toml::Value::String("auto".to_string()));
1000 table.insert(
1001 "loose-last-column".to_string(),
1002 toml::Value::Boolean(self.config.loose_last_column),
1003 );
1004
1005 Some((self.name().to_string(), toml::Value::Table(table)))
1006 }
1007
1008 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
1009 where
1010 Self: Sized,
1011 {
1012 let rule_config = crate::rule_config_serde::load_rule_config::<MD060Config>(config);
1013 let md013_config = crate::rule_config_serde::load_rule_config::<MD013Config>(config);
1014
1015 let md013_disabled = config.global.disable.iter().any(|r| r == "MD013");
1017
1018 Box::new(Self::from_config_struct(rule_config, md013_config, md013_disabled))
1019 }
1020}
1021
1022#[cfg(test)]
1023mod tests {
1024 use super::*;
1025 use crate::lint_context::LintContext;
1026 use crate::types::LineLength;
1027
1028 fn md013_with_line_length(line_length: usize) -> MD013Config {
1030 MD013Config {
1031 line_length: LineLength::from_const(line_length),
1032 tables: true, ..Default::default()
1034 }
1035 }
1036
1037 #[test]
1038 fn test_md060_disabled_by_default() {
1039 let rule = MD060TableFormat::default();
1040 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
1041 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1042
1043 let warnings = rule.check(&ctx).unwrap();
1044 assert_eq!(warnings.len(), 0);
1045
1046 let fixed = rule.fix(&ctx).unwrap();
1047 assert_eq!(fixed, content);
1048 }
1049
1050 #[test]
1051 fn test_md060_align_simple_ascii_table() {
1052 let rule = MD060TableFormat::new(true, "aligned".to_string());
1053
1054 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
1055 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1056
1057 let fixed = rule.fix(&ctx).unwrap();
1058 let expected = "| Name | Age |\n| ----- | --- |\n| Alice | 30 |";
1059 assert_eq!(fixed, expected);
1060
1061 let lines: Vec<&str> = fixed.lines().collect();
1063 assert_eq!(lines[0].len(), lines[1].len());
1064 assert_eq!(lines[1].len(), lines[2].len());
1065 }
1066
1067 #[test]
1068 fn test_md060_cjk_characters_aligned_correctly() {
1069 let rule = MD060TableFormat::new(true, "aligned".to_string());
1070
1071 let content = "| Name | Age |\n|---|---|\n| δΈζ | 30 |";
1072 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1073
1074 let fixed = rule.fix(&ctx).unwrap();
1075
1076 let lines: Vec<&str> = fixed.lines().collect();
1077 let cells_line1 = MD060TableFormat::parse_table_row(lines[0]);
1078 let cells_line3 = MD060TableFormat::parse_table_row(lines[2]);
1079
1080 let width1 = MD060TableFormat::calculate_cell_display_width(&cells_line1[0]);
1081 let width3 = MD060TableFormat::calculate_cell_display_width(&cells_line3[0]);
1082
1083 assert_eq!(width1, width3);
1084 }
1085
1086 #[test]
1087 fn test_md060_basic_emoji() {
1088 let rule = MD060TableFormat::new(true, "aligned".to_string());
1089
1090 let content = "| Status | Name |\n|---|---|\n| β
| Test |";
1091 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1092
1093 let fixed = rule.fix(&ctx).unwrap();
1094 assert!(fixed.contains("Status"));
1095 }
1096
1097 #[test]
1098 fn test_md060_zwj_emoji_skipped() {
1099 let rule = MD060TableFormat::new(true, "aligned".to_string());
1100
1101 let content = "| Emoji | Name |\n|---|---|\n| π¨βπ©βπ§βπ¦ | Family |";
1102 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1103
1104 let fixed = rule.fix(&ctx).unwrap();
1105 assert_eq!(fixed, content);
1106 }
1107
1108 #[test]
1109 fn test_md060_inline_code_with_escaped_pipes() {
1110 let rule = MD060TableFormat::new(true, "aligned".to_string());
1113
1114 let content = "| Pattern | Regex |\n|---|---|\n| Time | `[0-9]\\|[0-9]` |";
1116 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1117
1118 let fixed = rule.fix(&ctx).unwrap();
1119 assert!(fixed.contains(r"`[0-9]\|[0-9]`"), "Escaped pipes should be preserved");
1120 }
1121
1122 #[test]
1123 fn test_md060_compact_style() {
1124 let rule = MD060TableFormat::new(true, "compact".to_string());
1125
1126 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
1127 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1128
1129 let fixed = rule.fix(&ctx).unwrap();
1130 let expected = "| Name | Age |\n| --- | --- |\n| Alice | 30 |";
1131 assert_eq!(fixed, expected);
1132 }
1133
1134 #[test]
1135 fn test_md060_tight_style() {
1136 let rule = MD060TableFormat::new(true, "tight".to_string());
1137
1138 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
1139 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1140
1141 let fixed = rule.fix(&ctx).unwrap();
1142 let expected = "|Name|Age|\n|---|---|\n|Alice|30|";
1143 assert_eq!(fixed, expected);
1144 }
1145
1146 #[test]
1147 fn test_md060_aligned_no_space_style() {
1148 let rule = MD060TableFormat::new(true, "aligned-no-space".to_string());
1150
1151 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
1152 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1153
1154 let fixed = rule.fix(&ctx).unwrap();
1155
1156 let lines: Vec<&str> = fixed.lines().collect();
1158 assert_eq!(lines[0], "| Name | Age |", "Header should have spaces around content");
1159 assert_eq!(
1160 lines[1], "|-------|-----|",
1161 "Delimiter should have NO spaces around dashes"
1162 );
1163 assert_eq!(lines[2], "| Alice | 30 |", "Content should have spaces around content");
1164
1165 assert_eq!(lines[0].len(), lines[1].len());
1167 assert_eq!(lines[1].len(), lines[2].len());
1168 }
1169
1170 #[test]
1171 fn test_md060_aligned_no_space_preserves_alignment_indicators() {
1172 let rule = MD060TableFormat::new(true, "aligned-no-space".to_string());
1174
1175 let content = "| Left | Center | Right |\n|:---|:---:|---:|\n| A | B | C |";
1176 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1177
1178 let fixed = rule.fix(&ctx).unwrap();
1179 let lines: Vec<&str> = fixed.lines().collect();
1180
1181 assert!(
1183 fixed.contains("|:"),
1184 "Should have left alignment indicator adjacent to pipe"
1185 );
1186 assert!(
1187 fixed.contains(":|"),
1188 "Should have right alignment indicator adjacent to pipe"
1189 );
1190 assert!(
1192 lines[1].contains(":---") && lines[1].contains("---:"),
1193 "Should have center alignment colons"
1194 );
1195 }
1196
1197 #[test]
1198 fn test_md060_aligned_no_space_three_column_table() {
1199 let rule = MD060TableFormat::new(true, "aligned-no-space".to_string());
1201
1202 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 |";
1203 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1204
1205 let fixed = rule.fix(&ctx).unwrap();
1206 let lines: Vec<&str> = fixed.lines().collect();
1207
1208 assert!(lines[1].starts_with("|---"), "Delimiter should start with |---");
1210 assert!(lines[1].ends_with("---|"), "Delimiter should end with ---|");
1211 assert!(!lines[1].contains("| -"), "Delimiter should NOT have space after pipe");
1212 assert!(!lines[1].contains("- |"), "Delimiter should NOT have space before pipe");
1213 }
1214
1215 #[test]
1216 fn test_md060_aligned_no_space_auto_compacts_wide_tables() {
1217 let config = MD060Config {
1219 enabled: true,
1220 style: "aligned-no-space".to_string(),
1221 max_width: LineLength::from_const(50),
1222 column_align: ColumnAlign::Auto,
1223 column_align_header: None,
1224 column_align_body: None,
1225 loose_last_column: false,
1226 };
1227 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false);
1228
1229 let content = "| Very Long Column Header A | Very Long Column Header B | Very Long Column Header C |\n|---|---|---|\n| x | y | z |";
1231 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1232
1233 let fixed = rule.fix(&ctx).unwrap();
1234
1235 assert!(
1237 fixed.contains("| --- |"),
1238 "Should be compact format when exceeding max-width"
1239 );
1240 }
1241
1242 #[test]
1243 fn test_md060_aligned_no_space_cjk_characters() {
1244 let rule = MD060TableFormat::new(true, "aligned-no-space".to_string());
1246
1247 let content = "| Name | City |\n|---|---|\n| δΈζ | ζ±δΊ¬ |";
1248 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1249
1250 let fixed = rule.fix(&ctx).unwrap();
1251 let lines: Vec<&str> = fixed.lines().collect();
1252
1253 use unicode_width::UnicodeWidthStr;
1256 assert_eq!(
1257 lines[0].width(),
1258 lines[1].width(),
1259 "Header and delimiter should have same display width"
1260 );
1261 assert_eq!(
1262 lines[1].width(),
1263 lines[2].width(),
1264 "Delimiter and content should have same display width"
1265 );
1266
1267 assert!(!lines[1].contains("| -"), "Delimiter should NOT have space after pipe");
1269 }
1270
1271 #[test]
1272 fn test_md060_aligned_no_space_minimum_width() {
1273 let rule = MD060TableFormat::new(true, "aligned-no-space".to_string());
1275
1276 let content = "| A | B |\n|-|-|\n| 1 | 2 |";
1277 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1278
1279 let fixed = rule.fix(&ctx).unwrap();
1280 let lines: Vec<&str> = fixed.lines().collect();
1281
1282 assert!(lines[1].contains("---"), "Should have minimum 3 dashes");
1284 assert_eq!(lines[0].len(), lines[1].len());
1286 assert_eq!(lines[1].len(), lines[2].len());
1287 }
1288
1289 #[test]
1290 fn test_md060_any_style_consistency() {
1291 let rule = MD060TableFormat::new(true, "any".to_string());
1292
1293 let content = "| Name | Age |\n| --- | --- |\n| Alice | 30 |";
1295 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1296
1297 let fixed = rule.fix(&ctx).unwrap();
1298 assert_eq!(fixed, content);
1299
1300 let content_aligned = "| Name | Age |\n| ----- | --- |\n| Alice | 30 |";
1302 let ctx_aligned = LintContext::new(content_aligned, crate::config::MarkdownFlavor::Standard, None);
1303
1304 let fixed_aligned = rule.fix(&ctx_aligned).unwrap();
1305 assert_eq!(fixed_aligned, content_aligned);
1306 }
1307
1308 #[test]
1309 fn test_md060_empty_cells() {
1310 let rule = MD060TableFormat::new(true, "aligned".to_string());
1311
1312 let content = "| A | B |\n|---|---|\n| | X |";
1313 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1314
1315 let fixed = rule.fix(&ctx).unwrap();
1316 assert!(fixed.contains("|"));
1317 }
1318
1319 #[test]
1320 fn test_md060_mixed_content() {
1321 let rule = MD060TableFormat::new(true, "aligned".to_string());
1322
1323 let content = "| Name | Age | City |\n|---|---|---|\n| δΈζ | 30 | NYC |";
1324 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1325
1326 let fixed = rule.fix(&ctx).unwrap();
1327 assert!(fixed.contains("δΈζ"));
1328 assert!(fixed.contains("NYC"));
1329 }
1330
1331 #[test]
1332 fn test_md060_preserve_alignment_indicators() {
1333 let rule = MD060TableFormat::new(true, "aligned".to_string());
1334
1335 let content = "| Left | Center | Right |\n|:---|:---:|---:|\n| A | B | C |";
1336 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1337
1338 let fixed = rule.fix(&ctx).unwrap();
1339
1340 assert!(fixed.contains(":---"), "Should contain left alignment");
1341 assert!(fixed.contains(":----:"), "Should contain center alignment");
1342 assert!(fixed.contains("----:"), "Should contain right alignment");
1343 }
1344
1345 #[test]
1346 fn test_md060_minimum_column_width() {
1347 let rule = MD060TableFormat::new(true, "aligned".to_string());
1348
1349 let content = "| ID | Name |\n|-|-|\n| 1 | A |";
1352 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1353
1354 let fixed = rule.fix(&ctx).unwrap();
1355
1356 let lines: Vec<&str> = fixed.lines().collect();
1357 assert_eq!(lines[0].len(), lines[1].len());
1358 assert_eq!(lines[1].len(), lines[2].len());
1359
1360 assert!(fixed.contains("ID "), "Short content should be padded");
1362 assert!(fixed.contains("---"), "Delimiter should have at least 3 dashes");
1363 }
1364
1365 #[test]
1366 fn test_md060_auto_compact_exceeds_default_threshold() {
1367 let config = MD060Config {
1369 enabled: true,
1370 style: "aligned".to_string(),
1371 max_width: LineLength::from_const(0),
1372 column_align: ColumnAlign::Auto,
1373 column_align_header: None,
1374 column_align_body: None,
1375 loose_last_column: false,
1376 };
1377 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false);
1378
1379 let content = "| Very Long Column Header | Another Long Header | Third Very Long Header Column |\n|---|---|---|\n| Short | Data | Here |";
1383 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1384
1385 let fixed = rule.fix(&ctx).unwrap();
1386
1387 assert!(fixed.contains("| Very Long Column Header | Another Long Header | Third Very Long Header Column |"));
1389 assert!(fixed.contains("| --- | --- | --- |"));
1390 assert!(fixed.contains("| Short | Data | Here |"));
1391
1392 let lines: Vec<&str> = fixed.lines().collect();
1394 assert!(lines[0].len() != lines[1].len() || lines[1].len() != lines[2].len());
1396 }
1397
1398 #[test]
1399 fn test_md060_auto_compact_exceeds_explicit_threshold() {
1400 let config = MD060Config {
1402 enabled: true,
1403 style: "aligned".to_string(),
1404 max_width: LineLength::from_const(50),
1405 column_align: ColumnAlign::Auto,
1406 column_align_header: None,
1407 column_align_body: None,
1408 loose_last_column: false,
1409 };
1410 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 |";
1416 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1417
1418 let fixed = rule.fix(&ctx).unwrap();
1419
1420 assert!(
1422 fixed.contains("| Very Long Column Header A | Very Long Column Header B | Very Long Column Header C |")
1423 );
1424 assert!(fixed.contains("| --- | --- | --- |"));
1425 assert!(fixed.contains("| Data | Data | Data |"));
1426
1427 let lines: Vec<&str> = fixed.lines().collect();
1429 assert!(lines[0].len() != lines[2].len());
1430 }
1431
1432 #[test]
1433 fn test_md060_stays_aligned_under_threshold() {
1434 let config = MD060Config {
1436 enabled: true,
1437 style: "aligned".to_string(),
1438 max_width: LineLength::from_const(100),
1439 column_align: ColumnAlign::Auto,
1440 column_align_header: None,
1441 column_align_body: None,
1442 loose_last_column: false,
1443 };
1444 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false);
1445
1446 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
1448 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1449
1450 let fixed = rule.fix(&ctx).unwrap();
1451
1452 let expected = "| Name | Age |\n| ----- | --- |\n| Alice | 30 |";
1454 assert_eq!(fixed, expected);
1455
1456 let lines: Vec<&str> = fixed.lines().collect();
1457 assert_eq!(lines[0].len(), lines[1].len());
1458 assert_eq!(lines[1].len(), lines[2].len());
1459 }
1460
1461 #[test]
1462 fn test_md060_width_calculation_formula() {
1463 let config = MD060Config {
1465 enabled: true,
1466 style: "aligned".to_string(),
1467 max_width: LineLength::from_const(0),
1468 column_align: ColumnAlign::Auto,
1469 column_align_header: None,
1470 column_align_body: None,
1471 loose_last_column: false,
1472 };
1473 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(30), false);
1474
1475 let content = "| AAAAA | BBBBB | CCCCC |\n|---|---|---|\n| AAAAA | BBBBB | CCCCC |";
1479 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1480
1481 let fixed = rule.fix(&ctx).unwrap();
1482
1483 let lines: Vec<&str> = fixed.lines().collect();
1485 assert_eq!(lines[0].len(), lines[1].len());
1486 assert_eq!(lines[1].len(), lines[2].len());
1487 assert_eq!(lines[0].len(), 25); let config_tight = MD060Config {
1491 enabled: true,
1492 style: "aligned".to_string(),
1493 max_width: LineLength::from_const(24),
1494 column_align: ColumnAlign::Auto,
1495 column_align_header: None,
1496 column_align_body: None,
1497 loose_last_column: false,
1498 };
1499 let rule_tight = MD060TableFormat::from_config_struct(config_tight, md013_with_line_length(80), false);
1500
1501 let fixed_compact = rule_tight.fix(&ctx).unwrap();
1502
1503 assert!(fixed_compact.contains("| AAAAA | BBBBB | CCCCC |"));
1505 assert!(fixed_compact.contains("| --- | --- | --- |"));
1506 }
1507
1508 #[test]
1509 fn test_md060_very_wide_table_auto_compacts() {
1510 let config = MD060Config {
1511 enabled: true,
1512 style: "aligned".to_string(),
1513 max_width: LineLength::from_const(0),
1514 column_align: ColumnAlign::Auto,
1515 column_align_header: None,
1516 column_align_body: None,
1517 loose_last_column: false,
1518 };
1519 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false);
1520
1521 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 |";
1525 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1526
1527 let fixed = rule.fix(&ctx).unwrap();
1528
1529 assert!(fixed.contains("| Column One A | Column Two B | Column Three | Column Four D | Column Five E | Column Six FG | Column Seven | Column Eight |"));
1531 assert!(fixed.contains("| --- | --- | --- | --- | --- | --- | --- | --- |"));
1532 }
1533
1534 #[test]
1535 fn test_md060_inherit_from_md013_line_length() {
1536 let config = MD060Config {
1538 enabled: true,
1539 style: "aligned".to_string(),
1540 max_width: LineLength::from_const(0), column_align: ColumnAlign::Auto,
1542 column_align_header: None,
1543 column_align_body: None,
1544 loose_last_column: false,
1545 };
1546
1547 let rule_80 = MD060TableFormat::from_config_struct(config.clone(), md013_with_line_length(80), false);
1549 let rule_120 = MD060TableFormat::from_config_struct(config.clone(), md013_with_line_length(120), false);
1550
1551 let content = "| Column Header A | Column Header B | Column Header C |\n|---|---|---|\n| Some Data | More Data | Even More |";
1553 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1554
1555 let _fixed_80 = rule_80.fix(&ctx).unwrap();
1557
1558 let fixed_120 = rule_120.fix(&ctx).unwrap();
1560
1561 let lines_120: Vec<&str> = fixed_120.lines().collect();
1563 assert_eq!(lines_120[0].len(), lines_120[1].len());
1564 assert_eq!(lines_120[1].len(), lines_120[2].len());
1565 }
1566
1567 #[test]
1568 fn test_md060_edge_case_exactly_at_threshold() {
1569 let config = MD060Config {
1573 enabled: true,
1574 style: "aligned".to_string(),
1575 max_width: LineLength::from_const(17),
1576 column_align: ColumnAlign::Auto,
1577 column_align_header: None,
1578 column_align_body: None,
1579 loose_last_column: false,
1580 };
1581 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false);
1582
1583 let content = "| AAAAA | BBBBB |\n|---|---|\n| AAAAA | BBBBB |";
1584 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1585
1586 let fixed = rule.fix(&ctx).unwrap();
1587
1588 let lines: Vec<&str> = fixed.lines().collect();
1590 assert_eq!(lines[0].len(), 17);
1591 assert_eq!(lines[0].len(), lines[1].len());
1592 assert_eq!(lines[1].len(), lines[2].len());
1593
1594 let config_under = MD060Config {
1596 enabled: true,
1597 style: "aligned".to_string(),
1598 max_width: LineLength::from_const(16),
1599 column_align: ColumnAlign::Auto,
1600 column_align_header: None,
1601 column_align_body: None,
1602 loose_last_column: false,
1603 };
1604 let rule_under = MD060TableFormat::from_config_struct(config_under, md013_with_line_length(80), false);
1605
1606 let fixed_compact = rule_under.fix(&ctx).unwrap();
1607
1608 assert!(fixed_compact.contains("| AAAAA | BBBBB |"));
1610 assert!(fixed_compact.contains("| --- | --- |"));
1611 }
1612
1613 #[test]
1614 fn test_md060_auto_compact_warning_message() {
1615 let config = MD060Config {
1617 enabled: true,
1618 style: "aligned".to_string(),
1619 max_width: LineLength::from_const(50),
1620 column_align: ColumnAlign::Auto,
1621 column_align_header: None,
1622 column_align_body: None,
1623 loose_last_column: false,
1624 };
1625 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false);
1626
1627 let content = "| Very Long Column Header A | Very Long Column Header B | Very Long Column Header C |\n|---|---|---|\n| Data | Data | Data |";
1629 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1630
1631 let warnings = rule.check(&ctx).unwrap();
1632
1633 assert!(!warnings.is_empty(), "Should generate warnings");
1635
1636 let auto_compact_warnings: Vec<_> = warnings
1637 .iter()
1638 .filter(|w| w.message.contains("too wide for aligned formatting"))
1639 .collect();
1640
1641 assert!(!auto_compact_warnings.is_empty(), "Should have auto-compact warning");
1642
1643 let first_warning = auto_compact_warnings[0];
1645 assert!(first_warning.message.contains("85 chars > max-width: 50"));
1646 assert!(first_warning.message.contains("Table too wide for aligned formatting"));
1647 }
1648
1649 #[test]
1650 fn test_md060_issue_129_detect_style_from_all_rows() {
1651 let rule = MD060TableFormat::new(true, "any".to_string());
1655
1656 let content = "| a long heading | another long heading |\n\
1658 | -------------- | -------------------- |\n\
1659 | a | 1 |\n\
1660 | b b | 2 |\n\
1661 | c c c | 3 |";
1662 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1663
1664 let fixed = rule.fix(&ctx).unwrap();
1665
1666 assert!(
1668 fixed.contains("| a | 1 |"),
1669 "Should preserve aligned padding in first content row"
1670 );
1671 assert!(
1672 fixed.contains("| b b | 2 |"),
1673 "Should preserve aligned padding in second content row"
1674 );
1675 assert!(
1676 fixed.contains("| c c c | 3 |"),
1677 "Should preserve aligned padding in third content row"
1678 );
1679
1680 assert_eq!(fixed, content, "Table should be detected as aligned and preserved");
1682 }
1683
1684 #[test]
1685 fn test_md060_regular_alignment_warning_message() {
1686 let config = MD060Config {
1688 enabled: true,
1689 style: "aligned".to_string(),
1690 max_width: LineLength::from_const(100), column_align: ColumnAlign::Auto,
1692 column_align_header: None,
1693 column_align_body: None,
1694 loose_last_column: false,
1695 };
1696 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false);
1697
1698 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
1700 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1701
1702 let warnings = rule.check(&ctx).unwrap();
1703
1704 assert!(!warnings.is_empty(), "Should generate warnings");
1706
1707 assert!(warnings[0].message.contains("Table columns should be aligned"));
1709 assert!(!warnings[0].message.contains("too wide"));
1710 assert!(!warnings[0].message.contains("max-width"));
1711 }
1712
1713 #[test]
1716 fn test_md060_unlimited_when_md013_disabled() {
1717 let config = MD060Config {
1719 enabled: true,
1720 style: "aligned".to_string(),
1721 max_width: LineLength::from_const(0), column_align: ColumnAlign::Auto,
1723 column_align_header: None,
1724 column_align_body: None,
1725 loose_last_column: false,
1726 };
1727 let md013_config = MD013Config::default();
1728 let rule = MD060TableFormat::from_config_struct(config, md013_config, true );
1729
1730 let content = "| Very Long Column Header A | Very Long Column Header B | Very Long Column Header C |\n|---|---|---|\n| data | data | data |";
1732 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1733 let fixed = rule.fix(&ctx).unwrap();
1734
1735 let lines: Vec<&str> = fixed.lines().collect();
1737 assert_eq!(
1739 lines[0].len(),
1740 lines[1].len(),
1741 "Table should be aligned when MD013 is disabled"
1742 );
1743 }
1744
1745 #[test]
1746 fn test_md060_unlimited_when_md013_tables_false() {
1747 let config = MD060Config {
1749 enabled: true,
1750 style: "aligned".to_string(),
1751 max_width: LineLength::from_const(0),
1752 column_align: ColumnAlign::Auto,
1753 column_align_header: None,
1754 column_align_body: None,
1755 loose_last_column: false,
1756 };
1757 let md013_config = MD013Config {
1758 tables: false, line_length: LineLength::from_const(80),
1760 ..Default::default()
1761 };
1762 let rule = MD060TableFormat::from_config_struct(config, md013_config, false);
1763
1764 let content = "| Very Long Header A | Very Long Header B | Very Long Header C |\n|---|---|---|\n| x | y | z |";
1766 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1767 let fixed = rule.fix(&ctx).unwrap();
1768
1769 let lines: Vec<&str> = fixed.lines().collect();
1771 assert_eq!(
1772 lines[0].len(),
1773 lines[1].len(),
1774 "Table should be aligned when MD013.tables=false"
1775 );
1776 }
1777
1778 #[test]
1779 fn test_md060_unlimited_when_md013_line_length_zero() {
1780 let config = MD060Config {
1782 enabled: true,
1783 style: "aligned".to_string(),
1784 max_width: LineLength::from_const(0),
1785 column_align: ColumnAlign::Auto,
1786 column_align_header: None,
1787 column_align_body: None,
1788 loose_last_column: false,
1789 };
1790 let md013_config = MD013Config {
1791 tables: true,
1792 line_length: LineLength::from_const(0), ..Default::default()
1794 };
1795 let rule = MD060TableFormat::from_config_struct(config, md013_config, false);
1796
1797 let content = "| Very Long Header | Another Long Header | Third Long Header |\n|---|---|---|\n| x | y | z |";
1799 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1800 let fixed = rule.fix(&ctx).unwrap();
1801
1802 let lines: Vec<&str> = fixed.lines().collect();
1804 assert_eq!(
1805 lines[0].len(),
1806 lines[1].len(),
1807 "Table should be aligned when MD013.line_length=0"
1808 );
1809 }
1810
1811 #[test]
1812 fn test_md060_explicit_max_width_overrides_md013_settings() {
1813 let config = MD060Config {
1815 enabled: true,
1816 style: "aligned".to_string(),
1817 max_width: LineLength::from_const(50), column_align: ColumnAlign::Auto,
1819 column_align_header: None,
1820 column_align_body: None,
1821 loose_last_column: false,
1822 };
1823 let md013_config = MD013Config {
1824 tables: false, line_length: LineLength::from_const(0), ..Default::default()
1827 };
1828 let rule = MD060TableFormat::from_config_struct(config, md013_config, false);
1829
1830 let content = "| Very Long Column Header A | Very Long Column Header B | Very Long Column Header C |\n|---|---|---|\n| x | y | z |";
1832 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1833 let fixed = rule.fix(&ctx).unwrap();
1834
1835 assert!(
1837 fixed.contains("| --- |"),
1838 "Should be compact format due to explicit max_width"
1839 );
1840 }
1841
1842 #[test]
1843 fn test_md060_inherits_md013_line_length_when_tables_enabled() {
1844 let config = MD060Config {
1846 enabled: true,
1847 style: "aligned".to_string(),
1848 max_width: LineLength::from_const(0), column_align: ColumnAlign::Auto,
1850 column_align_header: None,
1851 column_align_body: None,
1852 loose_last_column: false,
1853 };
1854 let md013_config = MD013Config {
1855 tables: true,
1856 line_length: LineLength::from_const(50), ..Default::default()
1858 };
1859 let rule = MD060TableFormat::from_config_struct(config, md013_config, false);
1860
1861 let content = "| Very Long Column Header A | Very Long Column Header B | Very Long Column Header C |\n|---|---|---|\n| x | y | z |";
1863 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1864 let fixed = rule.fix(&ctx).unwrap();
1865
1866 assert!(
1868 fixed.contains("| --- |"),
1869 "Should be compact format when inheriting MD013 limit"
1870 );
1871 }
1872
1873 #[test]
1876 fn test_aligned_no_space_reformats_spaced_delimiter() {
1877 let config = MD060Config {
1880 enabled: true,
1881 style: "aligned-no-space".to_string(),
1882 max_width: LineLength::from_const(0),
1883 column_align: ColumnAlign::Auto,
1884 column_align_header: None,
1885 column_align_body: None,
1886 loose_last_column: false,
1887 };
1888 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
1889
1890 let content = "| Header 1 | Header 2 |\n| -------- | -------- |\n| Cell 1 | Cell 2 |";
1892 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1893 let fixed = rule.fix(&ctx).unwrap();
1894
1895 assert!(
1898 !fixed.contains("| ----"),
1899 "Delimiter should NOT have spaces after pipe. Got:\n{fixed}"
1900 );
1901 assert!(
1902 !fixed.contains("---- |"),
1903 "Delimiter should NOT have spaces before pipe. Got:\n{fixed}"
1904 );
1905 assert!(
1907 fixed.contains("|----"),
1908 "Delimiter should have dashes touching the leading pipe. Got:\n{fixed}"
1909 );
1910 }
1911
1912 #[test]
1913 fn test_aligned_reformats_compact_delimiter() {
1914 let config = MD060Config {
1917 enabled: true,
1918 style: "aligned".to_string(),
1919 max_width: LineLength::from_const(0),
1920 column_align: ColumnAlign::Auto,
1921 column_align_header: None,
1922 column_align_body: None,
1923 loose_last_column: false,
1924 };
1925 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
1926
1927 let content = "| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |";
1929 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1930 let fixed = rule.fix(&ctx).unwrap();
1931
1932 assert!(
1934 fixed.contains("| -------- | -------- |") || fixed.contains("| ---------- | ---------- |"),
1935 "Delimiter should have spaces around dashes. Got:\n{fixed}"
1936 );
1937 }
1938
1939 #[test]
1940 fn test_aligned_no_space_preserves_matching_table() {
1941 let config = MD060Config {
1943 enabled: true,
1944 style: "aligned-no-space".to_string(),
1945 max_width: LineLength::from_const(0),
1946 column_align: ColumnAlign::Auto,
1947 column_align_header: None,
1948 column_align_body: None,
1949 loose_last_column: false,
1950 };
1951 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
1952
1953 let content = "| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |";
1955 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1956 let fixed = rule.fix(&ctx).unwrap();
1957
1958 assert_eq!(
1960 fixed, content,
1961 "Table already in aligned-no-space style should be preserved"
1962 );
1963 }
1964
1965 #[test]
1966 fn test_aligned_preserves_matching_table() {
1967 let config = MD060Config {
1969 enabled: true,
1970 style: "aligned".to_string(),
1971 max_width: LineLength::from_const(0),
1972 column_align: ColumnAlign::Auto,
1973 column_align_header: None,
1974 column_align_body: None,
1975 loose_last_column: false,
1976 };
1977 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
1978
1979 let content = "| Header 1 | Header 2 |\n| -------- | -------- |\n| Cell 1 | Cell 2 |";
1981 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1982 let fixed = rule.fix(&ctx).unwrap();
1983
1984 assert_eq!(fixed, content, "Table already in aligned style should be preserved");
1986 }
1987
1988 #[test]
1989 fn test_cjk_table_display_width_consistency() {
1990 let table_lines = vec!["| εε | Age |", "|------|-----|", "| η°δΈ | 25 |"];
1996
1997 let is_aligned =
1999 MD060TableFormat::is_table_already_aligned(&table_lines, crate::config::MarkdownFlavor::Standard, false);
2000 assert!(
2001 !is_aligned,
2002 "Table with uneven raw line lengths should NOT be considered aligned"
2003 );
2004 }
2005
2006 #[test]
2007 fn test_cjk_width_calculation_in_aligned_check() {
2008 let cjk_width = MD060TableFormat::calculate_cell_display_width("εε");
2011 assert_eq!(cjk_width, 4, "Two CJK characters should have display width 4");
2012
2013 let ascii_width = MD060TableFormat::calculate_cell_display_width("Age");
2014 assert_eq!(ascii_width, 3, "Three ASCII characters should have display width 3");
2015
2016 let padded_cjk = MD060TableFormat::calculate_cell_display_width(" εε ");
2018 assert_eq!(padded_cjk, 4, "Padded CJK should have same width after trim");
2019
2020 let mixed = MD060TableFormat::calculate_cell_display_width(" ζ₯ζ¬θͺABC ");
2022 assert_eq!(mixed, 9, "Mixed CJK/ASCII content");
2024 }
2025
2026 #[test]
2029 fn test_md060_column_align_left() {
2030 let config = MD060Config {
2032 enabled: true,
2033 style: "aligned".to_string(),
2034 max_width: LineLength::from_const(0),
2035 column_align: ColumnAlign::Left,
2036 column_align_header: None,
2037 column_align_body: None,
2038 loose_last_column: false,
2039 };
2040 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2041
2042 let content = "| Name | Age | City |\n|---|---|---|\n| Alice | 30 | Seattle |\n| Bob | 25 | Portland |";
2043 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2044
2045 let fixed = rule.fix(&ctx).unwrap();
2046 let lines: Vec<&str> = fixed.lines().collect();
2047
2048 assert!(
2050 lines[2].contains("| Alice "),
2051 "Content should be left-aligned (Alice should have trailing padding)"
2052 );
2053 assert!(
2054 lines[3].contains("| Bob "),
2055 "Content should be left-aligned (Bob should have trailing padding)"
2056 );
2057 }
2058
2059 #[test]
2060 fn test_md060_column_align_center() {
2061 let config = MD060Config {
2063 enabled: true,
2064 style: "aligned".to_string(),
2065 max_width: LineLength::from_const(0),
2066 column_align: ColumnAlign::Center,
2067 column_align_header: None,
2068 column_align_body: None,
2069 loose_last_column: false,
2070 };
2071 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2072
2073 let content = "| Name | Age | City |\n|---|---|---|\n| Alice | 30 | Seattle |\n| Bob | 25 | Portland |";
2074 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2075
2076 let fixed = rule.fix(&ctx).unwrap();
2077 let lines: Vec<&str> = fixed.lines().collect();
2078
2079 assert!(
2082 lines[3].contains("| Bob |"),
2083 "Bob should be centered with padding on both sides. Got: {}",
2084 lines[3]
2085 );
2086 }
2087
2088 #[test]
2089 fn test_md060_column_align_right() {
2090 let config = MD060Config {
2092 enabled: true,
2093 style: "aligned".to_string(),
2094 max_width: LineLength::from_const(0),
2095 column_align: ColumnAlign::Right,
2096 column_align_header: None,
2097 column_align_body: None,
2098 loose_last_column: false,
2099 };
2100 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2101
2102 let content = "| Name | Age | City |\n|---|---|---|\n| Alice | 30 | Seattle |\n| Bob | 25 | Portland |";
2103 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2104
2105 let fixed = rule.fix(&ctx).unwrap();
2106 let lines: Vec<&str> = fixed.lines().collect();
2107
2108 assert!(
2110 lines[3].contains("| Bob |"),
2111 "Bob should be right-aligned with padding on left. Got: {}",
2112 lines[3]
2113 );
2114 }
2115
2116 #[test]
2117 fn test_md060_column_align_auto_respects_delimiter() {
2118 let config = MD060Config {
2120 enabled: true,
2121 style: "aligned".to_string(),
2122 max_width: LineLength::from_const(0),
2123 column_align: ColumnAlign::Auto,
2124 column_align_header: None,
2125 column_align_body: None,
2126 loose_last_column: false,
2127 };
2128 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2129
2130 let content = "| Left | Center | Right |\n|:---|:---:|---:|\n| A | B | C |";
2132 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2133
2134 let fixed = rule.fix(&ctx).unwrap();
2135
2136 assert!(fixed.contains("| A "), "Left column should be left-aligned");
2138 let lines: Vec<&str> = fixed.lines().collect();
2140 assert!(
2144 lines[2].contains(" C |"),
2145 "Right column should be right-aligned. Got: {}",
2146 lines[2]
2147 );
2148 }
2149
2150 #[test]
2151 fn test_md060_column_align_overrides_delimiter_indicators() {
2152 let config = MD060Config {
2154 enabled: true,
2155 style: "aligned".to_string(),
2156 max_width: LineLength::from_const(0),
2157 column_align: ColumnAlign::Right, column_align_header: None,
2159 column_align_body: None,
2160 loose_last_column: false,
2161 };
2162 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2163
2164 let content = "| Left | Center | Right |\n|:---|:---:|---:|\n| A | B | C |";
2166 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2167
2168 let fixed = rule.fix(&ctx).unwrap();
2169 let lines: Vec<&str> = fixed.lines().collect();
2170
2171 assert!(
2174 lines[2].contains(" A |") || lines[2].contains(" A |"),
2175 "Even left-indicated column should be right-aligned. Got: {}",
2176 lines[2]
2177 );
2178 }
2179
2180 #[test]
2181 fn test_md060_column_align_with_aligned_no_space() {
2182 let config = MD060Config {
2184 enabled: true,
2185 style: "aligned-no-space".to_string(),
2186 max_width: LineLength::from_const(0),
2187 column_align: ColumnAlign::Center,
2188 column_align_header: None,
2189 column_align_body: None,
2190 loose_last_column: false,
2191 };
2192 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2193
2194 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |\n| Bob | 25 |";
2195 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2196
2197 let fixed = rule.fix(&ctx).unwrap();
2198 let lines: Vec<&str> = fixed.lines().collect();
2199
2200 assert!(
2202 lines[1].contains("|---"),
2203 "Delimiter should have no spaces in aligned-no-space style. Got: {}",
2204 lines[1]
2205 );
2206 assert!(
2208 lines[3].contains("| Bob |"),
2209 "Content should be centered. Got: {}",
2210 lines[3]
2211 );
2212 }
2213
2214 #[test]
2215 fn test_md060_column_align_config_parsing() {
2216 let toml_str = r#"
2218enabled = true
2219style = "aligned"
2220column-align = "center"
2221"#;
2222 let config: MD060Config = toml::from_str(toml_str).expect("Should parse config");
2223 assert_eq!(config.column_align, ColumnAlign::Center);
2224
2225 let toml_str = r#"
2226enabled = true
2227style = "aligned"
2228column-align = "right"
2229"#;
2230 let config: MD060Config = toml::from_str(toml_str).expect("Should parse config");
2231 assert_eq!(config.column_align, ColumnAlign::Right);
2232
2233 let toml_str = r#"
2234enabled = true
2235style = "aligned"
2236column-align = "left"
2237"#;
2238 let config: MD060Config = toml::from_str(toml_str).expect("Should parse config");
2239 assert_eq!(config.column_align, ColumnAlign::Left);
2240
2241 let toml_str = r#"
2242enabled = true
2243style = "aligned"
2244column-align = "auto"
2245"#;
2246 let config: MD060Config = toml::from_str(toml_str).expect("Should parse config");
2247 assert_eq!(config.column_align, ColumnAlign::Auto);
2248 }
2249
2250 #[test]
2251 fn test_md060_column_align_default_is_auto() {
2252 let toml_str = r#"
2254enabled = true
2255style = "aligned"
2256"#;
2257 let config: MD060Config = toml::from_str(toml_str).expect("Should parse config");
2258 assert_eq!(config.column_align, ColumnAlign::Auto);
2259 }
2260
2261 #[test]
2262 fn test_md060_column_align_reformats_already_aligned_table() {
2263 let config = MD060Config {
2265 enabled: true,
2266 style: "aligned".to_string(),
2267 max_width: LineLength::from_const(0),
2268 column_align: ColumnAlign::Right,
2269 column_align_header: None,
2270 column_align_body: None,
2271 loose_last_column: false,
2272 };
2273 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2274
2275 let content = "| Name | Age |\n| ----- | --- |\n| Alice | 30 |\n| Bob | 25 |";
2277 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2278
2279 let fixed = rule.fix(&ctx).unwrap();
2280 let lines: Vec<&str> = fixed.lines().collect();
2281
2282 assert!(
2284 lines[2].contains("| Alice |") && lines[2].contains("| 30 |"),
2285 "Already aligned table should be reformatted with right alignment. Got: {}",
2286 lines[2]
2287 );
2288 assert!(
2289 lines[3].contains("| Bob |") || lines[3].contains("| Bob |"),
2290 "Bob should be right-aligned. Got: {}",
2291 lines[3]
2292 );
2293 }
2294
2295 #[test]
2296 fn test_md060_column_align_with_cjk_characters() {
2297 let config = MD060Config {
2299 enabled: true,
2300 style: "aligned".to_string(),
2301 max_width: LineLength::from_const(0),
2302 column_align: ColumnAlign::Center,
2303 column_align_header: None,
2304 column_align_body: None,
2305 loose_last_column: false,
2306 };
2307 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2308
2309 let content = "| Name | City |\n|---|---|\n| Alice | ζ±δΊ¬ |\n| Bob | LA |";
2310 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2311
2312 let fixed = rule.fix(&ctx).unwrap();
2313
2314 assert!(fixed.contains("Bob"), "Table should contain Bob");
2317 assert!(fixed.contains("ζ±δΊ¬"), "Table should contain ζ±δΊ¬");
2318 }
2319
2320 #[test]
2321 fn test_md060_column_align_ignored_for_compact_style() {
2322 let config = MD060Config {
2324 enabled: true,
2325 style: "compact".to_string(),
2326 max_width: LineLength::from_const(0),
2327 column_align: ColumnAlign::Right, column_align_header: None,
2329 column_align_body: None,
2330 loose_last_column: false,
2331 };
2332 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2333
2334 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |\n| Bob | 25 |";
2335 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2336
2337 let fixed = rule.fix(&ctx).unwrap();
2338
2339 assert!(
2341 fixed.contains("| Alice |"),
2342 "Compact style should have single space padding, not alignment. Got: {fixed}"
2343 );
2344 }
2345
2346 #[test]
2347 fn test_md060_column_align_ignored_for_tight_style() {
2348 let config = MD060Config {
2350 enabled: true,
2351 style: "tight".to_string(),
2352 max_width: LineLength::from_const(0),
2353 column_align: ColumnAlign::Center, column_align_header: None,
2355 column_align_body: None,
2356 loose_last_column: false,
2357 };
2358 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2359
2360 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |\n| Bob | 25 |";
2361 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2362
2363 let fixed = rule.fix(&ctx).unwrap();
2364
2365 assert!(
2367 fixed.contains("|Alice|"),
2368 "Tight style should have no spaces. Got: {fixed}"
2369 );
2370 }
2371
2372 #[test]
2373 fn test_md060_column_align_with_empty_cells() {
2374 let config = MD060Config {
2376 enabled: true,
2377 style: "aligned".to_string(),
2378 max_width: LineLength::from_const(0),
2379 column_align: ColumnAlign::Center,
2380 column_align_header: None,
2381 column_align_body: None,
2382 loose_last_column: false,
2383 };
2384 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2385
2386 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |\n| | 25 |";
2387 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2388
2389 let fixed = rule.fix(&ctx).unwrap();
2390 let lines: Vec<&str> = fixed.lines().collect();
2391
2392 assert!(
2394 lines[3].contains("| |") || lines[3].contains("| |"),
2395 "Empty cell should be padded correctly. Got: {}",
2396 lines[3]
2397 );
2398 }
2399
2400 #[test]
2401 fn test_md060_column_align_auto_preserves_already_aligned() {
2402 let config = MD060Config {
2404 enabled: true,
2405 style: "aligned".to_string(),
2406 max_width: LineLength::from_const(0),
2407 column_align: ColumnAlign::Auto,
2408 column_align_header: None,
2409 column_align_body: None,
2410 loose_last_column: false,
2411 };
2412 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2413
2414 let content = "| Name | Age |\n| ----- | --- |\n| Alice | 30 |\n| Bob | 25 |";
2416 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2417
2418 let fixed = rule.fix(&ctx).unwrap();
2419
2420 assert_eq!(
2422 fixed, content,
2423 "Already aligned table should be preserved with column-align=auto"
2424 );
2425 }
2426}