Terminal Markdown previewer — GUI-like experience.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: split markdown tables

RivoLink a6388376 fefe9d59

+383 -374
+4 -374
src/markdown/mod.rs
··· 1 + mod tables; 1 2 pub(crate) mod toc; 2 3 pub(crate) mod width; 3 4 mod wrapping; 4 5 6 + use tables::{handle_table_event, start_table, TableBuf}; 5 7 pub(crate) use width::{build_plain_lines, display_width, line_plain_text, truncate_display_width}; 6 8 7 9 use crate::theme::{app_theme, MarkdownTheme}; 8 - use pulldown_cmark::{ 9 - Alignment, CodeBlockKind, Event as MdEvent, HeadingLevel, Options, Parser, Tag, TagEnd, 10 - }; 10 + use pulldown_cmark::{CodeBlockKind, Event as MdEvent, HeadingLevel, Options, Parser, Tag, TagEnd}; 11 11 use ratatui::{ 12 12 style::{Color, Modifier, Style}, 13 13 text::{Line, Span}, ··· 21 21 easy::HighlightLines, highlighting::Theme, parsing::SyntaxSet, util::LinesWithEndings, 22 22 }; 23 23 use toc::{normalize_toc, TocEntry}; 24 - use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; 24 + use unicode_width::UnicodeWidthStr; 25 25 use width::expand_tabs; 26 26 use wrapping::push_wrapped_prefixed_lines; 27 27 ··· 48 48 in_em: bool, 49 49 in_strike: bool, 50 50 in_link: bool, 51 - } 52 - 53 - struct TableBuf { 54 - alignments: Vec<Alignment>, 55 - rows: Vec<Vec<String>>, 56 - header_count: usize, 57 - current_row: Vec<String>, 58 - current_cell: String, 59 - in_header: bool, 60 - } 61 - 62 - struct TableBorder<'a> { 63 - left: &'a str, 64 - fill: &'a str, 65 - cross: &'a str, 66 - right: &'a str, 67 51 } 68 52 69 53 struct CodeBlockRenderContext<'a> { ··· 507 491 ); 508 492 } 509 493 510 - fn handle_table_event( 511 - table: &mut Option<TableBuf>, 512 - ev: &MdEvent<'_>, 513 - lines: &mut Vec<Line<'static>>, 514 - render_width: usize, 515 - ) -> bool { 516 - let Some(tb) = table.as_mut() else { 517 - return false; 518 - }; 519 - 520 - match ev { 521 - MdEvent::Text(t) | MdEvent::Code(t) => { 522 - tb.push_text(t.as_ref()); 523 - true 524 - } 525 - MdEvent::Start(Tag::TableCell) => true, 526 - MdEvent::End(TagEnd::TableCell) => { 527 - tb.end_cell(); 528 - true 529 - } 530 - MdEvent::Start(Tag::TableRow) => true, 531 - MdEvent::End(TagEnd::TableRow) => { 532 - tb.end_row(); 533 - true 534 - } 535 - MdEvent::Start(Tag::TableHead) => { 536 - tb.in_header = true; 537 - true 538 - } 539 - MdEvent::End(TagEnd::TableHead) => { 540 - tb.end_header(); 541 - true 542 - } 543 - MdEvent::Start(Tag::Strong) 544 - | MdEvent::End(TagEnd::Strong) 545 - | MdEvent::Start(Tag::Emphasis) 546 - | MdEvent::End(TagEnd::Emphasis) 547 - | MdEvent::Start(Tag::Link { .. }) 548 - | MdEvent::End(TagEnd::Link) => true, 549 - MdEvent::End(TagEnd::Table) => { 550 - let rendered = tb.render(render_width); 551 - lines.extend(rendered); 552 - *table = None; 553 - true 554 - } 555 - _ => true, 556 - } 557 - } 558 - 559 494 fn start_list( 560 495 lines: &mut Vec<Line<'static>>, 561 496 last_block: LastBlock, ··· 567 502 Some(n) => ListKind::Ordered(n), 568 503 None => ListKind::Unordered, 569 504 }); 570 - } 571 - 572 - fn start_table(table: &mut Option<TableBuf>, aligns: &[Alignment]) { 573 - *table = Some(TableBuf::new(aligns.to_vec())); 574 505 } 575 506 576 507 fn end_list(lines: &mut Vec<Line<'static>>, list_stack: &mut Vec<ListKind>) { ··· 797 728 text.to_string(), 798 729 inline_text_style(theme, blockquote_depth, inline), 799 730 )); 800 - } 801 - } 802 - 803 - impl TableBuf { 804 - fn new(alignments: Vec<Alignment>) -> Self { 805 - Self { 806 - alignments, 807 - rows: vec![], 808 - header_count: 0, 809 - current_row: vec![], 810 - current_cell: String::new(), 811 - in_header: false, 812 - } 813 - } 814 - fn push_text(&mut self, t: &str) { 815 - self.current_cell.push_str(t); 816 - } 817 - fn end_cell(&mut self) { 818 - let cell = std::mem::take(&mut self.current_cell).trim().to_string(); 819 - self.current_row.push(cell); 820 - } 821 - fn end_row(&mut self) { 822 - let row = std::mem::take(&mut self.current_row); 823 - if !row.is_empty() { 824 - self.rows.push(row); 825 - } 826 - } 827 - fn end_header(&mut self) { 828 - self.end_row(); 829 - self.header_count = self.rows.len(); 830 - self.in_header = false; 831 - } 832 - 833 - fn render(&self, render_width: usize) -> Vec<Line<'static>> { 834 - let theme = &app_theme().markdown; 835 - if self.rows.is_empty() { 836 - return vec![]; 837 - } 838 - let col_count = self.rows.iter().map(|r| r.len()).max().unwrap_or(0); 839 - if col_count == 0 { 840 - return vec![]; 841 - } 842 - 843 - let mut col_widths: Vec<usize> = vec![1; col_count]; 844 - let mut min_widths: Vec<usize> = vec![4; col_count]; 845 - for row in &self.rows { 846 - for (ci, cell) in row.iter().enumerate() { 847 - if ci < col_count { 848 - col_widths[ci] = col_widths[ci].max(display_width(cell)); 849 - min_widths[ci] = min_widths[ci].max(min_table_cell_width(cell)); 850 - } 851 - } 852 - } 853 - 854 - fit_table_widths(&mut col_widths, &min_widths, render_width); 855 - 856 - let border = Style::default().fg(theme.table_border); 857 - let sep = Style::default().fg(theme.table_separator); 858 - let header = Style::default() 859 - .fg(theme.table_header) 860 - .add_modifier(Modifier::BOLD); 861 - let cell = Style::default().fg(theme.table_cell); 862 - let ind = ""; 863 - 864 - let mut out: Vec<Line<'static>> = Vec::new(); 865 - out.push(self.hline( 866 - ind, 867 - TableBorder { 868 - left: "┌", 869 - fill: "─", 870 - cross: "┬", 871 - right: "┐", 872 - }, 873 - &col_widths, 874 - border, 875 - )); 876 - 877 - for (ri, row) in self.rows.iter().enumerate() { 878 - let is_hdr = ri < self.header_count; 879 - let wrapped_cells: Vec<Vec<String>> = col_widths 880 - .iter() 881 - .copied() 882 - .enumerate() 883 - .take(col_count) 884 - .map(|(ci, width)| { 885 - wrap_table_cell(row.get(ci).map(|s| s.as_str()).unwrap_or(""), width) 886 - }) 887 - .collect(); 888 - let row_height = wrapped_cells 889 - .iter() 890 - .map(|lines| lines.len()) 891 - .max() 892 - .unwrap_or(1); 893 - 894 - for line_idx in 0..row_height { 895 - let mut spans = vec![Span::raw(ind), Span::styled("│", border)]; 896 - for (ci, width) in col_widths.iter().copied().enumerate().take(col_count) { 897 - let txt = wrapped_cells[ci] 898 - .get(line_idx) 899 - .map(|s| s.as_str()) 900 - .unwrap_or(""); 901 - let align = self.alignments.get(ci).copied().unwrap_or(Alignment::None); 902 - let pad = align_cell(txt, width, align); 903 - let st = if is_hdr { header } else { cell }; 904 - spans.push(Span::raw(" ")); 905 - spans.push(Span::styled(pad, st)); 906 - spans.push(Span::raw(" ")); 907 - spans.push(Span::styled("│", border)); 908 - } 909 - out.push(Line::from(spans)); 910 - } 911 - 912 - if is_hdr && ri == self.header_count - 1 { 913 - out.push(self.hline( 914 - ind, 915 - TableBorder { 916 - left: "╞", 917 - fill: "═", 918 - cross: "╪", 919 - right: "╡", 920 - }, 921 - &col_widths, 922 - sep, 923 - )); 924 - } else if !is_hdr && ri < self.rows.len() - 1 { 925 - out.push(self.hline( 926 - ind, 927 - TableBorder { 928 - left: "├", 929 - fill: "─", 930 - cross: "┼", 931 - right: "┤", 932 - }, 933 - &col_widths, 934 - border, 935 - )); 936 - } 937 - } 938 - 939 - out.push(self.hline( 940 - ind, 941 - TableBorder { 942 - left: "└", 943 - fill: "─", 944 - cross: "┴", 945 - right: "┘", 946 - }, 947 - &col_widths, 948 - border, 949 - )); 950 - out.push(Line::from("")); 951 - out 952 - } 953 - 954 - fn hline( 955 - &self, 956 - indent: &str, 957 - border: TableBorder<'_>, 958 - col_widths: &[usize], 959 - style: Style, 960 - ) -> Line<'static> { 961 - let mut spans = vec![ 962 - Span::raw(indent.to_string()), 963 - Span::styled(border.left.to_string(), style), 964 - ]; 965 - for (ci, &w) in col_widths.iter().enumerate() { 966 - spans.push(Span::styled(border.fill.repeat(w + 2), style)); 967 - if ci < col_widths.len() - 1 { 968 - spans.push(Span::styled(border.cross.to_string(), style)); 969 - } 970 - } 971 - spans.push(Span::styled(border.right.to_string(), style)); 972 - Line::from(spans) 973 - } 974 - } 975 - 976 - fn min_table_cell_width(text: &str) -> usize { 977 - let max_word = text 978 - .split_whitespace() 979 - .map(display_width) 980 - .max() 981 - .unwrap_or(0) 982 - .min(12); 983 - max_word.max(4) 984 - } 985 - 986 - fn fit_table_widths(col_widths: &mut [usize], min_widths: &[usize], render_width: usize) { 987 - if col_widths.is_empty() { 988 - return; 989 - } 990 - 991 - let col_count = col_widths.len(); 992 - let border_width = 3 * col_count + 1; 993 - let available = render_width.saturating_sub(border_width).max(col_count); 994 - let min_total: usize = min_widths.iter().sum(); 995 - 996 - if min_total >= available { 997 - let mut widths = vec![1; col_count]; 998 - let mut remaining = available.saturating_sub(col_count); 999 - let mut order: Vec<usize> = (0..col_count).collect(); 1000 - order.sort_by_key(|&idx| std::cmp::Reverse(min_widths[idx])); 1001 - for idx in order { 1002 - if remaining == 0 { 1003 - break; 1004 - } 1005 - let extra = (min_widths[idx].saturating_sub(1)).min(remaining); 1006 - widths[idx] += extra; 1007 - remaining -= extra; 1008 - } 1009 - col_widths.copy_from_slice(&widths); 1010 - return; 1011 - } 1012 - 1013 - while col_widths.iter().sum::<usize>() > available { 1014 - let Some((idx, _)) = col_widths 1015 - .iter() 1016 - .enumerate() 1017 - .filter(|(idx, width)| **width > min_widths[*idx]) 1018 - .max_by_key(|(_, width)| **width) 1019 - else { 1020 - break; 1021 - }; 1022 - col_widths[idx] -= 1; 1023 - } 1024 - } 1025 - 1026 - fn wrap_table_cell(text: &str, width: usize) -> Vec<String> { 1027 - if width == 0 { 1028 - return vec![String::new()]; 1029 - } 1030 - let expanded = expand_tabs(text, 0); 1031 - if expanded.is_empty() { 1032 - return vec![String::new()]; 1033 - } 1034 - 1035 - let mut lines = Vec::new(); 1036 - let mut current = String::new(); 1037 - let mut current_width = 0usize; 1038 - 1039 - for word in expanded.split_whitespace() { 1040 - let word_width = display_width(word); 1041 - 1042 - if word_width > width { 1043 - if !current.is_empty() { 1044 - lines.push(std::mem::take(&mut current)); 1045 - current_width = 0; 1046 - } 1047 - let mut chunk = String::new(); 1048 - let mut chunk_width = 0usize; 1049 - for ch in word.chars() { 1050 - let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0); 1051 - if chunk_width + ch_width > width && !chunk.is_empty() { 1052 - lines.push(std::mem::take(&mut chunk)); 1053 - chunk_width = 0; 1054 - } 1055 - chunk.push(ch); 1056 - chunk_width += ch_width; 1057 - } 1058 - if !chunk.is_empty() { 1059 - current = chunk; 1060 - current_width = chunk_width; 1061 - } 1062 - continue; 1063 - } 1064 - 1065 - let sep = if current.is_empty() { 0 } else { 1 }; 1066 - if current_width + sep + word_width > width && !current.is_empty() { 1067 - lines.push(std::mem::take(&mut current)); 1068 - current_width = 0; 1069 - } 1070 - if !current.is_empty() { 1071 - current.push(' '); 1072 - current_width += 1; 1073 - } 1074 - current.push_str(word); 1075 - current_width += word_width; 1076 - } 1077 - 1078 - if !current.is_empty() { 1079 - lines.push(current); 1080 - } 1081 - if lines.is_empty() { 1082 - lines.push(String::new()); 1083 - } 1084 - lines 1085 - } 1086 - 1087 - fn align_cell(text: &str, width: usize, align: Alignment) -> String { 1088 - let text = expand_tabs(text, 0); 1089 - let len = display_width(&text); 1090 - if len >= width { 1091 - return text; 1092 - } 1093 - let pad = width - len; 1094 - match align { 1095 - Alignment::Right => format!("{}{}", " ".repeat(pad), text), 1096 - Alignment::Center => { 1097 - let l = pad / 2; 1098 - format!("{}{}{}", " ".repeat(l), text, " ".repeat(pad - l)) 1099 - } 1100 - _ => format!("{}{}", text, " ".repeat(pad)), 1101 731 } 1102 732 } 1103 733
+379
src/markdown/tables.rs
··· 1 + use crate::theme::app_theme; 2 + use pulldown_cmark::{Alignment, Event as MdEvent, Tag, TagEnd}; 3 + use ratatui::{ 4 + style::{Modifier, Style}, 5 + text::{Line, Span}, 6 + }; 7 + use unicode_width::UnicodeWidthChar; 8 + 9 + use super::width::{display_width, expand_tabs}; 10 + 11 + pub(super) struct TableBuf { 12 + pub(super) alignments: Vec<Alignment>, 13 + rows: Vec<Vec<String>>, 14 + header_count: usize, 15 + current_row: Vec<String>, 16 + current_cell: String, 17 + pub(super) in_header: bool, 18 + } 19 + 20 + struct TableBorder<'a> { 21 + left: &'a str, 22 + fill: &'a str, 23 + cross: &'a str, 24 + right: &'a str, 25 + } 26 + 27 + pub(super) fn handle_table_event( 28 + table: &mut Option<TableBuf>, 29 + ev: &MdEvent<'_>, 30 + lines: &mut Vec<Line<'static>>, 31 + render_width: usize, 32 + ) -> bool { 33 + let Some(tb) = table.as_mut() else { 34 + return false; 35 + }; 36 + 37 + match ev { 38 + MdEvent::Text(t) | MdEvent::Code(t) => { 39 + tb.push_text(t.as_ref()); 40 + true 41 + } 42 + MdEvent::Start(Tag::TableCell) => true, 43 + MdEvent::End(TagEnd::TableCell) => { 44 + tb.end_cell(); 45 + true 46 + } 47 + MdEvent::Start(Tag::TableRow) => true, 48 + MdEvent::End(TagEnd::TableRow) => { 49 + tb.end_row(); 50 + true 51 + } 52 + MdEvent::Start(Tag::TableHead) => { 53 + tb.in_header = true; 54 + true 55 + } 56 + MdEvent::End(TagEnd::TableHead) => { 57 + tb.end_header(); 58 + true 59 + } 60 + MdEvent::Start(Tag::Strong) 61 + | MdEvent::End(TagEnd::Strong) 62 + | MdEvent::Start(Tag::Emphasis) 63 + | MdEvent::End(TagEnd::Emphasis) 64 + | MdEvent::Start(Tag::Link { .. }) 65 + | MdEvent::End(TagEnd::Link) => true, 66 + MdEvent::End(TagEnd::Table) => { 67 + let rendered = tb.render(render_width); 68 + lines.extend(rendered); 69 + *table = None; 70 + true 71 + } 72 + _ => true, 73 + } 74 + } 75 + 76 + pub(super) fn start_table(table: &mut Option<TableBuf>, aligns: &[Alignment]) { 77 + *table = Some(TableBuf::new(aligns.to_vec())); 78 + } 79 + 80 + impl TableBuf { 81 + fn new(alignments: Vec<Alignment>) -> Self { 82 + Self { 83 + alignments, 84 + rows: vec![], 85 + header_count: 0, 86 + current_row: vec![], 87 + current_cell: String::new(), 88 + in_header: false, 89 + } 90 + } 91 + fn push_text(&mut self, t: &str) { 92 + self.current_cell.push_str(t); 93 + } 94 + fn end_cell(&mut self) { 95 + let cell = std::mem::take(&mut self.current_cell).trim().to_string(); 96 + self.current_row.push(cell); 97 + } 98 + fn end_row(&mut self) { 99 + let row = std::mem::take(&mut self.current_row); 100 + if !row.is_empty() { 101 + self.rows.push(row); 102 + } 103 + } 104 + fn end_header(&mut self) { 105 + self.end_row(); 106 + self.header_count = self.rows.len(); 107 + self.in_header = false; 108 + } 109 + 110 + fn render(&self, render_width: usize) -> Vec<Line<'static>> { 111 + let theme = &app_theme().markdown; 112 + if self.rows.is_empty() { 113 + return vec![]; 114 + } 115 + let col_count = self.rows.iter().map(|r| r.len()).max().unwrap_or(0); 116 + if col_count == 0 { 117 + return vec![]; 118 + } 119 + 120 + let mut col_widths: Vec<usize> = vec![1; col_count]; 121 + let mut min_widths: Vec<usize> = vec![4; col_count]; 122 + for row in &self.rows { 123 + for (ci, cell) in row.iter().enumerate() { 124 + if ci < col_count { 125 + col_widths[ci] = col_widths[ci].max(display_width(cell)); 126 + min_widths[ci] = min_widths[ci].max(min_table_cell_width(cell)); 127 + } 128 + } 129 + } 130 + 131 + fit_table_widths(&mut col_widths, &min_widths, render_width); 132 + 133 + let border = Style::default().fg(theme.table_border); 134 + let sep = Style::default().fg(theme.table_separator); 135 + let header = Style::default() 136 + .fg(theme.table_header) 137 + .add_modifier(Modifier::BOLD); 138 + let cell = Style::default().fg(theme.table_cell); 139 + let ind = ""; 140 + 141 + let mut out: Vec<Line<'static>> = Vec::new(); 142 + out.push(self.hline( 143 + ind, 144 + TableBorder { 145 + left: "┌", 146 + fill: "─", 147 + cross: "┬", 148 + right: "┐", 149 + }, 150 + &col_widths, 151 + border, 152 + )); 153 + 154 + for (ri, row) in self.rows.iter().enumerate() { 155 + let is_hdr = ri < self.header_count; 156 + let wrapped_cells: Vec<Vec<String>> = col_widths 157 + .iter() 158 + .copied() 159 + .enumerate() 160 + .take(col_count) 161 + .map(|(ci, width)| { 162 + wrap_table_cell(row.get(ci).map(|s| s.as_str()).unwrap_or(""), width) 163 + }) 164 + .collect(); 165 + let row_height = wrapped_cells 166 + .iter() 167 + .map(|lines| lines.len()) 168 + .max() 169 + .unwrap_or(1); 170 + 171 + for line_idx in 0..row_height { 172 + let mut spans = vec![Span::raw(ind), Span::styled("│", border)]; 173 + for (ci, width) in col_widths.iter().copied().enumerate().take(col_count) { 174 + let txt = wrapped_cells[ci] 175 + .get(line_idx) 176 + .map(|s| s.as_str()) 177 + .unwrap_or(""); 178 + let align = self.alignments.get(ci).copied().unwrap_or(Alignment::None); 179 + let pad = align_cell(txt, width, align); 180 + let st = if is_hdr { header } else { cell }; 181 + spans.push(Span::raw(" ")); 182 + spans.push(Span::styled(pad, st)); 183 + spans.push(Span::raw(" ")); 184 + spans.push(Span::styled("│", border)); 185 + } 186 + out.push(Line::from(spans)); 187 + } 188 + 189 + if is_hdr && ri == self.header_count - 1 { 190 + out.push(self.hline( 191 + ind, 192 + TableBorder { 193 + left: "╞", 194 + fill: "═", 195 + cross: "╪", 196 + right: "╡", 197 + }, 198 + &col_widths, 199 + sep, 200 + )); 201 + } else if !is_hdr && ri < self.rows.len() - 1 { 202 + out.push(self.hline( 203 + ind, 204 + TableBorder { 205 + left: "├", 206 + fill: "─", 207 + cross: "┼", 208 + right: "┤", 209 + }, 210 + &col_widths, 211 + border, 212 + )); 213 + } 214 + } 215 + 216 + out.push(self.hline( 217 + ind, 218 + TableBorder { 219 + left: "└", 220 + fill: "─", 221 + cross: "┴", 222 + right: "┘", 223 + }, 224 + &col_widths, 225 + border, 226 + )); 227 + out.push(Line::from("")); 228 + out 229 + } 230 + 231 + fn hline( 232 + &self, 233 + indent: &str, 234 + border: TableBorder<'_>, 235 + col_widths: &[usize], 236 + style: Style, 237 + ) -> Line<'static> { 238 + let mut spans = vec![ 239 + Span::raw(indent.to_string()), 240 + Span::styled(border.left.to_string(), style), 241 + ]; 242 + for (ci, &w) in col_widths.iter().enumerate() { 243 + spans.push(Span::styled(border.fill.repeat(w + 2), style)); 244 + if ci < col_widths.len() - 1 { 245 + spans.push(Span::styled(border.cross.to_string(), style)); 246 + } 247 + } 248 + spans.push(Span::styled(border.right.to_string(), style)); 249 + Line::from(spans) 250 + } 251 + } 252 + 253 + fn min_table_cell_width(text: &str) -> usize { 254 + let max_word = text 255 + .split_whitespace() 256 + .map(display_width) 257 + .max() 258 + .unwrap_or(0) 259 + .min(12); 260 + max_word.max(4) 261 + } 262 + 263 + fn fit_table_widths(col_widths: &mut [usize], min_widths: &[usize], render_width: usize) { 264 + if col_widths.is_empty() { 265 + return; 266 + } 267 + 268 + let col_count = col_widths.len(); 269 + let border_width = 3 * col_count + 1; 270 + let available = render_width.saturating_sub(border_width).max(col_count); 271 + let min_total: usize = min_widths.iter().sum(); 272 + 273 + if min_total >= available { 274 + let mut widths = vec![1; col_count]; 275 + let mut remaining = available.saturating_sub(col_count); 276 + let mut order: Vec<usize> = (0..col_count).collect(); 277 + order.sort_by_key(|&idx| std::cmp::Reverse(min_widths[idx])); 278 + for idx in order { 279 + if remaining == 0 { 280 + break; 281 + } 282 + let extra = (min_widths[idx].saturating_sub(1)).min(remaining); 283 + widths[idx] += extra; 284 + remaining -= extra; 285 + } 286 + col_widths.copy_from_slice(&widths); 287 + return; 288 + } 289 + 290 + while col_widths.iter().sum::<usize>() > available { 291 + let Some((idx, _)) = col_widths 292 + .iter() 293 + .enumerate() 294 + .filter(|(idx, width)| **width > min_widths[*idx]) 295 + .max_by_key(|(_, width)| **width) 296 + else { 297 + break; 298 + }; 299 + col_widths[idx] -= 1; 300 + } 301 + } 302 + 303 + fn wrap_table_cell(text: &str, width: usize) -> Vec<String> { 304 + if width == 0 { 305 + return vec![String::new()]; 306 + } 307 + let expanded = expand_tabs(text, 0); 308 + if expanded.is_empty() { 309 + return vec![String::new()]; 310 + } 311 + 312 + let mut lines = Vec::new(); 313 + let mut current = String::new(); 314 + let mut current_width = 0usize; 315 + 316 + for word in expanded.split_whitespace() { 317 + let word_width = display_width(word); 318 + 319 + if word_width > width { 320 + if !current.is_empty() { 321 + lines.push(std::mem::take(&mut current)); 322 + current_width = 0; 323 + } 324 + let mut chunk = String::new(); 325 + let mut chunk_width = 0usize; 326 + for ch in word.chars() { 327 + let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0); 328 + if chunk_width + ch_width > width && !chunk.is_empty() { 329 + lines.push(std::mem::take(&mut chunk)); 330 + chunk_width = 0; 331 + } 332 + chunk.push(ch); 333 + chunk_width += ch_width; 334 + } 335 + if !chunk.is_empty() { 336 + current = chunk; 337 + current_width = chunk_width; 338 + } 339 + continue; 340 + } 341 + 342 + let sep = if current.is_empty() { 0 } else { 1 }; 343 + if current_width + sep + word_width > width && !current.is_empty() { 344 + lines.push(std::mem::take(&mut current)); 345 + current_width = 0; 346 + } 347 + if !current.is_empty() { 348 + current.push(' '); 349 + current_width += 1; 350 + } 351 + current.push_str(word); 352 + current_width += word_width; 353 + } 354 + 355 + if !current.is_empty() { 356 + lines.push(current); 357 + } 358 + if lines.is_empty() { 359 + lines.push(String::new()); 360 + } 361 + lines 362 + } 363 + 364 + fn align_cell(text: &str, width: usize, align: Alignment) -> String { 365 + let text = expand_tabs(text, 0); 366 + let len = display_width(&text); 367 + if len >= width { 368 + return text; 369 + } 370 + let pad = width - len; 371 + match align { 372 + Alignment::Right => format!("{}{}", " ".repeat(pad), text), 373 + Alignment::Center => { 374 + let l = pad / 2; 375 + format!("{}{}{}", " ".repeat(l), text, " ".repeat(pad - l)) 376 + } 377 + _ => format!("{}{}", text, " ".repeat(pad)), 378 + } 379 + }