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}
14
15pub struct TableUtils;
17
18impl TableUtils {
19 pub fn is_potential_table_row(line: &str) -> bool {
21 let trimmed = line.trim();
22 if trimmed.is_empty() || !trimmed.contains('|') {
23 return false;
24 }
25
26 if trimmed.starts_with("- ")
29 || trimmed.starts_with("* ")
30 || trimmed.starts_with("+ ")
31 || trimmed.starts_with("-\t")
32 || trimmed.starts_with("*\t")
33 || trimmed.starts_with("+\t")
34 {
35 return false;
36 }
37
38 if let Some(first_non_digit) = trimmed.find(|c: char| !c.is_ascii_digit())
40 && first_non_digit > 0
41 {
42 let after_digits = &trimmed[first_non_digit..];
43 if after_digits.starts_with(". ")
44 || after_digits.starts_with(".\t")
45 || after_digits.starts_with(") ")
46 || after_digits.starts_with(")\t")
47 {
48 return false;
49 }
50 }
51
52 if trimmed.starts_with("`") || trimmed.contains("``") {
54 return false;
55 }
56
57 let parts: Vec<&str> = trimmed.split('|').collect();
59 if parts.len() < 2 {
60 return false;
61 }
62
63 let mut valid_parts = 0;
65 let mut total_non_empty_parts = 0;
66
67 for part in &parts {
68 let part_trimmed = part.trim();
69 if part_trimmed.is_empty() {
71 continue;
72 }
73 total_non_empty_parts += 1;
74
75 if !part_trimmed.contains('\n') {
77 valid_parts += 1;
78 }
79 }
80
81 if total_non_empty_parts > 0 && valid_parts != total_non_empty_parts {
83 return false;
85 }
86
87 if total_non_empty_parts == 0 {
90 return trimmed.starts_with('|') && trimmed.ends_with('|') && parts.len() >= 3;
92 }
93
94 if trimmed.starts_with('|') && trimmed.ends_with('|') {
97 valid_parts >= 1
99 } else {
100 valid_parts >= 2
102 }
103 }
104
105 pub fn is_delimiter_row(line: &str) -> bool {
107 let trimmed = line.trim();
108 if !trimmed.contains('|') || !trimmed.contains('-') {
109 return false;
110 }
111
112 let parts: Vec<&str> = trimmed.split('|').collect();
114 let mut valid_delimiter_parts = 0;
115 let mut total_non_empty_parts = 0;
116
117 for part in &parts {
118 let part_trimmed = part.trim();
119 if part_trimmed.is_empty() {
120 continue; }
122
123 total_non_empty_parts += 1;
124
125 if part_trimmed.chars().all(|c| c == '-' || c == ':' || c.is_whitespace()) && part_trimmed.contains('-') {
127 valid_delimiter_parts += 1;
128 }
129 }
130
131 total_non_empty_parts > 0 && valid_delimiter_parts == total_non_empty_parts
133 }
134
135 pub fn find_table_blocks_with_code_info(
138 content: &str,
139 code_blocks: &[(usize, usize)],
140 code_spans: &[crate::lint_context::CodeSpan],
141 html_comment_ranges: &[crate::utils::skip_context::ByteRange],
142 ) -> Vec<TableBlock> {
143 let lines: Vec<&str> = content.lines().collect();
144 let mut tables = Vec::new();
145 let mut i = 0;
146
147 let mut line_positions = Vec::with_capacity(lines.len());
149 let mut pos = 0;
150 for line in &lines {
151 line_positions.push(pos);
152 pos += line.len() + 1; }
154
155 while i < lines.len() {
156 let line_start = line_positions[i];
158 let in_code =
159 crate::utils::code_block_utils::CodeBlockUtils::is_in_code_block_or_span(code_blocks, line_start)
160 || code_spans
161 .iter()
162 .any(|span| line_start >= span.byte_offset && line_start < span.byte_end);
163 let in_html_comment = html_comment_ranges
164 .iter()
165 .any(|range| line_start >= range.start && line_start < range.end);
166
167 if in_code || in_html_comment {
168 i += 1;
169 continue;
170 }
171
172 if Self::is_potential_table_row(lines[i]) {
174 if i + 1 < lines.len() && Self::is_delimiter_row(lines[i + 1]) {
176 let table_start = i;
178 let header_line = i;
179 let delimiter_line = i + 1;
180 let mut table_end = i + 1; let mut content_lines = Vec::new();
182
183 let mut j = i + 2;
185 while j < lines.len() {
186 let line = lines[j];
187 if line.trim().is_empty() {
188 break;
190 }
191 if Self::is_potential_table_row(line) {
192 content_lines.push(j);
193 table_end = j;
194 j += 1;
195 } else {
196 break;
198 }
199 }
200
201 tables.push(TableBlock {
202 start_line: table_start,
203 end_line: table_end,
204 header_line,
205 delimiter_line,
206 content_lines,
207 });
208 i = table_end + 1;
209 } else {
210 i += 1;
211 }
212 } else {
213 i += 1;
214 }
215 }
216
217 tables
218 }
219
220 pub fn find_table_blocks(content: &str, ctx: &crate::lint_context::LintContext) -> Vec<TableBlock> {
223 Self::find_table_blocks_with_code_info(content, &ctx.code_blocks, &ctx.code_spans(), ctx.html_comment_ranges())
224 }
225
226 pub fn count_cells(row: &str) -> usize {
229 Self::count_cells_with_flavor(row, crate::config::MarkdownFlavor::Standard)
230 }
231
232 pub fn count_cells_with_flavor(row: &str, flavor: crate::config::MarkdownFlavor) -> usize {
242 Self::split_table_row_with_flavor(row, flavor).len()
243 }
244
245 pub fn mask_pipes_in_inline_code(text: &str) -> String {
247 let mut result = String::new();
248 let chars: Vec<char> = text.chars().collect();
249 let mut i = 0;
250
251 while i < chars.len() {
252 if chars[i] == '`' {
253 let start = i;
255 let mut backtick_count = 0;
256 while i < chars.len() && chars[i] == '`' {
257 backtick_count += 1;
258 i += 1;
259 }
260
261 let mut found_closing = false;
263 let mut j = i;
264
265 while j < chars.len() {
266 if chars[j] == '`' {
267 let close_start = j;
269 let mut close_count = 0;
270 while j < chars.len() && chars[j] == '`' {
271 close_count += 1;
272 j += 1;
273 }
274
275 if close_count == backtick_count {
276 found_closing = true;
278
279 result.extend(chars[start..i].iter());
281
282 for &ch in chars.iter().take(close_start).skip(i) {
283 if ch == '|' {
284 result.push('_'); } else {
286 result.push(ch);
287 }
288 }
289
290 result.extend(chars[close_start..j].iter());
291 i = j;
292 break;
293 }
294 } else {
296 j += 1;
297 }
298 }
299
300 if !found_closing {
301 result.extend(chars[start..i].iter());
303 }
304 } else {
305 result.push(chars[i]);
306 i += 1;
307 }
308 }
309
310 result
311 }
312
313 pub fn mask_pipes_for_table_parsing(text: &str) -> String {
326 let mut result = String::new();
327 let chars: Vec<char> = text.chars().collect();
328 let mut i = 0;
329
330 while i < chars.len() {
331 if chars[i] == '\\' {
332 if i + 1 < chars.len() && chars[i + 1] == '\\' {
333 result.push('\\');
336 result.push('\\');
337 i += 2;
338 } else if i + 1 < chars.len() && chars[i + 1] == '|' {
339 result.push('\\');
341 result.push('_'); i += 2;
343 } else {
344 result.push(chars[i]);
346 i += 1;
347 }
348 } else {
349 result.push(chars[i]);
350 i += 1;
351 }
352 }
353
354 result
355 }
356
357 pub fn split_table_row_with_flavor(row: &str, flavor: crate::config::MarkdownFlavor) -> Vec<String> {
366 let trimmed = row.trim();
367
368 if !trimmed.contains('|') {
369 return Vec::new();
370 }
371
372 let masked = Self::mask_pipes_for_table_parsing(trimmed);
374
375 let final_masked = if flavor == crate::config::MarkdownFlavor::MkDocs {
377 Self::mask_pipes_in_inline_code(&masked)
378 } else {
379 masked
380 };
381
382 let has_leading = final_masked.starts_with('|');
383 let has_trailing = final_masked.ends_with('|');
384
385 let mut masked_content = final_masked.as_str();
386 let mut orig_content = trimmed;
387
388 if has_leading {
389 masked_content = &masked_content[1..];
390 orig_content = &orig_content[1..];
391 }
392
393 let stripped_trailing = has_trailing && !masked_content.is_empty();
395 if stripped_trailing {
396 masked_content = &masked_content[..masked_content.len() - 1];
397 orig_content = &orig_content[..orig_content.len() - 1];
398 }
399
400 if masked_content.is_empty() {
402 if stripped_trailing {
403 return vec![String::new()];
405 } else {
406 return Vec::new();
408 }
409 }
410
411 let masked_parts: Vec<&str> = masked_content.split('|').collect();
412 let mut cells = Vec::new();
413 let mut pos = 0;
414
415 for masked_cell in masked_parts {
416 let cell_len = masked_cell.len();
417 let orig_cell = if pos + cell_len <= orig_content.len() {
418 &orig_content[pos..pos + cell_len]
419 } else {
420 masked_cell
421 };
422 cells.push(orig_cell.to_string());
423 pos += cell_len + 1; }
425
426 cells
427 }
428
429 pub fn split_table_row(row: &str) -> Vec<String> {
431 Self::split_table_row_with_flavor(row, crate::config::MarkdownFlavor::Standard)
432 }
433
434 pub fn determine_pipe_style(line: &str) -> Option<&'static str> {
436 let trimmed = line.trim();
437 if !trimmed.contains('|') {
438 return None;
439 }
440
441 let has_leading = trimmed.starts_with('|');
442 let has_trailing = trimmed.ends_with('|');
443
444 match (has_leading, has_trailing) {
445 (true, true) => Some("leading_and_trailing"),
446 (true, false) => Some("leading_only"),
447 (false, true) => Some("trailing_only"),
448 (false, false) => Some("no_leading_or_trailing"),
449 }
450 }
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456 use crate::lint_context::LintContext;
457
458 #[test]
459 fn test_is_potential_table_row() {
460 assert!(TableUtils::is_potential_table_row("| Header 1 | Header 2 |"));
462 assert!(TableUtils::is_potential_table_row("| Cell 1 | Cell 2 |"));
463 assert!(TableUtils::is_potential_table_row("Cell 1 | Cell 2"));
464 assert!(TableUtils::is_potential_table_row("| Cell |")); assert!(TableUtils::is_potential_table_row("| A | B | C | D | E |"));
468
469 assert!(TableUtils::is_potential_table_row(" | Indented | Table | "));
471 assert!(TableUtils::is_potential_table_row("| Spaces | Around |"));
472
473 assert!(!TableUtils::is_potential_table_row("- List item"));
475 assert!(!TableUtils::is_potential_table_row("* Another list"));
476 assert!(!TableUtils::is_potential_table_row("+ Plus list"));
477 assert!(!TableUtils::is_potential_table_row("Regular text"));
478 assert!(!TableUtils::is_potential_table_row(""));
479 assert!(!TableUtils::is_potential_table_row(" "));
480
481 assert!(!TableUtils::is_potential_table_row("`code with | pipe`"));
483 assert!(!TableUtils::is_potential_table_row("``multiple | backticks``"));
484
485 assert!(!TableUtils::is_potential_table_row("Just one |"));
487 assert!(!TableUtils::is_potential_table_row("| Just one"));
488
489 let long_cell = "a".repeat(150);
491 assert!(TableUtils::is_potential_table_row(&format!("| {long_cell} | b |")));
492
493 assert!(!TableUtils::is_potential_table_row("| Cell with\nnewline | Other |"));
495
496 assert!(TableUtils::is_potential_table_row("|||")); assert!(TableUtils::is_potential_table_row("||||")); assert!(TableUtils::is_potential_table_row("| | |")); }
501
502 #[test]
503 fn test_list_items_with_pipes_not_table_rows() {
504 assert!(!TableUtils::is_potential_table_row("1. Item with | pipe"));
506 assert!(!TableUtils::is_potential_table_row("10. Item with | pipe"));
507 assert!(!TableUtils::is_potential_table_row("999. Item with | pipe"));
508 assert!(!TableUtils::is_potential_table_row("1) Item with | pipe"));
509 assert!(!TableUtils::is_potential_table_row("10) Item with | pipe"));
510
511 assert!(!TableUtils::is_potential_table_row("-\tItem with | pipe"));
513 assert!(!TableUtils::is_potential_table_row("*\tItem with | pipe"));
514 assert!(!TableUtils::is_potential_table_row("+\tItem with | pipe"));
515
516 assert!(!TableUtils::is_potential_table_row(" - Indented | pipe"));
518 assert!(!TableUtils::is_potential_table_row(" * Deep indent | pipe"));
519 assert!(!TableUtils::is_potential_table_row(" 1. Ordered indent | pipe"));
520
521 assert!(!TableUtils::is_potential_table_row("- [ ] task | pipe"));
523 assert!(!TableUtils::is_potential_table_row("- [x] done | pipe"));
524
525 assert!(!TableUtils::is_potential_table_row("1. foo | bar | baz"));
527 assert!(!TableUtils::is_potential_table_row("- alpha | beta | gamma"));
528
529 assert!(TableUtils::is_potential_table_row("| cell | cell |"));
531 assert!(TableUtils::is_potential_table_row("cell | cell"));
532 assert!(TableUtils::is_potential_table_row("| Header | Header |"));
533 }
534
535 #[test]
536 fn test_is_delimiter_row() {
537 assert!(TableUtils::is_delimiter_row("|---|---|"));
539 assert!(TableUtils::is_delimiter_row("| --- | --- |"));
540 assert!(TableUtils::is_delimiter_row("|:---|---:|"));
541 assert!(TableUtils::is_delimiter_row("|:---:|:---:|"));
542
543 assert!(TableUtils::is_delimiter_row("|-|--|"));
545 assert!(TableUtils::is_delimiter_row("|-------|----------|"));
546
547 assert!(TableUtils::is_delimiter_row("| --- | --- |"));
549 assert!(TableUtils::is_delimiter_row("| :--- | ---: |"));
550
551 assert!(TableUtils::is_delimiter_row("|---|---|---|---|"));
553
554 assert!(TableUtils::is_delimiter_row("--- | ---"));
556 assert!(TableUtils::is_delimiter_row(":--- | ---:"));
557
558 assert!(!TableUtils::is_delimiter_row("| Header | Header |"));
560 assert!(!TableUtils::is_delimiter_row("Regular text"));
561 assert!(!TableUtils::is_delimiter_row(""));
562 assert!(!TableUtils::is_delimiter_row("|||"));
563 assert!(!TableUtils::is_delimiter_row("| | |"));
564
565 assert!(!TableUtils::is_delimiter_row("| : | : |"));
567 assert!(!TableUtils::is_delimiter_row("| | |"));
568
569 assert!(!TableUtils::is_delimiter_row("| --- | text |"));
571 assert!(!TableUtils::is_delimiter_row("| abc | --- |"));
572 }
573
574 #[test]
575 fn test_count_cells() {
576 assert_eq!(TableUtils::count_cells("| Cell 1 | Cell 2 | Cell 3 |"), 3);
578 assert_eq!(TableUtils::count_cells("Cell 1 | Cell 2 | Cell 3"), 3);
579 assert_eq!(TableUtils::count_cells("| Cell 1 | Cell 2"), 2);
580 assert_eq!(TableUtils::count_cells("Cell 1 | Cell 2 |"), 2);
581
582 assert_eq!(TableUtils::count_cells("| Cell |"), 1);
584 assert_eq!(TableUtils::count_cells("Cell"), 0); assert_eq!(TableUtils::count_cells("| | | |"), 3);
588 assert_eq!(TableUtils::count_cells("| | | |"), 3);
589
590 assert_eq!(TableUtils::count_cells("| A | B | C | D | E | F |"), 6);
592
593 assert_eq!(TableUtils::count_cells("||"), 1); assert_eq!(TableUtils::count_cells("|||"), 2); assert_eq!(TableUtils::count_cells("Regular text"), 0);
599 assert_eq!(TableUtils::count_cells(""), 0);
600 assert_eq!(TableUtils::count_cells(" "), 0);
601
602 assert_eq!(TableUtils::count_cells(" | A | B | "), 2);
604 assert_eq!(TableUtils::count_cells("| A | B |"), 2);
605 }
606
607 #[test]
608 fn test_count_cells_with_escaped_pipes() {
609 assert_eq!(TableUtils::count_cells("| Challenge | Solution |"), 2);
615 assert_eq!(TableUtils::count_cells("| A | B | C |"), 3);
616 assert_eq!(TableUtils::count_cells("| One | Two |"), 2);
617
618 assert_eq!(TableUtils::count_cells(r"| Command | echo \| grep |"), 2);
620 assert_eq!(TableUtils::count_cells(r"| A | B \| C |"), 2); assert_eq!(TableUtils::count_cells(r"| Command | `echo \| grep` |"), 2);
624
625 assert_eq!(TableUtils::count_cells(r"| A | B \\| C |"), 3); assert_eq!(TableUtils::count_cells(r"| A | `B \\| C` |"), 3); assert_eq!(TableUtils::count_cells("| Command | `echo | grep` |"), 3);
632 assert_eq!(TableUtils::count_cells("| `code | one` | `code | two` |"), 4);
633 assert_eq!(TableUtils::count_cells("| `single|pipe` |"), 2);
634
635 assert_eq!(TableUtils::count_cells(r"| Hour formats | `^([0-1]?\d|2[0-3])` |"), 3);
638 assert_eq!(TableUtils::count_cells(r"| Hour formats | `^([0-1]?\d\|2[0-3])` |"), 2);
640 }
641
642 #[test]
643 fn test_determine_pipe_style() {
644 assert_eq!(
646 TableUtils::determine_pipe_style("| Cell 1 | Cell 2 |"),
647 Some("leading_and_trailing")
648 );
649 assert_eq!(
650 TableUtils::determine_pipe_style("| Cell 1 | Cell 2"),
651 Some("leading_only")
652 );
653 assert_eq!(
654 TableUtils::determine_pipe_style("Cell 1 | Cell 2 |"),
655 Some("trailing_only")
656 );
657 assert_eq!(
658 TableUtils::determine_pipe_style("Cell 1 | Cell 2"),
659 Some("no_leading_or_trailing")
660 );
661
662 assert_eq!(
664 TableUtils::determine_pipe_style(" | Cell 1 | Cell 2 | "),
665 Some("leading_and_trailing")
666 );
667 assert_eq!(
668 TableUtils::determine_pipe_style(" | Cell 1 | Cell 2 "),
669 Some("leading_only")
670 );
671
672 assert_eq!(TableUtils::determine_pipe_style("Regular text"), None);
674 assert_eq!(TableUtils::determine_pipe_style(""), None);
675 assert_eq!(TableUtils::determine_pipe_style(" "), None);
676
677 assert_eq!(TableUtils::determine_pipe_style("|"), Some("leading_and_trailing"));
679 assert_eq!(TableUtils::determine_pipe_style("| Cell"), Some("leading_only"));
680 assert_eq!(TableUtils::determine_pipe_style("Cell |"), Some("trailing_only"));
681 }
682
683 #[test]
684 fn test_find_table_blocks_simple() {
685 let content = "| Header 1 | Header 2 |
686|-----------|-----------|
687| Cell 1 | Cell 2 |
688| Cell 3 | Cell 4 |";
689
690 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
691
692 let tables = TableUtils::find_table_blocks(content, &ctx);
693 assert_eq!(tables.len(), 1);
694
695 let table = &tables[0];
696 assert_eq!(table.start_line, 0);
697 assert_eq!(table.end_line, 3);
698 assert_eq!(table.header_line, 0);
699 assert_eq!(table.delimiter_line, 1);
700 assert_eq!(table.content_lines, vec![2, 3]);
701 }
702
703 #[test]
704 fn test_find_table_blocks_multiple() {
705 let content = "Some text
706
707| Table 1 | Col A |
708|----------|-------|
709| Data 1 | Val 1 |
710
711More text
712
713| Table 2 | Col 2 |
714|----------|-------|
715| Data 2 | Data |";
716
717 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
718
719 let tables = TableUtils::find_table_blocks(content, &ctx);
720 assert_eq!(tables.len(), 2);
721
722 assert_eq!(tables[0].start_line, 2);
724 assert_eq!(tables[0].end_line, 4);
725 assert_eq!(tables[0].header_line, 2);
726 assert_eq!(tables[0].delimiter_line, 3);
727 assert_eq!(tables[0].content_lines, vec![4]);
728
729 assert_eq!(tables[1].start_line, 8);
731 assert_eq!(tables[1].end_line, 10);
732 assert_eq!(tables[1].header_line, 8);
733 assert_eq!(tables[1].delimiter_line, 9);
734 assert_eq!(tables[1].content_lines, vec![10]);
735 }
736
737 #[test]
738 fn test_find_table_blocks_no_content_rows() {
739 let content = "| Header 1 | Header 2 |
740|-----------|-----------|
741
742Next paragraph";
743
744 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
745
746 let tables = TableUtils::find_table_blocks(content, &ctx);
747 assert_eq!(tables.len(), 1);
748
749 let table = &tables[0];
750 assert_eq!(table.start_line, 0);
751 assert_eq!(table.end_line, 1); assert_eq!(table.content_lines.len(), 0);
753 }
754
755 #[test]
756 fn test_find_table_blocks_in_code_block() {
757 let content = "```
758| Not | A | Table |
759|-----|---|-------|
760| In | Code | Block |
761```
762
763| Real | Table |
764|------|-------|
765| Data | Here |";
766
767 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
768
769 let tables = TableUtils::find_table_blocks(content, &ctx);
770 assert_eq!(tables.len(), 1); let table = &tables[0];
773 assert_eq!(table.header_line, 6);
774 assert_eq!(table.delimiter_line, 7);
775 }
776
777 #[test]
778 fn test_find_table_blocks_no_tables() {
779 let content = "Just regular text
780No tables here
781- List item with | pipe
782* Another list item";
783
784 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
785
786 let tables = TableUtils::find_table_blocks(content, &ctx);
787 assert_eq!(tables.len(), 0);
788 }
789
790 #[test]
791 fn test_find_table_blocks_malformed() {
792 let content = "| Header without delimiter |
793| This looks like table |
794But no delimiter row
795
796| Proper | Table |
797|---------|-------|
798| Data | Here |";
799
800 let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard, None);
801
802 let tables = TableUtils::find_table_blocks(content, &ctx);
803 assert_eq!(tables.len(), 1); assert_eq!(tables[0].header_line, 4);
805 }
806
807 #[test]
808 fn test_edge_cases() {
809 assert!(!TableUtils::is_potential_table_row(""));
811 assert!(!TableUtils::is_delimiter_row(""));
812 assert_eq!(TableUtils::count_cells(""), 0);
813 assert_eq!(TableUtils::determine_pipe_style(""), None);
814
815 assert!(!TableUtils::is_potential_table_row(" "));
817 assert!(!TableUtils::is_delimiter_row(" "));
818 assert_eq!(TableUtils::count_cells(" "), 0);
819 assert_eq!(TableUtils::determine_pipe_style(" "), None);
820
821 assert!(!TableUtils::is_potential_table_row("|"));
823 assert!(!TableUtils::is_delimiter_row("|"));
824 assert_eq!(TableUtils::count_cells("|"), 0); let long_single = format!("| {} |", "a".repeat(200));
829 assert!(TableUtils::is_potential_table_row(&long_single)); let long_multi = format!("| {} | {} |", "a".repeat(200), "b".repeat(200));
832 assert!(TableUtils::is_potential_table_row(&long_multi)); assert!(TableUtils::is_potential_table_row("| 你好 | 世界 |"));
836 assert!(TableUtils::is_potential_table_row("| émoji | 🎉 |"));
837 assert_eq!(TableUtils::count_cells("| 你好 | 世界 |"), 2);
838 }
839
840 #[test]
841 fn test_table_block_struct() {
842 let block = TableBlock {
843 start_line: 0,
844 end_line: 5,
845 header_line: 0,
846 delimiter_line: 1,
847 content_lines: vec![2, 3, 4, 5],
848 };
849
850 let debug_str = format!("{block:?}");
852 assert!(debug_str.contains("TableBlock"));
853 assert!(debug_str.contains("start_line: 0"));
854
855 let cloned = block.clone();
857 assert_eq!(cloned.start_line, block.start_line);
858 assert_eq!(cloned.end_line, block.end_line);
859 assert_eq!(cloned.header_line, block.header_line);
860 assert_eq!(cloned.delimiter_line, block.delimiter_line);
861 assert_eq!(cloned.content_lines, block.content_lines);
862 }
863
864 #[test]
865 fn test_split_table_row() {
866 let cells = TableUtils::split_table_row("| Cell 1 | Cell 2 | Cell 3 |");
868 assert_eq!(cells.len(), 3);
869 assert_eq!(cells[0].trim(), "Cell 1");
870 assert_eq!(cells[1].trim(), "Cell 2");
871 assert_eq!(cells[2].trim(), "Cell 3");
872
873 let cells = TableUtils::split_table_row("| Cell 1 | Cell 2");
875 assert_eq!(cells.len(), 2);
876
877 let cells = TableUtils::split_table_row("| | | |");
879 assert_eq!(cells.len(), 3);
880
881 let cells = TableUtils::split_table_row("| Cell |");
883 assert_eq!(cells.len(), 1);
884 assert_eq!(cells[0].trim(), "Cell");
885
886 let cells = TableUtils::split_table_row("No pipes here");
888 assert_eq!(cells.len(), 0);
889 }
890
891 #[test]
892 fn test_split_table_row_with_escaped_pipes() {
893 let cells = TableUtils::split_table_row(r"| A | B \| C |");
895 assert_eq!(cells.len(), 2);
896 assert!(cells[1].contains(r"\|"), "Escaped pipe should be in cell content");
897
898 let cells = TableUtils::split_table_row(r"| A | B \\| C |");
900 assert_eq!(cells.len(), 3);
901 }
902
903 #[test]
904 fn test_split_table_row_with_flavor_mkdocs() {
905 let cells =
907 TableUtils::split_table_row_with_flavor("| Type | `x | y` |", crate::config::MarkdownFlavor::MkDocs);
908 assert_eq!(cells.len(), 2);
909 assert!(
910 cells[1].contains("`x | y`"),
911 "Inline code with pipe should be single cell in MkDocs flavor"
912 );
913
914 let cells =
916 TableUtils::split_table_row_with_flavor("| Type | `a | b | c` |", crate::config::MarkdownFlavor::MkDocs);
917 assert_eq!(cells.len(), 2);
918 assert!(cells[1].contains("`a | b | c`"));
919 }
920
921 #[test]
922 fn test_split_table_row_with_flavor_standard() {
923 let cells =
925 TableUtils::split_table_row_with_flavor("| Type | `x | y` |", crate::config::MarkdownFlavor::Standard);
926 assert_eq!(cells.len(), 3);
928 }
929}