#[must_use]
#[allow(dead_code)] pub fn has_wrapped_cells(content: &str) -> bool {
let lines: Vec<&str> = content.lines().collect();
for i in 0..lines.len() {
let line = lines[i].trim();
if !is_table_row(line) {
continue;
}
if i > 0 {
let prev_line = lines[i - 1].trim();
if is_table_row(prev_line) && is_continuation_row(line) {
return true;
}
}
}
false
}
fn is_table_row(line: &str) -> bool {
let trimmed = line.trim();
trimmed.starts_with('|') && trimmed.ends_with('|')
}
fn is_continuation_row(line: &str) -> bool {
let cells = split_table_cells(line);
if cells.is_empty() {
return false;
}
let non_empty_count = cells.iter().filter(|cell| !cell.trim().is_empty()).count();
non_empty_count <= 1
&& non_empty_count < cells.len()
&& cells.first().is_some_and(|c| c.trim().is_empty())
}
fn split_table_cells(line: &str) -> Vec<&str> {
let trimmed = line.trim();
if !trimmed.starts_with('|') || !trimmed.ends_with('|') {
return Vec::new();
}
let parts: Vec<&str> = trimmed.split('|').skip(1).collect();
parts
.iter()
.enumerate()
.filter(|(idx, s)| *idx < parts.len() - 1 || !s.is_empty())
.map(|(_, s)| *s)
.collect()
}
#[must_use]
#[allow(dead_code)] pub fn unwrap_table_rows(rows: &[&str]) -> Vec<String> {
if rows.is_empty() {
return Vec::new();
}
if rows_contain_code_fence(rows) {
return rows.iter().map(|&s| s.to_string()).collect();
}
if rows_contain_list_markers(rows) {
return rows.iter().map(|&s| s.to_string()).collect();
}
if rows_contain_blockquotes(rows) {
return rows.iter().map(|&s| s.to_string()).collect();
}
if has_incomplete_link_across_rows(rows) {
return rows.iter().map(|&s| s.to_string()).collect();
}
let mut result: Vec<String> = Vec::new();
let mut pending_row: Option<Vec<String>> = None;
for row in rows {
let cells = split_table_cells(row);
if cells.is_empty() {
continue;
}
if let Some(ref mut pending) = pending_row {
if is_continuation_row(row) {
for (idx, cell) in cells.iter().enumerate() {
let cell_trimmed = cell.trim();
if idx < pending.len() && !cell_trimmed.is_empty() {
if !pending[idx].is_empty() {
pending[idx].push(' ');
}
pending[idx].push_str(cell_trimmed);
}
}
} else {
let row_str = format_row(pending);
result.push(row_str);
pending_row = Some(cells.iter().map(|s| s.trim().to_string()).collect());
}
} else {
pending_row = Some(cells.iter().map(|s| s.trim().to_string()).collect());
}
}
if let Some(ref pending) = pending_row {
let row_str = format_row(pending);
result.push(row_str);
}
result
}
fn format_row(cells: &[String]) -> String {
let mut result = String::new();
result.push('|');
for cell in cells {
result.push(' ');
result.push_str(cell);
result.push(' ');
result.push('|');
}
result
}
fn contains_code_fence(line: &str) -> bool {
let trimmed = line.trim();
if trimmed.contains("```") {
return true;
}
if trimmed.contains("~~~") {
return true;
}
false
}
fn contains_list_marker(line: &str) -> bool {
let cells = split_table_cells(line);
cells.iter().any(|cell| {
let trimmed = cell.trim();
if trimmed.starts_with("- ") || trimmed.starts_with("* ") || trimmed.starts_with("+ ") {
return true;
}
if let Some(rest) = trimmed.strip_prefix(|c: char| c.is_ascii_digit()) {
if !rest.is_empty() && rest.starts_with(". ") {
return true;
}
}
false
})
}
fn contains_blockquote(line: &str) -> bool {
let cells = split_table_cells(line);
cells.iter().any(|cell| {
let trimmed = cell.trim();
trimmed.starts_with("> ")
})
}
fn rows_contain_code_fence(rows: &[&str]) -> bool {
rows.iter().any(|row| contains_code_fence(row))
}
fn rows_contain_list_markers(rows: &[&str]) -> bool {
rows.iter().any(|row| contains_list_marker(row))
}
fn rows_contain_blockquotes(rows: &[&str]) -> bool {
rows.iter().any(|row| contains_blockquote(row))
}
fn has_incomplete_link_across_rows(rows: &[&str]) -> bool {
for (row_idx, row) in rows.iter().enumerate() {
let chars: Vec<char> = row.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '[' {
if let Some(text_end) = find_closing_bracket(&chars, i + 1) {
if text_end + 1 < chars.len() && chars[text_end + 1] == '(' {
if find_closing_parenthesis_balanced(&chars, text_end + 2).is_none() {
if row_idx < rows.len() - 1 {
return true;
}
}
}
}
}
i += 1;
}
}
false
}
fn find_closing_bracket(chars: &[char], start: usize) -> Option<usize> {
let mut i = start;
while i < chars.len() {
if chars[i] == ']' && (i == 0 || chars[i - 1] != '\\') {
return Some(i);
}
i += 1;
}
None
}
fn find_closing_parenthesis_balanced(chars: &[char], start: usize) -> Option<usize> {
let mut depth = 1; let mut i = start;
while i < chars.len() {
match chars[i] {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
_ => {}
}
i += 1;
}
None }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_non_wrapped_table() {
let content = "| Name | Description |\n|------|-------------|\n| Item | Short desc |";
assert!(
!has_wrapped_cells(content),
"Should not detect wrapping in normal table"
);
}
#[test]
fn unwrap_single_wrapped_cell() {
let rows = vec!["| Item | This is a very |", "| | long description |"];
let unwrapped = unwrap_table_rows(&rows);
assert_eq!(unwrapped.len(), 1);
assert_eq!(unwrapped[0], "| Item | This is a very long description |");
}
#[test]
fn detect_wrapped_table_cell() {
let content = "| Name | Description |\n|------|-------------|\n| Item | This is a very |\n| | long description |";
assert!(has_wrapped_cells(content), "Should detect wrapped cells");
}
#[test]
fn unwrap_multiple_wrapped_columns() {
let rows = vec!["| x | very | z |", "| | long | |", "| | text | |"];
let unwrapped = unwrap_table_rows(&rows);
assert_eq!(unwrapped.len(), 1);
assert_eq!(unwrapped[0], "| x | very long text | z |");
}
#[test]
fn preserve_intentional_multiline_code_block() {
let rows = vec![
"| Code | Example |",
"| ```python | of code |",
"| def hello(): | inside |",
"| ``` | cell |",
];
let unwrapped = unwrap_table_rows(&rows);
assert_eq!(unwrapped.len(), 4);
}
#[test]
fn unwrap_wrapped_but_not_code() {
let rows = vec!["| A | This is a very |", "| | long description |"];
let unwrapped = unwrap_table_rows(&rows);
assert_eq!(unwrapped.len(), 1);
assert_eq!(unwrapped[0], "| A | This is a very long description |");
}
#[test]
fn detect_code_fence_in_cell() {
assert!(contains_code_fence("| ```python | code |"));
assert!(contains_code_fence("| ``` | end |"));
assert!(contains_code_fence("| ~~~bash | script |"));
assert!(!contains_code_fence("| normal text | here |"));
assert!(!contains_code_fence("| no code | fence |"));
}
#[test]
fn preserve_incomplete_link_across_wrap() {
let rows = vec more text |",
];
let unwrapped = unwrap_table_rows(&rows);
assert_eq!(
unwrapped.len(),
2,
"Should preserve rows when link spans boundary"
);
assert!(unwrapped[0].contains("[Docs]"));
assert!(unwrapped[1].contains("long/path)"));
}
#[test]
fn unwrap_complete_link_on_one_line() {
let rows = vec |",
"| | more description text |",
];
let unwrapped = unwrap_table_rows(&rows);
assert_eq!(unwrapped.len(), 1);
assert!(unwrapped[0].contains("[Docs](https://example.com/path)"));
assert!(unwrapped[0].contains("more description text"));
}
#[test]
fn detect_incomplete_link_in_rows() {
assert!(has_incomplete_link_across_rows(&[
"| [Link](http://x |",
"| /path) |"
]));
assert!(!has_incomplete_link_across_rows(&[
"| [Link](http://x/path) |",
"| more text |"
]));
}
#[test]
fn preserve_nested_list_in_table() {
let rows = vec![
"| Feature | Details |",
"| Items | - Parent |",
"| | - Child 1 |",
"| | - Child 2 |",
];
let unwrapped = unwrap_table_rows(&rows);
assert_eq!(unwrapped.len(), 4, "Should preserve nested list structure");
assert!(unwrapped[1].contains("- Parent"));
assert!(unwrapped[2].contains("- Child 1"));
assert!(unwrapped[3].contains("- Child 2"));
}
#[test]
fn preserve_blockquote_in_table() {
let rows = vec![
"| Author | Quote |",
"| Alice | > This is |",
"| | > a quote |",
];
let unwrapped = unwrap_table_rows(&rows);
assert_eq!(unwrapped.len(), 3, "Should preserve blockquote structure");
assert!(unwrapped[1].contains("> This is"));
assert!(unwrapped[2].contains("> a quote"));
}
#[test]
fn preserve_numbered_list_in_table() {
let rows = vec![
"| Steps | Description |",
"| Process | 1. First step |",
"| | 2. Second step |",
"| | 3. Third step |",
];
let unwrapped = unwrap_table_rows(&rows);
assert_eq!(
unwrapped.len(),
4,
"Should preserve numbered list structure"
);
assert!(unwrapped[1].contains("1. First"));
assert!(unwrapped[2].contains("2. Second"));
assert!(unwrapped[3].contains("3. Third"));
}
#[test]
fn unwrap_when_no_special_markers() {
let rows = vec!["| Item | This is a long |", "| | description text |"];
let unwrapped = unwrap_table_rows(&rows);
assert_eq!(unwrapped.len(), 1, "Should unwrap regular text");
assert_eq!(unwrapped[0], "| Item | This is a long description text |");
}
}