use std::fmt::Write as _;
const WIDTH: usize = 100;
pub(crate) fn first_line(s: &str) -> String {
let line = s.split(['\n', '\r']).next().unwrap_or("").replace('\t', " ");
if line.chars().count() <= WIDTH {
return line;
}
line.chars().take(WIDTH - 3).collect::<String>() + "..."
}
pub(crate) fn trim_lines(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for (i, line) in s.split('\n').enumerate() {
if i > 0 {
out.push('\n');
}
out.push_str(line.trim_end_matches([' ', '\t']));
}
out
}
#[derive(Default)]
pub(crate) struct Columns {
rows: Vec<Vec<String>>,
}
impl Columns {
pub(crate) fn push(&mut self, row: impl IntoIterator<Item = String>) {
self.rows.push(row.into_iter().collect());
}
pub(crate) fn render(&self) -> String {
let columns = self.rows.iter().map(Vec::len).max().unwrap_or(0);
let widths: Vec<usize> = (0..columns)
.map(|i| {
self.rows
.iter()
.filter_map(|r| r.get(i))
.map(|c| c.chars().count())
.max()
.unwrap_or(0)
})
.collect();
let mut out = String::new();
for row in &self.rows {
for (i, cell) in row.iter().enumerate() {
if i + 1 == row.len() {
out.push_str(cell);
} else {
let pad = widths[i] - cell.chars().count() + 2;
let _ = write!(out, "{cell}{:pad$}", "");
}
}
out.push('\n');
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_line_counts_characters() {
let cjk = "あ".repeat(120);
assert_eq!(first_line(&cjk).chars().count(), WIDTH);
assert_eq!(first_line("one\ntwo"), "one");
assert_eq!(first_line("a\tb"), "a b");
}
#[test]
fn columns_align_on_the_widest_cell() {
let mut c = Columns::default();
c.push(["skill".to_owned(), "missing".to_owned()]);
c.push(["a-much-longer-name".to_owned(), "up-to-date".to_owned()]);
assert_eq!(c.render(), "skill missing\na-much-longer-name up-to-date\n");
}
#[test]
fn trim_lines_removes_padding() {
assert_eq!(trim_lines("a \nb\t\n"), "a\nb\n");
}
}