1#[derive(Debug, Clone)]
7pub struct TableBlock {
8 pub start_line: usize,
9 pub end_line: usize,
10 pub header_line: usize,
11 pub delimiter_line: usize,
12 pub content_lines: Vec<usize>,
13 pub list_context: Option<ListTableContext>,
17}
18
19#[derive(Debug, Clone)]
21pub struct ListTableContext {
22 pub list_prefix: String,
24 pub content_indent: usize,
26}
27
28pub struct TableUtils;
30
31impl TableUtils {
32 fn has_unescaped_pipe_outside_spans(text: &str) -> bool {
44 let chars: Vec<char> = text.chars().collect();
45 let mut i = 0;
46 let mut in_code = false;
47 let mut code_delim_len = 0usize;
48 let mut in_math = false;
49 let mut math_delim_len = 0usize;
50
51 while i < chars.len() {
52 let ch = chars[i];
53
54 if ch == '\\' && !in_code && !in_math {
55 i += if i + 1 < chars.len() { 2 } else { 1 };
58 continue;
59 }
60
61 if ch == '`' && !in_math {
62 let mut run = 1usize;
63 while i + run < chars.len() && chars[i + run] == '`' {
64 run += 1;
65 }
66
67 if in_code {
68 if run == code_delim_len {
69 in_code = false;
70 code_delim_len = 0;
71 }
72 } else {
74 in_code = true;
75 code_delim_len = run;
76 }
77
78 i += run;
79 continue;
80 }
81
82 if ch == '$' && !in_code {
83 let mut run = 1usize;
84 while i + run < chars.len() && chars[i + run] == '$' {
85 run += 1;
86 }
87
88 if in_math {
89 if run == math_delim_len {
90 in_math = false;
91 math_delim_len = 0;
92 }
93 } else {
95 in_math = true;
96 math_delim_len = run;
97 }
98
99 i += run;
100 continue;
101 }
102
103 if ch == '|' && !in_code && !in_math {
104 return true;
105 }
106
107 i += 1;
108 }
109
110 false
111 }
112
113 pub fn is_potential_table_row(line: &str) -> bool {
115 let trimmed = line.trim();
116 if trimmed.is_empty() || !trimmed.contains('|') {
117 return false;
118 }
119
120 if trimmed.starts_with("- ")
123 || trimmed.starts_with("* ")
124 || trimmed.starts_with("+ ")
125 || trimmed.starts_with("-\t")
126 || trimmed.starts_with("*\t")
127 || trimmed.starts_with("+\t")
128 {
129 return false;
130 }
131
132 if let Some(first_non_digit) = trimmed.find(|c: char| !c.is_ascii_digit())
134 && first_non_digit > 0
135 {
136 let after_digits = &trimmed[first_non_digit..];
137 if after_digits.starts_with(". ")
138 || after_digits.starts_with(".\t")
139 || after_digits.starts_with(") ")
140 || after_digits.starts_with(")\t")
141 {
142 return false;
143 }
144 }
145
146 if trimmed.starts_with('#') {
148 let hash_count = trimmed.bytes().take_while(|&b| b == b'#').count();
149 if hash_count <= 6 {
150 let after_hashes = &trimmed[hash_count..];
151 if after_hashes.is_empty() || after_hashes.starts_with(' ') || after_hashes.starts_with('\t') {
152 return false;
153 }
154 }
155 }
156
157 let has_outer_pipes = trimmed.starts_with('|') && trimmed.ends_with('|');
160 if !has_outer_pipes && !Self::has_unescaped_pipe_outside_spans(trimmed) {
161 return false;
162 }
163
164 let parts: Vec<&str> = trimmed.split('|').collect();
166 if parts.len() < 2 {
167 return false;
168 }
169
170 let mut valid_parts = 0;
172 let mut total_non_empty_parts = 0;
173
174 for part in &parts {
175 let part_trimmed = part.trim();
176 if part_trimmed.is_empty() {
178 continue;
179 }
180 total_non_empty_parts += 1;
181
182 if !part_trimmed.contains('\n') {
184 valid_parts += 1;
185 }
186 }
187
188 if total_non_empty_parts > 0 && valid_parts != total_non_empty_parts {
190 return false;
192 }
193
194 if total_non_empty_parts == 0 {
197 return trimmed.starts_with('|') && trimmed.ends_with('|') && parts.len() >= 3;
199 }
200
201 if trimmed.starts_with('|') && trimmed.ends_with('|') {
204 valid_parts >= 1
206 } else {
207 valid_parts >= 2
209 }
210 }
211
212 pub fn is_delimiter_row(line: &str) -> bool {
214 let trimmed = line.trim();
215 if !trimmed.contains('|') || !trimmed.contains('-') {
216 return false;
217 }
218
219 let parts: Vec<&str> = trimmed.split('|').collect();
221 let mut valid_delimiter_parts = 0;
222 let mut total_non_empty_parts = 0;
223
224 for part in &parts {
225 let part_trimmed = part.trim();
226 if part_trimmed.is_empty() {
227 continue; }
229
230 total_non_empty_parts += 1;
231
232 if part_trimmed.chars().all(|c| c == '-' || c == ':' || c.is_whitespace()) && part_trimmed.contains('-') {
234 valid_delimiter_parts += 1;
235 }
236 }
237
238 total_non_empty_parts > 0 && valid_delimiter_parts == total_non_empty_parts
240 }
241
242 fn strip_blockquote_prefix(line: &str) -> &str {
244 let trimmed = line.trim_start();
245 if trimmed.starts_with('>') {
246 let mut rest = trimmed;
248 while rest.starts_with('>') {
249 rest = rest.strip_prefix('>').unwrap_or(rest);
250 rest = rest.trim_start_matches(' ');
251 }
252 rest
253 } else {
254 line
255 }
256 }
257
258 pub fn find_table_blocks_with_code_info(
261 content: &str,
262 code_blocks: &[(usize, usize)],
263 code_spans: &[crate::lint_context::CodeSpan],
264 html_comment_ranges: &[crate::utils::skip_context::ByteRange],
265 ) -> Vec<TableBlock> {
266 let lines: Vec<&str> = content.lines().collect();
267 let mut tables = Vec::new();
268 let mut i = 0;
269
270 let mut line_positions = Vec::with_capacity(lines.len());
272 let mut pos = 0;
273 for line in &lines {
274 line_positions.push(pos);
275 pos += line.len() + 1; }
277
278 let mut list_indent_stack: Vec<usize> = Vec::new();
282
283 while i < lines.len() {
284 let line_start = line_positions[i];
286 let in_code =
287 crate::utils::code_block_utils::CodeBlockUtils::is_in_code_block_or_span(code_blocks, line_start) || {
288 let idx = code_spans.partition_point(|span| span.byte_offset <= line_start);
290 idx > 0 && line_start < code_spans[idx - 1].byte_end
291 };
292 let in_html_comment = {
293 let idx = html_comment_ranges.partition_point(|range| range.start <= line_start);
295 idx > 0 && line_start < html_comment_ranges[idx - 1].end
296 };
297
298 if in_code || in_html_comment {
299 i += 1;
300 continue;
301 }
302
303 let line_content = Self::strip_blockquote_prefix(lines[i]);
305
306 let (list_prefix, list_content, content_indent) = Self::extract_list_prefix(line_content);
308 if !list_prefix.is_empty() {
309 while list_indent_stack.last().is_some_and(|&top| top >= content_indent) {
311 list_indent_stack.pop();
312 }
313 list_indent_stack.push(content_indent);
314 } else if !line_content.trim().is_empty() {
315 let leading = line_content.len() - line_content.trim_start().len();
317 while list_indent_stack.last().is_some_and(|&top| leading < top) {
318 list_indent_stack.pop();
319 }
320 }
321 let (is_same_line_list_table, effective_content) =
326 if !list_prefix.is_empty() && Self::is_potential_table_row_content(list_content) {
327 (true, list_content)
328 } else {
329 (false, line_content)
330 };
331
332 let continuation_indent = if !is_same_line_list_table && list_prefix.is_empty() {
335 let leading = line_content.len() - line_content.trim_start().len();
336 list_indent_stack
338 .iter()
339 .rev()
340 .find(|&&indent| leading >= indent)
341 .copied()
342 } else {
343 None
344 };
345
346 let is_continuation_list_table = continuation_indent.is_some()
347 && {
348 let indent = continuation_indent.unwrap();
349 let leading = line_content.len() - line_content.trim_start().len();
350 leading < indent + 4
352 }
353 && Self::is_potential_table_row(effective_content);
354
355 let is_any_list_table = is_same_line_list_table || is_continuation_list_table;
356
357 let effective_content_indent = if is_same_line_list_table {
359 content_indent
360 } else if is_continuation_list_table {
361 continuation_indent.unwrap()
362 } else {
363 0
364 };
365
366 if is_any_list_table || Self::is_potential_table_row(effective_content) {
368 let (next_line_content, delimiter_has_valid_indent) = if i + 1 < lines.len() {
371 let next_raw = Self::strip_blockquote_prefix(lines[i + 1]);
372 if is_any_list_table {
373 let leading_spaces = next_raw.len() - next_raw.trim_start().len();
375 if leading_spaces >= effective_content_indent {
376 (
378 Self::strip_list_continuation_indent(next_raw, effective_content_indent),
379 true,
380 )
381 } else {
382 (next_raw, false)
384 }
385 } else {
386 (next_raw, true)
387 }
388 } else {
389 ("", true)
390 };
391
392 let effective_is_list_table = is_any_list_table && delimiter_has_valid_indent;
394
395 if i + 1 < lines.len() && Self::is_delimiter_row(next_line_content) {
396 let table_start = i;
398 let header_line = i;
399 let delimiter_line = i + 1;
400 let mut table_end = i + 1; let mut content_lines = Vec::new();
402
403 let mut j = i + 2;
405 while j < lines.len() {
406 let line = lines[j];
407 let raw_content = Self::strip_blockquote_prefix(line);
409
410 let line_content = if effective_is_list_table {
412 Self::strip_list_continuation_indent(raw_content, effective_content_indent)
413 } else {
414 raw_content
415 };
416
417 if line_content.trim().is_empty() {
418 break;
420 }
421
422 if effective_is_list_table {
424 let leading_spaces = raw_content.len() - raw_content.trim_start().len();
425 if leading_spaces < effective_content_indent {
426 break;
428 }
429 }
430
431 if Self::is_potential_table_row(line_content) {
432 content_lines.push(j);
433 table_end = j;
434 j += 1;
435 } else {
436 break;
438 }
439 }
440
441 let list_context = if effective_is_list_table {
442 if is_same_line_list_table {
443 Some(ListTableContext {
445 list_prefix: list_prefix.to_string(),
446 content_indent: effective_content_indent,
447 })
448 } else {
449 Some(ListTableContext {
451 list_prefix: " ".repeat(effective_content_indent),
452 content_indent: effective_content_indent,
453 })
454 }
455 } else {
456 None
457 };
458
459 tables.push(TableBlock {
460 start_line: table_start,
461 end_line: table_end,
462 header_line,
463 delimiter_line,
464 content_lines,
465 list_context,
466 });
467 i = table_end + 1;
468 } else {
469 i += 1;
470 }
471 } else {
472 i += 1;
473 }
474 }
475
476 tables
477 }
478
479 fn strip_list_continuation_indent(line: &str, expected_indent: usize) -> &str {
482 let bytes = line.as_bytes();
483 let mut spaces = 0;
484
485 for &b in bytes {
486 if b == b' ' {
487 spaces += 1;
488 } else if b == b'\t' {
489 spaces = (spaces / 4 + 1) * 4;
491 } else {
492 break;
493 }
494
495 if spaces >= expected_indent {
496 break;
497 }
498 }
499
500 let strip_count = spaces.min(expected_indent).min(line.len());
502 let mut byte_count = 0;
504 let mut counted_spaces = 0;
505 for &b in bytes {
506 if counted_spaces >= strip_count {
507 break;
508 }
509 if b == b' ' {
510 counted_spaces += 1;
511 byte_count += 1;
512 } else if b == b'\t' {
513 counted_spaces = (counted_spaces / 4 + 1) * 4;
514 byte_count += 1;
515 } else {
516 break;
517 }
518 }
519
520 &line[byte_count..]
521 }
522
523 pub fn find_table_blocks(content: &str, ctx: &crate::lint_context::LintContext) -> Vec<TableBlock> {
526 Self::find_table_blocks_with_code_info(content, &ctx.code_blocks, &ctx.code_spans(), ctx.html_comment_ranges())
527 }
528
529 pub fn count_cells(row: &str) -> usize {
531 Self::count_cells_with_flavor(row, crate::config::MarkdownFlavor::Standard)
532 }
533
534 pub fn count_cells_with_flavor(row: &str, flavor: crate::config::MarkdownFlavor) -> usize {
541 let (_, content) = Self::extract_blockquote_prefix(row);
543 Self::split_table_row_with_flavor(content, flavor).len()
544 }
545
546 fn count_preceding_backslashes(chars: &[char], pos: usize) -> usize {
548 let mut count = 0;
549 let mut k = pos;
550 while k > 0 {
551 k -= 1;
552 if chars[k] == '\\' {
553 count += 1;
554 } else {
555 break;
556 }
557 }
558 count
559 }
560
561 pub fn mask_pipes_in_inline_code(text: &str) -> String {
567 let mut result = String::new();
568 let chars: Vec<char> = text.chars().collect();
569 let mut i = 0;
570
571 while i < chars.len() {
572 if chars[i] == '`' {
573 let preceding = Self::count_preceding_backslashes(&chars, i);
575 if preceding % 2 != 0 {
576 result.push(chars[i]);
578 i += 1;
579 continue;
580 }
581
582 let start = i;
584 let mut backtick_count = 0;
585 while i < chars.len() && chars[i] == '`' {
586 backtick_count += 1;
587 i += 1;
588 }
589
590 let mut found_closing = false;
592 let mut j = i;
593
594 while j < chars.len() {
595 if chars[j] == '`' {
596 let close_start = j;
603 let mut close_count = 0;
604 while j < chars.len() && chars[j] == '`' {
605 close_count += 1;
606 j += 1;
607 }
608
609 if close_count == backtick_count {
610 found_closing = true;
612
613 result.extend(chars[start..i].iter());
615
616 for &ch in chars.iter().take(close_start).skip(i) {
617 if ch == '|' {
618 result.push('_'); } else {
620 result.push(ch);
621 }
622 }
623
624 result.extend(chars[close_start..j].iter());
625 i = j;
626 break;
627 }
628 } else {
630 j += 1;
631 }
632 }
633
634 if !found_closing {
635 result.extend(chars[start..i].iter());
637 }
638 } else {
639 result.push(chars[i]);
640 i += 1;
641 }
642 }
643
644 result
645 }
646
647 pub fn mask_pipes_for_table_parsing(text: &str) -> String {
656 let mut result = String::new();
657 let chars: Vec<char> = text.chars().collect();
658 let mut i = 0;
659
660 while i < chars.len() {
661 if chars[i] == '\\' {
662 if i + 1 < chars.len() && chars[i + 1] == '\\' {
663 result.push('\\');
666 result.push('\\');
667 i += 2;
668 } else if i + 1 < chars.len() && chars[i + 1] == '|' {
669 result.push('\\');
671 result.push('_'); i += 2;
673 } else {
674 result.push(chars[i]);
676 i += 1;
677 }
678 } else {
679 result.push(chars[i]);
680 i += 1;
681 }
682 }
683
684 result
685 }
686
687 pub fn split_table_row_with_flavor(row: &str, _flavor: crate::config::MarkdownFlavor) -> Vec<String> {
694 let trimmed = row.trim();
695
696 if !trimmed.contains('|') {
697 return Vec::new();
698 }
699
700 let masked = Self::mask_pipes_for_table_parsing(trimmed);
702
703 let final_masked = Self::mask_pipes_in_inline_code(&masked);
705
706 let has_leading = final_masked.starts_with('|');
707 let has_trailing = final_masked.ends_with('|');
708
709 let mut masked_content = final_masked.as_str();
710 let mut orig_content = trimmed;
711
712 if has_leading {
713 masked_content = &masked_content[1..];
714 orig_content = &orig_content[1..];
715 }
716
717 let stripped_trailing = has_trailing && !masked_content.is_empty();
719 if stripped_trailing {
720 masked_content = &masked_content[..masked_content.len() - 1];
721 orig_content = &orig_content[..orig_content.len() - 1];
722 }
723
724 if masked_content.is_empty() {
726 if stripped_trailing {
727 return vec![String::new()];
729 } else {
730 return Vec::new();
732 }
733 }
734
735 let masked_parts: Vec<&str> = masked_content.split('|').collect();
736 let mut cells = Vec::new();
737 let mut pos = 0;
738
739 for masked_cell in masked_parts {
740 let cell_len = masked_cell.len();
741 let orig_cell = if pos + cell_len <= orig_content.len() {
742 &orig_content[pos..pos + cell_len]
743 } else {
744 masked_cell
745 };
746 cells.push(orig_cell.to_string());
747 pos += cell_len + 1; }
749
750 cells
751 }
752
753 pub fn split_table_row(row: &str) -> Vec<String> {
755 Self::split_table_row_with_flavor(row, crate::config::MarkdownFlavor::Standard)
756 }
757
758 pub fn determine_pipe_style(line: &str) -> Option<&'static str> {
763 let content = Self::strip_blockquote_prefix(line);
765 let trimmed = content.trim();
766 if !trimmed.contains('|') {
767 return None;
768 }
769
770 let has_leading = trimmed.starts_with('|');
771 let has_trailing = trimmed.ends_with('|');
772
773 match (has_leading, has_trailing) {
774 (true, true) => Some("leading_and_trailing"),
775 (true, false) => Some("leading_only"),
776 (false, true) => Some("trailing_only"),
777 (false, false) => Some("no_leading_or_trailing"),
778 }
779 }
780
781 pub fn extract_blockquote_prefix(line: &str) -> (&str, &str) {
786 let bytes = line.as_bytes();
788 let mut pos = 0;
789
790 while pos < bytes.len() && (bytes[pos] == b' ' || bytes[pos] == b'\t') {
792 pos += 1;
793 }
794
795 if pos >= bytes.len() || bytes[pos] != b'>' {
797 return ("", line);
798 }
799
800 while pos < bytes.len() {
802 if bytes[pos] == b'>' {
803 pos += 1;
804 if pos < bytes.len() && bytes[pos] == b' ' {
806 pos += 1;
807 }
808 } else if bytes[pos] == b' ' || bytes[pos] == b'\t' {
809 pos += 1;
810 } else {
811 break;
812 }
813 }
814
815 (&line[..pos], &line[pos..])
817 }
818
819 pub fn extract_list_prefix(line: &str) -> (&str, &str, usize) {
834 let bytes = line.as_bytes();
835
836 let leading_spaces = bytes.iter().take_while(|&&b| b == b' ' || b == b'\t').count();
838 let mut pos = leading_spaces;
839
840 if pos >= bytes.len() {
841 return ("", line, 0);
842 }
843
844 if matches!(bytes[pos], b'-' | b'*' | b'+') {
846 pos += 1;
847
848 if pos >= bytes.len() || bytes[pos] == b' ' || bytes[pos] == b'\t' {
850 if pos < bytes.len() && (bytes[pos] == b' ' || bytes[pos] == b'\t') {
852 pos += 1;
853 }
854 let content_indent = pos;
855 return (&line[..pos], &line[pos..], content_indent);
856 }
857 return ("", line, 0);
859 }
860
861 if bytes[pos].is_ascii_digit() {
863 let digit_start = pos;
864 while pos < bytes.len() && bytes[pos].is_ascii_digit() {
865 pos += 1;
866 }
867
868 if pos > digit_start && pos < bytes.len() {
870 if bytes[pos] == b'.' || bytes[pos] == b')' {
872 pos += 1;
873 if pos >= bytes.len() || bytes[pos] == b' ' || bytes[pos] == b'\t' {
874 if pos < bytes.len() && (bytes[pos] == b' ' || bytes[pos] == b'\t') {
876 pos += 1;
877 }
878 let content_indent = pos;
879 return (&line[..pos], &line[pos..], content_indent);
880 }
881 }
882 }
883 }
884
885 ("", line, 0)
886 }
887
888 pub fn extract_table_row_content<'a>(line: &'a str, table_block: &TableBlock, line_index: usize) -> &'a str {
893 let (_, after_blockquote) = Self::extract_blockquote_prefix(line);
895
896 if let Some(ref list_ctx) = table_block.list_context {
898 if line_index == 0 {
899 after_blockquote
901 .strip_prefix(&list_ctx.list_prefix)
902 .unwrap_or_else(|| Self::extract_list_prefix(after_blockquote).1)
903 } else {
904 Self::strip_list_continuation_indent(after_blockquote, list_ctx.content_indent)
906 }
907 } else {
908 after_blockquote
909 }
910 }
911
912 pub fn is_list_item_with_table_row(line: &str) -> bool {
915 let (prefix, content, _) = Self::extract_list_prefix(line);
916 if prefix.is_empty() {
917 return false;
918 }
919
920 let trimmed = content.trim();
923 if !trimmed.starts_with('|') {
924 return false;
925 }
926
927 Self::is_potential_table_row_content(content)
929 }
930
931 fn is_potential_table_row_content(content: &str) -> bool {
933 Self::is_potential_table_row(content)
934 }
935}
936
937#[cfg(test)]
938mod tests {
939 use super::*;
940 use crate::lint_context::LintContext;
941
942 #[test]
943 fn test_is_potential_table_row() {
944 assert!(TableUtils::is_potential_table_row("| Header 1 | Header 2 |"));
946 assert!(TableUtils::is_potential_table_row("| Cell 1 | Cell 2 |"));
947 assert!(TableUtils::is_potential_table_row("Cell 1 | Cell 2"));
948 assert!(TableUtils::is_potential_table_row("| Cell |")); assert!(TableUtils::is_potential_table_row("| A | B | C | D | E |"));
952
953 assert!(TableUtils::is_potential_table_row(" | Indented | Table | "));
955 assert!(TableUtils::is_potential_table_row("| Spaces | Around |"));
956
957 assert!(!TableUtils::is_potential_table_row("- List item"));
959 assert!(!TableUtils::is_potential_table_row("* Another list"));
960 assert!(!TableUtils::is_potential_table_row("+ Plus list"));
961 assert!(!TableUtils::is_potential_table_row("Regular text"));
962 assert!(!TableUtils::is_potential_table_row(""));
963 assert!(!TableUtils::is_potential_table_row(" "));
964
965 assert!(!TableUtils::is_potential_table_row("`code with | pipe`"));
967 assert!(!TableUtils::is_potential_table_row("``multiple | backticks``"));
968 assert!(!TableUtils::is_potential_table_row("Use ``a|b`` in prose"));
969 assert!(TableUtils::is_potential_table_row("| `fenced` | Uses ``` and ~~~ |"));
970 assert!(TableUtils::is_potential_table_row("`!foo && bar` | `(!foo) && bar`"));
971 assert!(!TableUtils::is_potential_table_row("`echo a | sed 's/a/b/'`"));
972
973 assert!(!TableUtils::is_potential_table_row(
975 "Text with $|S|$ math notation here."
976 ));
977 assert!(!TableUtils::is_potential_table_row(
978 "Size $|S|$ was even, check $|T|$ too."
979 ));
980 assert!(!TableUtils::is_potential_table_row("Display $$|A| + |B|$$ math here."));
981 assert!(TableUtils::is_potential_table_row("| cell with $|S|$ math |"));
983 assert!(TableUtils::is_potential_table_row("$a$ | $b$"));
985 assert!(TableUtils::is_potential_table_row("$f(x)$ and $g(x)$ | result"));
986 assert!(!TableUtils::is_potential_table_row("$5 | $10"));
990
991 assert!(!TableUtils::is_potential_table_row("Just one |"));
993 assert!(!TableUtils::is_potential_table_row("| Just one"));
994
995 let long_cell = "a".repeat(150);
997 assert!(TableUtils::is_potential_table_row(&format!("| {long_cell} | b |")));
998
999 assert!(!TableUtils::is_potential_table_row("| Cell with\nnewline | Other |"));
1001
1002 assert!(TableUtils::is_potential_table_row("|||")); assert!(TableUtils::is_potential_table_row("||||")); assert!(TableUtils::is_potential_table_row("| | |")); }
1007
1008 #[test]
1009 fn test_list_items_with_pipes_not_table_rows() {
1010 assert!(!TableUtils::is_potential_table_row("1. Item with | pipe"));
1012 assert!(!TableUtils::is_potential_table_row("10. Item with | pipe"));
1013 assert!(!TableUtils::is_potential_table_row("999. Item with | pipe"));
1014 assert!(!TableUtils::is_potential_table_row("1) Item with | pipe"));
1015 assert!(!TableUtils::is_potential_table_row("10) Item with | pipe"));
1016
1017 assert!(!TableUtils::is_potential_table_row("-\tItem with | pipe"));
1019 assert!(!TableUtils::is_potential_table_row("*\tItem with | pipe"));
1020 assert!(!TableUtils::is_potential_table_row("+\tItem with | pipe"));
1021
1022 assert!(!TableUtils::is_potential_table_row(" - Indented | pipe"));
1024 assert!(!TableUtils::is_potential_table_row(" * Deep indent | pipe"));
1025 assert!(!TableUtils::is_potential_table_row(" 1. Ordered indent | pipe"));
1026
1027 assert!(!TableUtils::is_potential_table_row("- [ ] task | pipe"));
1029 assert!(!TableUtils::is_potential_table_row("- [x] done | pipe"));
1030
1031 assert!(!TableUtils::is_potential_table_row("1. foo | bar | baz"));
1033 assert!(!TableUtils::is_potential_table_row("- alpha | beta | gamma"));
1034
1035 assert!(TableUtils::is_potential_table_row("| cell | cell |"));
1037 assert!(TableUtils::is_potential_table_row("cell | cell"));
1038 assert!(TableUtils::is_potential_table_row("| Header | Header |"));
1039 }
1040
1041 #[test]
1042 fn test_atx_headings_with_pipes_not_table_rows() {
1043 assert!(!TableUtils::is_potential_table_row("# Heading | with pipe"));
1045 assert!(!TableUtils::is_potential_table_row("## Heading | with pipe"));
1046 assert!(!TableUtils::is_potential_table_row("### Heading | with pipe"));
1047 assert!(!TableUtils::is_potential_table_row("#### Heading | with pipe"));
1048 assert!(!TableUtils::is_potential_table_row("##### Heading | with pipe"));
1049 assert!(!TableUtils::is_potential_table_row("###### Heading | with pipe"));
1050
1051 assert!(!TableUtils::is_potential_table_row("### col1 | col2 | col3"));
1053 assert!(!TableUtils::is_potential_table_row("## a|b|c"));
1054
1055 assert!(!TableUtils::is_potential_table_row("#\tHeading | pipe"));
1057 assert!(!TableUtils::is_potential_table_row("##\tHeading | pipe"));
1058
1059 assert!(!TableUtils::is_potential_table_row("# |"));
1061 assert!(!TableUtils::is_potential_table_row("## |"));
1062
1063 assert!(!TableUtils::is_potential_table_row(" ## Heading | pipe"));
1065 assert!(!TableUtils::is_potential_table_row(" ### Heading | pipe"));
1066
1067 assert!(!TableUtils::is_potential_table_row("#### ®aAA|ᯗ"));
1069
1070 assert!(TableUtils::is_potential_table_row("####### text | pipe"));
1074
1075 assert!(TableUtils::is_potential_table_row("#nospc|pipe"));
1077
1078 assert!(TableUtils::is_potential_table_row("| # Header | Value |"));
1080 assert!(TableUtils::is_potential_table_row("text | #tag"));
1081 }
1082
1083 #[test]
1084 fn test_is_delimiter_row() {
1085 assert!(TableUtils::is_delimiter_row("|---|---|"));
1087 assert!(TableUtils::is_delimiter_row("| --- | --- |"));
1088 assert!(TableUtils::is_delimiter_row("|:---|---:|"));
1089 assert!(TableUtils::is_delimiter_row("|:---:|:---:|"));
1090
1091 assert!(TableUtils::is_delimiter_row("|-|--|"));
1093 assert!(TableUtils::is_delimiter_row("|-------|----------|"));
1094
1095 assert!(TableUtils::is_delimiter_row("| --- | --- |"));
1097 assert!(TableUtils::is_delimiter_row("| :--- | ---: |"));
1098
1099 assert!(TableUtils::is_delimiter_row("|---|---|---|---|"));
1101
1102 assert!(TableUtils::is_delimiter_row("--- | ---"));
1104 assert!(TableUtils::is_delimiter_row(":--- | ---:"));
1105
1106 assert!(!TableUtils::is_delimiter_row("| Header | Header |"));
1108 assert!(!TableUtils::is_delimiter_row("Regular text"));
1109 assert!(!TableUtils::is_delimiter_row(""));
1110 assert!(!TableUtils::is_delimiter_row("|||"));
1111 assert!(!TableUtils::is_delimiter_row("| | |"));
1112
1113 assert!(!TableUtils::is_delimiter_row("| : | : |"));
1115 assert!(!TableUtils::is_delimiter_row("| | |"));
1116
1117 assert!(!TableUtils::is_delimiter_row("| --- | text |"));
1119 assert!(!TableUtils::is_delimiter_row("| abc | --- |"));
1120 }
1121
1122 #[test]
1123 fn test_count_cells() {
1124 assert_eq!(TableUtils::count_cells("| Cell 1 | Cell 2 | Cell 3 |"), 3);
1126 assert_eq!(TableUtils::count_cells("Cell 1 | Cell 2 | Cell 3"), 3);
1127 assert_eq!(TableUtils::count_cells("| Cell 1 | Cell 2"), 2);
1128 assert_eq!(TableUtils::count_cells("Cell 1 | Cell 2 |"), 2);
1129
1130 assert_eq!(TableUtils::count_cells("| Cell |"), 1);
1132 assert_eq!(TableUtils::count_cells("Cell"), 0); assert_eq!(TableUtils::count_cells("| | | |"), 3);
1136 assert_eq!(TableUtils::count_cells("| | | |"), 3);
1137
1138 assert_eq!(TableUtils::count_cells("| A | B | C | D | E | F |"), 6);
1140
1141 assert_eq!(TableUtils::count_cells("||"), 1); assert_eq!(TableUtils::count_cells("|||"), 2); assert_eq!(TableUtils::count_cells("Regular text"), 0);
1147 assert_eq!(TableUtils::count_cells(""), 0);
1148 assert_eq!(TableUtils::count_cells(" "), 0);
1149
1150 assert_eq!(TableUtils::count_cells(" | A | B | "), 2);
1152 assert_eq!(TableUtils::count_cells("| A | B |"), 2);
1153 }
1154
1155 #[test]
1156 fn test_count_cells_with_escaped_pipes() {
1157 assert_eq!(TableUtils::count_cells("| Challenge | Solution |"), 2);
1162 assert_eq!(TableUtils::count_cells("| A | B | C |"), 3);
1163 assert_eq!(TableUtils::count_cells("| One | Two |"), 2);
1164
1165 assert_eq!(TableUtils::count_cells(r"| Command | echo \| grep |"), 2);
1167 assert_eq!(TableUtils::count_cells(r"| A | B \| C |"), 2); assert_eq!(TableUtils::count_cells(r"| Command | `echo \| grep` |"), 2);
1171
1172 assert_eq!(TableUtils::count_cells(r"| A | B \\| C |"), 3); assert_eq!(TableUtils::count_cells(r"| A | `B \\| C` |"), 2);
1176
1177 assert_eq!(TableUtils::count_cells("| Command | `echo | grep` |"), 2);
1179 assert_eq!(TableUtils::count_cells("| `code | one` | `code | two` |"), 2);
1180 assert_eq!(TableUtils::count_cells("| `single|pipe` |"), 1);
1181
1182 assert_eq!(TableUtils::count_cells(r"| Hour formats | `^([0-1]?\d|2[0-3])` |"), 2);
1184 assert_eq!(TableUtils::count_cells(r"| Hour formats | `^([0-1]?\d\|2[0-3])` |"), 2);
1186 }
1187
1188 #[test]
1189 fn test_determine_pipe_style() {
1190 assert_eq!(
1192 TableUtils::determine_pipe_style("| Cell 1 | Cell 2 |"),
1193 Some("leading_and_trailing")
1194 );
1195 assert_eq!(
1196 TableUtils::determine_pipe_style("| Cell 1 | Cell 2"),
1197 Some("leading_only")
1198 );
1199 assert_eq!(
1200 TableUtils::determine_pipe_style("Cell 1 | Cell 2 |"),
1201 Some("trailing_only")
1202 );
1203 assert_eq!(
1204 TableUtils::determine_pipe_style("Cell 1 | Cell 2"),
1205 Some("no_leading_or_trailing")
1206 );
1207
1208 assert_eq!(
1210 TableUtils::determine_pipe_style(" | Cell 1 | Cell 2 | "),
1211 Some("leading_and_trailing")
1212 );
1213 assert_eq!(
1214 TableUtils::determine_pipe_style(" | Cell 1 | Cell 2 "),
1215 Some("leading_only")
1216 );
1217
1218 assert_eq!(TableUtils::determine_pipe_style("Regular text"), None);
1220 assert_eq!(TableUtils::determine_pipe_style(""), None);
1221 assert_eq!(TableUtils::determine_pipe_style(" "), None);
1222
1223 assert_eq!(TableUtils::determine_pipe_style("|"), Some("leading_and_trailing"));
1225 assert_eq!(TableUtils::determine_pipe_style("| Cell"), Some("leading_only"));
1226 assert_eq!(TableUtils::determine_pipe_style("Cell |"), Some("trailing_only"));
1227 }
1228
1229 #[test]
1230 fn test_find_table_blocks_simple() {
1231 let content = "| Header 1 | Header 2 |
1232|-----------|-----------|
1233| Cell 1 | Cell 2 |
1234| Cell 3 | Cell 4 |";
1235
1236 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1237
1238 let tables = TableUtils::find_table_blocks(content, &ctx);
1239 assert_eq!(tables.len(), 1);
1240
1241 let table = &tables[0];
1242 assert_eq!(table.start_line, 0);
1243 assert_eq!(table.end_line, 3);
1244 assert_eq!(table.header_line, 0);
1245 assert_eq!(table.delimiter_line, 1);
1246 assert_eq!(table.content_lines, vec![2, 3]);
1247 }
1248
1249 #[test]
1250 fn test_find_table_blocks_multiple() {
1251 let content = "Some text
1252
1253| Table 1 | Col A |
1254|----------|-------|
1255| Data 1 | Val 1 |
1256
1257More text
1258
1259| Table 2 | Col 2 |
1260|----------|-------|
1261| Data 2 | Data |";
1262
1263 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1264
1265 let tables = TableUtils::find_table_blocks(content, &ctx);
1266 assert_eq!(tables.len(), 2);
1267
1268 assert_eq!(tables[0].start_line, 2);
1270 assert_eq!(tables[0].end_line, 4);
1271 assert_eq!(tables[0].header_line, 2);
1272 assert_eq!(tables[0].delimiter_line, 3);
1273 assert_eq!(tables[0].content_lines, vec![4]);
1274
1275 assert_eq!(tables[1].start_line, 8);
1277 assert_eq!(tables[1].end_line, 10);
1278 assert_eq!(tables[1].header_line, 8);
1279 assert_eq!(tables[1].delimiter_line, 9);
1280 assert_eq!(tables[1].content_lines, vec![10]);
1281 }
1282
1283 #[test]
1284 fn test_find_table_blocks_no_content_rows() {
1285 let content = "| Header 1 | Header 2 |
1286|-----------|-----------|
1287
1288Next paragraph";
1289
1290 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1291
1292 let tables = TableUtils::find_table_blocks(content, &ctx);
1293 assert_eq!(tables.len(), 1);
1294
1295 let table = &tables[0];
1296 assert_eq!(table.start_line, 0);
1297 assert_eq!(table.end_line, 1); assert_eq!(table.content_lines.len(), 0);
1299 }
1300
1301 #[test]
1302 fn test_find_table_blocks_in_code_block() {
1303 let content = "```
1304| Not | A | Table |
1305|-----|---|-------|
1306| In | Code | Block |
1307```
1308
1309| Real | Table |
1310|------|-------|
1311| Data | Here |";
1312
1313 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1314
1315 let tables = TableUtils::find_table_blocks(content, &ctx);
1316 assert_eq!(tables.len(), 1); let table = &tables[0];
1319 assert_eq!(table.header_line, 6);
1320 assert_eq!(table.delimiter_line, 7);
1321 }
1322
1323 #[test]
1324 fn test_find_table_blocks_no_tables() {
1325 let content = "Just regular text
1326No tables here
1327- List item with | pipe
1328* Another list item";
1329
1330 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1331
1332 let tables = TableUtils::find_table_blocks(content, &ctx);
1333 assert_eq!(tables.len(), 0);
1334 }
1335
1336 #[test]
1337 fn test_find_table_blocks_malformed() {
1338 let content = "| Header without delimiter |
1339| This looks like table |
1340But no delimiter row
1341
1342| Proper | Table |
1343|---------|-------|
1344| Data | Here |";
1345
1346 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1347
1348 let tables = TableUtils::find_table_blocks(content, &ctx);
1349 assert_eq!(tables.len(), 1); assert_eq!(tables[0].header_line, 4);
1351 }
1352
1353 #[test]
1354 fn test_edge_cases() {
1355 assert!(!TableUtils::is_potential_table_row(""));
1357 assert!(!TableUtils::is_delimiter_row(""));
1358 assert_eq!(TableUtils::count_cells(""), 0);
1359 assert_eq!(TableUtils::determine_pipe_style(""), None);
1360
1361 assert!(!TableUtils::is_potential_table_row(" "));
1363 assert!(!TableUtils::is_delimiter_row(" "));
1364 assert_eq!(TableUtils::count_cells(" "), 0);
1365 assert_eq!(TableUtils::determine_pipe_style(" "), None);
1366
1367 assert!(!TableUtils::is_potential_table_row("|"));
1369 assert!(!TableUtils::is_delimiter_row("|"));
1370 assert_eq!(TableUtils::count_cells("|"), 0); let long_single = format!("| {} |", "a".repeat(200));
1375 assert!(TableUtils::is_potential_table_row(&long_single)); let long_multi = format!("| {} | {} |", "a".repeat(200), "b".repeat(200));
1378 assert!(TableUtils::is_potential_table_row(&long_multi)); assert!(TableUtils::is_potential_table_row("| 你好 | 世界 |"));
1382 assert!(TableUtils::is_potential_table_row("| émoji | 🎉 |"));
1383 assert_eq!(TableUtils::count_cells("| 你好 | 世界 |"), 2);
1384 }
1385
1386 #[test]
1387 fn test_table_block_struct() {
1388 let block = TableBlock {
1389 start_line: 0,
1390 end_line: 5,
1391 header_line: 0,
1392 delimiter_line: 1,
1393 content_lines: vec![2, 3, 4, 5],
1394 list_context: None,
1395 };
1396
1397 let debug_str = format!("{block:?}");
1399 assert!(debug_str.contains("TableBlock"));
1400 assert!(debug_str.contains("start_line: 0"));
1401
1402 let cloned = block.clone();
1404 assert_eq!(cloned.start_line, block.start_line);
1405 assert_eq!(cloned.end_line, block.end_line);
1406 assert_eq!(cloned.header_line, block.header_line);
1407 assert_eq!(cloned.delimiter_line, block.delimiter_line);
1408 assert_eq!(cloned.content_lines, block.content_lines);
1409 assert!(cloned.list_context.is_none());
1410 }
1411
1412 #[test]
1413 fn test_split_table_row() {
1414 let cells = TableUtils::split_table_row("| Cell 1 | Cell 2 | Cell 3 |");
1416 assert_eq!(cells.len(), 3);
1417 assert_eq!(cells[0].trim(), "Cell 1");
1418 assert_eq!(cells[1].trim(), "Cell 2");
1419 assert_eq!(cells[2].trim(), "Cell 3");
1420
1421 let cells = TableUtils::split_table_row("| Cell 1 | Cell 2");
1423 assert_eq!(cells.len(), 2);
1424
1425 let cells = TableUtils::split_table_row("| | | |");
1427 assert_eq!(cells.len(), 3);
1428
1429 let cells = TableUtils::split_table_row("| Cell |");
1431 assert_eq!(cells.len(), 1);
1432 assert_eq!(cells[0].trim(), "Cell");
1433
1434 let cells = TableUtils::split_table_row("No pipes here");
1436 assert_eq!(cells.len(), 0);
1437 }
1438
1439 #[test]
1440 fn test_split_table_row_with_escaped_pipes() {
1441 let cells = TableUtils::split_table_row(r"| A | B \| C |");
1443 assert_eq!(cells.len(), 2);
1444 assert!(cells[1].contains(r"\|"), "Escaped pipe should be in cell content");
1445
1446 let cells = TableUtils::split_table_row(r"| A | B \\| C |");
1448 assert_eq!(cells.len(), 3);
1449 }
1450
1451 #[test]
1452 fn test_split_table_row_with_flavor_mkdocs() {
1453 let cells =
1455 TableUtils::split_table_row_with_flavor("| Type | `x | y` |", crate::config::MarkdownFlavor::MkDocs);
1456 assert_eq!(cells.len(), 2);
1457 assert!(
1458 cells[1].contains("`x | y`"),
1459 "Inline code with pipe should be single cell in MkDocs flavor"
1460 );
1461
1462 let cells =
1464 TableUtils::split_table_row_with_flavor("| Type | `a | b | c` |", crate::config::MarkdownFlavor::MkDocs);
1465 assert_eq!(cells.len(), 2);
1466 assert!(cells[1].contains("`a | b | c`"));
1467 }
1468
1469 #[test]
1470 fn test_split_table_row_with_flavor_standard() {
1471 let cells =
1473 TableUtils::split_table_row_with_flavor("| Type | `x | y` |", crate::config::MarkdownFlavor::Standard);
1474 assert_eq!(
1475 cells.len(),
1476 2,
1477 "Pipes in code spans should not be cell delimiters, got {cells:?}"
1478 );
1479 assert!(
1480 cells[1].contains("`x | y`"),
1481 "Inline code with pipe should be single cell"
1482 );
1483 }
1484
1485 #[test]
1488 fn test_extract_blockquote_prefix_no_blockquote() {
1489 let (prefix, content) = TableUtils::extract_blockquote_prefix("| H1 | H2 |");
1491 assert_eq!(prefix, "");
1492 assert_eq!(content, "| H1 | H2 |");
1493 }
1494
1495 #[test]
1496 fn test_extract_blockquote_prefix_single_level() {
1497 let (prefix, content) = TableUtils::extract_blockquote_prefix("> | H1 | H2 |");
1499 assert_eq!(prefix, "> ");
1500 assert_eq!(content, "| H1 | H2 |");
1501 }
1502
1503 #[test]
1504 fn test_extract_blockquote_prefix_double_level() {
1505 let (prefix, content) = TableUtils::extract_blockquote_prefix(">> | H1 | H2 |");
1507 assert_eq!(prefix, ">> ");
1508 assert_eq!(content, "| H1 | H2 |");
1509 }
1510
1511 #[test]
1512 fn test_extract_blockquote_prefix_triple_level() {
1513 let (prefix, content) = TableUtils::extract_blockquote_prefix(">>> | H1 | H2 |");
1515 assert_eq!(prefix, ">>> ");
1516 assert_eq!(content, "| H1 | H2 |");
1517 }
1518
1519 #[test]
1520 fn test_extract_blockquote_prefix_with_spaces() {
1521 let (prefix, content) = TableUtils::extract_blockquote_prefix("> > | H1 | H2 |");
1523 assert_eq!(prefix, "> > ");
1524 assert_eq!(content, "| H1 | H2 |");
1525 }
1526
1527 #[test]
1528 fn test_extract_blockquote_prefix_indented() {
1529 let (prefix, content) = TableUtils::extract_blockquote_prefix(" > | H1 | H2 |");
1531 assert_eq!(prefix, " > ");
1532 assert_eq!(content, "| H1 | H2 |");
1533 }
1534
1535 #[test]
1536 fn test_extract_blockquote_prefix_no_space_after() {
1537 let (prefix, content) = TableUtils::extract_blockquote_prefix(">| H1 | H2 |");
1539 assert_eq!(prefix, ">");
1540 assert_eq!(content, "| H1 | H2 |");
1541 }
1542
1543 #[test]
1544 fn test_determine_pipe_style_in_blockquote() {
1545 assert_eq!(
1547 TableUtils::determine_pipe_style("> | H1 | H2 |"),
1548 Some("leading_and_trailing")
1549 );
1550 assert_eq!(
1551 TableUtils::determine_pipe_style("> H1 | H2"),
1552 Some("no_leading_or_trailing")
1553 );
1554 assert_eq!(
1555 TableUtils::determine_pipe_style(">> | H1 | H2 |"),
1556 Some("leading_and_trailing")
1557 );
1558 assert_eq!(TableUtils::determine_pipe_style(">>> | H1 | H2"), Some("leading_only"));
1559 }
1560
1561 #[test]
1562 fn test_list_table_delimiter_requires_indentation() {
1563 let content = "- List item with | pipe\n|---|---|\n| Cell 1 | Cell 2 |";
1568 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1569 let tables = TableUtils::find_table_blocks(content, &ctx);
1570
1571 assert_eq!(tables.len(), 1, "Should find exactly one table");
1574 assert!(
1575 tables[0].list_context.is_none(),
1576 "Should NOT have list context since delimiter has no indentation"
1577 );
1578 }
1579
1580 #[test]
1581 fn test_list_table_with_properly_indented_delimiter() {
1582 let content = "- | Header 1 | Header 2 |\n |----------|----------|\n | Cell 1 | Cell 2 |";
1585 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1586 let tables = TableUtils::find_table_blocks(content, &ctx);
1587
1588 assert_eq!(tables.len(), 1, "Should find exactly one table");
1590 assert_eq!(tables[0].start_line, 0, "Table should start at list item line");
1591 assert!(
1592 tables[0].list_context.is_some(),
1593 "Should be a list table since delimiter is properly indented"
1594 );
1595 }
1596
1597 #[test]
1598 fn test_mask_pipes_in_inline_code_regular_backticks() {
1599 let result = TableUtils::mask_pipes_in_inline_code("| `code | here` |");
1601 assert_eq!(result, "| `code _ here` |");
1602 }
1603
1604 #[test]
1605 fn test_mask_pipes_in_inline_code_escaped_backtick_not_code_span() {
1606 let result = TableUtils::mask_pipes_in_inline_code(r"| \`not code | still pipe\` |");
1609 assert_eq!(result, r"| \`not code | still pipe\` |");
1610 }
1611
1612 #[test]
1613 fn test_mask_pipes_in_inline_code_escaped_backslash_then_backtick() {
1614 let result = TableUtils::mask_pipes_in_inline_code(r"| \\`real code | masked\\` |");
1617 assert_eq!(result, r"| \\`real code _ masked\\` |");
1620 }
1621
1622 #[test]
1623 fn test_mask_pipes_in_inline_code_triple_backslash_before_backtick() {
1624 let result = TableUtils::mask_pipes_in_inline_code(r"| \\\`not code | pipe\\\` |");
1626 assert_eq!(result, r"| \\\`not code | pipe\\\` |");
1627 }
1628
1629 #[test]
1630 fn test_mask_pipes_in_inline_code_four_backslashes_before_backtick() {
1631 let result = TableUtils::mask_pipes_in_inline_code(r"| \\\\`code | here\\\\` |");
1633 assert_eq!(result, r"| \\\\`code _ here\\\\` |");
1634 }
1635
1636 #[test]
1637 fn test_mask_pipes_in_inline_code_no_backslash() {
1638 let result = TableUtils::mask_pipes_in_inline_code("before `a | b` after");
1640 assert_eq!(result, "before `a _ b` after");
1641 }
1642
1643 #[test]
1644 fn test_mask_pipes_in_inline_code_no_code_span() {
1645 let result = TableUtils::mask_pipes_in_inline_code("| col1 | col2 |");
1647 assert_eq!(result, "| col1 | col2 |");
1648 }
1649
1650 #[test]
1651 fn test_mask_pipes_in_inline_code_backslash_before_closing_backtick() {
1652 let result = TableUtils::mask_pipes_in_inline_code(r"| `foo\` | bar |");
1661 assert_eq!(result, r"| `foo\` | bar |");
1664 }
1665
1666 #[test]
1667 fn test_mask_pipes_in_inline_code_backslash_literal_with_pipe_inside() {
1668 let result = TableUtils::mask_pipes_in_inline_code(r"| `a\|b` | col2 |");
1672 assert_eq!(result, r"| `a\_b` | col2 |");
1673 }
1674
1675 #[test]
1676 fn test_count_preceding_backslashes() {
1677 let chars: Vec<char> = r"abc\\\`def".chars().collect();
1678 assert_eq!(TableUtils::count_preceding_backslashes(&chars, 6), 3);
1680
1681 let chars2: Vec<char> = r"abc\\`def".chars().collect();
1682 assert_eq!(TableUtils::count_preceding_backslashes(&chars2, 5), 2);
1684
1685 let chars3: Vec<char> = "`def".chars().collect();
1686 assert_eq!(TableUtils::count_preceding_backslashes(&chars3, 0), 0);
1688 }
1689
1690 #[test]
1691 fn test_has_unescaped_pipe_backslash_literal_in_code_span() {
1692 assert!(TableUtils::has_unescaped_pipe_outside_spans(r"`foo\` | bar"));
1695
1696 assert!(TableUtils::has_unescaped_pipe_outside_spans(r"\`foo | bar\`"));
1698
1699 assert!(!TableUtils::has_unescaped_pipe_outside_spans(r"`foo | bar`"));
1701 }
1702
1703 #[test]
1704 fn test_table_after_code_span_detected() {
1705 use crate::config::MarkdownFlavor;
1706
1707 let content = "`code`\n\n| A | B |\n|---|---|\n| 1 | 2 |\n";
1708 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
1709 assert!(!ctx.table_blocks.is_empty(), "Table after code span should be detected");
1710 }
1711
1712 #[test]
1713 fn test_table_inside_html_comment_not_detected() {
1714 use crate::config::MarkdownFlavor;
1715
1716 let content = "<!--\n| A | B |\n|---|---|\n| 1 | 2 |\n-->\n";
1717 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
1718 assert!(
1719 ctx.table_blocks.is_empty(),
1720 "Table inside HTML comment should not be detected"
1721 );
1722 }
1723}