From b576f90db35138edaf12fa0c0603620756cb8db4 Mon Sep 17 00:00:00 2001 From: Leynos Date: Fri, 13 Jun 2025 23:11:17 +0100 Subject: [PATCH 1/2] Fix escaped pipe handling and refactor table formatting --- src/lib.rs | 62 +++++++++++++++++++++++--------------------- tests/integration.rs | 2 +- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 209d3215..11f169c8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,9 +35,9 @@ pub fn split_cells(line: &str) -> Vec { if ch == '\\' { if let Some(&next) = chars.peek() { if next == '|' { + // `\|` escapes the pipe so it becomes part of the cell chars.next(); - cells.push(current.trim().to_string()); - current.clear(); + current.push('|'); continue; } } @@ -55,6 +55,29 @@ pub fn split_cells(line: &str) -> Vec { cells } +/// Formats the cells for a separator row based on column widths. +fn format_separator_cells(widths: &[usize], sep_cells: &[String]) -> Vec { + sep_cells + .iter() + .enumerate() + .map(|(i, cell)| { + let trimmed = cell.trim(); + let left = trimmed.starts_with(':'); + let right = trimmed.ends_with(':'); + let mut dashes = "-".repeat(widths[i].max(3)); + if left { + dashes.remove(0); + dashes.insert(0, ':'); + } + if right { + dashes.pop(); + dashes.push(':'); + } + dashes + }) + .collect() +} + /// Reflow a broken markdown table. /// /// # Panics @@ -131,15 +154,12 @@ pub fn reflow_table(lines: &[String]) -> Vec { cleaned.push(row); } - if !split_within_line - && cleaned - .iter() - .map(Vec::len) - .collect::>() - .len() - > 1 - { - return lines.to_vec(); + if !split_within_line { + if let Some(first_len) = cleaned.first().map(Vec::len) { + if cleaned.iter().any(|row| row.len() != first_len) { + return lines.to_vec(); + } + } } let mut widths = vec![0; max_cols]; @@ -166,25 +186,7 @@ pub fn reflow_table(lines: &[String]) -> Vec { while sep_cells.len() < widths.len() { sep_cells.push(String::new()); } - let sep_padded: Vec = sep_cells - .iter() - .enumerate() - .map(|(i, cell)| { - let trimmed = cell.trim(); - let left = trimmed.starts_with(':'); - let right = trimmed.ends_with(':'); - let mut dashes = "-".repeat(widths[i].max(3)); - if left { - dashes.remove(0); - dashes.insert(0, ':'); - } - if right { - dashes.pop(); - dashes.push(':'); - } - dashes - }) - .collect(); + let sep_padded = format_separator_cells(&widths, &sep_cells); let sep_line_out = format!("{}| {} |", indent, sep_padded.join(" | ")); if let Some(first) = out.first().cloned() { let mut with_sep = vec![first, sep_line_out]; diff --git a/tests/integration.rs b/tests/integration.rs index c20639cb..5cdb72cd 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -94,7 +94,7 @@ fn test_reflow_preserves_header(header_table: Vec) { #[rstest] fn test_reflow_handles_escaped_pipes(escaped_pipe_table: Vec) { - let expected = vec!["| X | Y |", "| a | b | 1 |", "| 2 | 3 |"]; + let expected = vec!["| X | Y |", "| a | b | 1 |", "| 2 | 3 |"]; assert_eq!(reflow_table(&escaped_pipe_table), expected); } From b6f7b7226448ce8dda98a4f08a251ef504adfca3 Mon Sep 17 00:00:00 2001 From: Leynos Date: Fri, 13 Jun 2025 23:34:00 +0100 Subject: [PATCH 2/2] Guard separator widths --- src/lib.rs | 9 ++++++++- tests/integration.rs | 4 ++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 11f169c8..6376953a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,6 +57,13 @@ pub fn split_cells(line: &str) -> Vec { /// Formats the cells for a separator row based on column widths. fn format_separator_cells(widths: &[usize], sep_cells: &[String]) -> Vec { + if sep_cells.len() != widths.len() { + // A malformed separator row could cause a panic below when indexing + // `widths`. Return the cells unchanged so the caller can decide how to + // handle the mismatch gracefully. + return sep_cells.to_vec(); + } + sep_cells .iter() .enumerate() @@ -156,7 +163,7 @@ pub fn reflow_table(lines: &[String]) -> Vec { if !split_within_line { if let Some(first_len) = cleaned.first().map(Vec::len) { - if cleaned.iter().any(|row| row.len() != first_len) { + if cleaned[1..].iter().any(|row| row.len() != first_len) { return lines.to_vec(); } } diff --git a/tests/integration.rs b/tests/integration.rs index 5cdb72cd..245a20df 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -94,6 +94,10 @@ fn test_reflow_preserves_header(header_table: Vec) { #[rstest] fn test_reflow_handles_escaped_pipes(escaped_pipe_table: Vec) { + // The fixture contains a header row followed by a row with an escaped + // pipe sequence (`a \| b`). After reflow the escaped pipe becomes a literal + // `|` inside the first data cell, so the table has three columns and the + // header row is padded to match. let expected = vec!["| X | Y |", "| a | b | 1 |", "| 2 | 3 |"]; assert_eq!(reflow_table(&escaped_pipe_table), expected); }