1use super::blockquote::strip_blockquote_prefix;
6
7#[derive(Debug, Clone)]
9pub struct TableBlock {
10 pub start_line: usize,
11 pub end_line: usize,
12 pub header_line: usize,
13 pub delimiter_line: usize,
14 pub content_lines: Vec<usize>,
15 pub list_context: Option<ListTableContext>,
19}
20
21#[derive(Debug, Clone)]
23pub struct ListTableContext {
24 pub list_prefix: String,
26 pub content_indent: usize,
28}
29
30pub struct TableUtils;
32
33impl TableUtils {
34 fn has_unescaped_pipe_outside_spans(text: &str) -> bool {
46 let chars: Vec<char> = text.chars().collect();
47 let mut i = 0;
48 let mut in_code = false;
49 let mut code_delim_len = 0usize;
50 let mut in_math = false;
51 let mut math_delim_len = 0usize;
52
53 while i < chars.len() {
54 let ch = chars[i];
55
56 if ch == '\\' && !in_code && !in_math {
57 i += if i + 1 < chars.len() { 2 } else { 1 };
60 continue;
61 }
62
63 if ch == '`' && !in_math {
64 let mut run = 1usize;
65 while i + run < chars.len() && chars[i + run] == '`' {
66 run += 1;
67 }
68
69 if in_code {
70 if run == code_delim_len {
71 in_code = false;
72 code_delim_len = 0;
73 }
74 } else {
76 in_code = true;
77 code_delim_len = run;
78 }
79
80 i += run;
81 continue;
82 }
83
84 if ch == '$' && !in_code {
85 let mut run = 1usize;
86 while i + run < chars.len() && chars[i + run] == '$' {
87 run += 1;
88 }
89
90 if in_math {
91 if run == math_delim_len {
92 in_math = false;
93 math_delim_len = 0;
94 }
95 } else {
97 in_math = true;
98 math_delim_len = run;
99 }
100
101 i += run;
102 continue;
103 }
104
105 if ch == '|' && !in_code && !in_math {
106 return true;
107 }
108
109 i += 1;
110 }
111
112 false
113 }
114
115 pub fn is_potential_table_row(line: &str) -> bool {
117 let trimmed = line.trim();
118 if trimmed.is_empty() || !trimmed.contains('|') {
119 return false;
120 }
121
122 if trimmed.starts_with("- ")
125 || trimmed.starts_with("* ")
126 || trimmed.starts_with("+ ")
127 || trimmed.starts_with("-\t")
128 || trimmed.starts_with("*\t")
129 || trimmed.starts_with("+\t")
130 {
131 return false;
132 }
133
134 if let Some(first_non_digit) = trimmed.find(|c: char| !c.is_ascii_digit())
136 && first_non_digit > 0
137 {
138 let after_digits = &trimmed[first_non_digit..];
139 if after_digits.starts_with(". ")
140 || after_digits.starts_with(".\t")
141 || after_digits.starts_with(") ")
142 || after_digits.starts_with(")\t")
143 {
144 return false;
145 }
146 }
147
148 if trimmed.starts_with('#') {
150 let hash_count = trimmed.bytes().take_while(|&b| b == b'#').count();
151 if hash_count <= 6 {
152 let after_hashes = &trimmed[hash_count..];
153 if after_hashes.is_empty() || after_hashes.starts_with(' ') || after_hashes.starts_with('\t') {
154 return false;
155 }
156 }
157 }
158
159 let has_outer_pipes = trimmed.starts_with('|') && trimmed.ends_with('|');
162 if !has_outer_pipes && !Self::has_unescaped_pipe_outside_spans(trimmed) {
163 return false;
164 }
165
166 let parts: Vec<&str> = trimmed.split('|').collect();
168 if parts.len() < 2 {
169 return false;
170 }
171
172 let mut valid_parts = 0;
174 let mut total_non_empty_parts = 0;
175
176 for part in &parts {
177 let part_trimmed = part.trim();
178 if part_trimmed.is_empty() {
180 continue;
181 }
182 total_non_empty_parts += 1;
183
184 if !part_trimmed.contains('\n') {
186 valid_parts += 1;
187 }
188 }
189
190 if total_non_empty_parts > 0 && valid_parts != total_non_empty_parts {
192 return false;
194 }
195
196 if total_non_empty_parts == 0 {
199 return trimmed.starts_with('|') && trimmed.ends_with('|') && parts.len() >= 3;
201 }
202
203 if trimmed.starts_with('|') && trimmed.ends_with('|') {
206 valid_parts >= 1
208 } else {
209 valid_parts >= 2
211 }
212 }
213
214 pub fn is_delimiter_row(line: &str) -> bool {
216 let trimmed = line.trim();
217 if !trimmed.contains('|') || !trimmed.contains('-') {
218 return false;
219 }
220
221 let parts: Vec<&str> = trimmed.split('|').collect();
223 let mut valid_delimiter_parts = 0;
224 let mut total_non_empty_parts = 0;
225
226 for part in &parts {
227 let part_trimmed = part.trim();
228 if part_trimmed.is_empty() {
229 continue; }
231
232 total_non_empty_parts += 1;
233
234 if part_trimmed.chars().all(|c| c == '-' || c == ':' || c.is_whitespace()) && part_trimmed.contains('-') {
236 valid_delimiter_parts += 1;
237 }
238 }
239
240 total_non_empty_parts > 0 && valid_delimiter_parts == total_non_empty_parts
242 }
243
244 pub fn find_table_blocks_with_code_info(
247 content: &str,
248 code_blocks: &[(usize, usize)],
249 code_spans: &[crate::lint_context::CodeSpan],
250 html_comment_ranges: &[crate::utils::skip_context::ByteRange],
251 ) -> Vec<TableBlock> {
252 let lines: Vec<&str> = content.lines().collect();
253 let mut tables = Vec::new();
254 let mut i = 0;
255
256 let mut line_positions = Vec::with_capacity(lines.len());
263 let content_bytes = content.as_bytes();
264 let mut pos = 0;
265 for line in &lines {
266 line_positions.push(pos);
267 pos += line.len();
268 if content_bytes.get(pos) == Some(&b'\r') {
269 pos += 1;
270 }
271 if content_bytes.get(pos) == Some(&b'\n') {
272 pos += 1;
273 }
274 }
275
276 let mut list_indent_stack: Vec<usize> = Vec::new();
280
281 while i < lines.len() {
282 let line_start = line_positions[i];
284 let in_code =
285 crate::utils::code_block_utils::CodeBlockUtils::is_in_code_block_or_span(code_blocks, line_start) || {
286 let idx = code_spans.partition_point(|span| span.byte_offset <= line_start);
288 idx > 0 && line_start < code_spans[idx - 1].byte_end
289 };
290 let in_html_comment = {
291 let idx = html_comment_ranges.partition_point(|range| range.start <= line_start);
293 idx > 0 && line_start < html_comment_ranges[idx - 1].end
294 };
295
296 if in_code || in_html_comment {
297 i += 1;
298 continue;
299 }
300
301 let line_content = strip_blockquote_prefix(lines[i]);
303
304 let (list_prefix, list_content, content_indent) = Self::extract_list_prefix(line_content);
306 if !list_prefix.is_empty() {
307 while list_indent_stack.last().is_some_and(|&top| top >= content_indent) {
309 list_indent_stack.pop();
310 }
311 list_indent_stack.push(content_indent);
312 } else if !line_content.trim().is_empty() {
313 let leading = line_content.len() - line_content.trim_start().len();
315 while list_indent_stack.last().is_some_and(|&top| leading < top) {
316 list_indent_stack.pop();
317 }
318 }
319 let (is_same_line_list_table, effective_content) =
324 if !list_prefix.is_empty() && Self::is_potential_table_row_content(list_content) {
325 (true, list_content)
326 } else {
327 (false, line_content)
328 };
329
330 let continuation_indent = if !is_same_line_list_table && list_prefix.is_empty() {
333 let leading = line_content.len() - line_content.trim_start().len();
334 list_indent_stack
336 .iter()
337 .rev()
338 .find(|&&indent| leading >= indent)
339 .copied()
340 } else {
341 None
342 };
343
344 let is_continuation_list_table = continuation_indent.is_some()
345 && {
346 let indent = continuation_indent.unwrap();
347 let leading = line_content.len() - line_content.trim_start().len();
348 leading < indent + 4
350 }
351 && Self::is_potential_table_row(effective_content);
352
353 let is_any_list_table = is_same_line_list_table || is_continuation_list_table;
354
355 let effective_content_indent = if is_same_line_list_table {
357 content_indent
358 } else if is_continuation_list_table {
359 continuation_indent.unwrap()
360 } else {
361 0
362 };
363
364 if is_any_list_table || Self::is_potential_table_row(effective_content) {
366 let (next_line_content, delimiter_has_valid_indent) = if i + 1 < lines.len() {
369 let next_raw = strip_blockquote_prefix(lines[i + 1]);
370 if is_any_list_table {
371 let leading_spaces = next_raw.len() - next_raw.trim_start().len();
373 if leading_spaces >= effective_content_indent {
374 (
376 Self::strip_list_continuation_indent(next_raw, effective_content_indent),
377 true,
378 )
379 } else {
380 (next_raw, false)
382 }
383 } else {
384 (next_raw, true)
385 }
386 } else {
387 ("", true)
388 };
389
390 let effective_is_list_table = is_any_list_table && delimiter_has_valid_indent;
392
393 if i + 1 < lines.len() && Self::is_delimiter_row(next_line_content) {
394 let table_start = i;
396 let header_line = i;
397 let delimiter_line = i + 1;
398 let mut table_end = i + 1; let mut content_lines = Vec::new();
400
401 let mut j = i + 2;
403 while j < lines.len() {
404 let line = lines[j];
405 let raw_content = strip_blockquote_prefix(line);
407
408 let line_content = if effective_is_list_table {
410 Self::strip_list_continuation_indent(raw_content, effective_content_indent)
411 } else {
412 raw_content
413 };
414
415 if line_content.trim().is_empty() {
416 break;
418 }
419
420 if effective_is_list_table {
422 let leading_spaces = raw_content.len() - raw_content.trim_start().len();
423 if leading_spaces < effective_content_indent {
424 break;
426 }
427 }
428
429 if Self::is_potential_table_row(line_content) {
430 content_lines.push(j);
431 table_end = j;
432 j += 1;
433 } else {
434 break;
436 }
437 }
438
439 let list_context = if effective_is_list_table {
440 if is_same_line_list_table {
441 Some(ListTableContext {
443 list_prefix: list_prefix.to_string(),
444 content_indent: effective_content_indent,
445 })
446 } else {
447 Some(ListTableContext {
449 list_prefix: " ".repeat(effective_content_indent),
450 content_indent: effective_content_indent,
451 })
452 }
453 } else {
454 None
455 };
456
457 tables.push(TableBlock {
458 start_line: table_start,
459 end_line: table_end,
460 header_line,
461 delimiter_line,
462 content_lines,
463 list_context,
464 });
465 i = table_end + 1;
466 } else {
467 i += 1;
468 }
469 } else {
470 i += 1;
471 }
472 }
473
474 tables
475 }
476
477 fn strip_list_continuation_indent(line: &str, expected_indent: usize) -> &str {
480 let bytes = line.as_bytes();
481 let mut spaces = 0;
482
483 for &b in bytes {
484 if b == b' ' {
485 spaces += 1;
486 } else if b == b'\t' {
487 spaces = (spaces / 4 + 1) * 4;
489 } else {
490 break;
491 }
492
493 if spaces >= expected_indent {
494 break;
495 }
496 }
497
498 let strip_count = spaces.min(expected_indent).min(line.len());
500 let mut byte_count = 0;
502 let mut counted_spaces = 0;
503 for &b in bytes {
504 if counted_spaces >= strip_count {
505 break;
506 }
507 if b == b' ' {
508 counted_spaces += 1;
509 byte_count += 1;
510 } else if b == b'\t' {
511 counted_spaces = (counted_spaces / 4 + 1) * 4;
512 byte_count += 1;
513 } else {
514 break;
515 }
516 }
517
518 &line[byte_count..]
519 }
520
521 pub fn find_table_blocks(content: &str, ctx: &crate::lint_context::LintContext) -> Vec<TableBlock> {
524 Self::find_table_blocks_with_code_info(content, &ctx.code_blocks, &ctx.code_spans(), ctx.html_comment_ranges())
525 }
526
527 pub fn count_cells(row: &str) -> usize {
529 Self::count_cells_with_flavor(row, crate::config::MarkdownFlavor::Standard)
530 }
531
532 pub fn count_cells_with_flavor(row: &str, flavor: crate::config::MarkdownFlavor) -> usize {
539 let (_, content) = Self::extract_blockquote_prefix(row);
541 Self::split_table_row_with_flavor(content, flavor).len()
542 }
543
544 fn count_preceding_backslashes(chars: &[char], pos: usize) -> usize {
546 let mut count = 0;
547 let mut k = pos;
548 while k > 0 {
549 k -= 1;
550 if chars[k] == '\\' {
551 count += 1;
552 } else {
553 break;
554 }
555 }
556 count
557 }
558
559 pub fn mask_pipes_in_inline_code(text: &str) -> String {
565 let mut result = String::new();
566 let chars: Vec<char> = text.chars().collect();
567 let mut i = 0;
568
569 while i < chars.len() {
570 if chars[i] == '`' {
571 let preceding = Self::count_preceding_backslashes(&chars, i);
573 if preceding % 2 != 0 {
574 result.push(chars[i]);
576 i += 1;
577 continue;
578 }
579
580 let start = i;
582 let mut backtick_count = 0;
583 while i < chars.len() && chars[i] == '`' {
584 backtick_count += 1;
585 i += 1;
586 }
587
588 let mut found_closing = false;
590 let mut j = i;
591
592 while j < chars.len() {
593 if chars[j] == '`' {
594 let close_start = j;
601 let mut close_count = 0;
602 while j < chars.len() && chars[j] == '`' {
603 close_count += 1;
604 j += 1;
605 }
606
607 if close_count == backtick_count {
608 found_closing = true;
610
611 result.extend(chars[start..i].iter());
613
614 for &ch in chars.iter().take(close_start).skip(i) {
615 if ch == '|' {
616 result.push('_'); } else {
618 result.push(ch);
619 }
620 }
621
622 result.extend(chars[close_start..j].iter());
623 i = j;
624 break;
625 }
626 } else {
628 j += 1;
629 }
630 }
631
632 if !found_closing {
633 result.extend(chars[start..i].iter());
635 }
636 } else {
637 result.push(chars[i]);
638 i += 1;
639 }
640 }
641
642 result
643 }
644
645 pub fn mask_pipes_for_table_parsing(text: &str) -> String {
654 let mut result = String::new();
655 let chars: Vec<char> = text.chars().collect();
656 let mut i = 0;
657
658 while i < chars.len() {
659 if chars[i] == '\\' {
660 if i + 1 < chars.len() && chars[i + 1] == '\\' {
661 result.push('\\');
664 result.push('\\');
665 i += 2;
666 } else if i + 1 < chars.len() && chars[i + 1] == '|' {
667 result.push('\\');
669 result.push('_'); i += 2;
671 } else {
672 result.push(chars[i]);
674 i += 1;
675 }
676 } else {
677 result.push(chars[i]);
678 i += 1;
679 }
680 }
681
682 result
683 }
684
685 pub fn split_table_row_with_flavor(row: &str, _flavor: crate::config::MarkdownFlavor) -> Vec<String> {
692 let trimmed = row.trim();
693
694 if !trimmed.contains('|') {
695 return Vec::new();
696 }
697
698 let masked = Self::mask_pipes_for_table_parsing(trimmed);
700
701 let final_masked = Self::mask_pipes_in_inline_code(&masked);
703
704 let has_leading = final_masked.starts_with('|');
705 let has_trailing = final_masked.ends_with('|');
706
707 let mut masked_content = final_masked.as_str();
708 let mut orig_content = trimmed;
709
710 if has_leading {
711 masked_content = &masked_content[1..];
712 orig_content = &orig_content[1..];
713 }
714
715 let stripped_trailing = has_trailing && !masked_content.is_empty();
717 if stripped_trailing {
718 masked_content = &masked_content[..masked_content.len() - 1];
719 orig_content = &orig_content[..orig_content.len() - 1];
720 }
721
722 if masked_content.is_empty() {
724 if stripped_trailing {
725 return vec![String::new()];
727 } else {
728 return Vec::new();
730 }
731 }
732
733 let masked_parts: Vec<&str> = masked_content.split('|').collect();
734 let mut cells = Vec::new();
735 let mut pos = 0;
736
737 for masked_cell in masked_parts {
738 let cell_len = masked_cell.len();
739 let orig_cell = if pos + cell_len <= orig_content.len() {
740 &orig_content[pos..pos + cell_len]
741 } else {
742 masked_cell
743 };
744 cells.push(orig_cell.to_string());
745 pos += cell_len + 1; }
747
748 cells
749 }
750
751 pub fn split_table_row(row: &str) -> Vec<String> {
753 Self::split_table_row_with_flavor(row, crate::config::MarkdownFlavor::Standard)
754 }
755
756 pub fn determine_pipe_style(line: &str) -> Option<&'static str> {
761 let content = strip_blockquote_prefix(line);
763 let trimmed = content.trim();
764 if !trimmed.contains('|') {
765 return None;
766 }
767
768 let has_leading = trimmed.starts_with('|');
769 let has_trailing = trimmed.ends_with('|');
770
771 match (has_leading, has_trailing) {
772 (true, true) => Some("leading_and_trailing"),
773 (true, false) => Some("leading_only"),
774 (false, true) => Some("trailing_only"),
775 (false, false) => Some("no_leading_or_trailing"),
776 }
777 }
778
779 pub fn extract_blockquote_prefix(line: &str) -> (&str, &str) {
784 let bytes = line.as_bytes();
786 let mut pos = 0;
787
788 while pos < bytes.len() && (bytes[pos] == b' ' || bytes[pos] == b'\t') {
790 pos += 1;
791 }
792
793 if pos >= bytes.len() || bytes[pos] != b'>' {
795 return ("", line);
796 }
797
798 while pos < bytes.len() {
800 if bytes[pos] == b'>' {
801 pos += 1;
802 if pos < bytes.len() && bytes[pos] == b' ' {
804 pos += 1;
805 }
806 } else if bytes[pos] == b' ' || bytes[pos] == b'\t' {
807 pos += 1;
808 } else {
809 break;
810 }
811 }
812
813 (&line[..pos], &line[pos..])
815 }
816
817 pub fn extract_list_prefix(line: &str) -> (&str, &str, usize) {
832 let bytes = line.as_bytes();
833
834 let leading_spaces = bytes.iter().take_while(|&&b| b == b' ' || b == b'\t').count();
836 let mut pos = leading_spaces;
837
838 if pos >= bytes.len() {
839 return ("", line, 0);
840 }
841
842 if matches!(bytes[pos], b'-' | b'*' | b'+') {
844 pos += 1;
845
846 if pos >= bytes.len() || bytes[pos] == b' ' || bytes[pos] == b'\t' {
848 if pos < bytes.len() && (bytes[pos] == b' ' || bytes[pos] == b'\t') {
850 pos += 1;
851 }
852 let content_indent = pos;
853 return (&line[..pos], &line[pos..], content_indent);
854 }
855 return ("", line, 0);
857 }
858
859 if bytes[pos].is_ascii_digit() {
861 let digit_start = pos;
862 while pos < bytes.len() && bytes[pos].is_ascii_digit() {
863 pos += 1;
864 }
865
866 if pos > digit_start && pos < bytes.len() {
868 if bytes[pos] == b'.' || bytes[pos] == b')' {
870 pos += 1;
871 if pos >= bytes.len() || bytes[pos] == b' ' || bytes[pos] == b'\t' {
872 if pos < bytes.len() && (bytes[pos] == b' ' || bytes[pos] == b'\t') {
874 pos += 1;
875 }
876 let content_indent = pos;
877 return (&line[..pos], &line[pos..], content_indent);
878 }
879 }
880 }
881 }
882
883 ("", line, 0)
884 }
885
886 pub fn extract_table_row_content<'a>(line: &'a str, table_block: &TableBlock, line_index: usize) -> &'a str {
891 let (_, after_blockquote) = Self::extract_blockquote_prefix(line);
893
894 if let Some(ref list_ctx) = table_block.list_context {
896 if line_index == 0 {
897 after_blockquote
899 .strip_prefix(&list_ctx.list_prefix)
900 .unwrap_or_else(|| Self::extract_list_prefix(after_blockquote).1)
901 } else {
902 Self::strip_list_continuation_indent(after_blockquote, list_ctx.content_indent)
904 }
905 } else {
906 after_blockquote
907 }
908 }
909
910 pub fn is_list_item_with_table_row(line: &str) -> bool {
913 let (prefix, content, _) = Self::extract_list_prefix(line);
914 if prefix.is_empty() {
915 return false;
916 }
917
918 let trimmed = content.trim();
921 if !trimmed.starts_with('|') {
922 return false;
923 }
924
925 Self::is_potential_table_row_content(content)
927 }
928
929 fn is_potential_table_row_content(content: &str) -> bool {
931 Self::is_potential_table_row(content)
932 }
933}
934
935#[cfg(test)]
936mod tests {
937 use super::*;
938 use crate::lint_context::LintContext;
939
940 #[test]
941 fn test_is_potential_table_row() {
942 assert!(TableUtils::is_potential_table_row("| Header 1 | Header 2 |"));
944 assert!(TableUtils::is_potential_table_row("| Cell 1 | Cell 2 |"));
945 assert!(TableUtils::is_potential_table_row("Cell 1 | Cell 2"));
946 assert!(TableUtils::is_potential_table_row("| Cell |")); assert!(TableUtils::is_potential_table_row("| A | B | C | D | E |"));
950
951 assert!(TableUtils::is_potential_table_row(" | Indented | Table | "));
953 assert!(TableUtils::is_potential_table_row("| Spaces | Around |"));
954
955 assert!(!TableUtils::is_potential_table_row("- List item"));
957 assert!(!TableUtils::is_potential_table_row("* Another list"));
958 assert!(!TableUtils::is_potential_table_row("+ Plus list"));
959 assert!(!TableUtils::is_potential_table_row("Regular text"));
960 assert!(!TableUtils::is_potential_table_row(""));
961 assert!(!TableUtils::is_potential_table_row(" "));
962
963 assert!(!TableUtils::is_potential_table_row("`code with | pipe`"));
965 assert!(!TableUtils::is_potential_table_row("``multiple | backticks``"));
966 assert!(!TableUtils::is_potential_table_row("Use ``a|b`` in prose"));
967 assert!(TableUtils::is_potential_table_row("| `fenced` | Uses ``` and ~~~ |"));
968 assert!(TableUtils::is_potential_table_row("`!foo && bar` | `(!foo) && bar`"));
969 assert!(!TableUtils::is_potential_table_row("`echo a | sed 's/a/b/'`"));
970
971 assert!(!TableUtils::is_potential_table_row(
973 "Text with $|S|$ math notation here."
974 ));
975 assert!(!TableUtils::is_potential_table_row(
976 "Size $|S|$ was even, check $|T|$ too."
977 ));
978 assert!(!TableUtils::is_potential_table_row("Display $$|A| + |B|$$ math here."));
979 assert!(TableUtils::is_potential_table_row("| cell with $|S|$ math |"));
981 assert!(TableUtils::is_potential_table_row("$a$ | $b$"));
983 assert!(TableUtils::is_potential_table_row("$f(x)$ and $g(x)$ | result"));
984 assert!(!TableUtils::is_potential_table_row("$5 | $10"));
988
989 assert!(!TableUtils::is_potential_table_row("Just one |"));
991 assert!(!TableUtils::is_potential_table_row("| Just one"));
992
993 let long_cell = "a".repeat(150);
995 assert!(TableUtils::is_potential_table_row(&format!("| {long_cell} | b |")));
996
997 assert!(!TableUtils::is_potential_table_row("| Cell with\nnewline | Other |"));
999
1000 assert!(TableUtils::is_potential_table_row("|||")); assert!(TableUtils::is_potential_table_row("||||")); assert!(TableUtils::is_potential_table_row("| | |")); }
1005
1006 #[test]
1007 fn test_list_items_with_pipes_not_table_rows() {
1008 assert!(!TableUtils::is_potential_table_row("1. Item with | pipe"));
1010 assert!(!TableUtils::is_potential_table_row("10. Item with | pipe"));
1011 assert!(!TableUtils::is_potential_table_row("999. Item with | pipe"));
1012 assert!(!TableUtils::is_potential_table_row("1) Item with | pipe"));
1013 assert!(!TableUtils::is_potential_table_row("10) Item with | pipe"));
1014
1015 assert!(!TableUtils::is_potential_table_row("-\tItem with | pipe"));
1017 assert!(!TableUtils::is_potential_table_row("*\tItem with | pipe"));
1018 assert!(!TableUtils::is_potential_table_row("+\tItem with | pipe"));
1019
1020 assert!(!TableUtils::is_potential_table_row(" - Indented | pipe"));
1022 assert!(!TableUtils::is_potential_table_row(" * Deep indent | pipe"));
1023 assert!(!TableUtils::is_potential_table_row(" 1. Ordered indent | pipe"));
1024
1025 assert!(!TableUtils::is_potential_table_row("- [ ] task | pipe"));
1027 assert!(!TableUtils::is_potential_table_row("- [x] done | pipe"));
1028
1029 assert!(!TableUtils::is_potential_table_row("1. foo | bar | baz"));
1031 assert!(!TableUtils::is_potential_table_row("- alpha | beta | gamma"));
1032
1033 assert!(TableUtils::is_potential_table_row("| cell | cell |"));
1035 assert!(TableUtils::is_potential_table_row("cell | cell"));
1036 assert!(TableUtils::is_potential_table_row("| Header | Header |"));
1037 }
1038
1039 #[test]
1040 fn test_atx_headings_with_pipes_not_table_rows() {
1041 assert!(!TableUtils::is_potential_table_row("# Heading | with pipe"));
1043 assert!(!TableUtils::is_potential_table_row("## Heading | with pipe"));
1044 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
1049 assert!(!TableUtils::is_potential_table_row("### col1 | col2 | col3"));
1051 assert!(!TableUtils::is_potential_table_row("## a|b|c"));
1052
1053 assert!(!TableUtils::is_potential_table_row("#\tHeading | pipe"));
1055 assert!(!TableUtils::is_potential_table_row("##\tHeading | pipe"));
1056
1057 assert!(!TableUtils::is_potential_table_row("# |"));
1059 assert!(!TableUtils::is_potential_table_row("## |"));
1060
1061 assert!(!TableUtils::is_potential_table_row(" ## Heading | pipe"));
1063 assert!(!TableUtils::is_potential_table_row(" ### Heading | pipe"));
1064
1065 assert!(!TableUtils::is_potential_table_row("#### ®aAA|ᯗ"));
1067
1068 assert!(TableUtils::is_potential_table_row("####### text | pipe"));
1072
1073 assert!(TableUtils::is_potential_table_row("#nospc|pipe"));
1075
1076 assert!(TableUtils::is_potential_table_row("| # Header | Value |"));
1078 assert!(TableUtils::is_potential_table_row("text | #tag"));
1079 }
1080
1081 #[test]
1082 fn test_is_delimiter_row() {
1083 assert!(TableUtils::is_delimiter_row("|---|---|"));
1085 assert!(TableUtils::is_delimiter_row("| --- | --- |"));
1086 assert!(TableUtils::is_delimiter_row("|:---|---:|"));
1087 assert!(TableUtils::is_delimiter_row("|:---:|:---:|"));
1088
1089 assert!(TableUtils::is_delimiter_row("|-|--|"));
1091 assert!(TableUtils::is_delimiter_row("|-------|----------|"));
1092
1093 assert!(TableUtils::is_delimiter_row("| --- | --- |"));
1095 assert!(TableUtils::is_delimiter_row("| :--- | ---: |"));
1096
1097 assert!(TableUtils::is_delimiter_row("|---|---|---|---|"));
1099
1100 assert!(TableUtils::is_delimiter_row("--- | ---"));
1102 assert!(TableUtils::is_delimiter_row(":--- | ---:"));
1103
1104 assert!(!TableUtils::is_delimiter_row("| Header | Header |"));
1106 assert!(!TableUtils::is_delimiter_row("Regular text"));
1107 assert!(!TableUtils::is_delimiter_row(""));
1108 assert!(!TableUtils::is_delimiter_row("|||"));
1109 assert!(!TableUtils::is_delimiter_row("| | |"));
1110
1111 assert!(!TableUtils::is_delimiter_row("| : | : |"));
1113 assert!(!TableUtils::is_delimiter_row("| | |"));
1114
1115 assert!(!TableUtils::is_delimiter_row("| --- | text |"));
1117 assert!(!TableUtils::is_delimiter_row("| abc | --- |"));
1118 }
1119
1120 #[test]
1121 fn test_count_cells() {
1122 assert_eq!(TableUtils::count_cells("| Cell 1 | Cell 2 | Cell 3 |"), 3);
1124 assert_eq!(TableUtils::count_cells("Cell 1 | Cell 2 | Cell 3"), 3);
1125 assert_eq!(TableUtils::count_cells("| Cell 1 | Cell 2"), 2);
1126 assert_eq!(TableUtils::count_cells("Cell 1 | Cell 2 |"), 2);
1127
1128 assert_eq!(TableUtils::count_cells("| Cell |"), 1);
1130 assert_eq!(TableUtils::count_cells("Cell"), 0); assert_eq!(TableUtils::count_cells("| | | |"), 3);
1134 assert_eq!(TableUtils::count_cells("| | | |"), 3);
1135
1136 assert_eq!(TableUtils::count_cells("| A | B | C | D | E | F |"), 6);
1138
1139 assert_eq!(TableUtils::count_cells("||"), 1); assert_eq!(TableUtils::count_cells("|||"), 2); assert_eq!(TableUtils::count_cells("Regular text"), 0);
1145 assert_eq!(TableUtils::count_cells(""), 0);
1146 assert_eq!(TableUtils::count_cells(" "), 0);
1147
1148 assert_eq!(TableUtils::count_cells(" | A | B | "), 2);
1150 assert_eq!(TableUtils::count_cells("| A | B |"), 2);
1151 }
1152
1153 #[test]
1154 fn test_count_cells_with_escaped_pipes() {
1155 assert_eq!(TableUtils::count_cells("| Challenge | Solution |"), 2);
1160 assert_eq!(TableUtils::count_cells("| A | B | C |"), 3);
1161 assert_eq!(TableUtils::count_cells("| One | Two |"), 2);
1162
1163 assert_eq!(TableUtils::count_cells(r"| Command | echo \| grep |"), 2);
1165 assert_eq!(TableUtils::count_cells(r"| A | B \| C |"), 2); assert_eq!(TableUtils::count_cells(r"| Command | `echo \| grep` |"), 2);
1169
1170 assert_eq!(TableUtils::count_cells(r"| A | B \\| C |"), 3); assert_eq!(TableUtils::count_cells(r"| A | `B \\| C` |"), 2);
1174
1175 assert_eq!(TableUtils::count_cells("| Command | `echo | grep` |"), 2);
1177 assert_eq!(TableUtils::count_cells("| `code | one` | `code | two` |"), 2);
1178 assert_eq!(TableUtils::count_cells("| `single|pipe` |"), 1);
1179
1180 assert_eq!(TableUtils::count_cells(r"| Hour formats | `^([0-1]?\d|2[0-3])` |"), 2);
1182 assert_eq!(TableUtils::count_cells(r"| Hour formats | `^([0-1]?\d\|2[0-3])` |"), 2);
1184 }
1185
1186 #[test]
1187 fn test_determine_pipe_style() {
1188 assert_eq!(
1190 TableUtils::determine_pipe_style("| Cell 1 | Cell 2 |"),
1191 Some("leading_and_trailing")
1192 );
1193 assert_eq!(
1194 TableUtils::determine_pipe_style("| Cell 1 | Cell 2"),
1195 Some("leading_only")
1196 );
1197 assert_eq!(
1198 TableUtils::determine_pipe_style("Cell 1 | Cell 2 |"),
1199 Some("trailing_only")
1200 );
1201 assert_eq!(
1202 TableUtils::determine_pipe_style("Cell 1 | Cell 2"),
1203 Some("no_leading_or_trailing")
1204 );
1205
1206 assert_eq!(
1208 TableUtils::determine_pipe_style(" | Cell 1 | Cell 2 | "),
1209 Some("leading_and_trailing")
1210 );
1211 assert_eq!(
1212 TableUtils::determine_pipe_style(" | Cell 1 | Cell 2 "),
1213 Some("leading_only")
1214 );
1215
1216 assert_eq!(TableUtils::determine_pipe_style("Regular text"), None);
1218 assert_eq!(TableUtils::determine_pipe_style(""), None);
1219 assert_eq!(TableUtils::determine_pipe_style(" "), None);
1220
1221 assert_eq!(TableUtils::determine_pipe_style("|"), Some("leading_and_trailing"));
1223 assert_eq!(TableUtils::determine_pipe_style("| Cell"), Some("leading_only"));
1224 assert_eq!(TableUtils::determine_pipe_style("Cell |"), Some("trailing_only"));
1225 }
1226
1227 #[test]
1228 fn test_find_table_blocks_simple() {
1229 let content = "| Header 1 | Header 2 |
1230|-----------|-----------|
1231| Cell 1 | Cell 2 |
1232| Cell 3 | Cell 4 |";
1233
1234 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1235
1236 let tables = TableUtils::find_table_blocks(content, &ctx);
1237 assert_eq!(tables.len(), 1);
1238
1239 let table = &tables[0];
1240 assert_eq!(table.start_line, 0);
1241 assert_eq!(table.end_line, 3);
1242 assert_eq!(table.header_line, 0);
1243 assert_eq!(table.delimiter_line, 1);
1244 assert_eq!(table.content_lines, vec![2, 3]);
1245 }
1246
1247 #[test]
1248 fn test_find_table_blocks_multiple() {
1249 let content = "Some text
1250
1251| Table 1 | Col A |
1252|----------|-------|
1253| Data 1 | Val 1 |
1254
1255More text
1256
1257| Table 2 | Col 2 |
1258|----------|-------|
1259| Data 2 | Data |";
1260
1261 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1262
1263 let tables = TableUtils::find_table_blocks(content, &ctx);
1264 assert_eq!(tables.len(), 2);
1265
1266 assert_eq!(tables[0].start_line, 2);
1268 assert_eq!(tables[0].end_line, 4);
1269 assert_eq!(tables[0].header_line, 2);
1270 assert_eq!(tables[0].delimiter_line, 3);
1271 assert_eq!(tables[0].content_lines, vec![4]);
1272
1273 assert_eq!(tables[1].start_line, 8);
1275 assert_eq!(tables[1].end_line, 10);
1276 assert_eq!(tables[1].header_line, 8);
1277 assert_eq!(tables[1].delimiter_line, 9);
1278 assert_eq!(tables[1].content_lines, vec![10]);
1279 }
1280
1281 #[test]
1282 fn test_find_table_blocks_no_content_rows() {
1283 let content = "| Header 1 | Header 2 |
1284|-----------|-----------|
1285
1286Next paragraph";
1287
1288 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1289
1290 let tables = TableUtils::find_table_blocks(content, &ctx);
1291 assert_eq!(tables.len(), 1);
1292
1293 let table = &tables[0];
1294 assert_eq!(table.start_line, 0);
1295 assert_eq!(table.end_line, 1); assert_eq!(table.content_lines.len(), 0);
1297 }
1298
1299 #[test]
1300 fn test_find_table_blocks_in_code_block() {
1301 let content = "```
1302| Not | A | Table |
1303|-----|---|-------|
1304| In | Code | Block |
1305```
1306
1307| Real | Table |
1308|------|-------|
1309| Data | Here |";
1310
1311 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1312
1313 let tables = TableUtils::find_table_blocks(content, &ctx);
1314 assert_eq!(tables.len(), 1); let table = &tables[0];
1317 assert_eq!(table.header_line, 6);
1318 assert_eq!(table.delimiter_line, 7);
1319 }
1320
1321 #[test]
1322 fn test_find_table_blocks_no_tables() {
1323 let content = "Just regular text
1324No tables here
1325- List item with | pipe
1326* Another list item";
1327
1328 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1329
1330 let tables = TableUtils::find_table_blocks(content, &ctx);
1331 assert_eq!(tables.len(), 0);
1332 }
1333
1334 #[test]
1335 fn test_find_table_blocks_malformed() {
1336 let content = "| Header without delimiter |
1337| This looks like table |
1338But no delimiter row
1339
1340| Proper | Table |
1341|---------|-------|
1342| Data | Here |";
1343
1344 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1345
1346 let tables = TableUtils::find_table_blocks(content, &ctx);
1347 assert_eq!(tables.len(), 1); assert_eq!(tables[0].header_line, 4);
1349 }
1350
1351 #[test]
1352 fn test_edge_cases() {
1353 assert!(!TableUtils::is_potential_table_row(""));
1355 assert!(!TableUtils::is_delimiter_row(""));
1356 assert_eq!(TableUtils::count_cells(""), 0);
1357 assert_eq!(TableUtils::determine_pipe_style(""), None);
1358
1359 assert!(!TableUtils::is_potential_table_row(" "));
1361 assert!(!TableUtils::is_delimiter_row(" "));
1362 assert_eq!(TableUtils::count_cells(" "), 0);
1363 assert_eq!(TableUtils::determine_pipe_style(" "), None);
1364
1365 assert!(!TableUtils::is_potential_table_row("|"));
1367 assert!(!TableUtils::is_delimiter_row("|"));
1368 assert_eq!(TableUtils::count_cells("|"), 0); let long_single = format!("| {} |", "a".repeat(200));
1373 assert!(TableUtils::is_potential_table_row(&long_single)); let long_multi = format!("| {} | {} |", "a".repeat(200), "b".repeat(200));
1376 assert!(TableUtils::is_potential_table_row(&long_multi)); assert!(TableUtils::is_potential_table_row("| 你好 | 世界 |"));
1380 assert!(TableUtils::is_potential_table_row("| émoji | 🎉 |"));
1381 assert_eq!(TableUtils::count_cells("| 你好 | 世界 |"), 2);
1382 }
1383
1384 #[test]
1385 fn test_table_block_struct() {
1386 let block = TableBlock {
1387 start_line: 0,
1388 end_line: 5,
1389 header_line: 0,
1390 delimiter_line: 1,
1391 content_lines: vec![2, 3, 4, 5],
1392 list_context: None,
1393 };
1394
1395 let debug_str = format!("{block:?}");
1397 assert!(debug_str.contains("TableBlock"));
1398 assert!(debug_str.contains("start_line: 0"));
1399
1400 let cloned = block.clone();
1402 assert_eq!(cloned.start_line, block.start_line);
1403 assert_eq!(cloned.end_line, block.end_line);
1404 assert_eq!(cloned.header_line, block.header_line);
1405 assert_eq!(cloned.delimiter_line, block.delimiter_line);
1406 assert_eq!(cloned.content_lines, block.content_lines);
1407 assert!(cloned.list_context.is_none());
1408 }
1409
1410 #[test]
1411 fn test_split_table_row() {
1412 let cells = TableUtils::split_table_row("| Cell 1 | Cell 2 | Cell 3 |");
1414 assert_eq!(cells.len(), 3);
1415 assert_eq!(cells[0].trim(), "Cell 1");
1416 assert_eq!(cells[1].trim(), "Cell 2");
1417 assert_eq!(cells[2].trim(), "Cell 3");
1418
1419 let cells = TableUtils::split_table_row("| Cell 1 | Cell 2");
1421 assert_eq!(cells.len(), 2);
1422
1423 let cells = TableUtils::split_table_row("| | | |");
1425 assert_eq!(cells.len(), 3);
1426
1427 let cells = TableUtils::split_table_row("| Cell |");
1429 assert_eq!(cells.len(), 1);
1430 assert_eq!(cells[0].trim(), "Cell");
1431
1432 let cells = TableUtils::split_table_row("No pipes here");
1434 assert_eq!(cells.len(), 0);
1435 }
1436
1437 #[test]
1438 fn test_split_table_row_with_escaped_pipes() {
1439 let cells = TableUtils::split_table_row(r"| A | B \| C |");
1441 assert_eq!(cells.len(), 2);
1442 assert!(cells[1].contains(r"\|"), "Escaped pipe should be in cell content");
1443
1444 let cells = TableUtils::split_table_row(r"| A | B \\| C |");
1446 assert_eq!(cells.len(), 3);
1447 }
1448
1449 #[test]
1450 fn test_split_table_row_with_flavor_mkdocs() {
1451 let cells =
1453 TableUtils::split_table_row_with_flavor("| Type | `x | y` |", crate::config::MarkdownFlavor::MkDocs);
1454 assert_eq!(cells.len(), 2);
1455 assert!(
1456 cells[1].contains("`x | y`"),
1457 "Inline code with pipe should be single cell in MkDocs flavor"
1458 );
1459
1460 let cells =
1462 TableUtils::split_table_row_with_flavor("| Type | `a | b | c` |", crate::config::MarkdownFlavor::MkDocs);
1463 assert_eq!(cells.len(), 2);
1464 assert!(cells[1].contains("`a | b | c`"));
1465 }
1466
1467 #[test]
1468 fn test_split_table_row_with_flavor_standard() {
1469 let cells =
1471 TableUtils::split_table_row_with_flavor("| Type | `x | y` |", crate::config::MarkdownFlavor::Standard);
1472 assert_eq!(
1473 cells.len(),
1474 2,
1475 "Pipes in code spans should not be cell delimiters, got {cells:?}"
1476 );
1477 assert!(
1478 cells[1].contains("`x | y`"),
1479 "Inline code with pipe should be single cell"
1480 );
1481 }
1482
1483 #[test]
1486 fn test_extract_blockquote_prefix_no_blockquote() {
1487 let (prefix, content) = TableUtils::extract_blockquote_prefix("| H1 | H2 |");
1489 assert_eq!(prefix, "");
1490 assert_eq!(content, "| H1 | H2 |");
1491 }
1492
1493 #[test]
1494 fn test_extract_blockquote_prefix_single_level() {
1495 let (prefix, content) = TableUtils::extract_blockquote_prefix("> | H1 | H2 |");
1497 assert_eq!(prefix, "> ");
1498 assert_eq!(content, "| H1 | H2 |");
1499 }
1500
1501 #[test]
1502 fn test_extract_blockquote_prefix_double_level() {
1503 let (prefix, content) = TableUtils::extract_blockquote_prefix(">> | H1 | H2 |");
1505 assert_eq!(prefix, ">> ");
1506 assert_eq!(content, "| H1 | H2 |");
1507 }
1508
1509 #[test]
1510 fn test_extract_blockquote_prefix_triple_level() {
1511 let (prefix, content) = TableUtils::extract_blockquote_prefix(">>> | H1 | H2 |");
1513 assert_eq!(prefix, ">>> ");
1514 assert_eq!(content, "| H1 | H2 |");
1515 }
1516
1517 #[test]
1518 fn test_extract_blockquote_prefix_with_spaces() {
1519 let (prefix, content) = TableUtils::extract_blockquote_prefix("> > | H1 | H2 |");
1521 assert_eq!(prefix, "> > ");
1522 assert_eq!(content, "| H1 | H2 |");
1523 }
1524
1525 #[test]
1526 fn test_extract_blockquote_prefix_indented() {
1527 let (prefix, content) = TableUtils::extract_blockquote_prefix(" > | H1 | H2 |");
1529 assert_eq!(prefix, " > ");
1530 assert_eq!(content, "| H1 | H2 |");
1531 }
1532
1533 #[test]
1534 fn test_extract_blockquote_prefix_no_space_after() {
1535 let (prefix, content) = TableUtils::extract_blockquote_prefix(">| H1 | H2 |");
1537 assert_eq!(prefix, ">");
1538 assert_eq!(content, "| H1 | H2 |");
1539 }
1540
1541 #[test]
1542 fn test_determine_pipe_style_in_blockquote() {
1543 assert_eq!(
1545 TableUtils::determine_pipe_style("> | H1 | H2 |"),
1546 Some("leading_and_trailing")
1547 );
1548 assert_eq!(
1549 TableUtils::determine_pipe_style("> H1 | H2"),
1550 Some("no_leading_or_trailing")
1551 );
1552 assert_eq!(
1553 TableUtils::determine_pipe_style(">> | H1 | H2 |"),
1554 Some("leading_and_trailing")
1555 );
1556 assert_eq!(TableUtils::determine_pipe_style(">>> | H1 | H2"), Some("leading_only"));
1557 }
1558
1559 #[test]
1560 fn test_list_table_delimiter_requires_indentation() {
1561 let content = "- List item with | pipe\n|---|---|\n| Cell 1 | Cell 2 |";
1566 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1567 let tables = TableUtils::find_table_blocks(content, &ctx);
1568
1569 assert_eq!(tables.len(), 1, "Should find exactly one table");
1572 assert!(
1573 tables[0].list_context.is_none(),
1574 "Should NOT have list context since delimiter has no indentation"
1575 );
1576 }
1577
1578 #[test]
1579 fn test_list_table_with_properly_indented_delimiter() {
1580 let content = "- | Header 1 | Header 2 |\n |----------|----------|\n | Cell 1 | Cell 2 |";
1583 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
1584 let tables = TableUtils::find_table_blocks(content, &ctx);
1585
1586 assert_eq!(tables.len(), 1, "Should find exactly one table");
1588 assert_eq!(tables[0].start_line, 0, "Table should start at list item line");
1589 assert!(
1590 tables[0].list_context.is_some(),
1591 "Should be a list table since delimiter is properly indented"
1592 );
1593 }
1594
1595 #[test]
1596 fn test_mask_pipes_in_inline_code_regular_backticks() {
1597 let result = TableUtils::mask_pipes_in_inline_code("| `code | here` |");
1599 assert_eq!(result, "| `code _ here` |");
1600 }
1601
1602 #[test]
1603 fn test_mask_pipes_in_inline_code_escaped_backtick_not_code_span() {
1604 let result = TableUtils::mask_pipes_in_inline_code(r"| \`not code | still pipe\` |");
1607 assert_eq!(result, r"| \`not code | still pipe\` |");
1608 }
1609
1610 #[test]
1611 fn test_mask_pipes_in_inline_code_escaped_backslash_then_backtick() {
1612 let result = TableUtils::mask_pipes_in_inline_code(r"| \\`real code | masked\\` |");
1615 assert_eq!(result, r"| \\`real code _ masked\\` |");
1618 }
1619
1620 #[test]
1621 fn test_mask_pipes_in_inline_code_triple_backslash_before_backtick() {
1622 let result = TableUtils::mask_pipes_in_inline_code(r"| \\\`not code | pipe\\\` |");
1624 assert_eq!(result, r"| \\\`not code | pipe\\\` |");
1625 }
1626
1627 #[test]
1628 fn test_mask_pipes_in_inline_code_four_backslashes_before_backtick() {
1629 let result = TableUtils::mask_pipes_in_inline_code(r"| \\\\`code | here\\\\` |");
1631 assert_eq!(result, r"| \\\\`code _ here\\\\` |");
1632 }
1633
1634 #[test]
1635 fn test_mask_pipes_in_inline_code_no_backslash() {
1636 let result = TableUtils::mask_pipes_in_inline_code("before `a | b` after");
1638 assert_eq!(result, "before `a _ b` after");
1639 }
1640
1641 #[test]
1642 fn test_mask_pipes_in_inline_code_no_code_span() {
1643 let result = TableUtils::mask_pipes_in_inline_code("| col1 | col2 |");
1645 assert_eq!(result, "| col1 | col2 |");
1646 }
1647
1648 #[test]
1649 fn test_mask_pipes_in_inline_code_backslash_before_closing_backtick() {
1650 let result = TableUtils::mask_pipes_in_inline_code(r"| `foo\` | bar |");
1659 assert_eq!(result, r"| `foo\` | bar |");
1662 }
1663
1664 #[test]
1665 fn test_mask_pipes_in_inline_code_backslash_literal_with_pipe_inside() {
1666 let result = TableUtils::mask_pipes_in_inline_code(r"| `a\|b` | col2 |");
1670 assert_eq!(result, r"| `a\_b` | col2 |");
1671 }
1672
1673 #[test]
1674 fn test_count_preceding_backslashes() {
1675 let chars: Vec<char> = r"abc\\\`def".chars().collect();
1676 assert_eq!(TableUtils::count_preceding_backslashes(&chars, 6), 3);
1678
1679 let chars2: Vec<char> = r"abc\\`def".chars().collect();
1680 assert_eq!(TableUtils::count_preceding_backslashes(&chars2, 5), 2);
1682
1683 let chars3: Vec<char> = "`def".chars().collect();
1684 assert_eq!(TableUtils::count_preceding_backslashes(&chars3, 0), 0);
1686 }
1687
1688 #[test]
1689 fn test_has_unescaped_pipe_backslash_literal_in_code_span() {
1690 assert!(TableUtils::has_unescaped_pipe_outside_spans(r"`foo\` | bar"));
1693
1694 assert!(TableUtils::has_unescaped_pipe_outside_spans(r"\`foo | bar\`"));
1696
1697 assert!(!TableUtils::has_unescaped_pipe_outside_spans(r"`foo | bar`"));
1699 }
1700
1701 #[test]
1702 fn test_table_after_code_span_detected() {
1703 use crate::config::MarkdownFlavor;
1704
1705 let content = "`code`\n\n| A | B |\n|---|---|\n| 1 | 2 |\n";
1706 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
1707 assert!(!ctx.table_blocks.is_empty(), "Table after code span should be detected");
1708 }
1709
1710 #[test]
1711 fn test_table_inside_html_comment_not_detected() {
1712 use crate::config::MarkdownFlavor;
1713
1714 let content = "<!--\n| A | B |\n|---|---|\n| 1 | 2 |\n-->\n";
1715 let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
1716 assert!(
1717 ctx.table_blocks.is_empty(),
1718 "Table inside HTML comment should not be detected"
1719 );
1720 }
1721}