rumdl_lib/utils/
table_utils.rs

1/// Shared table detection and processing utilities for markdown linting rules
2///
3/// This module provides optimized table detection and processing functionality
4/// that can be shared across multiple table-related rules (MD055, MD056, MD058).
5/// Represents a table block in the document
6#[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
15/// Shared table detection utilities
16pub struct TableUtils;
17
18impl TableUtils {
19    /// Check if a line looks like a potential table row
20    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        // Skip lines that are clearly not table rows
27        if trimmed.starts_with("- ") || trimmed.starts_with("* ") || trimmed.starts_with("+ ") {
28            return false;
29        }
30
31        // Skip lines that are clearly code or inline code
32        if trimmed.starts_with("`") || trimmed.contains("``") {
33            return false;
34        }
35
36        // Must have at least 2 parts when split by |
37        let parts: Vec<&str> = trimmed.split('|').collect();
38        if parts.len() < 2 {
39            return false;
40        }
41
42        // Check if it looks like a table row by having reasonable content between pipes
43        let mut valid_parts = 0;
44        let mut total_non_empty_parts = 0;
45
46        for part in &parts {
47            let part_trimmed = part.trim();
48            // Skip empty parts (from leading/trailing pipes)
49            if part_trimmed.is_empty() {
50                continue;
51            }
52            total_non_empty_parts += 1;
53
54            // Count parts that look like table cells (reasonable content, no newlines)
55            if !part_trimmed.contains('\n') {
56                valid_parts += 1;
57            }
58        }
59
60        // Check if all non-empty parts are valid (no newlines)
61        if total_non_empty_parts > 0 && valid_parts != total_non_empty_parts {
62            // Some cells contain newlines, not a valid table row
63            return false;
64        }
65
66        // GFM allows tables with all empty cells (e.g., |||)
67        // These are valid if they have proper table formatting (leading and trailing pipes)
68        if total_non_empty_parts == 0 {
69            // Empty cells are only valid with proper pipe formatting
70            return trimmed.starts_with('|') && trimmed.ends_with('|') && parts.len() >= 3;
71        }
72
73        // GFM allows single-column tables, so >= 1 valid part is enough
74        // when the line has proper table formatting (pipes)
75        if trimmed.starts_with('|') && trimmed.ends_with('|') {
76            // Properly formatted table row with pipes on both ends
77            valid_parts >= 1
78        } else {
79            // For rows without proper pipe formatting, require at least 2 cells
80            valid_parts >= 2
81        }
82    }
83
84    /// Check if a line is a table delimiter row (e.g., |---|---|)
85    pub fn is_delimiter_row(line: &str) -> bool {
86        let trimmed = line.trim();
87        if !trimmed.contains('|') || !trimmed.contains('-') {
88            return false;
89        }
90
91        // Split by pipes and check each part
92        let parts: Vec<&str> = trimmed.split('|').collect();
93        let mut valid_delimiter_parts = 0;
94        let mut total_non_empty_parts = 0;
95
96        for part in &parts {
97            let part_trimmed = part.trim();
98            if part_trimmed.is_empty() {
99                continue; // Skip empty parts from leading/trailing pipes
100            }
101
102            total_non_empty_parts += 1;
103
104            // Check if this part looks like a delimiter (contains dashes and optionally colons)
105            if part_trimmed.chars().all(|c| c == '-' || c == ':' || c.is_whitespace()) && part_trimmed.contains('-') {
106                valid_delimiter_parts += 1;
107            }
108        }
109
110        // All non-empty parts must be valid delimiters, and there must be at least one
111        total_non_empty_parts > 0 && valid_delimiter_parts == total_non_empty_parts
112    }
113
114    /// Find all table blocks in the content with optimized detection
115    /// This version accepts code_blocks and code_spans directly for use during LintContext construction
116    pub fn find_table_blocks_with_code_info(
117        content: &str,
118        code_blocks: &[(usize, usize)],
119        code_spans: &[crate::lint_context::CodeSpan],
120    ) -> Vec<TableBlock> {
121        let lines: Vec<&str> = content.lines().collect();
122        let mut tables = Vec::new();
123        let mut i = 0;
124
125        // Pre-compute line positions for efficient code block checking
126        let mut line_positions = Vec::with_capacity(lines.len());
127        let mut pos = 0;
128        for line in &lines {
129            line_positions.push(pos);
130            pos += line.len() + 1; // +1 for newline
131        }
132
133        while i < lines.len() {
134            // Skip lines in code blocks using provided code blocks
135            let line_start = line_positions[i];
136            let in_code =
137                crate::utils::code_block_utils::CodeBlockUtils::is_in_code_block_or_span(code_blocks, line_start)
138                    || code_spans
139                        .iter()
140                        .any(|span| line_start >= span.byte_offset && line_start < span.byte_end);
141            if in_code {
142                i += 1;
143                continue;
144            }
145
146            // Look for potential table start
147            if Self::is_potential_table_row(lines[i]) {
148                // Check if the next line is a delimiter row
149                if i + 1 < lines.len() && Self::is_delimiter_row(lines[i + 1]) {
150                    // Found a table! Find its end
151                    let table_start = i;
152                    let header_line = i;
153                    let delimiter_line = i + 1;
154                    let mut table_end = i + 1; // Include the delimiter row
155                    let mut content_lines = Vec::new();
156
157                    // Continue while we have table rows
158                    let mut j = i + 2;
159                    while j < lines.len() {
160                        let line = lines[j];
161                        if line.trim().is_empty() {
162                            // Empty line ends the table
163                            break;
164                        }
165                        if Self::is_potential_table_row(line) {
166                            content_lines.push(j);
167                            table_end = j;
168                            j += 1;
169                        } else {
170                            // Non-table line ends the table
171                            break;
172                        }
173                    }
174
175                    tables.push(TableBlock {
176                        start_line: table_start,
177                        end_line: table_end,
178                        header_line,
179                        delimiter_line,
180                        content_lines,
181                    });
182                    i = table_end + 1;
183                } else {
184                    i += 1;
185                }
186            } else {
187                i += 1;
188            }
189        }
190
191        tables
192    }
193
194    /// Find all table blocks in the content with optimized detection
195    /// This is a backward-compatible wrapper that accepts LintContext
196    pub fn find_table_blocks(content: &str, ctx: &crate::lint_context::LintContext) -> Vec<TableBlock> {
197        Self::find_table_blocks_with_code_info(content, &ctx.code_blocks, &ctx.code_spans())
198    }
199
200    /// Count the number of cells in a table row
201    pub fn count_cells(row: &str) -> usize {
202        let trimmed = row.trim();
203
204        // Skip non-table rows
205        if !trimmed.contains('|') {
206            return 0;
207        }
208
209        // Users shouldn't have to escape pipes in regex patterns, etc.
210        let masked_row = Self::mask_pipes_in_inline_code(trimmed);
211
212        // Handle case with leading/trailing pipes
213        let mut cell_count = 0;
214        let parts: Vec<&str> = masked_row.split('|').collect();
215
216        for (i, part) in parts.iter().enumerate() {
217            // Skip first part if it's empty and there's a leading pipe
218            if i == 0 && part.trim().is_empty() && parts.len() > 1 {
219                continue;
220            }
221
222            // Skip last part if it's empty and there's a trailing pipe
223            if i == parts.len() - 1 && part.trim().is_empty() && parts.len() > 1 {
224                continue;
225            }
226
227            cell_count += 1;
228        }
229
230        cell_count
231    }
232
233    /// Mask pipes inside inline code blocks with a placeholder character
234    pub fn mask_pipes_in_inline_code(text: &str) -> String {
235        let mut result = String::new();
236        let chars: Vec<char> = text.chars().collect();
237        let mut i = 0;
238
239        while i < chars.len() {
240            if chars[i] == '`' {
241                // Count consecutive backticks at start
242                let start = i;
243                let mut backtick_count = 0;
244                while i < chars.len() && chars[i] == '`' {
245                    backtick_count += 1;
246                    i += 1;
247                }
248
249                // Look for matching closing backticks
250                let mut found_closing = false;
251                let mut j = i;
252
253                while j < chars.len() {
254                    if chars[j] == '`' {
255                        // Count potential closing backticks
256                        let close_start = j;
257                        let mut close_count = 0;
258                        while j < chars.len() && chars[j] == '`' {
259                            close_count += 1;
260                            j += 1;
261                        }
262
263                        if close_count == backtick_count {
264                            // Found matching closing backticks
265                            found_closing = true;
266
267                            // Valid inline code - add with pipes masked
268                            result.extend(chars[start..i].iter());
269
270                            for &ch in chars.iter().take(close_start).skip(i) {
271                                if ch == '|' {
272                                    result.push('_'); // Mask pipe with underscore
273                                } else {
274                                    result.push(ch);
275                                }
276                            }
277
278                            result.extend(chars[close_start..j].iter());
279                            i = j;
280                            break;
281                        }
282                        // If not matching, continue searching (j is already past these backticks)
283                    } else {
284                        j += 1;
285                    }
286                }
287
288                if !found_closing {
289                    // No matching closing found, treat as regular text
290                    result.extend(chars[start..i].iter());
291                }
292            } else {
293                result.push(chars[i]);
294                i += 1;
295            }
296        }
297
298        result
299    }
300
301    /// Determine the pipe style of a table row
302    pub fn determine_pipe_style(line: &str) -> Option<&'static str> {
303        let trimmed = line.trim();
304        if !trimmed.contains('|') {
305            return None;
306        }
307
308        let has_leading = trimmed.starts_with('|');
309        let has_trailing = trimmed.ends_with('|');
310
311        match (has_leading, has_trailing) {
312            (true, true) => Some("leading_and_trailing"),
313            (true, false) => Some("leading_only"),
314            (false, true) => Some("trailing_only"),
315            (false, false) => Some("no_leading_or_trailing"),
316        }
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323    use crate::lint_context::LintContext;
324
325    #[test]
326    fn test_is_potential_table_row() {
327        // Basic valid table rows
328        assert!(TableUtils::is_potential_table_row("| Header 1 | Header 2 |"));
329        assert!(TableUtils::is_potential_table_row("| Cell 1 | Cell 2 |"));
330        assert!(TableUtils::is_potential_table_row("Cell 1 | Cell 2"));
331        assert!(TableUtils::is_potential_table_row("| Cell |")); // Single-column tables are valid in GFM
332
333        // Multiple cells
334        assert!(TableUtils::is_potential_table_row("| A | B | C | D | E |"));
335
336        // With whitespace
337        assert!(TableUtils::is_potential_table_row("  | Indented | Table |  "));
338        assert!(TableUtils::is_potential_table_row("| Spaces | Around |"));
339
340        // Not table rows
341        assert!(!TableUtils::is_potential_table_row("- List item"));
342        assert!(!TableUtils::is_potential_table_row("* Another list"));
343        assert!(!TableUtils::is_potential_table_row("+ Plus list"));
344        assert!(!TableUtils::is_potential_table_row("Regular text"));
345        assert!(!TableUtils::is_potential_table_row(""));
346        assert!(!TableUtils::is_potential_table_row("   "));
347
348        // Code blocks
349        assert!(!TableUtils::is_potential_table_row("`code with | pipe`"));
350        assert!(!TableUtils::is_potential_table_row("``multiple | backticks``"));
351
352        // Single pipe not enough
353        assert!(!TableUtils::is_potential_table_row("Just one |"));
354        assert!(!TableUtils::is_potential_table_row("| Just one"));
355
356        // Very long cells are valid in tables (no length limit for cell content)
357        let long_cell = "a".repeat(150);
358        assert!(TableUtils::is_potential_table_row(&format!("| {long_cell} | b |")));
359
360        // Cells with newlines
361        assert!(!TableUtils::is_potential_table_row("| Cell with\nnewline | Other |"));
362
363        // Empty cells (Issue #129)
364        assert!(TableUtils::is_potential_table_row("|||")); // Two empty cells
365        assert!(TableUtils::is_potential_table_row("||||")); // Three empty cells
366        assert!(TableUtils::is_potential_table_row("| | |")); // Two empty cells with spaces
367    }
368
369    #[test]
370    fn test_is_delimiter_row() {
371        // Basic delimiter rows
372        assert!(TableUtils::is_delimiter_row("|---|---|"));
373        assert!(TableUtils::is_delimiter_row("| --- | --- |"));
374        assert!(TableUtils::is_delimiter_row("|:---|---:|"));
375        assert!(TableUtils::is_delimiter_row("|:---:|:---:|"));
376
377        // With varying dash counts
378        assert!(TableUtils::is_delimiter_row("|-|--|"));
379        assert!(TableUtils::is_delimiter_row("|-------|----------|"));
380
381        // With whitespace
382        assert!(TableUtils::is_delimiter_row("|  ---  |  ---  |"));
383        assert!(TableUtils::is_delimiter_row("| :--- | ---: |"));
384
385        // Multiple columns
386        assert!(TableUtils::is_delimiter_row("|---|---|---|---|"));
387
388        // Without leading/trailing pipes
389        assert!(TableUtils::is_delimiter_row("--- | ---"));
390        assert!(TableUtils::is_delimiter_row(":--- | ---:"));
391
392        // Not delimiter rows
393        assert!(!TableUtils::is_delimiter_row("| Header | Header |"));
394        assert!(!TableUtils::is_delimiter_row("Regular text"));
395        assert!(!TableUtils::is_delimiter_row(""));
396        assert!(!TableUtils::is_delimiter_row("|||"));
397        assert!(!TableUtils::is_delimiter_row("| | |"));
398
399        // Must have dashes
400        assert!(!TableUtils::is_delimiter_row("| : | : |"));
401        assert!(!TableUtils::is_delimiter_row("|    |    |"));
402
403        // Mixed content
404        assert!(!TableUtils::is_delimiter_row("| --- | text |"));
405        assert!(!TableUtils::is_delimiter_row("| abc | --- |"));
406    }
407
408    #[test]
409    fn test_count_cells() {
410        // Basic counts
411        assert_eq!(TableUtils::count_cells("| Cell 1 | Cell 2 | Cell 3 |"), 3);
412        assert_eq!(TableUtils::count_cells("Cell 1 | Cell 2 | Cell 3"), 3);
413        assert_eq!(TableUtils::count_cells("| Cell 1 | Cell 2"), 2);
414        assert_eq!(TableUtils::count_cells("Cell 1 | Cell 2 |"), 2);
415
416        // Single cell
417        assert_eq!(TableUtils::count_cells("| Cell |"), 1);
418        assert_eq!(TableUtils::count_cells("Cell"), 0); // No pipe
419
420        // Empty cells
421        assert_eq!(TableUtils::count_cells("|  |  |  |"), 3);
422        assert_eq!(TableUtils::count_cells("| | | |"), 3);
423
424        // Many cells
425        assert_eq!(TableUtils::count_cells("| A | B | C | D | E | F |"), 6);
426
427        // Edge cases
428        assert_eq!(TableUtils::count_cells("||"), 1); // One empty cell
429        assert_eq!(TableUtils::count_cells("|||"), 2); // Two empty cells
430
431        // No table
432        assert_eq!(TableUtils::count_cells("Regular text"), 0);
433        assert_eq!(TableUtils::count_cells(""), 0);
434        assert_eq!(TableUtils::count_cells("   "), 0);
435
436        // Whitespace handling
437        assert_eq!(TableUtils::count_cells("  | A | B |  "), 2);
438        assert_eq!(TableUtils::count_cells("|   A   |   B   |"), 2);
439    }
440
441    #[test]
442    fn test_count_cells_with_inline_code() {
443        // Test the user's actual example from Issue #34
444        assert_eq!(TableUtils::count_cells("| Challenge | Solution |"), 2);
445        assert_eq!(
446            TableUtils::count_cells("| Hour:minute:second formats | `^([0-1]?\\d|2[0-3]):[0-5]\\d:[0-5]\\d$` |"),
447            2
448        );
449
450        // Test basic inline code with pipes
451        assert_eq!(TableUtils::count_cells("| Command | `echo | grep` |"), 2);
452        assert_eq!(TableUtils::count_cells("| A | `code | with | pipes` | B |"), 3);
453
454        // Test escaped pipes (correct GFM)
455        assert_eq!(TableUtils::count_cells("| Command | `echo \\| grep` |"), 2);
456
457        // Test multiple inline code blocks
458        assert_eq!(TableUtils::count_cells("| `code | one` | `code | two` |"), 2);
459
460        // Test edge cases
461        assert_eq!(TableUtils::count_cells("| Empty inline | `` | cell |"), 3);
462        assert_eq!(TableUtils::count_cells("| `single|pipe` |"), 1);
463
464        // Test that basic table structure still works
465        assert_eq!(TableUtils::count_cells("| A | B | C |"), 3);
466        assert_eq!(TableUtils::count_cells("| One | Two |"), 2);
467    }
468
469    #[test]
470    fn test_determine_pipe_style() {
471        // All pipe styles
472        assert_eq!(
473            TableUtils::determine_pipe_style("| Cell 1 | Cell 2 |"),
474            Some("leading_and_trailing")
475        );
476        assert_eq!(
477            TableUtils::determine_pipe_style("| Cell 1 | Cell 2"),
478            Some("leading_only")
479        );
480        assert_eq!(
481            TableUtils::determine_pipe_style("Cell 1 | Cell 2 |"),
482            Some("trailing_only")
483        );
484        assert_eq!(
485            TableUtils::determine_pipe_style("Cell 1 | Cell 2"),
486            Some("no_leading_or_trailing")
487        );
488
489        // With whitespace
490        assert_eq!(
491            TableUtils::determine_pipe_style("  | Cell 1 | Cell 2 |  "),
492            Some("leading_and_trailing")
493        );
494        assert_eq!(
495            TableUtils::determine_pipe_style("  | Cell 1 | Cell 2  "),
496            Some("leading_only")
497        );
498
499        // No pipes
500        assert_eq!(TableUtils::determine_pipe_style("Regular text"), None);
501        assert_eq!(TableUtils::determine_pipe_style(""), None);
502        assert_eq!(TableUtils::determine_pipe_style("   "), None);
503
504        // Single pipe cases
505        assert_eq!(TableUtils::determine_pipe_style("|"), Some("leading_and_trailing"));
506        assert_eq!(TableUtils::determine_pipe_style("| Cell"), Some("leading_only"));
507        assert_eq!(TableUtils::determine_pipe_style("Cell |"), Some("trailing_only"));
508    }
509
510    #[test]
511    fn test_find_table_blocks_simple() {
512        let content = "| Header 1 | Header 2 |
513|-----------|-----------|
514| Cell 1    | Cell 2    |
515| Cell 3    | Cell 4    |";
516
517        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
518
519        let tables = TableUtils::find_table_blocks(content, &ctx);
520        assert_eq!(tables.len(), 1);
521
522        let table = &tables[0];
523        assert_eq!(table.start_line, 0);
524        assert_eq!(table.end_line, 3);
525        assert_eq!(table.header_line, 0);
526        assert_eq!(table.delimiter_line, 1);
527        assert_eq!(table.content_lines, vec![2, 3]);
528    }
529
530    #[test]
531    fn test_find_table_blocks_multiple() {
532        let content = "Some text
533
534| Table 1 | Col A |
535|----------|-------|
536| Data 1   | Val 1 |
537
538More text
539
540| Table 2 | Col 2 |
541|----------|-------|
542| Data 2   | Data  |";
543
544        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
545
546        let tables = TableUtils::find_table_blocks(content, &ctx);
547        assert_eq!(tables.len(), 2);
548
549        // First table
550        assert_eq!(tables[0].start_line, 2);
551        assert_eq!(tables[0].end_line, 4);
552        assert_eq!(tables[0].header_line, 2);
553        assert_eq!(tables[0].delimiter_line, 3);
554        assert_eq!(tables[0].content_lines, vec![4]);
555
556        // Second table
557        assert_eq!(tables[1].start_line, 8);
558        assert_eq!(tables[1].end_line, 10);
559        assert_eq!(tables[1].header_line, 8);
560        assert_eq!(tables[1].delimiter_line, 9);
561        assert_eq!(tables[1].content_lines, vec![10]);
562    }
563
564    #[test]
565    fn test_find_table_blocks_no_content_rows() {
566        let content = "| Header 1 | Header 2 |
567|-----------|-----------|
568
569Next paragraph";
570
571        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
572
573        let tables = TableUtils::find_table_blocks(content, &ctx);
574        assert_eq!(tables.len(), 1);
575
576        let table = &tables[0];
577        assert_eq!(table.start_line, 0);
578        assert_eq!(table.end_line, 1); // Just header and delimiter
579        assert_eq!(table.content_lines.len(), 0);
580    }
581
582    #[test]
583    fn test_find_table_blocks_in_code_block() {
584        let content = "```
585| Not | A | Table |
586|-----|---|-------|
587| In  | Code | Block |
588```
589
590| Real | Table |
591|------|-------|
592| Data | Here  |";
593
594        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
595
596        let tables = TableUtils::find_table_blocks(content, &ctx);
597        assert_eq!(tables.len(), 1); // Only the table outside code block
598
599        let table = &tables[0];
600        assert_eq!(table.header_line, 6);
601        assert_eq!(table.delimiter_line, 7);
602    }
603
604    #[test]
605    fn test_find_table_blocks_no_tables() {
606        let content = "Just regular text
607No tables here
608- List item with | pipe
609* Another list item";
610
611        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
612
613        let tables = TableUtils::find_table_blocks(content, &ctx);
614        assert_eq!(tables.len(), 0);
615    }
616
617    #[test]
618    fn test_find_table_blocks_malformed() {
619        let content = "| Header without delimiter |
620| This looks like table |
621But no delimiter row
622
623| Proper | Table |
624|---------|-------|
625| Data    | Here  |";
626
627        let ctx = LintContext::new(content, crate::config::MarkdownFlavor::Standard);
628
629        let tables = TableUtils::find_table_blocks(content, &ctx);
630        assert_eq!(tables.len(), 1); // Only the proper table
631        assert_eq!(tables[0].header_line, 4);
632    }
633
634    #[test]
635    fn test_edge_cases() {
636        // Test empty content
637        assert!(!TableUtils::is_potential_table_row(""));
638        assert!(!TableUtils::is_delimiter_row(""));
639        assert_eq!(TableUtils::count_cells(""), 0);
640        assert_eq!(TableUtils::determine_pipe_style(""), None);
641
642        // Test whitespace only
643        assert!(!TableUtils::is_potential_table_row("   "));
644        assert!(!TableUtils::is_delimiter_row("   "));
645        assert_eq!(TableUtils::count_cells("   "), 0);
646        assert_eq!(TableUtils::determine_pipe_style("   "), None);
647
648        // Test single character
649        assert!(!TableUtils::is_potential_table_row("|"));
650        assert!(!TableUtils::is_delimiter_row("|"));
651        assert_eq!(TableUtils::count_cells("|"), 0); // Need at least 2 parts
652
653        // Test very long lines are valid table rows (no length limit)
654        // Test both single-column and multi-column long lines
655        let long_single = format!("| {} |", "a".repeat(200));
656        assert!(TableUtils::is_potential_table_row(&long_single)); // Single-column table with long content
657
658        let long_multi = format!("| {} | {} |", "a".repeat(200), "b".repeat(200));
659        assert!(TableUtils::is_potential_table_row(&long_multi)); // Multi-column table with long content
660
661        // Test unicode
662        assert!(TableUtils::is_potential_table_row("| 你好 | 世界 |"));
663        assert!(TableUtils::is_potential_table_row("| émoji | 🎉 |"));
664        assert_eq!(TableUtils::count_cells("| 你好 | 世界 |"), 2);
665    }
666
667    #[test]
668    fn test_table_block_struct() {
669        let block = TableBlock {
670            start_line: 0,
671            end_line: 5,
672            header_line: 0,
673            delimiter_line: 1,
674            content_lines: vec![2, 3, 4, 5],
675        };
676
677        // Test Debug trait
678        let debug_str = format!("{block:?}");
679        assert!(debug_str.contains("TableBlock"));
680        assert!(debug_str.contains("start_line: 0"));
681
682        // Test Clone trait
683        let cloned = block.clone();
684        assert_eq!(cloned.start_line, block.start_line);
685        assert_eq!(cloned.end_line, block.end_line);
686        assert_eq!(cloned.header_line, block.header_line);
687        assert_eq!(cloned.delimiter_line, block.delimiter_line);
688        assert_eq!(cloned.content_lines, block.content_lines);
689    }
690}