1//! This module contains [`Truncate`] structure, used to decrease width of a [`Table`]s or a cell on a [`Table`] by truncating the width.
2//!
3//! [`Table`]: crate::Table
4
5use std::{borrow::Cow, iter, marker::PhantomData};
6
7use crate::{
8 grid::{
9 config::{ColoredConfig, SpannedConfig},
10 dimension::CompleteDimensionVecRecords,
11 records::{EmptyRecords, ExactRecords, PeekableRecords, Records, RecordsMut},
12 util::string::{string_width, string_width_multiline},
13 },
14 settings::{
15 measurement::Measurement,
16 peaker::{Peaker, PriorityNone},
17 CellOption, TableOption, Width,
18 },
19};
20
21use super::util::{cut_str, get_table_widths, get_table_widths_with_total};
22
23/// Truncate cut the string to a given width if its length exceeds it.
24/// Otherwise keeps the content of a cell untouched.
25///
26/// The function is color aware if a `color` feature is on.
27///
28/// Be aware that it doesn't consider padding.
29/// So if you want to set a exact width you might need to use [`Padding`] to set it to 0.
30///
31/// ## Example
32///
33/// ```
34/// use tabled::{Table, settings::{object::Segment, Width, Modify}};
35///
36/// let table = Table::new(&["Hello World!"])
37/// .with(Modify::new(Segment::all()).with(Width::truncate(3)));
38/// ```
39///
40/// [`Padding`]: crate::settings::Padding
41#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
42pub struct Truncate<'a, W = usize, P = PriorityNone> {
43 width: W,
44 suffix: Option<TruncateSuffix<'a>>,
45 multiline: bool,
46 _priority: PhantomData<P>,
47}
48#[cfg(feature = "color")]
49#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
50struct TruncateSuffix<'a> {
51 text: Cow<'a, str>,
52 limit: SuffixLimit,
53 try_color: bool,
54}
55
56#[cfg(not(feature = "color"))]
57#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
58struct TruncateSuffix<'a> {
59 text: Cow<'a, str>,
60 limit: SuffixLimit,
61}
62
63impl Default for TruncateSuffix<'_> {
64 fn default() -> Self {
65 Self {
66 text: Cow::default(),
67 limit: SuffixLimit::Cut,
68 #[cfg(feature = "color")]
69 try_color: false,
70 }
71 }
72}
73
74/// A suffix limit settings.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
76pub enum SuffixLimit {
77 /// Cut the suffix.
78 Cut,
79 /// Don't show the suffix.
80 Ignore,
81 /// Use a string with n chars instead.
82 Replace(char),
83}
84
85impl<W> Truncate<'static, W>
86where
87 W: Measurement<Width>,
88{
89 /// Creates a [`Truncate`] object
90 pub fn new(width: W) -> Truncate<'static, W> {
91 Self {
92 width,
93 multiline: false,
94 suffix: None,
95 _priority: PhantomData,
96 }
97 }
98}
99
100impl<'a, W, P> Truncate<'a, W, P> {
101 /// Sets a suffix which will be appended to a resultant string.
102 ///
103 /// The suffix is used in 3 circumstances:
104 /// 1. If original string is *bigger* than the suffix.
105 /// We cut more of the original string and append the suffix.
106 /// 2. If suffix is bigger than the original string.
107 /// We cut the suffix to fit in the width by default.
108 /// But you can peak the behaviour by using [`Truncate::suffix_limit`]
109 pub fn suffix<S: Into<Cow<'a, str>>>(self, suffix: S) -> Truncate<'a, W, P> {
110 let mut suff = self.suffix.unwrap_or_default();
111 suff.text = suffix.into();
112
113 Truncate {
114 width: self.width,
115 multiline: self.multiline,
116 suffix: Some(suff),
117 _priority: PhantomData,
118 }
119 }
120
121 /// Sets a suffix limit, which is used when the suffix is too big to be used.
122 pub fn suffix_limit(self, limit: SuffixLimit) -> Truncate<'a, W, P> {
123 let mut suff = self.suffix.unwrap_or_default();
124 suff.limit = limit;
125
126 Truncate {
127 width: self.width,
128 multiline: self.multiline,
129 suffix: Some(suff),
130 _priority: PhantomData,
131 }
132 }
133
134 /// Use trancate logic per line, not as a string as a whole.
135 pub fn multiline(self) -> Truncate<'a, W, P> {
136 Truncate {
137 width: self.width,
138 multiline: true,
139 suffix: self.suffix,
140 _priority: self._priority,
141 }
142 }
143
144 #[cfg(feature = "color")]
145 /// Sets a optional logic to try to colorize a suffix.
146 pub fn suffix_try_color(self, color: bool) -> Truncate<'a, W, P> {
147 let mut suff = self.suffix.unwrap_or_default();
148 suff.try_color = color;
149
150 Truncate {
151 width: self.width,
152 multiline: self.multiline,
153 suffix: Some(suff),
154 _priority: PhantomData,
155 }
156 }
157}
158
159impl<'a, W, P> Truncate<'a, W, P> {
160 /// Priority defines the logic by which a truncate will be applied when is done for the whole table.
161 ///
162 /// - [`PriorityNone`] which cuts the columns one after another.
163 /// - [`PriorityMax`] cuts the biggest columns first.
164 /// - [`PriorityMin`] cuts the lowest columns first.
165 ///
166 /// [`PriorityMax`]: crate::settings::peaker::PriorityMax
167 /// [`PriorityMin`]: crate::settings::peaker::PriorityMin
168 pub fn priority<PP: Peaker>(self) -> Truncate<'a, W, PP> {
169 Truncate {
170 width: self.width,
171 multiline: self.multiline,
172 suffix: self.suffix,
173 _priority: PhantomData,
174 }
175 }
176}
177
178impl Truncate<'_, (), ()> {
179 /// Truncate a given string
180 pub fn truncate_text(text: &str, width: usize) -> Cow<'_, str> {
181 truncate_text(text, width, suffix:"", _suffix_color:false)
182 }
183}
184
185impl<W, P, R> CellOption<R, ColoredConfig> for Truncate<'_, W, P>
186where
187 W: Measurement<Width>,
188 R: Records + ExactRecords + PeekableRecords + RecordsMut<String>,
189 for<'a> &'a R: Records,
190{
191 fn change(self, records: &mut R, cfg: &mut ColoredConfig, entity: papergrid::config::Entity) {
192 let available = self.width.measure(&*records, cfg);
193
194 let mut width = available;
195 let mut suffix = Cow::Borrowed("");
196
197 if let Some(x) = self.suffix.as_ref() {
198 let (cutted_suffix, rest_width) = make_suffix(x, width);
199 suffix = cutted_suffix;
200 width = rest_width;
201 };
202
203 let count_rows = records.count_rows();
204 let count_columns = records.count_columns();
205
206 let colorize = need_suffix_color_preservation(&self.suffix);
207
208 for pos in entity.iter(count_rows, count_columns) {
209 let is_valid_pos = pos.0 < count_rows && pos.1 < count_columns;
210 if !is_valid_pos {
211 continue;
212 }
213
214 let text = records.get_text(pos);
215
216 let cell_width = string_width_multiline(text);
217 if available >= cell_width {
218 continue;
219 }
220
221 let text =
222 truncate_multiline(text, &suffix, width, available, colorize, self.multiline);
223
224 records.set(pos, text.into_owned());
225 }
226 }
227}
228
229fn truncate_multiline<'a>(
230 text: &'a str,
231 suffix: &'a str,
232 width: usize,
233 twidth: usize,
234 suffix_color: bool,
235 multiline: bool,
236) -> Cow<'a, str> {
237 if multiline {
238 let mut buf: String = String::new();
239 for (i: usize, line: Cow<'_, str>) in crate::grid::util::string::get_lines(text).enumerate() {
240 if i != 0 {
241 buf.push(ch:'\n');
242 }
243
244 let line: Cow<'_, str> = make_text_truncated(&line, suffix, width, twidth, suffix_color);
245 buf.push_str(&line);
246 }
247
248 Cow::Owned(buf)
249 } else {
250 make_text_truncated(text, suffix, width, twidth, suffix_color)
251 }
252}
253
254fn make_text_truncated<'a>(
255 text: &'a str,
256 suffix: &'a str,
257 width: usize,
258 twidth: usize,
259 suffix_color: bool,
260) -> Cow<'a, str> {
261 if width == 0 {
262 if twidth == 0 {
263 Cow::Borrowed("")
264 } else {
265 Cow::Borrowed(suffix)
266 }
267 } else {
268 truncate_text(text, width, suffix, suffix_color)
269 }
270}
271
272fn need_suffix_color_preservation(_suffix: &Option<TruncateSuffix<'_>>) -> bool {
273 #[cfg(not(feature = "color"))]
274 {
275 false
276 }
277 #[cfg(feature = "color")]
278 {
279 _suffix.as_ref().map_or(false, |s| s.try_color)
280 }
281}
282
283fn make_suffix<'a>(suffix: &'a TruncateSuffix<'_>, width: usize) -> (Cow<'a, str>, usize) {
284 let suffix_length: usize = string_width(&suffix.text);
285 if width > suffix_length {
286 return (Cow::Borrowed(suffix.text.as_ref()), width - suffix_length);
287 }
288
289 match suffix.limit {
290 SuffixLimit::Ignore => (Cow::Borrowed(""), width),
291 SuffixLimit::Cut => {
292 let suffix: Cow<'_, str> = cut_str(&suffix.text, width);
293 (suffix, 0)
294 }
295 SuffixLimit::Replace(c: char) => {
296 let suffix: Cow<'_, str> = Cow::Owned(iter::repeat(elt:c).take(width).collect());
297 (suffix, 0)
298 }
299 }
300}
301
302impl<W, P, R> TableOption<R, CompleteDimensionVecRecords<'static>, ColoredConfig>
303 for Truncate<'_, W, P>
304where
305 W: Measurement<Width>,
306 P: Peaker,
307 R: Records + ExactRecords + PeekableRecords + RecordsMut<String>,
308 for<'a> &'a R: Records,
309{
310 fn change(
311 self,
312 records: &mut R,
313 cfg: &mut ColoredConfig,
314 dims: &mut CompleteDimensionVecRecords<'static>,
315 ) {
316 if records.count_rows() == 0 || records.count_columns() == 0 {
317 return;
318 }
319
320 let width = self.width.measure(&*records, cfg);
321 let (widths, total) = get_table_widths_with_total(&*records, cfg);
322 if total <= width {
323 return;
324 }
325
326 let suffix = self.suffix.as_ref().map(|s| TruncateSuffix {
327 text: Cow::Borrowed(&s.text),
328 limit: s.limit,
329 #[cfg(feature = "color")]
330 try_color: s.try_color,
331 });
332
333 let priority = P::create();
334 let multiline = self.multiline;
335 let widths = truncate_total_width(
336 records, cfg, widths, total, width, priority, suffix, multiline,
337 );
338
339 let _ = dims.set_widths(widths);
340 }
341}
342
343#[allow(clippy::too_many_arguments)]
344fn truncate_total_width<P, R>(
345 records: &mut R,
346 cfg: &mut ColoredConfig,
347 mut widths: Vec<usize>,
348 total: usize,
349 width: usize,
350 priority: P,
351 suffix: Option<TruncateSuffix<'_>>,
352 multiline: bool,
353) -> Vec<usize>
354where
355 for<'a> &'a R: Records,
356 P: Peaker,
357 R: Records + PeekableRecords + ExactRecords + RecordsMut<String>,
358{
359 let count_rows: usize = records.count_rows();
360 let count_columns: usize = records.count_columns();
361
362 let min_widths: Vec = get_table_widths(records:EmptyRecords::new(count_rows, cols:count_columns), cfg);
363
364 decrease_widths(&mut widths, &min_widths, total_width:total, width, peeaker:priority);
365
366 let points: Vec<((usize, usize), usize)> = get_decrease_cell_list(cfg, &widths, &min_widths, (count_rows, count_columns));
367
368 for ((row: usize, col: usize), width) in points {
369 let mut truncate: Truncate<'_, {unknown}> = Truncate::new(width);
370 truncate.suffix = suffix.clone();
371 truncate.multiline = multiline;
372 CellOption::change(self:truncate, records, cfg, (row, col).into());
373 }
374
375 widths
376}
377
378fn truncate_text<'a>(
379 text: &'a str,
380 width: usize,
381 suffix: &str,
382 _suffix_color: bool,
383) -> Cow<'a, str> {
384 let content = cut_str(text, width);
385 if suffix.is_empty() {
386 return content;
387 }
388
389 #[cfg(feature = "color")]
390 {
391 if _suffix_color {
392 if let Some(block) = ansi_str::get_blocks(text).last() {
393 if block.has_ansi() {
394 let style = block.style();
395 Cow::Owned(format!(
396 "{}{}{}{}",
397 content,
398 style.start(),
399 suffix,
400 style.end()
401 ))
402 } else {
403 let mut content = content.into_owned();
404 content.push_str(suffix);
405 Cow::Owned(content)
406 }
407 } else {
408 let mut content = content.into_owned();
409 content.push_str(suffix);
410 Cow::Owned(content)
411 }
412 } else {
413 let mut content = content.into_owned();
414 content.push_str(suffix);
415 Cow::Owned(content)
416 }
417 }
418
419 #[cfg(not(feature = "color"))]
420 {
421 let mut content = content.into_owned();
422 content.push_str(suffix);
423 Cow::Owned(content)
424 }
425}
426
427fn get_decrease_cell_list(
428 cfg: &SpannedConfig,
429 widths: &[usize],
430 min_widths: &[usize],
431 shape: (usize, usize),
432) -> Vec<((usize, usize), usize)> {
433 let mut points = Vec::new();
434 (0..shape.1).for_each(|col| {
435 (0..shape.0)
436 .filter(|&row| cfg.is_cell_visible((row, col)))
437 .for_each(|row| {
438 let (width, width_min) = match cfg.get_column_span((row, col)) {
439 Some(span) => {
440 let width = (col..col + span).map(|i| widths[i]).sum::<usize>();
441 let min_width = (col..col + span).map(|i| min_widths[i]).sum::<usize>();
442 let count_borders = count_borders(cfg, col, col + span, shape.1);
443 (width + count_borders, min_width + count_borders)
444 }
445 None => (widths[col], min_widths[col]),
446 };
447
448 if width >= width_min {
449 let padding = cfg.get_padding((row, col).into());
450 let width = width.saturating_sub(padding.left.size + padding.right.size);
451
452 points.push(((row, col), width));
453 }
454 });
455 });
456
457 points
458}
459
460fn decrease_widths<F>(
461 widths: &mut [usize],
462 min_widths: &[usize],
463 total_width: usize,
464 mut width: usize,
465 mut peeaker: F,
466) where
467 F: Peaker,
468{
469 let mut empty_list = 0;
470 for col in 0..widths.len() {
471 if widths[col] == 0 || widths[col] <= min_widths[col] {
472 empty_list += 1;
473 }
474 }
475
476 while width != total_width {
477 if empty_list == widths.len() {
478 break;
479 }
480
481 let col = match peeaker.peak(min_widths, widths) {
482 Some(col) => col,
483 None => break,
484 };
485
486 if widths[col] == 0 || widths[col] <= min_widths[col] {
487 continue;
488 }
489
490 widths[col] -= 1;
491
492 if widths[col] == 0 || widths[col] <= min_widths[col] {
493 empty_list += 1;
494 }
495
496 width += 1;
497 }
498}
499
500fn count_borders(cfg: &SpannedConfig, start: usize, end: usize, count_columns: usize) -> usize {
501 (start..end)
502 .skip(1)
503 .filter(|&i: usize| cfg.has_vertical(col:i, count_columns))
504 .count()
505}
506