use std::collections::BTreeSet;
#[derive(Debug, Clone)]
pub struct GridCellRect {
pub start_row: usize,
pub start_col: usize,
pub row_span: usize,
pub col_span: usize,
pub content: String,
}
#[derive(Debug, Clone)]
pub struct GridLayout {
pub cols_pos: Vec<usize>,
pub row_seps: Vec<usize>,
pub cells: Vec<GridCellRect>,
}
#[allow(clippy::needless_range_loop)]
pub fn analyze_grid(lines: &[&str]) -> Option<GridLayout> {
if lines.is_empty() {
return None;
}
let max_width = lines.iter().map(|l| l.chars().count()).max().unwrap_or(0);
let grid: Vec<Vec<char>> = lines
.iter()
.map(|l| {
let mut chars: Vec<char> = l.chars().collect();
chars.resize(max_width, ' ');
chars
})
.collect();
let nlines = grid.len();
let is_sep_line: Vec<bool> = grid
.iter()
.map(|row| {
row.contains(&'+')
&& row
.iter()
.all(|&c| matches!(c, '+' | '-' | '=' | ':' | '|' | ' '))
})
.collect();
let mut col_set: BTreeSet<usize> = BTreeSet::new();
for (i, row) in grid.iter().enumerate() {
if !is_sep_line[i] {
continue;
}
for (j, &c) in row.iter().enumerate() {
if c == '+' {
col_set.insert(j);
}
}
}
let cols_pos: Vec<usize> = col_set.into_iter().collect();
if cols_pos.len() < 2 {
return None;
}
let ncols = cols_pos.len() - 1;
let row_seps: Vec<usize> = (0..nlines).filter(|&i| is_sep_line[i]).collect();
if row_seps.len() < 2 {
return None;
}
let nrows = row_seps.len() - 1;
let mut occupied = vec![vec![false; ncols]; nrows];
let mut cells: Vec<GridCellRect> = Vec::new();
for sr in 0..nrows {
for sc in 0..ncols {
if occupied[sr][sc] {
continue;
}
let i = row_seps[sr];
let j = cols_pos[sc];
if grid[i][j] != '+' {
continue;
}
let Some((er, ec, content)) = find_grid_cell(&grid, i, j, sr, sc, &cols_pos, &row_seps)
else {
continue;
};
for r in sr..er {
for c in sc..ec {
occupied[r][c] = true;
}
}
cells.push(GridCellRect {
start_row: sr,
start_col: sc,
row_span: er - sr,
col_span: ec - sc,
content,
});
}
}
Some(GridLayout {
cols_pos,
row_seps,
cells,
})
}
#[allow(clippy::needless_range_loop)]
fn find_grid_cell(
grid: &[Vec<char>],
i: usize,
j: usize,
sr: usize,
sc: usize,
cols_pos: &[usize],
row_seps: &[usize],
) -> Option<(usize, usize, String)> {
let nrows = row_seps.len() - 1;
let ncols = cols_pos.len() - 1;
for ec in (sc + 1)..=ncols {
let k = cols_pos[ec];
let top_ok = (j + 1..k).all(|c| matches!(grid[i][c], '-' | '=' | ':' | '+'));
if !top_ok {
break;
}
for er in (sr + 1)..=nrows {
let l = row_seps[er];
let left_ok = (i + 1..l).all(|r| matches!(grid[r][j], '|' | '+'));
if !left_ok {
break;
}
let right_ok = (i + 1..l).all(|r| matches!(grid[r][k], '|' | '+'));
if !right_ok {
continue;
}
let bot_ok = (j + 1..k).all(|c| matches!(grid[l][c], '-' | '=' | ':' | '+'));
if !bot_ok {
continue;
}
if grid[l][j] != '+' || grid[l][k] != '+' {
continue;
}
let interior_split = (i + 1..l).any(|m| {
grid[m][j] == '+'
&& grid[m][k] == '+'
&& (j + 1..k).all(|c| matches!(grid[m][c], '-' | '=' | ':' | '+'))
});
if interior_split {
continue;
}
let mut content_lines: Vec<String> = Vec::new();
for r in (i + 1)..l {
let slice: String = grid[r][j + 1..k].iter().collect();
let stripped = slice.strip_prefix(' ').unwrap_or(&slice).to_string();
content_lines.push(stripped.trim_end().to_string());
}
let first = content_lines.iter().position(|s| !s.is_empty());
let last = content_lines.iter().rposition(|s| !s.is_empty());
let content = match (first, last) {
(Some(f), Some(l)) => content_lines[f..=l].join("\n"),
_ => String::new(),
};
return Some((er, ec, content));
}
}
None
}