comfy_table/utils/formatting/content_format.rs
1#[cfg(feature = "tty")]
2use crossterm::style::{style, Stylize};
3use unicode_segmentation::UnicodeSegmentation;
4use unicode_width::UnicodeWidthStr;
5
6use super::content_split::measure_text_width;
7use super::content_split::split_line;
8
9use crate::cell::Cell;
10use crate::row::Row;
11use crate::style::CellAlignment;
12#[cfg(feature = "tty")]
13use crate::style::{map_attribute, map_color};
14use crate::table::Table;
15use crate::utils::ColumnDisplayInfo;
16
17pub fn delimiter(cell: &Cell, info: &ColumnDisplayInfo, table: &Table) -> char {
18 // Determine, which delimiter should be used
19 if let Some(delimiter) = cell.delimiter {
20 delimiter
21 } else if let Some(delimiter) = info.delimiter {
22 delimiter
23 } else if let Some(delimiter) = table.delimiter {
24 delimiter
25 } else {
26 ' '
27 }
28}
29
30/// Returns the formatted content of the table.
31/// The content is organized in the following structure
32///
33/// tc stands for table content and represents the returned value
34/// ``` text
35/// column1 column2
36/// row1 tc[0][0][0] tc[0][0][1] <-line1
37/// tc[0][1][0] tc[0][1][1] <-line2
38/// tc[0][2][0] tc[0][2][1] <-line3
39///
40/// row2 tc[1][0][0] tc[1][0][1] <-line1
41/// tc[1][1][0] tc[1][1][1] <-line2
42/// tc[1][2][0] tc[1][2][1] <-line3
43/// ```
44///
45/// The strings for each row will be padded and aligned according to their respective column.
46pub fn format_content(table: &Table, display_info: &[ColumnDisplayInfo]) -> Vec<Vec<Vec<String>>> {
47 // The content of the whole table
48 let mut table_content = Vec::with_capacity(table.rows.len() + 1);
49
50 // Format table header if it exists
51 if let Some(header) = table.header() {
52 table_content.push(format_row(header, display_info, table));
53 }
54
55 for row in table.rows.iter() {
56 table_content.push(format_row(row, display_info, table));
57 }
58 table_content
59}
60
61pub fn format_row(
62 row: &Row,
63 display_infos: &[ColumnDisplayInfo],
64 table: &Table,
65) -> Vec<Vec<String>> {
66 // The content of this specific row
67 let mut temp_row_content = Vec::with_capacity(display_infos.len());
68
69 let mut cell_iter = row.cells.iter();
70 // Now iterate over all cells and handle them according to their alignment
71 for info in display_infos.iter() {
72 if info.is_hidden {
73 cell_iter.next();
74 continue;
75 }
76 // Each cell is divided into several lines divided by newline
77 // Every line that's too long will be split into multiple lines
78 let mut cell_lines = Vec::new();
79
80 // Check if the row has as many cells as the table has columns.
81 // If that's not the case, create a new cell with empty spaces.
82 let cell = if let Some(cell) = cell_iter.next() {
83 cell
84 } else {
85 cell_lines.push(" ".repeat(info.width().into()));
86 temp_row_content.push(cell_lines);
87 continue;
88 };
89
90 // The delimiter is configurable, determine which one should be used for this cell.
91 let delimiter = delimiter(cell, info, table);
92
93 // Iterate over each line and split it into multiple lines if necessary.
94 // Newlines added by the user will be preserved.
95 for line in cell.content.iter() {
96 if measure_text_width(line) > info.content_width.into() {
97 let mut parts = split_line(line, info, delimiter);
98 cell_lines.append(&mut parts);
99 } else {
100 cell_lines.push(line.into());
101 }
102 }
103
104 // Remove all unneeded lines of this cell, if the row's height is capped to a certain
105 // amount of lines and there're too many lines in this cell.
106 // This then truncates and inserts a '...' string at the end of the last line to indicate
107 // that the cell has been truncated.
108 if let Some(lines) = row.max_height {
109 if cell_lines.len() > lines {
110 // We already have to many lines. Cut off the surplus lines.
111 let _ = cell_lines.split_off(lines);
112
113 // Directly access the last line.
114 let last_line = cell_lines
115 .get_mut(lines - 1)
116 .expect("We know it's this long.");
117
118 // Truncate any ansi codes, as the following cutoff might break ansi code
119 // otherwise anyway. This could be handled smarter, but it's simple and just works.
120 #[cfg(feature = "custom_styling")]
121 {
122 let stripped = console::strip_ansi_codes(last_line).to_string();
123 *last_line = stripped;
124 }
125
126 let max_width: usize = info.content_width.into();
127 let indicator_width = table.truncation_indicator.width();
128
129 let mut truncate_at = 0;
130 // Start the accumulated_width with the indicator_width, which is the minimum width
131 // we may show anyway.
132 let mut accumulated_width = indicator_width;
133 let mut full_string_fits = false;
134
135 // Leave these print statements in here in case we ever have to debug this annoying
136 // stuff again.
137 //println!("\nSTART:");
138 //println!("\nMax width: {max_width}, Indicator width: {indicator_width}");
139 //println!("Full line hex: {last_line}");
140 //println!(
141 // "Full line hex: {}",
142 // last_line
143 // .as_bytes()
144 // .iter()
145 // .map(|byte| format!("{byte:02x}"))
146 // .collect::<Vec<String>>()
147 // .join(", ")
148 //);
149
150 // Iterate through the UTF-8 graphemes.
151 // Check the `split_long_word` inline function docs to see why we're using
152 // graphemes.
153 // **Note:** The `index` here is the **byte** index. So we cannot just
154 // String::truncate afterwards. We have to convert to a byte vector to perform
155 // the truncation first.
156 let mut grapheme_iter = last_line.grapheme_indices(true).peekable();
157 while let Some((index, grapheme)) = grapheme_iter.next() {
158 // Leave these print statements in here in case we ever have to debug this
159 // annoying stuff again
160 //println!(
161 // "Current index: {index}, Next grapheme: {grapheme} (width: {})",
162 // grapheme.width()
163 //);
164 //println!(
165 // "Next grapheme hex: {}",
166 // grapheme
167 // .as_bytes()
168 // .iter()
169 // .map(|byte| format!("{byte:02x}"))
170 // .collect::<Vec<String>>()
171 // .join(", ")
172 //);
173
174 // Immediately save where to truncate in case this grapheme doesn't fit.
175 // The index is just before the current grapheme actually starts.
176 truncate_at = index;
177 // Check if the next grapheme would break the boundary of the allowed line
178 // length.
179 let new_width = accumulated_width + grapheme.width();
180 //println!(
181 // "Next width: {new_width}/{max_width} ({accumulated_width} + {})",
182 // grapheme.width()
183 //);
184 if new_width > max_width {
185 //println!(
186 // "Breaking: {:?}",
187 // accumulated_width + grapheme.width() > max_width
188 //);
189 break;
190 }
191
192 // The grapheme seems to fit. Save the index and check the next one.
193 accumulated_width += grapheme.width();
194
195 // This is a special case.
196 // We reached the last char, meaning that full last line + the indicator fit.
197 if grapheme_iter.peek().is_none() {
198 full_string_fits = true
199 }
200 }
201
202 // Only do any truncation logic if the line doesn't fit.
203 if !full_string_fits {
204 // Truncate the string at the byte index just behind the last valid grapheme
205 // and overwrite the last line with the new truncated string.
206 let mut last_line_bytes = last_line.clone().into_bytes();
207 last_line_bytes.truncate(truncate_at);
208 let new_last_line = String::from_utf8(last_line_bytes)
209 .expect("We cut at an exact char boundary");
210 *last_line = new_last_line;
211 }
212
213 // Push the truncation indicator.
214 last_line.push_str(&table.truncation_indicator);
215 }
216 }
217
218 // Iterate over all generated lines of this cell and align them
219 let cell_lines = cell_lines
220 .iter()
221 .map(|line| align_line(table, info, cell, line.to_string()));
222
223 temp_row_content.push(cell_lines.collect());
224 }
225
226 // Right now, we have a different structure than desired.
227 // The content is organized by `row->cell->line`.
228 // We want to remove the cell from our datastructure, since this makes the next step a lot easier.
229 // In the end it should look like this: `row->lines->column`.
230 // To achieve this, we calculate the max amount of lines for the current row.
231 // Afterwards, we iterate over each cell and convert the current structure to the desired one.
232 // This step basically transforms this:
233 // tc[0][0][0] tc[0][1][0]
234 // tc[0][0][1] tc[0][1][1]
235 // tc[0][0][2] This part of the line is missing
236 //
237 // to this:
238 // tc[0][0][0] tc[0][0][1]
239 // tc[0][1][0] tc[0][1][1]
240 // tc[0][2][0] tc[0][2][1] <- Now filled with placeholder (spaces)
241 let max_lines = temp_row_content.iter().map(Vec::len).max().unwrap_or(0);
242 let mut row_content = Vec::with_capacity(max_lines * display_infos.len());
243
244 // Each column should have `max_lines` for this row.
245 // Cells content with fewer lines will simply be topped up with empty strings.
246 for index in 0..max_lines {
247 let mut line = Vec::with_capacity(display_infos.len());
248 let mut cell_iter = temp_row_content.iter();
249
250 for info in display_infos.iter() {
251 if info.is_hidden {
252 continue;
253 }
254 let cell = cell_iter.next().unwrap();
255 match cell.get(index) {
256 // The current cell has content for this line. Append it
257 Some(content) => line.push(content.clone()),
258 // The current cell doesn't have content for this line.
259 // Fill with a placeholder (empty spaces)
260 None => line.push(" ".repeat(info.width().into())),
261 }
262 }
263 row_content.push(line);
264 }
265
266 row_content
267}
268
269/// Apply the alignment for a column. Alignment can be either Left/Right/Center.
270/// In every case all lines will be exactly the same character length `info.width - padding long`
271/// This is needed, so we can simply insert it into the border frame later on.
272/// Padding is applied in this function as well.
273#[allow(unused_variables)]
274fn align_line(table: &Table, info: &ColumnDisplayInfo, cell: &Cell, mut line: String) -> String {
275 let content_width = info.content_width;
276 let remaining: usize = usize::from(content_width).saturating_sub(measure_text_width(&line));
277
278 // Apply the styling before aligning the line, if the user requests it.
279 // That way non-delimiter whitespaces won't have stuff like underlines.
280 #[cfg(feature = "tty")]
281 if table.should_style() && table.style_text_only {
282 line = style_line(line, cell);
283 }
284
285 // Determine the alignment of the column cells.
286 // Cell settings overwrite the columns Alignment settings.
287 // Default is Left
288 let alignment = if let Some(alignment) = cell.alignment {
289 alignment
290 } else if let Some(alignment) = info.cell_alignment {
291 alignment
292 } else {
293 CellAlignment::Left
294 };
295
296 // Apply left/right/both side padding depending on the alignment of the column
297 match alignment {
298 CellAlignment::Left => {
299 line += &" ".repeat(remaining);
300 }
301 CellAlignment::Right => {
302 line = " ".repeat(remaining) + &line;
303 }
304 CellAlignment::Center => {
305 let left_padding = (remaining as f32 / 2f32).ceil() as usize;
306 let right_padding = (remaining as f32 / 2f32).floor() as usize;
307 line = " ".repeat(left_padding) + &line + &" ".repeat(right_padding);
308 }
309 }
310
311 line = pad_line(&line, info);
312
313 #[cfg(feature = "tty")]
314 if table.should_style() && !table.style_text_only {
315 return style_line(line, cell);
316 }
317
318 line
319}
320
321/// Apply the column's padding to this line
322fn pad_line(line: &str, info: &ColumnDisplayInfo) -> String {
323 let mut padded_line = String::new();
324
325 padded_line += &" ".repeat(info.padding.0.into());
326 padded_line += line;
327 padded_line += &" ".repeat(info.padding.1.into());
328
329 padded_line
330}
331
332#[cfg(feature = "tty")]
333fn style_line(line: String, cell: &Cell) -> String {
334 // Just return the line, if there's no need to style.
335 if cell.fg.is_none() && cell.bg.is_none() && cell.attributes.is_empty() {
336 return line;
337 }
338
339 let mut content = style(line);
340
341 // Apply text color
342 if let Some(color) = cell.fg {
343 content = content.with(map_color(color));
344 }
345
346 // Apply background color
347 if let Some(color) = cell.bg {
348 content = content.on(map_color(color));
349 }
350
351 for attribute in cell.attributes.iter() {
352 content = content.attribute(map_attribute(*attribute));
353 }
354
355 content.to_string()
356}