use super::LexSerializer;
use lex_core::lex::ast::{Table, TableCell, TableCellAlignment, TableRow};
pub(super) enum Slot<'a> {
Origin(&'a TableCell),
Colspan,
Rowspan,
Empty,
}
impl Slot<'_> {
pub(super) fn text(&self) -> &str {
match self {
Slot::Origin(cell) => cell.content.as_string().trim(),
Slot::Colspan => ">>",
Slot::Rowspan => "^^",
Slot::Empty => "",
}
}
fn lines(&self) -> Vec<&str> {
let raw = self.text();
if raw.is_empty() {
vec![""]
} else {
raw.lines().map(str::trim).collect()
}
}
fn is_multiline(&self) -> bool {
self.text().contains('\n')
}
}
pub(super) fn build_grid<'a>(rows: &[&'a TableRow]) -> Vec<Vec<Slot<'a>>> {
let mut carry: Vec<usize> = Vec::new();
let mut grid: Vec<Vec<Slot>> = Vec::with_capacity(rows.len());
for row in rows {
let mut slots: Vec<Slot> = Vec::new();
let mut cells = row.cells.iter();
let mut col = 0;
loop {
if carry.get(col).copied().unwrap_or(0) > 0 {
carry[col] -= 1;
slots.push(Slot::Rowspan);
col += 1;
} else if let Some(cell) = cells.next() {
let span = cell.colspan.max(1);
if cell.rowspan > 1 {
for c in col..col + span {
if carry.len() <= c {
carry.resize(c + 1, 0);
}
carry[c] += cell.rowspan - 1;
}
}
slots.push(Slot::Origin(cell));
for _ in 1..span {
slots.push(Slot::Colspan);
}
col += span;
} else if carry.iter().skip(col).any(|&r| r > 0) {
slots.push(Slot::Empty);
col += 1;
} else {
break;
}
}
grid.push(slots);
}
grid
}
pub(super) fn emit_pipe_table(serializer: &mut LexSerializer, table: &Table) {
let all_rows: Vec<&TableRow> = table
.header_rows
.iter()
.chain(table.body_rows.iter())
.collect();
if all_rows.is_empty() {
return;
}
let grid = build_grid(&all_rows);
let col_count = grid.iter().map(Vec::len).max().unwrap_or(0);
if col_count == 0 {
return;
}
let aligns = compute_column_aligns(&grid, col_count);
let widths = compute_column_widths(&grid, col_count, &aligns);
let header_count = table.header_rows.len();
let multiline = grid.iter().flatten().any(Slot::is_multiline);
if multiline {
emit_multiline_table(serializer, &grid, &widths, &aligns, header_count);
} else {
for (idx, slots) in grid.iter().enumerate() {
for line in format_grid_row_lines(slots, &widths) {
serializer.write_line(&line);
}
if header_count > 0 && idx + 1 == header_count {
serializer.write_line(&format_separator_row(&widths, &aligns));
}
}
}
}
fn emit_multiline_table(
serializer: &mut LexSerializer,
grid: &[Vec<Slot>],
widths: &[usize],
aligns: &[TableCellAlignment],
header_count: usize,
) {
let has_align = aligns.iter().any(|a| *a != TableCellAlignment::None);
for (idx, slots) in grid.iter().enumerate() {
if idx > 0 {
serializer.ensure_blank_lines(1);
}
for line in format_grid_row_lines(slots, widths) {
serializer.write_line(&line);
}
if has_align && header_count > 0 && idx + 1 == header_count {
serializer.ensure_blank_lines(1);
serializer.write_line(&format_separator_row(widths, aligns));
}
}
}
fn compute_column_aligns(grid: &[Vec<Slot>], col_count: usize) -> Vec<TableCellAlignment> {
let mut aligns = vec![TableCellAlignment::None; col_count];
for slots in grid {
for (col, slot) in slots.iter().enumerate() {
if let Slot::Origin(cell) = slot {
if aligns[col] == TableCellAlignment::None && cell.align != TableCellAlignment::None
{
aligns[col] = cell.align;
}
}
}
}
aligns
}
fn compute_column_widths(
grid: &[Vec<Slot>],
col_count: usize,
aligns: &[TableCellAlignment],
) -> Vec<usize> {
let mut widths = vec![0usize; col_count];
for slots in grid {
for (col, slot) in slots.iter().enumerate() {
let cell_width = slot
.text()
.lines()
.map(|line| line.trim().chars().count())
.max()
.unwrap_or(0);
widths[col] = widths[col].max(cell_width);
}
}
for (col, w) in widths.iter_mut().enumerate() {
let min = match aligns.get(col).copied().unwrap_or(TableCellAlignment::None) {
TableCellAlignment::Center => 5, TableCellAlignment::Left | TableCellAlignment::Right => 4, TableCellAlignment::None => 3,
};
if *w < min {
*w = min;
}
}
widths
}
fn push_cell(out: &mut String, text: &str, width: usize) {
out.push(' ');
out.push_str(text);
for _ in text.chars().count()..width {
out.push(' ');
}
out.push(' ');
out.push('|');
}
fn format_grid_row_lines(slots: &[Slot], widths: &[usize]) -> Vec<String> {
let columns: Vec<Vec<&str>> = slots.iter().map(Slot::lines).collect();
let height = columns.iter().map(Vec::len).max().unwrap_or(1);
let row_capacity = widths.iter().take(columns.len()).sum::<usize>() + columns.len() * 3 + 1;
(0..height)
.map(|line_idx| {
let mut out = String::with_capacity(row_capacity);
out.push('|');
for (col, column) in columns.iter().enumerate() {
let width = widths.get(col).copied().unwrap_or(0);
let text = column.get(line_idx).copied().unwrap_or("");
push_cell(&mut out, text, width);
}
out
})
.collect()
}
fn format_separator_row(widths: &[usize], aligns: &[TableCellAlignment]) -> String {
let mut out = String::from("|");
for (i, &w) in widths.iter().enumerate() {
out.push(' ');
let align = aligns.get(i).copied().unwrap_or(TableCellAlignment::None);
match align {
TableCellAlignment::Left => {
out.push(':');
for _ in 1..w {
out.push('-');
}
}
TableCellAlignment::Right => {
for _ in 0..w.saturating_sub(1) {
out.push('-');
}
out.push(':');
}
TableCellAlignment::Center => {
out.push(':');
for _ in 0..w.saturating_sub(2) {
out.push('-');
}
out.push(':');
}
TableCellAlignment::None => {
for _ in 0..w {
out.push('-');
}
}
}
out.push(' ');
out.push('|');
}
out
}