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