1use crate::cells::{cell_len, set_cell_size};
9use crate::console::{Justify, Overflow};
10use crate::errors::Result;
11use crate::markup;
12use crate::segment::Segment;
13use crate::style::{Style, StyleType};
14use crate::theme::Theme;
15
16pub const DEFAULT_TAB_SIZE: usize = 8;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct Span {
28 pub start: usize,
29 pub end: usize,
30 pub style: StyleType,
31}
32
33#[derive(Debug, Clone, Default)]
35pub struct Text {
36 plain: String,
37 spans: Vec<Span>,
38 style: StyleType,
40 justify: Justify,
42 overflow: Option<Overflow>,
45 no_wrap: Option<bool>,
48}
49
50impl Text {
51 fn strip_control_codes(text: &str) -> String {
56 if text
57 .bytes()
58 .any(|b| matches!(b, 0x07 | 0x08 | 0x0b | 0x0c | 0x0d))
59 {
60 text.chars()
61 .filter(|c| !matches!(c, '\u{7}' | '\u{8}' | '\u{b}' | '\u{c}' | '\r'))
62 .collect()
63 } else {
64 text.to_string()
65 }
66 }
67
68 pub fn new(plain: impl Into<String>) -> Self {
70 Text {
71 plain: Text::strip_control_codes(&plain.into()),
72 spans: Vec::new(),
73 style: StyleType::default(),
74 justify: Justify::Default,
75 overflow: None,
76 no_wrap: None,
77 }
78 }
79
80 pub fn styled(plain: impl Into<String>, style: impl Into<StyleType>) -> Self {
83 Text {
84 plain: plain.into(),
85 spans: Vec::new(),
86 style: style.into(),
87 justify: Justify::Default,
88 overflow: None,
89 no_wrap: None,
90 }
91 }
92
93 pub fn justify(mut self, justify: Justify) -> Self {
95 self.justify = justify;
96 self
97 }
98
99 pub fn set_justify(&mut self, justify: Justify) {
101 self.justify = justify;
102 }
103
104 pub fn get_justify(&self) -> Justify {
106 self.justify
107 }
108
109 pub fn overflow(mut self, overflow: Overflow) -> Self {
111 self.overflow = Some(overflow);
112 self
113 }
114
115 pub fn set_overflow(&mut self, overflow: Option<Overflow>) {
118 self.overflow = overflow;
119 }
120
121 pub fn get_overflow(&self) -> Option<Overflow> {
123 self.overflow
124 }
125
126 pub fn no_wrap(mut self, no_wrap: bool) -> Self {
128 self.no_wrap = Some(no_wrap);
129 self
130 }
131
132 pub fn set_no_wrap(&mut self, no_wrap: Option<bool>) {
135 self.no_wrap = no_wrap;
136 }
137
138 pub fn get_no_wrap(&self) -> Option<bool> {
140 self.no_wrap
141 }
142
143 pub fn truncate(&mut self, max_width: usize, overflow: Option<Overflow>, pad: bool) {
151 let overflow = overflow.or(self.overflow).unwrap_or(Overflow::Fold);
152 if overflow == Overflow::Ignore {
153 return;
154 }
155 let length = cell_len(&self.plain);
156 if length > max_width {
157 let plain = if overflow == Overflow::Ellipsis {
158 format!(
160 "{}…",
161 set_cell_size(&self.plain, max_width.saturating_sub(1))
162 )
163 } else {
164 set_cell_size(&self.plain, max_width)
165 };
166 self.set_plain(plain);
167 } else if pad {
168 let plain = set_cell_size(&self.plain, max_width);
169 self.set_plain(plain);
170 }
171 }
172
173 fn set_plain(&mut self, plain: String) {
177 let length = plain.len();
178 self.plain = plain;
179 self.spans.retain(|span| span.start < length);
180 for span in &mut self.spans {
181 span.end = span.end.min(length);
182 }
183 }
184
185 pub fn blank_copy(&self) -> Text {
188 Text {
189 plain: String::new(),
190 spans: Vec::new(),
191 style: self.style.clone(),
192 justify: self.justify,
193 overflow: self.overflow,
194 no_wrap: self.no_wrap,
195 }
196 }
197
198 pub fn divide(&self, offsets: &[usize]) -> Vec<Text> {
208 if offsets.is_empty() {
209 return vec![self.clone()];
210 }
211 let mut bounds = Vec::with_capacity(offsets.len() + 2);
212 bounds.push(0);
213 bounds.extend(offsets.iter().copied());
214 bounds.push(self.plain.len());
215
216 let mut lines: Vec<Text> = bounds
217 .windows(2)
218 .map(|w| {
219 let (start, end) = (w[0].min(self.plain.len()), w[1].min(self.plain.len()));
220 let mut line = self.blank_copy();
221 if start < end {
222 line.plain = self.plain[start..end].to_string();
223 }
224 line
225 })
226 .collect();
227
228 for span in &self.spans {
229 for (index, window) in bounds.windows(2).enumerate() {
230 let (line_start, line_end) = (window[0], window[1]);
231 let new_start = span.start.max(line_start) - line_start;
232 let new_end = span.end.min(line_end).saturating_sub(line_start);
233 if new_end > new_start {
234 lines[index].spans.push(Span {
235 start: new_start,
236 end: new_end,
237 style: span.style.clone(),
238 });
239 }
240 }
241 }
242 lines
243 }
244
245 pub fn split(&self, separator: &str, include_separator: bool, allow_blank: bool) -> Vec<Text> {
254 assert!(!separator.is_empty(), "separator must not be empty");
255 if !self.plain.contains(separator) {
256 return vec![self.clone()];
257 }
258 let matches: Vec<usize> = self
259 .plain
260 .match_indices(separator)
261 .map(|(i, _)| i)
262 .collect();
263 let mut lines = if include_separator {
264 let offsets: Vec<usize> = matches.iter().map(|s| s + separator.len()).collect();
265 self.divide(&offsets)
266 } else {
267 let mut offsets = Vec::with_capacity(matches.len() * 2);
269 for start in &matches {
270 offsets.push(*start);
271 offsets.push(start + separator.len());
272 }
273 self.divide(&offsets)
274 .into_iter()
275 .filter(|line| line.plain != separator)
276 .collect()
277 };
278 if !allow_blank && self.plain.ends_with(separator) {
279 lines.pop();
280 }
281 lines
282 }
283
284 pub fn pad(&mut self, count: usize, character: char) {
286 self.pad_left(count, character);
287 self.pad_right(count, character);
288 }
289
290 pub fn pad_left(&mut self, count: usize, character: char) {
293 if count == 0 {
294 return;
295 }
296 let padding: String = std::iter::repeat_n(character, count).collect();
297 let offset = padding.len();
298 self.plain.insert_str(0, &padding);
299 for span in &mut self.spans {
300 span.start += offset;
301 span.end += offset;
302 }
303 }
304
305 pub fn pad_right(&mut self, count: usize, character: char) {
308 if count == 0 {
309 return;
310 }
311 self.plain.extend(std::iter::repeat_n(character, count));
312 }
313
314 pub fn right_crop(&mut self, amount: usize) {
317 if amount == 0 {
318 return;
319 }
320 let max_offset = self.plain.len().saturating_sub(amount);
321 let plain = self.plain[..max_offset].to_string();
322 self.set_plain(plain);
323 }
324
325 pub fn rstrip(&mut self) {
327 let plain = self.plain.trim_end().to_string();
328 self.set_plain(plain);
329 }
330
331 pub fn rstrip_end(&mut self, size: usize) {
337 let length = self.cell_len();
338 if length <= size {
339 return;
340 }
341 let excess = length - size;
342 let whitespace = self.plain.len() - self.plain.trim_end().len();
343 if whitespace > 0 {
344 self.right_crop(whitespace.min(excess));
345 }
346 }
347
348 fn extend_style(&mut self, count: usize) {
357 if count == 0 {
358 return;
359 }
360 let length = self.plain.len();
361 self.plain.extend(std::iter::repeat_n(' ', count));
362 for span in &mut self.spans {
363 if span.end >= length {
364 span.end += count;
365 }
366 }
367 }
368
369 pub fn expand_tabs(&mut self, tab_size: usize) {
370 if !self.plain.contains('\t') || tab_size == 0 {
371 return;
372 }
373 let mut result = Text::new("");
379 for line in self.split("\n", true, false) {
380 if !line.plain.contains('\t') {
381 result = result.append_text(&line);
382 continue;
383 }
384 let mut cell_position = 0usize;
385 for mut part in line.split("\t", true, false) {
386 if part.plain.ends_with('\t') {
387 part.plain.pop();
390 part.plain.push(' ');
391 cell_position += part.cell_len();
392 let remainder = cell_position % tab_size;
393 if remainder != 0 {
394 let spaces = tab_size - remainder;
395 part.extend_style(spaces);
396 cell_position += spaces;
397 }
398 } else {
399 cell_position += part.cell_len();
400 }
401 result = result.append_text(&part);
402 }
403 }
404 self.plain = result.plain;
405 self.spans = result.spans;
406 }
407
408 pub fn join(&self, lines: &[Text]) -> Text {
411 let mut joined = self.blank_copy();
412 let last = lines.len().saturating_sub(1);
413 for (index, line) in lines.iter().enumerate() {
414 joined = joined.append_text(line);
415 if !self.plain.is_empty() && index != last {
416 joined = joined.append_text(self);
417 }
418 }
419 joined
420 }
421
422 pub fn highlight_words(
425 &mut self,
426 words: &[&str],
427 style: impl Into<StyleType>,
428 case_sensitive: bool,
429 ) -> Result<usize> {
430 let alternation = words
431 .iter()
432 .map(|word| fancy_regex::escape(word).into_owned())
433 .collect::<Vec<_>>()
434 .join("|");
435 if alternation.is_empty() {
436 return Ok(0);
437 }
438 let pattern = if case_sensitive {
439 alternation
440 } else {
441 format!("(?i){alternation}")
442 };
443 self.highlight_regex(&pattern, Some(style.into()), "")
444 }
445
446 pub fn highlight_regex(
457 &mut self,
458 pattern: &str,
459 style: Option<StyleType>,
460 style_prefix: &str,
461 ) -> Result<usize> {
462 let regex = fancy_regex::Regex::new(pattern)
463 .map_err(|e| crate::errors::RichError::Regex(format!("invalid pattern: {e}")))?;
464 Ok(self.highlight_with_regex(®ex, style, style_prefix))
465 }
466
467 pub(crate) fn highlight_with_regex(
473 &mut self,
474 regex: &fancy_regex::Regex,
475 style: Option<StyleType>,
476 style_prefix: &str,
477 ) -> usize {
478 let names: Vec<(usize, String)> = regex
480 .capture_names()
481 .enumerate()
482 .filter_map(|(index, name)| name.map(|name| (index, name.to_string())))
483 .collect();
484
485 let plain = std::mem::take(&mut self.plain);
488 let mut count = 0;
489 for captures in regex.captures_iter(&plain) {
490 let Ok(captures) = captures else { break };
491 if let (Some(style), Some(whole)) = (style.as_ref(), captures.get(0)) {
492 if whole.end() > whole.start() {
493 self.spans.push(Span {
494 start: whole.start(),
495 end: whole.end(),
496 style: style.clone(),
497 });
498 }
499 }
500 count += 1;
501 for (index, name) in &names {
502 if let Some(group) = captures.get(*index) {
503 if group.end() > group.start() {
504 self.spans.push(Span {
505 start: group.start(),
506 end: group.end(),
507 style: StyleType::Name(format!("{style_prefix}{name}")),
508 });
509 }
510 }
511 }
512 }
513 self.plain = plain;
514 count
515 }
516
517 pub fn from_markup(markup_text: &str) -> Result<Text> {
522 markup::render(markup_text)
523 }
524
525 pub fn plain(&self) -> &str {
527 &self.plain
528 }
529
530 pub fn spans(&self) -> &[Span] {
532 &self.spans
533 }
534
535 pub fn cell_len(&self) -> usize {
537 cell_len(&self.plain)
538 }
539
540 pub fn is_empty(&self) -> bool {
542 self.plain.is_empty()
543 }
544
545 pub fn append(&mut self, text: &str, style: Option<StyleType>) {
548 let start = self.plain.len();
549 self.plain.push_str(text);
550 let end = self.plain.len();
551 if let Some(style) = style {
552 self.spans.push(Span { start, end, style });
553 }
554 }
555
556 pub fn append_text(mut self, other: &Text) -> Text {
560 let offset = self.plain.len();
561 self.plain.push_str(&other.plain);
562 let end = self.plain.len();
563 if !other.style.is_null_style() {
564 self.spans.push(Span {
565 start: offset,
566 end,
567 style: other.style.clone(),
568 });
569 }
570 for span in &other.spans {
571 self.spans.push(Span {
572 start: span.start + offset,
573 end: span.end + offset,
574 style: span.style.clone(),
575 });
576 }
577 self
578 }
579
580 pub fn stylize(&mut self, style: impl Into<StyleType>, start: usize, end: usize) {
590 let end = end.min(self.plain.len());
591 if start >= end {
592 return;
593 }
594 self.spans.push(Span {
595 start,
596 end,
597 style: style.into(),
598 });
599 }
600
601 pub(crate) fn push_span(&mut self, span: Span) {
603 self.spans.push(span);
604 }
605
606 pub fn set_base_style(&mut self, style: impl Into<StyleType>) {
608 self.style = style.into();
609 }
610
611 pub fn render(&self, theme: &Theme, base_style: &Style) -> Vec<Segment> {
617 self.render_joined(theme, base_style, None)
618 }
619
620 pub fn measurement(&self) -> (usize, usize) {
623 let expanded;
629 let plain = if self.plain.contains('\t') {
630 let mut text = self.clone();
631 text.expand_tabs(DEFAULT_TAB_SIZE);
632 expanded = text.plain;
633 &expanded
634 } else {
635 &self.plain
636 };
637 let max_line = plain.split('\n').map(cell_len).max().unwrap_or(0);
638 let min_word = plain
639 .split_whitespace()
640 .map(cell_len)
641 .max()
642 .unwrap_or(max_line);
643 (min_word, max_line)
644 }
645
646 pub fn render_lines(
649 &self,
650 theme: &Theme,
651 base_style: &Style,
652 width: Option<usize>,
653 ) -> Vec<Vec<Segment>> {
654 self.render_lines_justified(theme, base_style, width, self.justify)
655 }
656
657 pub fn render_lines_justified(
660 &self,
661 theme: &Theme,
662 base_style: &Style,
663 width: Option<usize>,
664 justify: Justify,
665 ) -> Vec<Vec<Segment>> {
666 self.render_lines_wrapped(
667 theme,
668 base_style,
669 width,
670 justify,
671 self.overflow.unwrap_or(Overflow::Fold),
672 self.no_wrap.unwrap_or(false),
673 )
674 }
675
676 pub fn render_lines_wrapped(
684 &self,
685 theme: &Theme,
686 base_style: &Style,
687 width: Option<usize>,
688 justify: Justify,
689 overflow: Overflow,
690 no_wrap: bool,
691 ) -> Vec<Vec<Segment>> {
692 if self.plain.contains('\t') {
697 let mut expanded = self.clone();
698 expanded.expand_tabs(DEFAULT_TAB_SIZE);
699 return expanded
700 .render_lines_wrapped(theme, base_style, width, justify, overflow, no_wrap);
701 }
702
703 let resolved: Vec<Style> = self
707 .spans
708 .iter()
709 .map(|span| theme.get_style_or_null(&span.style))
710 .collect();
711 let effective_base = base_style.combine(&theme.get_style_or_null(&self.style));
712 let no_wrap = no_wrap || overflow == Overflow::Ignore;
714 let mut lines: Vec<Vec<Segment>> = Vec::new();
715 for (start, end) in self.wrapped_ranges(width, overflow, no_wrap) {
716 lines.push(self.line_segments(&resolved, start, end, &effective_base));
717 }
718 let Some(width) = width else {
719 return lines;
720 };
721 if justify != Justify::Default {
722 let last = lines.len().saturating_sub(1);
723 for (index, line) in lines.iter_mut().enumerate() {
724 *line = justify_line(line, width, justify, &effective_base, index == last);
727 }
728 }
729 if overflow != Overflow::Ignore {
730 for line in &mut lines {
731 *line = truncate_line(line, width, overflow);
732 }
733 }
734 lines
735 }
736
737 pub fn render_joined_wrapped(
740 &self,
741 theme: &Theme,
742 base_style: &Style,
743 width: usize,
744 justify: Justify,
745 overflow: Overflow,
746 no_wrap: bool,
747 ) -> Vec<Segment> {
748 let lines =
749 self.render_lines_wrapped(theme, base_style, Some(width), justify, overflow, no_wrap);
750 let mut segments = Vec::new();
751 let last = lines.len().saturating_sub(1);
752 for (index, line) in lines.into_iter().enumerate() {
753 segments.extend(line);
754 if index != last {
755 segments.push(Segment::line());
756 }
757 }
758 segments
759 }
760
761 fn render_joined(
764 &self,
765 theme: &Theme,
766 base_style: &Style,
767 width: Option<usize>,
768 ) -> Vec<Segment> {
769 let lines = self.render_lines(theme, base_style, width);
770 let mut segments = Vec::new();
771 let last = lines.len().saturating_sub(1);
772 for (index, line) in lines.into_iter().enumerate() {
773 segments.extend(line);
774 if index != last {
775 segments.push(Segment::line());
776 }
777 }
778 segments
779 }
780
781 fn wrapped_ranges(
784 &self,
785 width: Option<usize>,
786 overflow: Overflow,
787 no_wrap: bool,
788 ) -> Vec<(usize, usize)> {
789 let mut hard: Vec<(usize, usize)> = Vec::new();
790 let mut start = 0;
791 for (i, byte) in self.plain.bytes().enumerate() {
792 if byte == b'\n' {
793 hard.push((start, i));
794 start = i + 1;
795 }
796 }
797 hard.push((start, self.plain.len()));
798
799 let Some(width) = width else {
800 return hard;
801 };
802 if no_wrap {
803 return hard;
804 }
805
806 let mut ranges: Vec<(usize, usize)> = Vec::new();
807 for (a, b) in hard {
808 let sub = &self.plain[a..b];
809 let breaks = crate::wrap::divide_line(sub, width, overflow == Overflow::Fold);
812 let mut cuts = vec![a];
813 for char_offset in breaks {
814 cuts.push(a + char_to_byte(sub, char_offset));
815 }
816 cuts.push(b);
817 for window in cuts.windows(2) {
818 ranges.push((window[0], window[1]));
819 }
820 }
821 ranges
822 }
823
824 fn line_segments(
834 &self,
835 resolved: &[Style],
836 start: usize,
837 end: usize,
838 effective_base: &Style,
839 ) -> Vec<Segment> {
840 if start >= end {
841 return Vec::new();
842 }
843 let mut points: Vec<usize> = vec![start, end];
844 for span in &self.spans {
845 let span_start = span.start.clamp(start, end);
846 let span_end = span.end.clamp(start, end);
847 points.push(span_start);
848 points.push(span_end);
849 }
850 points.sort_unstable();
851 points.dedup();
852
853 let mut segments = Vec::new();
854 for window in points.windows(2) {
855 let (a, b) = (window[0], window[1]);
856 if a >= b {
857 continue;
858 }
859 let slice = &self.plain[a..b];
860 if slice.is_empty() {
861 continue;
862 }
863 let mut style = effective_base.clone();
864 for (span, span_style) in self.spans.iter().zip(resolved) {
865 if span.start <= a && span.end >= b {
866 style = style.combine(span_style);
867 }
868 }
869 segments.push(Segment::new(slice, Some(style)));
870 }
871 segments
872 }
873}
874
875fn char_to_byte(text: &str, char_idx: usize) -> usize {
877 text.char_indices()
878 .nth(char_idx)
879 .map(|(byte, _)| byte)
880 .unwrap_or(text.len())
881}
882
883fn truncate_line(line: &[Segment], width: usize, overflow: Overflow) -> Vec<Segment> {
896 if overflow == Overflow::Ignore {
897 return line.to_vec();
898 }
899 let total: usize = line.iter().map(Segment::cell_length).sum();
900 if total <= width {
901 return line.to_vec();
902 }
903 let ellipsis = overflow == Overflow::Ellipsis;
904 let keep = if ellipsis {
906 width.saturating_sub(1)
907 } else {
908 width
909 };
910
911 let mut result: Vec<Segment> = Vec::new();
912 let mut used = 0usize;
913 let mut cut_style: Option<Style> = None;
916 for segment in line {
917 let length = segment.cell_length();
918 if used + length <= keep {
919 result.push(segment.clone());
920 used += length;
921 continue;
922 }
923 cut_style = segment.style.clone();
924 if used < keep {
925 result.push(Segment::new(
929 set_cell_size(&segment.text, keep - used),
930 segment.style.clone(),
931 ));
932 }
933 break;
934 }
935 if ellipsis {
936 match result.last_mut() {
940 Some(last) if !last.control && last.style == cut_style => last.text.push('…'),
941 _ => result.push(Segment::new("…", cut_style)),
942 }
943 }
944 result
945}
946
947fn split_words(line: &[Segment]) -> Vec<Vec<Segment>> {
951 let mut words: Vec<Vec<Segment>> = Vec::new();
952 let mut current: Vec<Segment> = Vec::new();
953 for segment in line {
954 for (index, piece) in segment.text.split(' ').enumerate() {
956 if index > 0 {
957 words.push(std::mem::take(&mut current));
958 }
959 if !piece.is_empty() {
960 current.push(Segment::new(piece, segment.style.clone()));
961 }
962 }
963 }
964 words.push(current);
965 if words.last().is_some_and(|w| w.is_empty()) {
969 words.pop();
970 }
971 words
972}
973
974fn full_justify(line: &[Segment], width: usize, style: &Style) -> Vec<Segment> {
979 let words = split_words(line);
980 let words_size: usize = words
981 .iter()
982 .map(|word| word.iter().map(Segment::cell_length).sum::<usize>())
983 .sum();
984 let mut num_spaces = words.len().saturating_sub(1);
985 let mut spaces = vec![1usize; num_spaces];
986 if !spaces.is_empty() {
987 let mut index = 0;
988 while words_size + num_spaces < width {
989 let slot = spaces.len() - index - 1;
990 spaces[slot] += 1;
991 num_spaces += 1;
992 index = (index + 1) % spaces.len();
993 }
994 }
995
996 let mut out: Vec<Segment> = Vec::new();
997 for (index, word) in words.iter().enumerate() {
998 out.extend(word.iter().cloned());
999 if let Some(&gap) = spaces.get(index) {
1000 let before = word.last().and_then(|s| s.style.clone());
1003 let after = words
1004 .get(index + 1)
1005 .and_then(|w| w.first())
1006 .and_then(|s| s.style.clone());
1007 let gap_style = if before == after {
1008 before.unwrap_or_else(|| style.clone())
1009 } else {
1010 style.clone()
1011 };
1012 out.push(Segment::new(" ".repeat(gap), Some(gap_style)));
1013 }
1014 }
1015 out
1016}
1017
1018fn justify_line(
1024 line: &[Segment],
1025 width: usize,
1026 justify: Justify,
1027 style: &Style,
1028 is_last: bool,
1029) -> Vec<Segment> {
1030 if justify == Justify::Full {
1032 return if is_last {
1035 line.to_vec()
1036 } else {
1037 full_justify(line, width, style)
1038 };
1039 }
1040 let line_width: usize = line.iter().map(Segment::cell_length).sum();
1041 let excess = width.saturating_sub(line_width);
1042 let (left, right) = match justify {
1043 Justify::Right => (excess, 0),
1044 Justify::Center => (excess / 2, excess - excess / 2),
1045 Justify::Left | Justify::Full | Justify::Default => (0, excess),
1047 };
1048 let mut out = Vec::with_capacity(line.len() + 2);
1049 if left > 0 {
1050 out.push(Segment::new(" ".repeat(left), Some(style.clone())));
1051 }
1052 out.extend(line.iter().cloned());
1053 if right > 0 {
1054 out.push(Segment::new(" ".repeat(right), Some(style.clone())));
1055 }
1056 out
1057}
1058
1059#[cfg(test)]
1060mod tests {
1061 use super::*;
1062
1063 #[test]
1081 fn full_justify_matches_upstream() {
1082 let text = Text::new("aaa bbb ccc ddddddddddddddddddd ee ff").justify(Justify::Full);
1083 let plain: Vec<String> = text
1084 .render_lines(&Theme::default_theme(), &Style::new(), Some(20))
1085 .iter()
1086 .map(|line| line.iter().map(|s| s.text.as_str()).collect())
1087 .collect();
1088 assert_eq!(
1089 plain,
1090 vec!["aaa bbb ccc", "ddddddddddddddddddd", "ee ff"]
1091 );
1092 assert_eq!(plain[0].chars().count(), 20);
1093 }
1094
1095 #[test]
1096 fn append_creates_spans() {
1097 let mut text = Text::new("");
1098 text.append("hello", Some(Style::parse("bold").unwrap().into()));
1099 text.append(" world", None);
1100 assert_eq!(text.plain(), "hello world");
1101 assert_eq!(text.spans().len(), 1);
1102 }
1103
1104 #[test]
1105 fn render_flattens_overlapping_spans() {
1106 let mut text = Text::new("abcdef");
1107 text.stylize(Style::parse("bold").unwrap(), 0, 4);
1108 text.stylize(Style::parse("red").unwrap(), 2, 6);
1109 let segments = text.render(&Theme::default_theme(), &Style::new());
1110 let rendered: Vec<_> = segments.iter().map(|s| s.text.clone()).collect();
1112 assert_eq!(rendered, vec!["ab", "cd", "ef"]);
1113 }
1114
1115 #[test]
1119 fn truncate_matches_upstream() {
1120 for (overflow, expected) in [
1121 (Overflow::Fold, "hello"),
1122 (Overflow::Crop, "hello"),
1123 (Overflow::Ellipsis, "hell…"),
1124 (Overflow::Ignore, "hello world"),
1125 ] {
1126 let mut text = Text::new("hello world");
1127 text.truncate(5, Some(overflow), false);
1128 assert_eq!(text.plain(), expected, "overflow {overflow:?}");
1129 }
1130 }
1131
1132 #[test]
1135 fn truncate_pads_only_when_short() {
1136 let mut short = Text::new("hi");
1137 short.truncate(6, Some(Overflow::Crop), true);
1138 assert_eq!(short.plain(), "hi ");
1139
1140 let mut exact = Text::new("hi");
1141 exact.truncate(2, Some(Overflow::Crop), true);
1142 assert_eq!(exact.plain(), "hi");
1143 }
1144
1145 #[test]
1147 fn truncate_trims_dangling_spans() {
1148 let mut text = Text::new("hello world");
1149 text.stylize(Style::parse("bold").unwrap(), 6, 11);
1150 text.stylize(Style::parse("red").unwrap(), 0, 5);
1151 text.truncate(3, Some(Overflow::Crop), false);
1152 assert_eq!(text.plain(), "hel");
1153 assert_eq!(text.spans().len(), 1);
1156 assert!(text.spans().iter().all(|s| s.end <= text.plain().len()));
1157 }
1158
1159 use crate::protocol::Renderable;
1160
1161 #[test]
1165 fn text_overflow_beats_console_options() {
1166 let console = crate::Console::builder().width(8).build();
1167 let mut options = console.options();
1168 options.overflow = Some(Overflow::Ellipsis);
1169 options.no_wrap = Some(true);
1170
1171 let from_options = Text::new("the quick brown fox");
1173 assert_eq!(
1174 plain_of(&from_options.rich_render(&console, &options)),
1175 "the qui…"
1176 );
1177
1178 let from_text = Text::new("the quick brown fox").overflow(Overflow::Crop);
1180 assert_eq!(
1181 plain_of(&from_text.rich_render(&console, &options)),
1182 "the quic"
1183 );
1184 }
1185
1186 #[test]
1188 fn overflow_defaults_to_fold() {
1189 let console = crate::Console::builder().width(8).build();
1190 let text = Text::new("supercalifragilistic");
1191 let rendered = plain_of(&text.rich_render(&console, &console.options()));
1192 assert_eq!(rendered, "supercal\nifragili\nstic");
1193 }
1194
1195 fn plain_of(segments: &[Segment]) -> String {
1198 segments
1199 .iter()
1200 .filter(|s| !s.control)
1201 .map(|s| s.text.as_str())
1202 .collect()
1203 }
1204}