1use crate::rule::{LintError, LintResult, LintWarning, Rule, RuleCategory, Severity};
2use crate::utils::mdg;
3use crate::utils::range_utils::calculate_line_range;
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 pad_width: usize,
46 column_align: ColumnAlign,
48 column_align_header: Option<ColumnAlign>,
50 column_align_body: Option<ColumnAlign>,
52}
53
54#[derive(Debug, Clone, Default)]
188pub struct MD060TableFormat {
189 config: MD060Config,
190 md013_config: MD013Config,
191 md013_disabled: bool,
192}
193
194impl MD060TableFormat {
195 pub fn new(enabled: bool, style: String) -> Self {
196 use crate::types::LineLength;
197 Self {
198 config: MD060Config {
199 enabled,
200 style,
201 max_width: LineLength::from_const(0),
202 column_align: ColumnAlign::Auto,
203 column_align_header: None,
204 column_align_body: None,
205 loose_last_column: false,
206 aligned_delimiter: false,
207 pad_width: None,
208 },
209 md013_config: MD013Config::default(),
210 md013_disabled: false,
211 }
212 }
213
214 pub fn from_config_struct(config: MD060Config, md013_config: MD013Config, md013_disabled: bool) -> Self {
215 Self {
216 config,
217 md013_config,
218 md013_disabled,
219 }
220 }
221
222 fn default_pad_width(style: &str) -> usize {
226 usize::from(style != "tight")
227 }
228
229 fn pad_width_for(&self, target_style: &str) -> usize {
238 self.config
239 .pad_width
240 .unwrap_or_else(|| Self::default_pad_width(target_style))
241 }
242
243 fn effective_max_width(&self) -> usize {
253 if !self.config.max_width.is_unlimited() {
255 return self.config.max_width.get();
256 }
257
258 if self.md013_disabled || !self.md013_config.tables || self.md013_config.line_length.is_unlimited() {
263 return usize::MAX; }
265
266 self.md013_config.line_length.get()
268 }
269
270 fn contains_problematic_chars(text: &str) -> bool {
281 text.contains('\u{200D}') || text.contains('\u{200B}') || text.contains('\u{200C}') || text.contains('\u{2060}') }
286
287 fn calculate_cell_display_width(cell_content: &str) -> usize {
288 let masked = TableUtils::mask_pipes_in_inline_code(cell_content);
289 masked.trim().width()
290 }
291
292 #[cfg(test)]
295 fn parse_table_row(line: &str) -> Vec<String> {
296 TableUtils::split_table_row(line)
297 }
298
299 fn parse_table_row_with_flavor(line: &str, flavor: crate::config::MarkdownFlavor) -> Vec<String> {
303 TableUtils::split_table_row_with_flavor(line, flavor)
304 }
305
306 fn is_delimiter_row(row: &[String]) -> bool {
307 if row.is_empty() {
308 return false;
309 }
310 row.iter().all(|cell| {
311 let trimmed = cell.trim();
312 !trimmed.is_empty()
315 && trimmed.contains('-')
316 && trimmed.chars().all(|c| c == '-' || c == ':' || c.is_whitespace())
317 })
318 }
319
320 fn extract_blockquote_prefix(line: &str) -> (&str, &str) {
323 TableUtils::extract_blockquote_prefix(line)
324 }
325
326 fn mdg_table_indent(
335 table_lines: &[&str],
336 list_content_indent: Option<usize>,
337 flavor: crate::config::MarkdownFlavor,
338 ) -> Option<String> {
339 if flavor != crate::config::MarkdownFlavor::MDG
340 || !table_lines
341 .iter()
342 .filter(|line| line.contains('|'))
343 .all(|line| mdg::is_table_row(line))
344 {
345 return None;
346 }
347
348 let indent = list_content_indent.unwrap_or(0).max(mdg::MIN_TABLE_INDENT);
349 (indent <= mdg::MAX_TABLE_INDENT).then(|| " ".repeat(indent))
350 }
351
352 fn parse_column_alignments(delimiter_row: &[String]) -> Vec<ColumnAlignment> {
353 delimiter_row
354 .iter()
355 .map(|cell| {
356 let trimmed = cell.trim();
357 let has_left_colon = trimmed.starts_with(':');
358 let has_right_colon = trimmed.ends_with(':');
359
360 match (has_left_colon, has_right_colon) {
361 (true, true) => ColumnAlignment::Center,
362 (false, true) => ColumnAlignment::Right,
363 _ => ColumnAlignment::Left,
364 }
365 })
366 .collect()
367 }
368
369 fn calculate_column_widths(
370 table_lines: &[&str],
371 flavor: crate::config::MarkdownFlavor,
372 loose_last_column: bool,
373 ) -> Vec<usize> {
374 let mut column_widths = Vec::new();
375 let mut delimiter_cells: Option<Vec<String>> = None;
376 let mut is_header = true;
377 let mut header_last_col_width: Option<usize> = None;
378
379 for line in table_lines {
380 let cells = Self::parse_table_row_with_flavor(line, flavor);
381
382 if Self::is_delimiter_row(&cells) {
384 delimiter_cells = Some(cells);
385 is_header = false;
386 continue;
387 }
388
389 for (i, cell) in cells.iter().enumerate() {
390 let width = Self::calculate_cell_display_width(cell);
391 if i >= column_widths.len() {
392 column_widths.push(width);
393 } else {
394 column_widths[i] = column_widths[i].max(width);
395 }
396 }
397
398 if is_header && !cells.is_empty() {
400 let last_idx = cells.len() - 1;
401 header_last_col_width = Some(Self::calculate_cell_display_width(&cells[last_idx]));
402 is_header = false;
403 }
404 }
405
406 if loose_last_column
408 && let Some(header_width) = header_last_col_width
409 && let Some(last) = column_widths.last_mut()
410 {
411 *last = header_width;
412 }
413
414 let mut final_widths: Vec<usize> = column_widths.iter().map(|&w| w.max(3)).collect();
417
418 if let Some(delimiter_cells) = delimiter_cells {
421 for (i, cell) in delimiter_cells.iter().enumerate() {
422 if i < final_widths.len() {
423 let trimmed = cell.trim();
424 let has_left_colon = trimmed.starts_with(':');
425 let has_right_colon = trimmed.ends_with(':');
426 let colon_count = (has_left_colon as usize) + (has_right_colon as usize);
427
428 let min_width_for_delimiter = 3 + colon_count;
430 final_widths[i] = final_widths[i].max(min_width_for_delimiter);
431 }
432 }
433 }
434
435 final_widths
436 }
437
438 fn format_table_row(
439 cells: &[String],
440 column_widths: &[usize],
441 column_alignments: &[ColumnAlignment],
442 options: &RowFormatOptions,
443 ) -> String {
444 let pad = " ".repeat(options.pad_width);
445 let formatted_cells: Vec<String> = cells
446 .iter()
447 .enumerate()
448 .map(|(i, cell)| {
449 let target_width = column_widths.get(i).copied().unwrap_or(0);
450
451 match options.row_type {
452 RowType::Delimiter => {
453 let trimmed = cell.trim();
454 let has_left_colon = trimmed.starts_with(':');
455 let has_right_colon = trimmed.ends_with(':');
456
457 let extra_width = if options.compact_delimiter {
462 2 * options.pad_width
463 } else {
464 0
465 };
466 let dash_count = if has_left_colon && has_right_colon {
467 (target_width + extra_width).saturating_sub(2)
468 } else if has_left_colon || has_right_colon {
469 (target_width + extra_width).saturating_sub(1)
470 } else {
471 target_width + extra_width
472 };
473
474 let dashes = "-".repeat(dash_count.max(3)); let delimiter_content = if has_left_colon && has_right_colon {
476 format!(":{dashes}:")
477 } else if has_left_colon {
478 format!(":{dashes}")
479 } else if has_right_colon {
480 format!("{dashes}:")
481 } else {
482 dashes
483 };
484
485 if options.compact_delimiter {
487 delimiter_content
488 } else {
489 format!("{pad}{delimiter_content}{pad}")
490 }
491 }
492 RowType::Header | RowType::Body => {
493 let trimmed = cell.trim();
494 let current_width = Self::calculate_cell_display_width(cell);
495 let padding = target_width.saturating_sub(current_width);
496
497 let effective_align = match options.row_type {
499 RowType::Header => options.column_align_header.unwrap_or(options.column_align),
500 RowType::Body => options.column_align_body.unwrap_or(options.column_align),
501 RowType::Delimiter => unreachable!(),
502 };
503
504 let alignment = match effective_align {
506 ColumnAlign::Auto => column_alignments.get(i).copied().unwrap_or(ColumnAlignment::Left),
507 ColumnAlign::Left => ColumnAlignment::Left,
508 ColumnAlign::Center => ColumnAlignment::Center,
509 ColumnAlign::Right => ColumnAlignment::Right,
510 };
511
512 match alignment {
513 ColumnAlignment::Left => {
514 format!("{pad}{trimmed}{}{pad}", " ".repeat(padding))
516 }
517 ColumnAlignment::Center => {
518 let left_padding = padding / 2;
520 let right_padding = padding - left_padding;
521 format!(
522 "{pad}{}{trimmed}{}{pad}",
523 " ".repeat(left_padding),
524 " ".repeat(right_padding)
525 )
526 }
527 ColumnAlignment::Right => {
528 format!("{pad}{}{trimmed}{pad}", " ".repeat(padding))
530 }
531 }
532 }
533 }
534 })
535 .collect();
536
537 format!("|{}|", formatted_cells.join("|"))
538 }
539
540 fn format_table_unaligned(cells: &[String], pad_width: usize) -> String {
545 if pad_width == 0 {
546 return Self::format_table_tight(cells);
547 }
548
549 let pad = " ".repeat(pad_width);
550 let formatted_cells: Vec<String> = cells
551 .iter()
552 .map(|cell| match cell.trim() {
553 "" if pad_width == 1 => " ".to_string(),
558 trimmed => format!("{pad}{trimmed}{pad}"),
559 })
560 .collect();
561 format!("|{}|", formatted_cells.join("|"))
562 }
563
564 fn format_table_tight(cells: &[String]) -> String {
565 let formatted_cells: Vec<String> = cells.iter().map(|cell| cell.trim().to_string()).collect();
566 format!("|{}|", formatted_cells.join("|"))
567 }
568
569 fn format_delimiter_aligned_to_header(delim_cells: &[String], header_widths: &[usize], pad_width: usize) -> String {
578 let pad = " ".repeat(pad_width);
579 let formatted_cells: Vec<String> = delim_cells
580 .iter()
581 .enumerate()
582 .map(|(i, cell)| {
583 let target_width = header_widths.get(i).copied().unwrap_or(0);
584 let trimmed = cell.trim();
585 let has_left_colon = trimmed.starts_with(':');
586 let has_right_colon = trimmed.ends_with(':');
587 let colon_count = usize::from(has_left_colon) + usize::from(has_right_colon);
588
589 let dash_count = target_width.saturating_sub(colon_count).max(1);
591 let dashes = "-".repeat(dash_count);
592 let delimiter_content = match (has_left_colon, has_right_colon) {
593 (true, true) => format!(":{dashes}:"),
594 (true, false) => format!(":{dashes}"),
595 (false, true) => format!("{dashes}:"),
596 (false, false) => dashes,
597 };
598 format!("{pad}{delimiter_content}{pad}")
599 })
600 .collect();
601
602 format!("|{}|", formatted_cells.join("|"))
603 }
604
605 fn header_cell_widths(header_cells: &[String]) -> Vec<usize> {
608 header_cells
609 .iter()
610 .map(|c| Self::calculate_cell_display_width(c))
611 .collect()
612 }
613
614 fn is_table_already_aligned(
626 table_lines: &[&str],
627 flavor: crate::config::MarkdownFlavor,
628 compact_delimiter: bool,
629 pad_width: Option<usize>,
630 ) -> bool {
631 if table_lines.len() < 2 {
632 return false;
633 }
634
635 let first_width = UnicodeWidthStr::width(table_lines[0]);
639 if !table_lines
640 .iter()
641 .all(|line| UnicodeWidthStr::width(*line) == first_width)
642 {
643 return false;
644 }
645
646 let parsed: Vec<Vec<String>> = table_lines
648 .iter()
649 .map(|line| Self::parse_table_row_with_flavor(line, flavor))
650 .collect();
651
652 if parsed.is_empty() {
653 return false;
654 }
655
656 let num_columns = parsed[0].len();
657 if !parsed.iter().all(|row| row.len() == num_columns) {
658 return false;
659 }
660
661 if let Some(delimiter_row) = parsed.get(1) {
664 if !Self::is_delimiter_row(delimiter_row) {
665 return false;
666 }
667 for cell in delimiter_row {
669 let trimmed = cell.trim();
670 let dash_count = trimmed.chars().filter(|&c| c == '-').count();
671 if dash_count < 1 {
672 return false;
673 }
674 }
675
676 let delimiter_padded_as_targeted = if let Some(pad_width) = pad_width {
679 let expected_padding = if compact_delimiter { 0 } else { pad_width };
680 delimiter_row.iter().all(|cell| {
681 if cell.trim_matches(' ') != cell.trim() {
682 return false;
683 }
684 let leading = cell.len() - cell.trim_start_matches(' ').len();
685 let trailing = cell.len() - cell.trim_end_matches(' ').len();
686 leading == expected_padding && trailing == expected_padding
687 })
688 } else {
689 let delimiter_has_spaces = delimiter_row
690 .iter()
691 .all(|cell| cell.starts_with(' ') && cell.ends_with(' '));
692 delimiter_has_spaces != compact_delimiter
693 };
694 if !delimiter_padded_as_targeted {
695 return false;
696 }
697 }
698
699 for col_idx in 0..num_columns {
703 let mut widths = Vec::new();
704 for (row_idx, row) in parsed.iter().enumerate() {
705 if row_idx == 1 {
707 continue;
708 }
709 if let Some(cell) = row.get(col_idx) {
710 widths.push(cell.width());
711 }
712 }
713 if !widths.is_empty() && !widths.iter().all(|&w| w == widths[0]) {
715 return false;
716 }
717 }
718
719 if let Some(delimiter_row) = parsed.get(1) {
724 let alignments = Self::parse_column_alignments(delimiter_row);
725 for (col_idx, alignment) in alignments.iter().enumerate() {
726 if *alignment == ColumnAlignment::Left && pad_width.is_none() {
727 continue;
728 }
729 for (row_idx, row) in parsed.iter().enumerate() {
730 if row_idx == 1 {
732 continue;
733 }
734 if let Some(cell) = row.get(col_idx) {
735 if pad_width.is_some() && cell.trim_matches(' ') != cell.trim() {
736 return false;
737 }
738 if cell.trim().is_empty() {
739 continue;
740 }
741 let left_pad = cell.len() - cell.trim_start().len();
743 let right_pad = cell.len() - cell.trim_end().len();
744
745 if let Some(pad_width) = pad_width {
746 let left_pad = cell.len() - cell.trim_start_matches(' ').len();
747 let right_pad = cell.len() - cell.trim_end_matches(' ').len();
748 if left_pad < pad_width || right_pad < pad_width {
751 return false;
752 }
753 if (*alignment == ColumnAlignment::Left && left_pad != pad_width)
754 || (*alignment == ColumnAlignment::Right && right_pad != pad_width)
755 {
756 return false;
757 }
758 }
759
760 match alignment {
761 ColumnAlignment::Center => {
762 if left_pad.abs_diff(right_pad) > 1 {
764 return false;
765 }
766 }
767 ColumnAlignment::Right => {
768 if left_pad < right_pad {
770 return false;
771 }
772 }
773 ColumnAlignment::Left => {}
774 }
775 }
776 }
777 }
778 }
779
780 true
781 }
782
783 fn detect_table_style(
784 table_lines: &[&str],
785 flavor: crate::config::MarkdownFlavor,
786 pad_width: Option<usize>,
787 ) -> Option<String> {
788 if table_lines.is_empty() {
789 return None;
790 }
791
792 let mut is_tight = true;
795 let mut is_compact = true;
796 let configured_pad = pad_width.map(|width| " ".repeat(width));
797
798 for line in table_lines {
799 let cells = Self::parse_table_row_with_flavor(line, flavor);
800
801 if cells.is_empty() {
802 continue;
803 }
804
805 if Self::is_delimiter_row(&cells) {
807 continue;
808 }
809
810 let row_has_no_padding = cells.iter().all(|cell| !cell.starts_with(' ') && !cell.ends_with(' '));
812
813 let row_has_single_space = cells.iter().all(|cell| match cell.trim() {
817 "" => cell == " ",
818 trimmed => cell == &format!(" {trimmed} "),
819 });
820
821 let row_has_configured_padding = configured_pad.as_ref().is_some_and(|pad| {
825 cells.iter().all(|cell| match cell.trim() {
826 "" if pad.len() == 1 => cell == " ",
827 trimmed => cell == &format!("{pad}{trimmed}{pad}"),
828 })
829 });
830
831 if !row_has_no_padding {
833 is_tight = false;
834 }
835
836 if !row_has_single_space && !row_has_configured_padding {
838 is_compact = false;
839 }
840
841 if !is_tight && !is_compact {
843 return Some("aligned".to_string());
844 }
845 }
846
847 if is_tight {
849 Some("tight".to_string())
850 } else if is_compact {
851 Some("compact".to_string())
852 } else {
853 Some("aligned".to_string())
854 }
855 }
856
857 fn fix_table_block(
858 &self,
859 lines: &[&str],
860 table_block: &crate::utils::table_utils::TableBlock,
861 flavor: crate::config::MarkdownFlavor,
862 ) -> TableFormatResult {
863 let mut result = Vec::new();
864 let mut auto_compacted = false;
865 let mut aligned_width = None;
866
867 let table_lines: Vec<&str> = std::iter::once(lines[table_block.header_line])
868 .chain(std::iter::once(lines[table_block.delimiter_line]))
869 .chain(table_block.content_lines.iter().map(|&idx| lines[idx]))
870 .collect();
871
872 if flavor != crate::config::MarkdownFlavor::MDG
875 && TableUtils::header_is_indented_as_code(table_lines[0], table_lines[1], table_block)
876 {
877 return TableFormatResult {
878 lines: table_lines.iter().map(std::string::ToString::to_string).collect(),
879 auto_compacted: false,
880 aligned_width: None,
881 };
882 }
883
884 let (mut blockquote_prefix, _) = Self::extract_blockquote_prefix(table_lines[0]);
887
888 let list_context = &table_block.list_context;
890 if list_context.is_some() {
891 let markers = blockquote_prefix.trim_end_matches([' ', '\t']);
894 blockquote_prefix = if blockquote_prefix.len() > markers.len() {
895 &blockquote_prefix[..=markers.len()]
896 } else {
897 markers
898 };
899 }
900 let header_prefix = if list_context.is_none() {
904 let after_quote = Self::extract_blockquote_prefix(table_lines[0]).1;
905 (!TableUtils::extract_list_prefix(after_quote).0.is_empty())
906 .then(|| TableUtils::row_prefix(table_lines[0], table_lines[0], table_block, 0))
907 } else {
908 None
909 };
910 let (list_prefix, continuation_indent) = if let Some(ctx) = list_context {
911 (ctx.list_prefix.as_str(), " ".repeat(ctx.content_indent))
912 } else {
913 ("", String::new())
914 };
915 let mdg_indent = Self::mdg_table_indent(
916 &table_lines,
917 list_context.as_ref().map(|ctx| ctx.content_indent),
918 flavor,
919 );
920
921 let container_indented = mdg_indent.is_none() && TableUtils::is_container_indented(table_lines[0], table_block);
922
923 let stripped_lines: Vec<&str> = table_lines
925 .iter()
926 .enumerate()
927 .map(|(i, line)| {
928 let after_blockquote = Self::extract_blockquote_prefix(line).1;
929 if mdg_indent.is_some() {
930 after_blockquote.trim_start_matches([' ', '\t'])
931 } else if list_context.is_some() {
932 if i == 0 {
933 after_blockquote.strip_prefix(list_prefix).unwrap_or_else(|| {
935 crate::utils::table_utils::TableUtils::extract_list_prefix(after_blockquote).1
936 })
937 } else {
938 after_blockquote
940 .strip_prefix(&continuation_indent)
941 .unwrap_or(after_blockquote.trim_start())
942 }
943 } else if i == 0 && header_prefix.is_some() {
944 crate::utils::table_utils::TableUtils::extract_table_row_content(line, table_block, 0)
945 } else {
946 after_blockquote
947 }
948 })
949 .collect();
950
951 let formatted_indices: Vec<usize> = stripped_lines
955 .iter()
956 .enumerate()
957 .filter_map(|(i, line)| (i < 2 || line.contains('|')).then_some(i))
958 .collect();
959 if formatted_indices
961 .iter()
962 .any(|&i| Self::contains_problematic_chars(table_lines[i]))
963 {
964 return TableFormatResult {
965 lines: table_lines.iter().map(std::string::ToString::to_string).collect(),
966 auto_compacted: false,
967 aligned_width: None,
968 };
969 }
970 let stripped_lines: Vec<&str> = formatted_indices.iter().map(|&i| stripped_lines[i]).collect();
971
972 let style = self.config.style.as_str();
973
974 match style {
975 "any" => {
976 let detected_style = Self::detect_table_style(&stripped_lines, flavor, self.config.pad_width);
977 if detected_style.is_none() {
978 return TableFormatResult {
979 lines: table_lines.iter().map(std::string::ToString::to_string).collect(),
980 auto_compacted: false,
981 aligned_width: None,
982 };
983 }
984
985 let target_style = detected_style.unwrap();
986 let pad_width = self.pad_width_for(&target_style);
987
988 let delimiter_cells = Self::parse_table_row_with_flavor(stripped_lines[1], flavor);
990 let column_alignments = Self::parse_column_alignments(&delimiter_cells);
991
992 for (row_idx, line) in stripped_lines.iter().enumerate() {
993 let cells = Self::parse_table_row_with_flavor(line, flavor);
994 match target_style.as_str() {
995 "tight" | "compact" => result.push(Self::format_table_unaligned(&cells, pad_width)),
996 _ => {
997 let column_widths =
998 Self::calculate_column_widths(&stripped_lines, flavor, self.config.loose_last_column);
999 let row_type = match row_idx {
1000 0 => RowType::Header,
1001 1 => RowType::Delimiter,
1002 _ => RowType::Body,
1003 };
1004 let options = RowFormatOptions {
1005 row_type,
1006 compact_delimiter: false,
1007 pad_width,
1008 column_align: self.config.column_align,
1009 column_align_header: self.config.column_align_header,
1010 column_align_body: self.config.column_align_body,
1011 };
1012 result.push(Self::format_table_row(
1013 &cells,
1014 &column_widths,
1015 &column_alignments,
1016 &options,
1017 ));
1018 }
1019 }
1020 }
1021 }
1022 "compact" | "tight" => {
1023 let pad_width = self.pad_width_for(style);
1024 let header_widths = if self.config.aligned_delimiter && stripped_lines.len() >= 2 {
1025 let header_cells = Self::parse_table_row_with_flavor(stripped_lines[0], flavor);
1026 Some(Self::header_cell_widths(&header_cells))
1027 } else {
1028 None
1029 };
1030
1031 for (row_idx, line) in stripped_lines.iter().enumerate() {
1032 let cells = Self::parse_table_row_with_flavor(line, flavor);
1033 if row_idx == 1
1034 && let Some(widths) = &header_widths
1035 {
1036 result.push(Self::format_delimiter_aligned_to_header(&cells, widths, pad_width));
1037 continue;
1038 }
1039 result.push(Self::format_table_unaligned(&cells, pad_width));
1040 }
1041 }
1042 "aligned" | "aligned-no-space" => {
1043 let compact_delimiter = style == "aligned-no-space";
1044 let pad_width = self.pad_width_for(style);
1045
1046 let needs_reformat = self.config.column_align != ColumnAlign::Auto
1049 || self.config.column_align_header.is_some()
1050 || self.config.column_align_body.is_some()
1051 || self.config.loose_last_column;
1052
1053 if !needs_reformat
1054 && Self::is_table_already_aligned(&stripped_lines, flavor, compact_delimiter, self.config.pad_width)
1055 {
1056 return TableFormatResult {
1057 lines: table_lines.iter().map(std::string::ToString::to_string).collect(),
1058 auto_compacted: false,
1059 aligned_width: None,
1060 };
1061 }
1062
1063 let column_widths =
1064 Self::calculate_column_widths(&stripped_lines, flavor, self.config.loose_last_column);
1065
1066 let num_columns = column_widths.len();
1069 let calc_aligned_width = 1 + num_columns * (2 * pad_width + 1) + column_widths.iter().sum::<usize>();
1070 aligned_width = Some(calc_aligned_width);
1071
1072 if calc_aligned_width > self.effective_max_width() {
1077 auto_compacted = true;
1078 let compact_pad_width = self.pad_width_for("compact");
1081 let header_widths = if self.config.aligned_delimiter && stripped_lines.len() >= 2 {
1082 let header_cells = Self::parse_table_row_with_flavor(stripped_lines[0], flavor);
1083 Some(Self::header_cell_widths(&header_cells))
1084 } else {
1085 None
1086 };
1087 for (row_idx, line) in stripped_lines.iter().enumerate() {
1088 let cells = Self::parse_table_row_with_flavor(line, flavor);
1089 if row_idx == 1
1090 && let Some(widths) = &header_widths
1091 {
1092 result.push(Self::format_delimiter_aligned_to_header(
1093 &cells,
1094 widths,
1095 compact_pad_width,
1096 ));
1097 continue;
1098 }
1099 result.push(Self::format_table_unaligned(&cells, compact_pad_width));
1100 }
1101 } else {
1102 let delimiter_cells = Self::parse_table_row_with_flavor(stripped_lines[1], flavor);
1104 let column_alignments = Self::parse_column_alignments(&delimiter_cells);
1105
1106 for (row_idx, line) in stripped_lines.iter().enumerate() {
1107 let cells = Self::parse_table_row_with_flavor(line, flavor);
1108 let row_type = match row_idx {
1109 0 => RowType::Header,
1110 1 => RowType::Delimiter,
1111 _ => RowType::Body,
1112 };
1113 let options = RowFormatOptions {
1114 row_type,
1115 compact_delimiter,
1116 pad_width,
1117 column_align: self.config.column_align,
1118 column_align_header: self.config.column_align_header,
1119 column_align_body: self.config.column_align_body,
1120 };
1121 result.push(Self::format_table_row(
1122 &cells,
1123 &column_widths,
1124 &column_alignments,
1125 &options,
1126 ));
1127 }
1128 }
1129 }
1130 _ => {
1131 return TableFormatResult {
1132 lines: table_lines.iter().map(std::string::ToString::to_string).collect(),
1133 auto_compacted: false,
1134 aligned_width: None,
1135 };
1136 }
1137 }
1138
1139 for (row_idx, (original, formatted)) in stripped_lines.iter().zip(&result).enumerate() {
1144 if row_idx == 1 {
1145 continue; }
1147 let original_cells = Self::parse_table_row_with_flavor(original, flavor);
1148 let formatted_cells = Self::parse_table_row_with_flavor(formatted, flavor);
1149 if !original_cells
1150 .iter()
1151 .map(|cell| cell.trim())
1152 .eq(formatted_cells.iter().map(|cell| cell.trim()))
1153 {
1154 return TableFormatResult {
1155 lines: table_lines.iter().map(std::string::ToString::to_string).collect(),
1156 auto_compacted: false,
1157 aligned_width: None,
1158 };
1159 }
1160 }
1161
1162 let prefixed_result: Vec<String> = result
1164 .into_iter()
1165 .enumerate()
1166 .map(|(i, line)| {
1167 if let Some(mdg_indent) = &mdg_indent {
1168 format!("{blockquote_prefix}{mdg_indent}{line}")
1169 } else if list_context.is_some() {
1170 if i == 0 {
1171 format!("{blockquote_prefix}{list_prefix}{line}")
1173 } else {
1174 format!("{blockquote_prefix}{continuation_indent}{line}")
1176 }
1177 } else if let Some(header_prefix) = header_prefix.filter(|_| i == 0) {
1178 format!("{header_prefix}{line}")
1179 } else if header_prefix.is_some() {
1180 let own = Self::extract_blockquote_prefix(table_lines[i]).0;
1181 format!("{own}{line}")
1182 } else if container_indented && blockquote_prefix.is_empty() {
1183 let own = table_lines[formatted_indices[i]];
1185 let indent = &own[..own.len() - own.trim_start_matches([' ', '\t']).len()];
1186 format!("{indent}{line}")
1187 } else {
1188 format!("{blockquote_prefix}{line}")
1189 }
1190 })
1191 .collect();
1192
1193 let mut output: Vec<String> = table_lines.iter().map(std::string::ToString::to_string).collect();
1194 for (index, formatted) in formatted_indices.into_iter().zip(prefixed_result) {
1195 output[index] = formatted;
1196 }
1197
1198 TableFormatResult {
1199 lines: output,
1200 auto_compacted,
1201 aligned_width,
1202 }
1203 }
1204}
1205
1206impl Rule for MD060TableFormat {
1207 fn name(&self) -> &'static str {
1208 "MD060"
1209 }
1210
1211 fn description(&self) -> &'static str {
1212 "Table columns should be consistently aligned"
1213 }
1214
1215 fn category(&self) -> RuleCategory {
1216 RuleCategory::Table
1217 }
1218
1219 fn should_skip(&self, ctx: &crate::lint_context::LintContext) -> bool {
1220 !ctx.likely_has_tables()
1221 }
1222
1223 fn check(&self, ctx: &crate::lint_context::LintContext) -> LintResult {
1224 let mut warnings = Vec::new();
1225
1226 let lines = ctx.raw_lines();
1227 let table_blocks = &ctx.table_blocks;
1228
1229 for table_block in table_blocks.iter().filter(|block| block.parser_confirmed) {
1230 let format_result = self.fix_table_block(lines, table_block, ctx.flavor);
1231
1232 let table_line_indices: Vec<usize> = std::iter::once(table_block.header_line)
1233 .chain(std::iter::once(table_block.delimiter_line))
1234 .chain(table_block.content_lines.iter().copied())
1235 .collect();
1236
1237 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());
1244 for (i, &line_idx) in table_line_indices.iter().enumerate() {
1245 let fixed_line = &format_result.lines[i];
1246 if line_idx < lines.len() - 1 || ctx.content.ends_with('\n') {
1248 fixed_table_lines.push(format!("{fixed_line}\n"));
1249 } else {
1250 fixed_table_lines.push(fixed_line.clone());
1251 }
1252 }
1253 let table_replacement = fixed_table_lines.concat();
1254 let table_range = ctx.line_span_byte_range(table_start_line, table_end_line);
1255
1256 for (i, &line_idx) in table_line_indices.iter().enumerate() {
1257 let original = lines[line_idx];
1258 let fixed = &format_result.lines[i];
1259
1260 if original != fixed {
1261 let (start_line, start_col, end_line, end_col) = calculate_line_range(line_idx + 1, original);
1262
1263 let message = if format_result.auto_compacted {
1264 if let Some(width) = format_result.aligned_width {
1265 format!(
1266 "Table too wide for aligned formatting ({} chars > max-width: {})",
1267 width,
1268 self.effective_max_width()
1269 )
1270 } else {
1271 "Table too wide for aligned formatting".to_string()
1272 }
1273 } else {
1274 "Table columns should be aligned".to_string()
1275 };
1276
1277 warnings.push(LintWarning {
1280 rule_name: Some(self.name().to_string()),
1281 severity: Severity::Warning,
1282 message,
1283 line: start_line,
1284 column: start_col,
1285 end_line,
1286 end_column: end_col,
1287 fix: Some(crate::rule::Fix::new(table_range.clone(), table_replacement.clone())),
1288 });
1289 }
1290 }
1291 }
1292
1293 Ok(warnings)
1294 }
1295
1296 fn fix(&self, ctx: &crate::lint_context::LintContext) -> Result<String, LintError> {
1297 let content = ctx.content;
1298 let lines = ctx.raw_lines();
1299 let table_blocks = &ctx.table_blocks;
1300
1301 if !table_blocks.iter().any(|block| block.parser_confirmed) {
1304 return Ok(content.to_string());
1305 }
1306
1307 let mut result_lines: Vec<String> = lines.iter().map(|&s| s.to_string()).collect();
1308
1309 for table_block in table_blocks.iter().filter(|block| block.parser_confirmed) {
1310 let format_result = self.fix_table_block(lines, table_block, ctx.flavor);
1311
1312 let table_line_indices: Vec<usize> = std::iter::once(table_block.header_line)
1313 .chain(std::iter::once(table_block.delimiter_line))
1314 .chain(table_block.content_lines.iter().copied())
1315 .collect();
1316
1317 let any_disabled = table_line_indices
1320 .iter()
1321 .any(|&line_idx| ctx.inline_config().is_rule_disabled(self.name(), line_idx + 1));
1322
1323 if any_disabled {
1324 continue;
1325 }
1326
1327 for (i, &line_idx) in table_line_indices.iter().enumerate() {
1328 result_lines[line_idx].clone_from(&format_result.lines[i]);
1329 }
1330 }
1331
1332 let mut fixed = result_lines.join("\n");
1333 let original_trailing_newlines = content.len() - content.trim_end_matches('\n').len();
1338 fixed.truncate(fixed.trim_end_matches('\n').len());
1339 fixed.push_str(&"\n".repeat(original_trailing_newlines));
1340 Ok(fixed)
1341 }
1342
1343 fn as_any(&self) -> &dyn std::any::Any {
1344 self
1345 }
1346
1347 crate::impl_rule_config_sections!(MD060Config);
1348
1349 fn from_config(config: &crate::config::Config) -> Box<dyn Rule>
1350 where
1351 Self: Sized,
1352 {
1353 let rule_config = crate::rule_config_serde::load_rule_config::<MD060Config>(config);
1354 let md013_config = MD013Config::from_document_config(config);
1355
1356 let md013_disabled = config.global.disable.iter().any(|r| r == "MD013");
1358
1359 Box::new(Self::from_config_struct(rule_config, md013_config, md013_disabled))
1360 }
1361}
1362
1363#[cfg(test)]
1364mod tests {
1365 use super::*;
1366 use crate::lint_context::LintContext;
1367 use crate::types::LineLength;
1368
1369 fn md013_with_line_length(line_length: usize) -> MD013Config {
1371 MD013Config {
1372 line_length: LineLength::from_const(line_length),
1373 tables: true, ..Default::default()
1375 }
1376 }
1377
1378 #[test]
1379 fn test_md060_align_simple_ascii_table() {
1380 let rule = MD060TableFormat::new(true, "aligned".to_string());
1381
1382 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
1383 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1384
1385 let fixed = rule.fix(&ctx).unwrap();
1386 let expected = "| Name | Age |\n| ----- | --- |\n| Alice | 30 |";
1387 assert_eq!(fixed, expected);
1388
1389 let lines: Vec<&str> = fixed.lines().collect();
1391 assert_eq!(lines[0].len(), lines[1].len());
1392 assert_eq!(lines[1].len(), lines[2].len());
1393 }
1394
1395 #[test]
1396 fn table_quick_fix_preserves_final_newline_like_the_native_fixer() {
1397 let rule = MD060TableFormat::new(true, "aligned".to_string());
1398 for newline in ["", "\n", "\n\n", "\r\n"] {
1399 let input = format!("|Name|Value|\n|---|---|\n|Zulu|2|{newline}");
1400 let ctx = LintContext::new(&input, crate::config::MarkdownFlavor::Standard, None);
1401 let warnings = rule.check(&ctx).unwrap();
1402 assert!(!warnings.is_empty());
1403 let edit = warnings[0].fix.as_ref().unwrap();
1406 let mut fixed = input.clone();
1407 fixed.replace_range(edit.range.clone(), &edit.replacement);
1408 assert_eq!(fixed, rule.fix(&ctx).unwrap());
1409 }
1410 }
1411
1412 #[test]
1413 fn test_md060_cjk_characters_aligned_correctly() {
1414 let rule = MD060TableFormat::new(true, "aligned".to_string());
1415
1416 let content = "| Name | Age |\n|---|---|\n| 中文 | 30 |";
1417 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1418
1419 let fixed = rule.fix(&ctx).unwrap();
1420
1421 let lines: Vec<&str> = fixed.lines().collect();
1422 let cells_line1 = MD060TableFormat::parse_table_row(lines[0]);
1423 let cells_line3 = MD060TableFormat::parse_table_row(lines[2]);
1424
1425 let width1 = MD060TableFormat::calculate_cell_display_width(&cells_line1[0]);
1426 let width3 = MD060TableFormat::calculate_cell_display_width(&cells_line3[0]);
1427
1428 assert_eq!(width1, width3);
1429 }
1430
1431 #[test]
1432 fn test_md060_basic_emoji() {
1433 let rule = MD060TableFormat::new(true, "aligned".to_string());
1434
1435 let content = "| Status | Name |\n|---|---|\n| ✅ | Test |";
1436 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1437
1438 let fixed = rule.fix(&ctx).unwrap();
1439 assert!(fixed.contains("Status"));
1440 }
1441
1442 #[test]
1443 fn test_md060_zwj_emoji_skipped() {
1444 let rule = MD060TableFormat::new(true, "aligned".to_string());
1445
1446 let content = "| Emoji | Name |\n|---|---|\n| 👨👩👧👦 | Family |";
1447 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1448
1449 let fixed = rule.fix(&ctx).unwrap();
1450 assert_eq!(fixed, content);
1451 }
1452
1453 #[test]
1454 fn test_md060_inline_code_with_escaped_pipes() {
1455 let rule = MD060TableFormat::new(true, "aligned".to_string());
1458
1459 let content = "| Pattern | Regex |\n|---|---|\n| Time | `[0-9]\\|[0-9]` |";
1461 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1462
1463 let fixed = rule.fix(&ctx).unwrap();
1464 assert!(fixed.contains(r"`[0-9]\|[0-9]`"), "Escaped pipes should be preserved");
1465 }
1466
1467 #[test]
1468 fn test_md060_compact_style() {
1469 let rule = MD060TableFormat::new(true, "compact".to_string());
1470
1471 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
1472 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1473
1474 let fixed = rule.fix(&ctx).unwrap();
1475 let expected = "| Name | Age |\n| --- | --- |\n| Alice | 30 |";
1476 assert_eq!(fixed, expected);
1477 }
1478
1479 #[test]
1480 fn test_md060_tight_style() {
1481 let rule = MD060TableFormat::new(true, "tight".to_string());
1482
1483 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
1484 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1485
1486 let fixed = rule.fix(&ctx).unwrap();
1487 let expected = "|Name|Age|\n|---|---|\n|Alice|30|";
1488 assert_eq!(fixed, expected);
1489 }
1490
1491 #[test]
1492 fn test_md060_aligned_no_space_style() {
1493 let rule = MD060TableFormat::new(true, "aligned-no-space".to_string());
1495
1496 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
1497 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1498
1499 let fixed = rule.fix(&ctx).unwrap();
1500
1501 let lines: Vec<&str> = fixed.lines().collect();
1503 assert_eq!(lines[0], "| Name | Age |", "Header should have spaces around content");
1504 assert_eq!(
1505 lines[1], "|-------|-----|",
1506 "Delimiter should have NO spaces around dashes"
1507 );
1508 assert_eq!(lines[2], "| Alice | 30 |", "Content should have spaces around content");
1509
1510 assert_eq!(lines[0].len(), lines[1].len());
1512 assert_eq!(lines[1].len(), lines[2].len());
1513 }
1514
1515 #[test]
1516 fn test_md060_aligned_no_space_preserves_alignment_indicators() {
1517 let rule = MD060TableFormat::new(true, "aligned-no-space".to_string());
1519
1520 let content = "| Left | Center | Right |\n|:---|:---:|---:|\n| A | B | C |";
1521 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1522
1523 let fixed = rule.fix(&ctx).unwrap();
1524 let lines: Vec<&str> = fixed.lines().collect();
1525
1526 assert!(
1528 fixed.contains("|:"),
1529 "Should have left alignment indicator adjacent to pipe"
1530 );
1531 assert!(
1532 fixed.contains(":|"),
1533 "Should have right alignment indicator adjacent to pipe"
1534 );
1535 assert!(
1537 lines[1].contains(":---") && lines[1].contains("---:"),
1538 "Should have center alignment colons"
1539 );
1540 }
1541
1542 #[test]
1543 fn test_md060_aligned_no_space_three_column_table() {
1544 let rule = MD060TableFormat::new(true, "aligned-no-space".to_string());
1546
1547 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 |";
1548 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1549
1550 let fixed = rule.fix(&ctx).unwrap();
1551 let lines: Vec<&str> = fixed.lines().collect();
1552
1553 assert!(lines[1].starts_with("|---"), "Delimiter should start with |---");
1555 assert!(lines[1].ends_with("---|"), "Delimiter should end with ---|");
1556 assert!(!lines[1].contains("| -"), "Delimiter should NOT have space after pipe");
1557 assert!(!lines[1].contains("- |"), "Delimiter should NOT have space before pipe");
1558 }
1559
1560 #[test]
1561 fn test_md060_aligned_no_space_auto_compacts_wide_tables() {
1562 let config = MD060Config {
1564 enabled: true,
1565 style: "aligned-no-space".to_string(),
1566 max_width: LineLength::from_const(50),
1567 column_align: ColumnAlign::Auto,
1568 column_align_header: None,
1569 column_align_body: None,
1570 loose_last_column: false,
1571 aligned_delimiter: false,
1572 pad_width: None,
1573 };
1574 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false);
1575
1576 let content = "| Very Long Column Header A | Very Long Column Header B | Very Long Column Header C |\n|---|---|---|\n| x | y | z |";
1578 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1579
1580 let fixed = rule.fix(&ctx).unwrap();
1581
1582 assert!(
1584 fixed.contains("| --- |"),
1585 "Should be compact format when exceeding max-width"
1586 );
1587 }
1588
1589 #[test]
1590 fn test_md060_aligned_no_space_cjk_characters() {
1591 let rule = MD060TableFormat::new(true, "aligned-no-space".to_string());
1593
1594 let content = "| Name | City |\n|---|---|\n| 中文 | 東京 |";
1595 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1596
1597 let fixed = rule.fix(&ctx).unwrap();
1598 let lines: Vec<&str> = fixed.lines().collect();
1599
1600 use unicode_width::UnicodeWidthStr;
1603 assert_eq!(
1604 lines[0].width(),
1605 lines[1].width(),
1606 "Header and delimiter should have same display width"
1607 );
1608 assert_eq!(
1609 lines[1].width(),
1610 lines[2].width(),
1611 "Delimiter and content should have same display width"
1612 );
1613
1614 assert!(!lines[1].contains("| -"), "Delimiter should NOT have space after pipe");
1616 }
1617
1618 #[test]
1619 fn test_md060_aligned_no_space_minimum_width() {
1620 let rule = MD060TableFormat::new(true, "aligned-no-space".to_string());
1622
1623 let content = "| A | B |\n|-|-|\n| 1 | 2 |";
1624 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1625
1626 let fixed = rule.fix(&ctx).unwrap();
1627 let lines: Vec<&str> = fixed.lines().collect();
1628
1629 assert!(lines[1].contains("---"), "Should have minimum 3 dashes");
1631 assert_eq!(lines[0].len(), lines[1].len());
1633 assert_eq!(lines[1].len(), lines[2].len());
1634 }
1635
1636 #[test]
1637 fn test_md060_any_style_consistency() {
1638 let rule = MD060TableFormat::new(true, "any".to_string());
1639
1640 let content = "| Name | Age |\n| --- | --- |\n| Alice | 30 |";
1642 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1643
1644 let fixed = rule.fix(&ctx).unwrap();
1645 assert_eq!(fixed, content);
1646
1647 let content_aligned = "| Name | Age |\n| ----- | --- |\n| Alice | 30 |";
1649 let ctx_aligned = LintContext::new(content_aligned, crate::config::MarkdownFlavor::Standard, None);
1650
1651 let fixed_aligned = rule.fix(&ctx_aligned).unwrap();
1652 assert_eq!(fixed_aligned, content_aligned);
1653 }
1654
1655 #[test]
1656 fn test_md060_empty_cells() {
1657 let rule = MD060TableFormat::new(true, "aligned".to_string());
1658
1659 let content = "| A | B |\n|---|---|\n| | X |";
1660 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1661
1662 let fixed = rule.fix(&ctx).unwrap();
1663 assert!(fixed.contains('|'));
1664 }
1665
1666 #[test]
1667 fn test_md060_mixed_content() {
1668 let rule = MD060TableFormat::new(true, "aligned".to_string());
1669
1670 let content = "| Name | Age | City |\n|---|---|---|\n| 中文 | 30 | NYC |";
1671 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1672
1673 let fixed = rule.fix(&ctx).unwrap();
1674 assert!(fixed.contains("中文"));
1675 assert!(fixed.contains("NYC"));
1676 }
1677
1678 #[test]
1679 fn test_md060_preserve_alignment_indicators() {
1680 let rule = MD060TableFormat::new(true, "aligned".to_string());
1681
1682 let content = "| Left | Center | Right |\n|:---|:---:|---:|\n| A | B | C |";
1683 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1684
1685 let fixed = rule.fix(&ctx).unwrap();
1686
1687 assert!(fixed.contains(":---"), "Should contain left alignment");
1688 assert!(fixed.contains(":----:"), "Should contain center alignment");
1689 assert!(fixed.contains("----:"), "Should contain right alignment");
1690 }
1691
1692 #[test]
1693 fn test_md060_minimum_column_width() {
1694 let rule = MD060TableFormat::new(true, "aligned".to_string());
1695
1696 let content = "| ID | Name |\n|-|-|\n| 1 | A |";
1699 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1700
1701 let fixed = rule.fix(&ctx).unwrap();
1702
1703 let lines: Vec<&str> = fixed.lines().collect();
1704 assert_eq!(lines[0].len(), lines[1].len());
1705 assert_eq!(lines[1].len(), lines[2].len());
1706
1707 assert!(fixed.contains("ID "), "Short content should be padded");
1709 assert!(fixed.contains("---"), "Delimiter should have at least 3 dashes");
1710 }
1711
1712 #[test]
1713 fn test_md060_auto_compact_exceeds_default_threshold() {
1714 let config = MD060Config {
1716 enabled: true,
1717 style: "aligned".to_string(),
1718 max_width: LineLength::from_const(0),
1719 column_align: ColumnAlign::Auto,
1720 column_align_header: None,
1721 column_align_body: None,
1722 loose_last_column: false,
1723 aligned_delimiter: false,
1724 pad_width: None,
1725 };
1726 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false);
1727
1728 let content = "| Very Long Column Header | Another Long Header | Third Very Long Header Column |\n|---|---|---|\n| Short | Data | Here |";
1732 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1733
1734 let fixed = rule.fix(&ctx).unwrap();
1735
1736 assert!(fixed.contains("| Very Long Column Header | Another Long Header | Third Very Long Header Column |"));
1738 assert!(fixed.contains("| --- | --- | --- |"));
1739 assert!(fixed.contains("| Short | Data | Here |"));
1740
1741 let lines: Vec<&str> = fixed.lines().collect();
1743 assert!(lines[0].len() != lines[1].len() || lines[1].len() != lines[2].len());
1745 }
1746
1747 #[test]
1748 fn test_md060_auto_compact_exceeds_explicit_threshold() {
1749 let config = MD060Config {
1751 enabled: true,
1752 style: "aligned".to_string(),
1753 max_width: LineLength::from_const(50),
1754 column_align: ColumnAlign::Auto,
1755 column_align_header: None,
1756 column_align_body: None,
1757 loose_last_column: false,
1758 aligned_delimiter: false,
1759 pad_width: None,
1760 };
1761 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 |";
1767 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1768
1769 let fixed = rule.fix(&ctx).unwrap();
1770
1771 assert!(
1773 fixed.contains("| Very Long Column Header A | Very Long Column Header B | Very Long Column Header C |")
1774 );
1775 assert!(fixed.contains("| --- | --- | --- |"));
1776 assert!(fixed.contains("| Data | Data | Data |"));
1777
1778 let lines: Vec<&str> = fixed.lines().collect();
1780 assert!(lines[0].len() != lines[2].len());
1781 }
1782
1783 #[test]
1784 fn test_md060_stays_aligned_under_threshold() {
1785 let config = MD060Config {
1787 enabled: true,
1788 style: "aligned".to_string(),
1789 max_width: LineLength::from_const(100),
1790 column_align: ColumnAlign::Auto,
1791 column_align_header: None,
1792 column_align_body: None,
1793 loose_last_column: false,
1794 aligned_delimiter: false,
1795 pad_width: None,
1796 };
1797 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false);
1798
1799 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
1801 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1802
1803 let fixed = rule.fix(&ctx).unwrap();
1804
1805 let expected = "| Name | Age |\n| ----- | --- |\n| Alice | 30 |";
1807 assert_eq!(fixed, expected);
1808
1809 let lines: Vec<&str> = fixed.lines().collect();
1810 assert_eq!(lines[0].len(), lines[1].len());
1811 assert_eq!(lines[1].len(), lines[2].len());
1812 }
1813
1814 #[test]
1815 fn test_md060_width_calculation_formula() {
1816 let config = MD060Config {
1818 enabled: true,
1819 style: "aligned".to_string(),
1820 max_width: LineLength::from_const(0),
1821 column_align: ColumnAlign::Auto,
1822 column_align_header: None,
1823 column_align_body: None,
1824 loose_last_column: false,
1825 aligned_delimiter: false,
1826 pad_width: None,
1827 };
1828 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(30), false);
1829
1830 let content = "| AAAAA | BBBBB | CCCCC |\n|---|---|---|\n| AAAAA | BBBBB | CCCCC |";
1834 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1835
1836 let fixed = rule.fix(&ctx).unwrap();
1837
1838 let lines: Vec<&str> = fixed.lines().collect();
1840 assert_eq!(lines[0].len(), lines[1].len());
1841 assert_eq!(lines[1].len(), lines[2].len());
1842 assert_eq!(lines[0].len(), 25); let config_tight = MD060Config {
1846 enabled: true,
1847 style: "aligned".to_string(),
1848 max_width: LineLength::from_const(24),
1849 column_align: ColumnAlign::Auto,
1850 column_align_header: None,
1851 column_align_body: None,
1852 loose_last_column: false,
1853 aligned_delimiter: false,
1854 pad_width: None,
1855 };
1856 let rule_tight = MD060TableFormat::from_config_struct(config_tight, md013_with_line_length(80), false);
1857
1858 let fixed_compact = rule_tight.fix(&ctx).unwrap();
1859
1860 assert!(fixed_compact.contains("| AAAAA | BBBBB | CCCCC |"));
1862 assert!(fixed_compact.contains("| --- | --- | --- |"));
1863 }
1864
1865 #[test]
1866 fn test_md060_very_wide_table_auto_compacts() {
1867 let config = MD060Config {
1868 enabled: true,
1869 style: "aligned".to_string(),
1870 max_width: LineLength::from_const(0),
1871 column_align: ColumnAlign::Auto,
1872 column_align_header: None,
1873 column_align_body: None,
1874 loose_last_column: false,
1875 aligned_delimiter: false,
1876 pad_width: None,
1877 };
1878 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false);
1879
1880 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 |";
1884 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1885
1886 let fixed = rule.fix(&ctx).unwrap();
1887
1888 assert!(fixed.contains("| Column One A | Column Two B | Column Three | Column Four D | Column Five E | Column Six FG | Column Seven | Column Eight |"));
1890 assert!(fixed.contains("| --- | --- | --- | --- | --- | --- | --- | --- |"));
1891 }
1892
1893 #[test]
1894 fn test_md060_inherit_from_md013_line_length() {
1895 let config = MD060Config {
1897 enabled: true,
1898 style: "aligned".to_string(),
1899 max_width: LineLength::from_const(0), column_align: ColumnAlign::Auto,
1901 column_align_header: None,
1902 column_align_body: None,
1903 loose_last_column: false,
1904 aligned_delimiter: false,
1905 pad_width: None,
1906 };
1907
1908 let rule_80 = MD060TableFormat::from_config_struct(config.clone(), md013_with_line_length(80), false);
1910 let rule_120 = MD060TableFormat::from_config_struct(config.clone(), md013_with_line_length(120), false);
1911
1912 let content = "| Column Header A | Column Header B | Column Header C |\n|---|---|---|\n| Some Data | More Data | Even More |";
1914 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1915
1916 let _fixed_80 = rule_80.fix(&ctx).unwrap();
1918
1919 let fixed_120 = rule_120.fix(&ctx).unwrap();
1921
1922 let lines_120: Vec<&str> = fixed_120.lines().collect();
1924 assert_eq!(lines_120[0].len(), lines_120[1].len());
1925 assert_eq!(lines_120[1].len(), lines_120[2].len());
1926 }
1927
1928 #[test]
1929 fn test_md060_edge_case_exactly_at_threshold() {
1930 let config = MD060Config {
1934 enabled: true,
1935 style: "aligned".to_string(),
1936 max_width: LineLength::from_const(17),
1937 column_align: ColumnAlign::Auto,
1938 column_align_header: None,
1939 column_align_body: None,
1940 loose_last_column: false,
1941 aligned_delimiter: false,
1942 pad_width: None,
1943 };
1944 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false);
1945
1946 let content = "| AAAAA | BBBBB |\n|---|---|\n| AAAAA | BBBBB |";
1947 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1948
1949 let fixed = rule.fix(&ctx).unwrap();
1950
1951 let lines: Vec<&str> = fixed.lines().collect();
1953 assert_eq!(lines[0].len(), 17);
1954 assert_eq!(lines[0].len(), lines[1].len());
1955 assert_eq!(lines[1].len(), lines[2].len());
1956
1957 let config_under = MD060Config {
1959 enabled: true,
1960 style: "aligned".to_string(),
1961 max_width: LineLength::from_const(16),
1962 column_align: ColumnAlign::Auto,
1963 column_align_header: None,
1964 column_align_body: None,
1965 loose_last_column: false,
1966 aligned_delimiter: false,
1967 pad_width: None,
1968 };
1969 let rule_under = MD060TableFormat::from_config_struct(config_under, md013_with_line_length(80), false);
1970
1971 let fixed_compact = rule_under.fix(&ctx).unwrap();
1972
1973 assert!(fixed_compact.contains("| AAAAA | BBBBB |"));
1975 assert!(fixed_compact.contains("| --- | --- |"));
1976 }
1977
1978 #[test]
1979 fn test_md060_auto_compact_warning_message() {
1980 let config = MD060Config {
1982 enabled: true,
1983 style: "aligned".to_string(),
1984 max_width: LineLength::from_const(50),
1985 column_align: ColumnAlign::Auto,
1986 column_align_header: None,
1987 column_align_body: None,
1988 loose_last_column: false,
1989 aligned_delimiter: false,
1990 pad_width: None,
1991 };
1992 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false);
1993
1994 let content = "| Very Long Column Header A | Very Long Column Header B | Very Long Column Header C |\n|---|---|---|\n| Data | Data | Data |";
1996 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1997
1998 let warnings = rule.check(&ctx).unwrap();
1999
2000 assert!(!warnings.is_empty(), "Should generate warnings");
2002
2003 let auto_compact_warnings: Vec<_> = warnings
2004 .iter()
2005 .filter(|w| w.message.contains("too wide for aligned formatting"))
2006 .collect();
2007
2008 assert!(!auto_compact_warnings.is_empty(), "Should have auto-compact warning");
2009
2010 let first_warning = auto_compact_warnings[0];
2012 assert!(first_warning.message.contains("85 chars > max-width: 50"));
2013 assert!(first_warning.message.contains("Table too wide for aligned formatting"));
2014 }
2015
2016 #[test]
2017 fn test_md060_issue_129_detect_style_from_all_rows() {
2018 let rule = MD060TableFormat::new(true, "any".to_string());
2022
2023 let content = "| a long heading | another long heading |\n\
2025 | -------------- | -------------------- |\n\
2026 | a | 1 |\n\
2027 | b b | 2 |\n\
2028 | c c c | 3 |";
2029 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2030
2031 let fixed = rule.fix(&ctx).unwrap();
2032
2033 assert!(
2035 fixed.contains("| a | 1 |"),
2036 "Should preserve aligned padding in first content row"
2037 );
2038 assert!(
2039 fixed.contains("| b b | 2 |"),
2040 "Should preserve aligned padding in second content row"
2041 );
2042 assert!(
2043 fixed.contains("| c c c | 3 |"),
2044 "Should preserve aligned padding in third content row"
2045 );
2046
2047 assert_eq!(fixed, content, "Table should be detected as aligned and preserved");
2049 }
2050
2051 #[test]
2052 fn test_md060_regular_alignment_warning_message() {
2053 let config = MD060Config {
2055 enabled: true,
2056 style: "aligned".to_string(),
2057 max_width: LineLength::from_const(100), column_align: ColumnAlign::Auto,
2059 column_align_header: None,
2060 column_align_body: None,
2061 loose_last_column: false,
2062 aligned_delimiter: false,
2063 pad_width: None,
2064 };
2065 let rule = MD060TableFormat::from_config_struct(config, md013_with_line_length(80), false);
2066
2067 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |";
2069 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2070
2071 let warnings = rule.check(&ctx).unwrap();
2072
2073 assert!(!warnings.is_empty(), "Should generate warnings");
2075
2076 assert!(warnings[0].message.contains("Table columns should be aligned"));
2078 assert!(!warnings[0].message.contains("too wide"));
2079 assert!(!warnings[0].message.contains("max-width"));
2080 }
2081
2082 #[test]
2085 fn test_md060_unlimited_when_md013_disabled() {
2086 let config = MD060Config {
2088 enabled: true,
2089 style: "aligned".to_string(),
2090 max_width: LineLength::from_const(0), column_align: ColumnAlign::Auto,
2092 column_align_header: None,
2093 column_align_body: None,
2094 loose_last_column: false,
2095 aligned_delimiter: false,
2096 pad_width: None,
2097 };
2098 let md013_config = MD013Config::default();
2099 let rule = MD060TableFormat::from_config_struct(config, md013_config, true );
2100
2101 let content = "| Very Long Column Header A | Very Long Column Header B | Very Long Column Header C |\n|---|---|---|\n| data | data | data |";
2103 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2104 let fixed = rule.fix(&ctx).unwrap();
2105
2106 let lines: Vec<&str> = fixed.lines().collect();
2108 assert_eq!(
2110 lines[0].len(),
2111 lines[1].len(),
2112 "Table should be aligned when MD013 is disabled"
2113 );
2114 }
2115
2116 #[test]
2117 fn test_md060_unlimited_when_md013_tables_false() {
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 aligned_delimiter: false,
2128 pad_width: None,
2129 };
2130 let md013_config = MD013Config {
2131 tables: false, line_length: LineLength::from_const(80),
2133 ..Default::default()
2134 };
2135 let rule = MD060TableFormat::from_config_struct(config, md013_config, false);
2136
2137 let content = "| Very Long Header A | Very Long Header B | Very Long Header C |\n|---|---|---|\n| x | y | z |";
2139 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2140 let fixed = rule.fix(&ctx).unwrap();
2141
2142 let lines: Vec<&str> = fixed.lines().collect();
2144 assert_eq!(
2145 lines[0].len(),
2146 lines[1].len(),
2147 "Table should be aligned when MD013.tables=false"
2148 );
2149 }
2150
2151 #[test]
2152 fn test_md060_unlimited_when_md013_line_length_zero() {
2153 let config = MD060Config {
2155 enabled: true,
2156 style: "aligned".to_string(),
2157 max_width: LineLength::from_const(0),
2158 column_align: ColumnAlign::Auto,
2159 column_align_header: None,
2160 column_align_body: None,
2161 loose_last_column: false,
2162 aligned_delimiter: false,
2163 pad_width: None,
2164 };
2165 let md013_config = MD013Config {
2166 tables: true,
2167 line_length: LineLength::from_const(0), ..Default::default()
2169 };
2170 let rule = MD060TableFormat::from_config_struct(config, md013_config, false);
2171
2172 let content = "| Very Long Header | Another Long Header | Third Long Header |\n|---|---|---|\n| x | y | z |";
2174 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2175 let fixed = rule.fix(&ctx).unwrap();
2176
2177 let lines: Vec<&str> = fixed.lines().collect();
2179 assert_eq!(
2180 lines[0].len(),
2181 lines[1].len(),
2182 "Table should be aligned when MD013.line_length=0"
2183 );
2184 }
2185
2186 #[test]
2187 fn test_md060_explicit_max_width_overrides_md013_settings() {
2188 let config = MD060Config {
2190 enabled: true,
2191 style: "aligned".to_string(),
2192 max_width: LineLength::from_const(50), column_align: ColumnAlign::Auto,
2194 column_align_header: None,
2195 column_align_body: None,
2196 loose_last_column: false,
2197 aligned_delimiter: false,
2198 pad_width: None,
2199 };
2200 let md013_config = MD013Config {
2201 tables: false, line_length: LineLength::from_const(0), ..Default::default()
2204 };
2205 let rule = MD060TableFormat::from_config_struct(config, md013_config, false);
2206
2207 let content = "| Very Long Column Header A | Very Long Column Header B | Very Long Column Header C |\n|---|---|---|\n| x | y | z |";
2209 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2210 let fixed = rule.fix(&ctx).unwrap();
2211
2212 assert!(
2214 fixed.contains("| --- |"),
2215 "Should be compact format due to explicit max_width"
2216 );
2217 }
2218
2219 #[test]
2220 fn test_md060_inherits_md013_line_length_when_tables_enabled() {
2221 let config = MD060Config {
2223 enabled: true,
2224 style: "aligned".to_string(),
2225 max_width: LineLength::from_const(0), column_align: ColumnAlign::Auto,
2227 column_align_header: None,
2228 column_align_body: None,
2229 loose_last_column: false,
2230 aligned_delimiter: false,
2231 pad_width: None,
2232 };
2233 let md013_config = MD013Config {
2234 tables: true,
2235 line_length: LineLength::from_const(50), ..Default::default()
2237 };
2238 let rule = MD060TableFormat::from_config_struct(config, md013_config, false);
2239
2240 let content = "| Very Long Column Header A | Very Long Column Header B | Very Long Column Header C |\n|---|---|---|\n| x | y | z |";
2242 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2243 let fixed = rule.fix(&ctx).unwrap();
2244
2245 assert!(
2247 fixed.contains("| --- |"),
2248 "Should be compact format when inheriting MD013 limit"
2249 );
2250 }
2251
2252 #[test]
2255 fn test_aligned_no_space_reformats_spaced_delimiter() {
2256 let config = MD060Config {
2259 enabled: true,
2260 style: "aligned-no-space".to_string(),
2261 max_width: LineLength::from_const(0),
2262 column_align: ColumnAlign::Auto,
2263 column_align_header: None,
2264 column_align_body: None,
2265 loose_last_column: false,
2266 aligned_delimiter: false,
2267 pad_width: None,
2268 };
2269 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2270
2271 let content = "| Header 1 | Header 2 |\n| -------- | -------- |\n| Cell 1 | Cell 2 |";
2273 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2274 let fixed = rule.fix(&ctx).unwrap();
2275
2276 assert!(
2279 !fixed.contains("| ----"),
2280 "Delimiter should NOT have spaces after pipe. Got:\n{fixed}"
2281 );
2282 assert!(
2283 !fixed.contains("---- |"),
2284 "Delimiter should NOT have spaces before pipe. Got:\n{fixed}"
2285 );
2286 assert!(
2288 fixed.contains("|----"),
2289 "Delimiter should have dashes touching the leading pipe. Got:\n{fixed}"
2290 );
2291 }
2292
2293 #[test]
2294 fn test_aligned_reformats_compact_delimiter() {
2295 let config = MD060Config {
2298 enabled: true,
2299 style: "aligned".to_string(),
2300 max_width: LineLength::from_const(0),
2301 column_align: ColumnAlign::Auto,
2302 column_align_header: None,
2303 column_align_body: None,
2304 loose_last_column: false,
2305 aligned_delimiter: false,
2306 pad_width: None,
2307 };
2308 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2309
2310 let content = "| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |";
2312 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2313 let fixed = rule.fix(&ctx).unwrap();
2314
2315 assert!(
2317 fixed.contains("| -------- | -------- |") || fixed.contains("| ---------- | ---------- |"),
2318 "Delimiter should have spaces around dashes. Got:\n{fixed}"
2319 );
2320 }
2321
2322 #[test]
2323 fn test_aligned_no_space_preserves_matching_table() {
2324 let config = MD060Config {
2326 enabled: true,
2327 style: "aligned-no-space".to_string(),
2328 max_width: LineLength::from_const(0),
2329 column_align: ColumnAlign::Auto,
2330 column_align_header: None,
2331 column_align_body: None,
2332 loose_last_column: false,
2333 aligned_delimiter: false,
2334 pad_width: None,
2335 };
2336 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2337
2338 let content = "| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |";
2340 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2341 let fixed = rule.fix(&ctx).unwrap();
2342
2343 assert_eq!(
2345 fixed, content,
2346 "Table already in aligned-no-space style should be preserved"
2347 );
2348 }
2349
2350 #[test]
2351 fn test_aligned_preserves_matching_table() {
2352 let config = MD060Config {
2354 enabled: true,
2355 style: "aligned".to_string(),
2356 max_width: LineLength::from_const(0),
2357 column_align: ColumnAlign::Auto,
2358 column_align_header: None,
2359 column_align_body: None,
2360 loose_last_column: false,
2361 aligned_delimiter: false,
2362 pad_width: None,
2363 };
2364 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2365
2366 let content = "| Header 1 | Header 2 |\n| -------- | -------- |\n| Cell 1 | Cell 2 |";
2368 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2369 let fixed = rule.fix(&ctx).unwrap();
2370
2371 assert_eq!(fixed, content, "Table already in aligned style should be preserved");
2373 }
2374
2375 #[test]
2376 fn test_cjk_table_display_width_consistency() {
2377 let table_lines = vec!["| 名前 | Age |", "|------|-----|", "| 田中 | 25 |"];
2383
2384 let is_aligned = MD060TableFormat::is_table_already_aligned(
2386 &table_lines,
2387 crate::config::MarkdownFlavor::Standard,
2388 false,
2389 None,
2390 );
2391 assert!(
2392 !is_aligned,
2393 "Table with uneven raw line lengths should NOT be considered aligned"
2394 );
2395 }
2396
2397 #[test]
2398 fn test_cjk_width_calculation_in_aligned_check() {
2399 let cjk_width = MD060TableFormat::calculate_cell_display_width("名前");
2402 assert_eq!(cjk_width, 4, "Two CJK characters should have display width 4");
2403
2404 let ascii_width = MD060TableFormat::calculate_cell_display_width("Age");
2405 assert_eq!(ascii_width, 3, "Three ASCII characters should have display width 3");
2406
2407 let padded_cjk = MD060TableFormat::calculate_cell_display_width(" 名前 ");
2409 assert_eq!(padded_cjk, 4, "Padded CJK should have same width after trim");
2410
2411 let mixed = MD060TableFormat::calculate_cell_display_width(" 日本語ABC ");
2413 assert_eq!(mixed, 9, "Mixed CJK/ASCII content");
2415 }
2416
2417 #[test]
2420 fn test_md060_column_align_left() {
2421 let config = MD060Config {
2423 enabled: true,
2424 style: "aligned".to_string(),
2425 max_width: LineLength::from_const(0),
2426 column_align: ColumnAlign::Left,
2427 column_align_header: None,
2428 column_align_body: None,
2429 loose_last_column: false,
2430 aligned_delimiter: false,
2431 pad_width: None,
2432 };
2433 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2434
2435 let content = "| Name | Age | City |\n|---|---|---|\n| Alice | 30 | Seattle |\n| Bob | 25 | Portland |";
2436 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2437
2438 let fixed = rule.fix(&ctx).unwrap();
2439 let lines: Vec<&str> = fixed.lines().collect();
2440
2441 assert!(
2443 lines[2].contains("| Alice "),
2444 "Content should be left-aligned (Alice should have trailing padding)"
2445 );
2446 assert!(
2447 lines[3].contains("| Bob "),
2448 "Content should be left-aligned (Bob should have trailing padding)"
2449 );
2450 }
2451
2452 #[test]
2453 fn test_md060_column_align_center() {
2454 let config = MD060Config {
2456 enabled: true,
2457 style: "aligned".to_string(),
2458 max_width: LineLength::from_const(0),
2459 column_align: ColumnAlign::Center,
2460 column_align_header: None,
2461 column_align_body: None,
2462 loose_last_column: false,
2463 aligned_delimiter: false,
2464 pad_width: None,
2465 };
2466 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2467
2468 let content = "| Name | Age | City |\n|---|---|---|\n| Alice | 30 | Seattle |\n| Bob | 25 | Portland |";
2469 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2470
2471 let fixed = rule.fix(&ctx).unwrap();
2472 let lines: Vec<&str> = fixed.lines().collect();
2473
2474 assert!(
2477 lines[3].contains("| Bob |"),
2478 "Bob should be centered with padding on both sides. Got: {}",
2479 lines[3]
2480 );
2481 }
2482
2483 #[test]
2484 fn test_md060_column_align_right() {
2485 let config = MD060Config {
2487 enabled: true,
2488 style: "aligned".to_string(),
2489 max_width: LineLength::from_const(0),
2490 column_align: ColumnAlign::Right,
2491 column_align_header: None,
2492 column_align_body: None,
2493 loose_last_column: false,
2494 aligned_delimiter: false,
2495 pad_width: None,
2496 };
2497 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2498
2499 let content = "| Name | Age | City |\n|---|---|---|\n| Alice | 30 | Seattle |\n| Bob | 25 | Portland |";
2500 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2501
2502 let fixed = rule.fix(&ctx).unwrap();
2503 let lines: Vec<&str> = fixed.lines().collect();
2504
2505 assert!(
2507 lines[3].contains("| Bob |"),
2508 "Bob should be right-aligned with padding on left. Got: {}",
2509 lines[3]
2510 );
2511 }
2512
2513 #[test]
2514 fn test_md060_column_align_auto_respects_delimiter() {
2515 let config = MD060Config {
2517 enabled: true,
2518 style: "aligned".to_string(),
2519 max_width: LineLength::from_const(0),
2520 column_align: ColumnAlign::Auto,
2521 column_align_header: None,
2522 column_align_body: None,
2523 loose_last_column: false,
2524 aligned_delimiter: false,
2525 pad_width: None,
2526 };
2527 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2528
2529 let content = "| Left | Center | Right |\n|:---|:---:|---:|\n| A | B | C |";
2531 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2532
2533 let fixed = rule.fix(&ctx).unwrap();
2534
2535 assert!(fixed.contains("| A "), "Left column should be left-aligned");
2537 let lines: Vec<&str> = fixed.lines().collect();
2539 assert!(
2543 lines[2].contains(" C |"),
2544 "Right column should be right-aligned. Got: {}",
2545 lines[2]
2546 );
2547 }
2548
2549 #[test]
2550 fn test_md060_column_align_overrides_delimiter_indicators() {
2551 let config = MD060Config {
2553 enabled: true,
2554 style: "aligned".to_string(),
2555 max_width: LineLength::from_const(0),
2556 column_align: ColumnAlign::Right, column_align_header: None,
2558 column_align_body: None,
2559 loose_last_column: false,
2560 aligned_delimiter: false,
2561 pad_width: None,
2562 };
2563 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2564
2565 let content = "| Left | Center | Right |\n|:---|:---:|---:|\n| A | B | C |";
2567 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2568
2569 let fixed = rule.fix(&ctx).unwrap();
2570 let lines: Vec<&str> = fixed.lines().collect();
2571
2572 assert!(
2575 lines[2].contains(" A |") || lines[2].contains(" A |"),
2576 "Even left-indicated column should be right-aligned. Got: {}",
2577 lines[2]
2578 );
2579 }
2580
2581 #[test]
2582 fn test_md060_column_align_with_aligned_no_space() {
2583 let config = MD060Config {
2585 enabled: true,
2586 style: "aligned-no-space".to_string(),
2587 max_width: LineLength::from_const(0),
2588 column_align: ColumnAlign::Center,
2589 column_align_header: None,
2590 column_align_body: None,
2591 loose_last_column: false,
2592 aligned_delimiter: false,
2593 pad_width: None,
2594 };
2595 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2596
2597 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |\n| Bob | 25 |";
2598 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2599
2600 let fixed = rule.fix(&ctx).unwrap();
2601 let lines: Vec<&str> = fixed.lines().collect();
2602
2603 assert!(
2605 lines[1].contains("|---"),
2606 "Delimiter should have no spaces in aligned-no-space style. Got: {}",
2607 lines[1]
2608 );
2609 assert!(
2611 lines[3].contains("| Bob |"),
2612 "Content should be centered. Got: {}",
2613 lines[3]
2614 );
2615 }
2616
2617 #[test]
2618 fn test_md060_column_align_config_parsing() {
2619 let toml_str = r#"
2621enabled = true
2622style = "aligned"
2623column-align = "center"
2624"#;
2625 let config: MD060Config = toml::from_str(toml_str).expect("Should parse config");
2626 assert_eq!(config.column_align, ColumnAlign::Center);
2627
2628 let toml_str = r#"
2629enabled = true
2630style = "aligned"
2631column-align = "right"
2632"#;
2633 let config: MD060Config = toml::from_str(toml_str).expect("Should parse config");
2634 assert_eq!(config.column_align, ColumnAlign::Right);
2635
2636 let toml_str = r#"
2637enabled = true
2638style = "aligned"
2639column-align = "left"
2640"#;
2641 let config: MD060Config = toml::from_str(toml_str).expect("Should parse config");
2642 assert_eq!(config.column_align, ColumnAlign::Left);
2643
2644 let toml_str = r#"
2645enabled = true
2646style = "aligned"
2647column-align = "auto"
2648"#;
2649 let config: MD060Config = toml::from_str(toml_str).expect("Should parse config");
2650 assert_eq!(config.column_align, ColumnAlign::Auto);
2651 }
2652
2653 #[test]
2654 fn test_md060_column_align_default_is_auto() {
2655 let toml_str = r#"
2657enabled = true
2658style = "aligned"
2659"#;
2660 let config: MD060Config = toml::from_str(toml_str).expect("Should parse config");
2661 assert_eq!(config.column_align, ColumnAlign::Auto);
2662 }
2663
2664 #[test]
2665 fn test_md060_column_align_reformats_already_aligned_table() {
2666 let config = MD060Config {
2668 enabled: true,
2669 style: "aligned".to_string(),
2670 max_width: LineLength::from_const(0),
2671 column_align: ColumnAlign::Right,
2672 column_align_header: None,
2673 column_align_body: None,
2674 loose_last_column: false,
2675 aligned_delimiter: false,
2676 pad_width: None,
2677 };
2678 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2679
2680 let content = "| Name | Age |\n| ----- | --- |\n| Alice | 30 |\n| Bob | 25 |";
2682 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2683
2684 let fixed = rule.fix(&ctx).unwrap();
2685 let lines: Vec<&str> = fixed.lines().collect();
2686
2687 assert!(
2689 lines[2].contains("| Alice |") && lines[2].contains("| 30 |"),
2690 "Already aligned table should be reformatted with right alignment. Got: {}",
2691 lines[2]
2692 );
2693 assert!(
2694 lines[3].contains("| Bob |") || lines[3].contains("| Bob |"),
2695 "Bob should be right-aligned. Got: {}",
2696 lines[3]
2697 );
2698 }
2699
2700 #[test]
2701 fn test_md060_column_align_with_cjk_characters() {
2702 let config = MD060Config {
2704 enabled: true,
2705 style: "aligned".to_string(),
2706 max_width: LineLength::from_const(0),
2707 column_align: ColumnAlign::Center,
2708 column_align_header: None,
2709 column_align_body: None,
2710 loose_last_column: false,
2711 aligned_delimiter: false,
2712 pad_width: None,
2713 };
2714 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2715
2716 let content = "| Name | City |\n|---|---|\n| Alice | 東京 |\n| Bob | LA |";
2717 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2718
2719 let fixed = rule.fix(&ctx).unwrap();
2720
2721 assert!(fixed.contains("Bob"), "Table should contain Bob");
2724 assert!(fixed.contains("東京"), "Table should contain 東京");
2725 }
2726
2727 #[test]
2728 fn test_md060_column_align_ignored_for_compact_style() {
2729 let config = MD060Config {
2731 enabled: true,
2732 style: "compact".to_string(),
2733 max_width: LineLength::from_const(0),
2734 column_align: ColumnAlign::Right, column_align_header: None,
2736 column_align_body: None,
2737 loose_last_column: false,
2738 aligned_delimiter: false,
2739 pad_width: None,
2740 };
2741 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2742
2743 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |\n| Bob | 25 |";
2744 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2745
2746 let fixed = rule.fix(&ctx).unwrap();
2747
2748 assert!(
2750 fixed.contains("| Alice |"),
2751 "Compact style should have single space padding, not alignment. Got: {fixed}"
2752 );
2753 }
2754
2755 #[test]
2756 fn test_md060_column_align_ignored_for_tight_style() {
2757 let config = MD060Config {
2759 enabled: true,
2760 style: "tight".to_string(),
2761 max_width: LineLength::from_const(0),
2762 column_align: ColumnAlign::Center, column_align_header: None,
2764 column_align_body: None,
2765 loose_last_column: false,
2766 aligned_delimiter: false,
2767 pad_width: None,
2768 };
2769 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2770
2771 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |\n| Bob | 25 |";
2772 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2773
2774 let fixed = rule.fix(&ctx).unwrap();
2775
2776 assert!(
2778 fixed.contains("|Alice|"),
2779 "Tight style should have no spaces. Got: {fixed}"
2780 );
2781 }
2782
2783 #[test]
2784 fn test_md060_column_align_with_empty_cells() {
2785 let config = MD060Config {
2787 enabled: true,
2788 style: "aligned".to_string(),
2789 max_width: LineLength::from_const(0),
2790 column_align: ColumnAlign::Center,
2791 column_align_header: None,
2792 column_align_body: None,
2793 loose_last_column: false,
2794 aligned_delimiter: false,
2795 pad_width: None,
2796 };
2797 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2798
2799 let content = "| Name | Age |\n|---|---|\n| Alice | 30 |\n| | 25 |";
2800 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2801
2802 let fixed = rule.fix(&ctx).unwrap();
2803 let lines: Vec<&str> = fixed.lines().collect();
2804
2805 assert!(
2807 lines[3].contains("| |") || lines[3].contains("| |"),
2808 "Empty cell should be padded correctly. Got: {}",
2809 lines[3]
2810 );
2811 }
2812
2813 #[test]
2814 fn test_md060_column_align_auto_preserves_already_aligned() {
2815 let config = MD060Config {
2817 enabled: true,
2818 style: "aligned".to_string(),
2819 max_width: LineLength::from_const(0),
2820 column_align: ColumnAlign::Auto,
2821 column_align_header: None,
2822 column_align_body: None,
2823 loose_last_column: false,
2824 aligned_delimiter: false,
2825 pad_width: None,
2826 };
2827 let rule = MD060TableFormat::from_config_struct(config, MD013Config::default(), false);
2828
2829 let content = "| Name | Age |\n| ----- | --- |\n| Alice | 30 |\n| Bob | 25 |";
2831 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2832
2833 let fixed = rule.fix(&ctx).unwrap();
2834
2835 assert_eq!(
2837 fixed, content,
2838 "Already aligned table should be preserved with column-align=auto"
2839 );
2840 }
2841
2842 #[test]
2843 fn test_cjk_table_display_aligned_not_flagged() {
2844 use crate::config::MarkdownFlavor;
2848
2849 let table_lines: Vec<&str> = vec![
2851 "| Header | Name |",
2852 "| ------ | ---- |",
2853 "| Hello | Test |",
2854 "| 你好 | Test |",
2855 ];
2856
2857 let result = MD060TableFormat::is_table_already_aligned(&table_lines, MarkdownFlavor::Standard, false, None);
2858 assert!(
2859 result,
2860 "Table with CJK characters that is display-aligned should be recognized as aligned"
2861 );
2862 }
2863
2864 #[test]
2865 fn test_cjk_table_not_reformatted_when_aligned() {
2866 let rule = MD060TableFormat::new(true, "aligned".to_string());
2868 let content = "| Header | Name |\n| ------ | ---- |\n| Hello | Test |\n| 你好 | Test |\n";
2870 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2871
2872 let fixed = rule.fix(&ctx).unwrap();
2874 assert_eq!(fixed, content, "Display-aligned CJK table should not be reformatted");
2875 }
2876
2877 #[test]
2893 fn md060_pandoc_grid_tables_not_flagged() {
2894 let rule = MD060TableFormat::new(true, "aligned".to_string());
2895 let content = "\
2896+---+---+
2897| a | b |
2898+===+===+
2899| 1 | 2 |
2900+---+---+
2901";
2902 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
2905 let result = rule.check(&ctx).unwrap();
2906 assert!(
2907 result.is_empty(),
2908 "MD060 should not flag Pandoc grid tables (excluded by table_blocks): {result:?}"
2909 );
2910
2911 let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2912 let result_std = rule.check(&ctx_std).unwrap();
2913 assert!(
2914 result_std.is_empty(),
2915 "MD060 should not flag grid-table-like content under Standard: {result_std:?}"
2916 );
2917 }
2918
2919 #[test]
2920 fn md060_pandoc_multi_line_tables_not_flagged() {
2921 let rule = MD060TableFormat::new(true, "aligned".to_string());
2922 let content = "\
2923--------- -----------
2924Header 1 Header 2
2925--------- -----------
2926Cell 1 Cell 2
2927--------- -----------
2928";
2929 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
2930 let result = rule.check(&ctx).unwrap();
2931 assert!(
2932 result.is_empty(),
2933 "MD060 should not flag Pandoc multi-line tables: {result:?}"
2934 );
2935
2936 let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2937 let result_std = rule.check(&ctx_std).unwrap();
2938 assert!(
2939 result_std.is_empty(),
2940 "MD060 should not flag multi-line table content under Standard: {result_std:?}"
2941 );
2942 }
2943
2944 #[test]
2945 fn md060_pandoc_line_blocks_not_flagged() {
2946 let rule = MD060TableFormat::new(true, "aligned".to_string());
2947 let content = "| First line\n| Second line\n";
2949 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
2950 let result = rule.check(&ctx).unwrap();
2951 assert!(
2952 result.is_empty(),
2953 "MD060 should not treat Pandoc line blocks as tables: {result:?}"
2954 );
2955
2956 let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2957 let result_std = rule.check(&ctx_std).unwrap();
2958 assert!(
2959 result_std.is_empty(),
2960 "MD060 should not treat line-block-like content as tables under Standard: {result_std:?}"
2961 );
2962 }
2963
2964 #[test]
2965 fn md060_pandoc_pipe_table_captions_not_flagged() {
2966 let rule = MD060TableFormat::new(true, "aligned".to_string());
2967 let content = "\
2970| H1 | H2 |
2971| -- | -- |
2972| a | b |
2973
2974: My table caption
2975";
2976 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Pandoc, None);
2977 let result = rule.check(&ctx).unwrap();
2978 assert!(
2979 result.is_empty(),
2980 "MD060 should not flag the pipe-table caption line: {result:?}"
2981 );
2982
2983 let ctx_std = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
2984 let result_std = rule.check(&ctx_std).unwrap();
2985 assert!(
2986 result_std.is_empty(),
2987 "MD060 already-aligned table with caption should have no warnings under Standard: {result_std:?}"
2988 );
2989 }
2990
2991 #[test]
2992 fn test_fix_preserves_trailing_blank_lines_and_is_idempotent() {
2993 let rule = MD060TableFormat::new(true, "aligned".to_string());
2997
2998 for input in ["# \n\n\n\n", "text\n\n\n", "no trailing newline", "only blanks\n\n"] {
3000 let ctx = LintContext::new(input, crate::config::MarkdownFlavor::Standard, None);
3001 assert_eq!(
3002 rule.fix(&ctx).unwrap(),
3003 input,
3004 "MD060 must not alter table-free content: {input:?}"
3005 );
3006 }
3007
3008 let with_table = "| a | b |\n|---|---|\n| 1 | 2 |\n\n\n";
3011 let ctx = LintContext::new(with_table, crate::config::MarkdownFlavor::Standard, None);
3012 let once = rule.fix(&ctx).unwrap();
3013 assert!(
3014 once.ends_with("\n\n\n"),
3015 "trailing blank lines must be preserved, got: {once:?}"
3016 );
3017 let ctx2 = LintContext::new(&once, crate::config::MarkdownFlavor::Standard, None);
3018 let twice = rule.fix(&ctx2).unwrap();
3019 assert_eq!(once, twice, "MD060 fix must be idempotent with trailing blank lines");
3020 }
3021
3022 fn indent_block(block: &str, indent: usize) -> String {
3024 let spaces = " ".repeat(indent);
3025 block.lines().fold(String::new(), |mut indented, line| {
3026 indented.push_str(&spaces);
3027 indented.push_str(line);
3028 indented.push('\n');
3029 indented
3030 })
3031 }
3032
3033 #[test]
3034 fn test_mdg_table_indent_stays_within_enclosing_list_item() {
3035 let rule = MD060TableFormat::new(true, "aligned".to_string());
3040 let written = "| name | price |\n|---|---|\n| a | b |\n";
3041 let formatted = "| name | price |\n| ---- | ----- |\n| a | b |\n";
3042
3043 let cases = [
3045 ("* Given", 2, 2),
3046 ("* Given", 5, 2),
3047 ("1. Given", 3, 3),
3048 ("1. Given", 5, 3),
3049 ("* Given\n * Nested", 4, 4),
3050 ("* Given\n * Nested", 5, 4),
3051 ];
3052
3053 for (item, indent, expected_indent) in cases {
3054 let input = format!("# Feature: F\n\n{item}\n\n{}", indent_block(written, indent));
3055 let expected = format!("# Feature: F\n\n{item}\n\n{}", indent_block(formatted, expected_indent));
3056 let ctx = LintContext::new(&input, crate::config::MarkdownFlavor::MDG, None);
3057
3058 assert_eq!(
3059 rule.fix(&ctx).unwrap(),
3060 expected,
3061 "a {indent}-space table under {item:?} belongs at {expected_indent} spaces"
3062 );
3063
3064 let fixed_ctx = LintContext::new(&expected, crate::config::MarkdownFlavor::MDG, None);
3065 assert!(rule.check(&fixed_ctx).unwrap().is_empty());
3066 assert_eq!(rule.fix(&fixed_ctx).unwrap(), expected, "MD060 fix must be idempotent");
3067 }
3068 }
3069
3070 #[test]
3071 fn test_mdg_table_below_a_deep_list_item_keeps_list_indentation() {
3072 let rule = MD060TableFormat::new(true, "aligned".to_string());
3076 let input = "# Feature: F\n\n* A\n * B\n * C\n\n | name | price |\n |---|---|\n | a | b |\n";
3077 let expected = "# Feature: F\n\n* A\n * B\n * C\n\n | name | price |\n | ---- | ----- |\n | a | b |\n";
3078
3079 let mdg = rule
3080 .fix(&LintContext::new(input, crate::config::MarkdownFlavor::MDG, None))
3081 .unwrap();
3082 assert_eq!(mdg, expected);
3083 assert_eq!(
3084 mdg,
3085 rule.fix(&LintContext::new(input, crate::config::MarkdownFlavor::Standard, None))
3086 .unwrap()
3087 );
3088 }
3089
3090 #[test]
3091 fn test_mdg_tab_indented_table_is_normalized_not_flattened() {
3092 let rule = MD060TableFormat::new(true, "aligned".to_string());
3096 let input = "# Feature: F\n\n#### Examples:\n\n\t\t| a | bb |\n\t\t|---|---|\n\t\t| 1 | 2 |\n";
3097 let expected = "# Feature: F\n\n#### Examples:\n\n | a | bb |\n | --- | --- |\n | 1 | 2 |\n";
3098
3099 let ctx = LintContext::new(input, crate::config::MarkdownFlavor::MDG, None);
3100 assert_eq!(rule.fix(&ctx).unwrap(), expected);
3101
3102 let fixed_ctx = LintContext::new(expected, crate::config::MarkdownFlavor::MDG, None);
3103 assert!(rule.check(&fixed_ctx).unwrap().is_empty());
3104
3105 let standard = rule
3106 .fix(&LintContext::new(input, crate::config::MarkdownFlavor::Standard, None))
3107 .unwrap();
3108 assert_eq!(
3109 standard, input,
3110 "tab-indented rows are indented code outside MDG and stay untouched"
3111 );
3112 }
3113
3114 #[test]
3115 fn test_mdg_table_row_needs_a_pipe_behind_its_indent() {
3116 let rule = MD060TableFormat::new(true, "aligned".to_string());
3121
3122 for input in [
3123 "# Feature: F\n\n > | a | b |\n > |---|---|\n > | 1 | 2 |\n",
3124 "# Feature: F\n\n - | a | b |\n |---|---|\n | 1 | 2 |\n",
3125 ] {
3126 let mdg = rule
3127 .fix(&LintContext::new(input, crate::config::MarkdownFlavor::MDG, None))
3128 .unwrap();
3129 assert_eq!(
3130 mdg,
3131 rule.fix(&LintContext::new(input, crate::config::MarkdownFlavor::Standard, None))
3132 .unwrap(),
3133 "{input:?} is not a Gherkin table, so both flavors must agree"
3134 );
3135
3136 let fixed_ctx = LintContext::new(&mdg, crate::config::MarkdownFlavor::MDG, None);
3137 assert_eq!(rule.fix(&fixed_ctx).unwrap(), mdg, "MD060 fix must converge");
3138 }
3139 }
3140}