mdtablefix 0.3.1

`mdtablefix` unb0rks and reflows Markdown tables so that each column has a uniform width. When the `--wrap` option is used, it also wraps paragraphs and list items to 80 columns.
Documentation
//! Helper functions for table reflow.
//!
//! The routines here parse raw rows, calculate cell widths, and format
//! aligned output for the main [`reflow_table`] function.

use regex::Regex;

use crate::table::{SEP_RE, format_separator_cells, split_cells};

static SENTINEL_RE: std::sync::LazyLock<Regex> =
    std::sync::LazyLock::new(|| Regex::new(r"\|\s*\|\s*").unwrap());

pub(crate) fn parse_rows(trimmed: &[String]) -> (Vec<Vec<String>>, bool) {
    let raw = trimmed.join(" ");
    let chunks: Vec<&str> = SENTINEL_RE.split(&raw).collect();
    let split_within_line = chunks.len() > trimmed.len();

    let cells = collect_cells(&chunks);
    let rows = split_into_rows(cells);

    (rows, split_within_line)
}

fn collect_cells(chunks: &[&str]) -> Vec<String> {
    let mut cells = Vec::new();
    for (idx, chunk) in chunks.iter().enumerate() {
        let mut ch = (*chunk).to_string();
        if idx != chunks.len() - 1 {
            ch.push_str(" |ROW_END|");
        }
        cells.extend(split_cells(&ch));
    }
    cells
}

fn split_into_rows(cells: Vec<String>) -> Vec<Vec<String>> {
    let mut rows = Vec::new();
    let mut current = Vec::new();
    for cell in cells {
        if cell == "ROW_END" {
            if !current.is_empty() {
                rows.push(std::mem::take(&mut current));
            }
        } else {
            current.push(cell);
        }
    }
    if !current.is_empty() {
        rows.push(current);
    }
    rows
}

pub(crate) fn clean_rows(rows: Vec<Vec<String>>) -> Vec<Vec<String>> {
    let mut cleaned = Vec::new();
    for mut row in rows {
        row.retain(|c| !c.is_empty());
        cleaned.push(row);
    }
    cleaned
}

pub(crate) fn calculate_widths(rows: &[Vec<String>], max_cols: usize) -> Vec<usize> {
    let mut widths = vec![0; max_cols];
    for row in rows {
        for (idx, cell) in row.iter().enumerate() {
            widths[idx] = widths[idx].max(cell.len());
        }
    }
    widths
}

pub(crate) fn format_rows(rows: &[Vec<String>], widths: &[usize], indent: &str) -> Vec<String> {
    rows.iter()
        .map(|row| {
            let padded: Vec<String> = row
                .iter()
                .enumerate()
                .map(|(i, c)| format!("{:<width$}", c, width = widths[i]))
                .collect();
            format!("{}| {} |", indent, padded.join(" | "))
        })
        .collect()
}

pub(crate) fn insert_separator(
    out: Vec<String>,
    sep_cells: Option<Vec<String>>,
    widths: &[usize],
    indent: &str,
) -> Vec<String> {
    if let Some(mut cells) = sep_cells {
        while cells.len() < widths.len() {
            cells.push(String::new());
        }
        let sep_padded = format_separator_cells(widths, &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];
            with_sep.extend(out.into_iter().skip(1));
            return with_sep;
        }
        return vec![sep_line_out];
    }
    out
}

pub(crate) fn detect_separator(
    sep_line: Option<&String>,
    rows: &[Vec<String>],
    max_cols: usize,
) -> (Option<Vec<String>>, Option<usize>) {
    let mut sep_cells: Option<Vec<String>> = sep_line.map(|l| split_cells(l));
    let mut sep_row_idx: Option<usize> = None;

    let sep_invalid = invalid_separator(sep_cells.as_ref(), max_cols);
    if should_use_second_row_as_separator(sep_invalid, rows) {
        sep_cells = Some(rows[1].clone());
        sep_row_idx = Some(1);
    }

    (sep_cells, sep_row_idx)
}

fn invalid_separator(sep_cells: Option<&Vec<String>>, max_cols: usize) -> bool {
    match sep_cells {
        Some(c) => c.len() != max_cols,
        None => true,
    }
}

fn should_use_second_row_as_separator(sep_invalid: bool, rows: &[Vec<String>]) -> bool {
    sep_invalid && second_row_is_separator(rows)
}

fn second_row_is_separator(rows: &[Vec<String>]) -> bool {
    rows.len() > 1 && rows[1].iter().all(|c| SEP_RE.is_match(c))
}