use std::ops::Range;
use crate::render::layout::display_width;
use crate::render::markdown::block::BlockKind;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TableAlign {
Auto,
Manual,
}
impl TableAlign {
pub fn parse(s: &str) -> Option<TableAlign> {
Some(match s.trim().to_ascii_lowercase().as_str() {
"auto" | "on" | "true" => TableAlign::Auto,
"manual" | "off" | "false" => TableAlign::Manual,
_ => return None,
})
}
}
pub fn bounds(kinds: &[BlockKind], line: usize) -> Option<(usize, usize)> {
if kinds.get(line) != Some(&BlockKind::Table) {
return None;
}
let mut s = line;
while s > 0 && kinds[s - 1] == BlockKind::Table {
s -= 1;
}
let mut e = line;
while e + 1 < kinds.len() && kinds[e + 1] == BlockKind::Table {
e += 1;
}
Some((s, e))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Align {
None,
Left,
Center,
Right,
}
pub fn align(rows: &[String]) -> Vec<String> {
let indent: String = rows
.first()
.map(|r| r.chars().take_while(|c| *c == ' ' || *c == '\t').collect())
.unwrap_or_default();
let cells: Vec<Vec<String>> = rows.iter().map(|r| split(r)).collect();
let delim = rows.iter().position(|r| is_delimiter(r));
let cols = cells.iter().map(Vec::len).max().unwrap_or(0);
let aligns: Vec<Align> = (0..cols)
.map(|c| match delim.and_then(|d| cells[d].get(c)) {
Some(cell) => match (cell.starts_with(':'), cell.ends_with(':')) {
(true, true) => Align::Center,
(true, false) => Align::Left,
(false, true) => Align::Right,
(false, false) => Align::None,
},
None => Align::None,
})
.collect();
let widths: Vec<usize> = (0..cols)
.map(|c| {
cells
.iter()
.enumerate()
.filter(|(i, _)| Some(*i) != delim)
.filter_map(|(_, row)| row.get(c))
.map(|cell| display_width(cell) as usize)
.max()
.unwrap_or(0)
.max(3)
})
.collect();
cells
.iter()
.enumerate()
.map(|(i, row)| {
let mut out = indent.clone();
out.push('|');
for c in 0..cols {
let w = widths[c];
if Some(i) == delim {
let (l, r) = match aligns[c] {
Align::None => ('-', '-'),
Align::Left => (':', '-'),
Align::Center => (':', ':'),
Align::Right => ('-', ':'),
};
out.push(l);
out.push_str(&"-".repeat(w));
out.push(r);
} else {
let cell = row.get(c).map(String::as_str).unwrap_or("");
let gap = w - display_width(cell) as usize;
let (before, after) = match aligns[c] {
Align::Right => (gap, 0),
Align::Center => (gap / 2, gap - gap / 2),
_ => (0, gap),
};
out.push(' ');
out.push_str(&" ".repeat(before));
out.push_str(cell);
out.push_str(&" ".repeat(after));
out.push(' ');
}
out.push('|');
}
out
})
.collect()
}
pub fn cursor_col(old: &str, new: &str, col: usize) -> usize {
let (o, n) = (spans(old), spans(new));
let Some(last) = o.len().checked_sub(1) else {
return col;
};
let i = o.iter().position(|s| col <= s.raw.end).unwrap_or(last);
if col == o[i].raw.end {
return n.get(i).map_or(col, |s| s.raw.end);
}
let offset = col.saturating_sub(o[i].text.start).min(o[i].text.len());
match n.get(i) {
Some(s) => s.text.start + offset,
None => col,
}
}
struct Span {
raw: Range<usize>,
text: Range<usize>,
}
fn spans(row: &str) -> Vec<Span> {
let chars: Vec<char> = row.chars().collect();
let mut bars = Vec::new();
let mut escaped = false;
for (i, c) in chars.iter().enumerate() {
match c {
'\\' => escaped = !escaped,
'|' if !escaped => bars.push(i),
_ => escaped = false,
}
}
let ws = |c: &char| *c == ' ' || *c == '\t';
let first = chars.iter().position(|c| !ws(c));
let last = chars.iter().rposition(|c| !ws(c));
let mut start = 0;
let mut end = chars.len();
if let (Some(&b), Some(f)) = (bars.first(), first) {
if b == f {
start = b + 1;
bars.remove(0);
}
}
if let (Some(&b), Some(l)) = (bars.last(), last) {
if b == l {
end = b;
bars.pop();
}
}
let mut out = Vec::new();
let mut from = start;
for to in bars.into_iter().chain(std::iter::once(end)) {
let mut a = from;
let mut z = to;
while a < z && ws(&chars[a]) {
a += 1;
}
while z > a && ws(&chars[z - 1]) {
z -= 1;
}
out.push(Span { raw: from..to, text: a..z });
from = to + 1;
}
out
}
fn split(row: &str) -> Vec<String> {
let mut cells = Vec::new();
let mut cur = String::new();
let mut escaped = false;
for c in row.trim().chars() {
match c {
'\\' => {
escaped = !escaped;
cur.push(c);
}
'|' if !escaped => cells.push(std::mem::take(&mut cur)),
_ => {
escaped = false;
cur.push(c);
}
}
}
cells.push(cur);
let t = row.trim();
if t.starts_with('|') {
cells.remove(0);
}
if t.len() > 1 && t.ends_with('|') && !t.ends_with("\\|") {
cells.pop();
}
cells.into_iter().map(|c| c.trim().to_string()).collect()
}
fn is_delimiter(row: &str) -> bool {
let mut dashes = false;
let mut bars = false;
for c in row.trim().chars() {
match c {
'-' => dashes = true,
'|' => bars = true,
':' | ' ' | '\t' => {}
_ => return false,
}
}
dashes && bars
}
#[cfg(test)]
mod tests {
use super::*;
fn aligned(text: &str) -> String {
let rows: Vec<String> = text.lines().map(str::to_string).collect();
align(&rows).join("\n")
}
#[test]
fn pads_every_cell_to_its_column() {
let messy = "| Name | Role |\n|---|---|\n| Alice | Maintainer of the parser |\n| Bob | Docs |";
assert_eq!(
aligned(messy),
"| Name | Role |\n\
|-------|--------------------------|\n\
| Alice | Maintainer of the parser |\n\
| Bob | Docs |"
);
}
#[test]
fn already_aligned_is_unchanged() {
let table = "| a | b |\n|-----|-----|\n| 1 | 2 |";
assert_eq!(aligned(table), table);
}
#[test]
fn keeps_alignment_markers_and_pads_by_them() {
let t = "| l | c | r |\n|:--|:-:|--:|\n| left | mid | 1 |";
assert_eq!(
aligned(t),
"| l | c | r |\n\
|:-----|:---:|----:|\n\
| left | mid | 1 |"
);
}
#[test]
fn adds_outer_bars_and_fills_short_rows() {
let t = "a | b\n--|--\n1";
assert_eq!(aligned(t), "| a | b |\n|-----|-----|\n| 1 | |");
}
#[test]
fn wide_characters_pad_by_display_width() {
let t = "| 名前 | x |\n|---|---|\n| ab | y |";
assert_eq!(aligned(t), "| 名前 | x |\n|------|-----|\n| ab | y |");
}
#[test]
fn escaped_bar_stays_in_its_cell() {
let t = "| a \\| b | c |\n|---|---|";
assert_eq!(aligned(t), "| a \\| b | c |\n|--------|-----|");
}
#[test]
fn keeps_the_first_rows_indent() {
let t = " | a | b |\n|---|---|";
assert_eq!(aligned(t), " | a | b |\n |-----|-----|");
}
#[test]
fn cursor_keeps_its_character() {
let old = "| Alice | Maintainer |";
let new = "| Alice | Maintainer |";
assert_eq!(cursor_col(old, new, 12), 12, "the n of Maintainer");
let old = "|a|bcd|";
let new = "| a | bcd |";
assert_eq!(cursor_col(old, new, 4), 9, "the c of bcd");
assert_eq!(cursor_col(old, new, 6), 12, "on the closing bar");
assert_eq!(cursor_col("a | b", "| a | b |", 4), 8, "no leading bar");
}
#[test]
fn bounds_cover_the_whole_table() {
use BlockKind::*;
let kinds = [Paragraph, Table, Table, Table, Blank];
assert_eq!(bounds(&kinds, 2), Some((1, 3)));
assert_eq!(bounds(&kinds, 0), None);
}
}