1use std::cmp::Ordering;
7use std::collections::BTreeMap;
8use std::sync::Arc;
9
10use regex::Regex;
11
12use crate::Renderable;
13use crate::cells::cell_len;
14use crate::console::{Console, ConsoleOptions, JustifyMethod};
15use crate::control::strip_control_codes;
16use crate::error::Result;
17use crate::markup;
18use crate::measure::Measurement;
19use crate::segment::{Segment, Segments};
20use crate::style::{Style, StyleMeta};
21
22#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct Span {
27 pub start: usize,
29 pub end: usize,
31 pub style: Style,
33 pub meta: Option<StyleMeta>,
35}
36
37impl Span {
38 pub fn new(start: usize, end: usize, style: Style) -> Self {
40 Span {
41 start,
42 end,
43 style,
44 meta: None,
45 }
46 }
47
48 pub fn new_with_meta(start: usize, end: usize, style: Style, meta: Option<StyleMeta>) -> Self {
50 Span {
51 start,
52 end,
53 style,
54 meta: meta.and_then(|m| if m.is_empty() { None } else { Some(m) }),
55 }
56 }
57
58 pub fn is_empty(&self) -> bool {
60 self.end <= self.start
61 }
62
63 pub fn split(&self, offset: usize) -> (Span, Option<Span>) {
91 if offset < self.start {
92 return (self.clone(), None);
93 }
94 if offset >= self.end {
95 return (self.clone(), None);
96 }
97
98 let span1 = Span::new_with_meta(
99 self.start,
100 offset.min(self.end),
101 self.style,
102 self.meta.clone(),
103 );
104 let span2 = Span::new_with_meta(span1.end, self.end, self.style, self.meta.clone());
105 (span1, Some(span2))
106 }
107
108 pub fn move_by(&self, offset: isize) -> Span {
120 let new_start = (self.start as isize + offset).max(0) as usize;
121 let new_end = (self.end as isize + offset).max(0) as usize;
122 Span::new_with_meta(new_start, new_end, self.style, self.meta.clone())
123 }
124
125 pub fn right_crop(&self, offset: usize) -> Span {
138 if offset >= self.end {
139 return self.clone();
140 }
141 Span::new_with_meta(
142 self.start,
143 offset.min(self.end),
144 self.style,
145 self.meta.clone(),
146 )
147 }
148
149 pub fn extend(&self, cells: usize) -> Span {
159 if cells == 0 {
160 return self.clone();
161 }
162 Span::new_with_meta(self.start, self.end + cells, self.style, self.meta.clone())
163 }
164}
165
166#[derive(Debug, Clone)]
170pub enum TextPart {
171 Plain(String),
173 Styled(String, Style),
175 Text(Text),
177}
178
179impl From<&str> for TextPart {
180 fn from(s: &str) -> Self {
181 TextPart::Plain(s.to_string())
182 }
183}
184
185impl From<String> for TextPart {
186 fn from(s: String) -> Self {
187 TextPart::Plain(s)
188 }
189}
190
191impl From<Text> for TextPart {
192 fn from(t: Text) -> Self {
193 TextPart::Text(t)
194 }
195}
196
197impl From<(&str, Style)> for TextPart {
198 fn from((s, style): (&str, Style)) -> Self {
199 TextPart::Styled(s.to_string(), style)
200 }
201}
202
203impl From<(String, Style)> for TextPart {
204 fn from((s, style): (String, Style)) -> Self {
205 TextPart::Styled(s, style)
206 }
207}
208
209#[derive(Debug, Clone, Default)]
225pub struct Text {
226 text: String,
228 spans: Vec<Span>,
230 style: Option<Style>,
232 meta: Option<StyleMeta>,
234}
235
236impl Text {
237 pub fn new() -> Self {
239 Text::default()
240 }
241
242 pub fn plain(text: impl Into<String>) -> Self {
244 Text {
245 text: text.into(),
246 spans: Vec::new(),
247 style: None,
248 meta: None,
249 }
250 }
251
252 pub fn styled(text: impl Into<String>, style: Style) -> Self {
259 let text = text.into();
260 Text {
261 text,
262 spans: Vec::new(),
263 style: Some(style),
264 meta: None,
265 }
266 }
267
268 pub fn styled_with_meta(text: impl Into<String>, style: Style, meta: StyleMeta) -> Self {
270 let text = text.into();
271 Text {
272 text,
273 spans: Vec::new(),
274 style: Some(style),
275 meta: if meta.is_empty() { None } else { Some(meta) },
276 }
277 }
278
279 pub fn from_markup(markup: &str, emoji: bool) -> Result<Text> {
301 crate::markup::render(markup, emoji)
302 }
303
304 pub fn from_ansi(ansi_text: &str) -> Text {
311 let mut decoder = crate::ansi::AnsiDecoder::new();
312 let joiner = Text::styled("\n", Style::new());
316 joiner.join(decoder.decode(ansi_text))
317 }
318
319 pub fn assemble<I, P>(parts: I) -> Self
340 where
341 I: IntoIterator<Item = P>,
342 P: Into<TextPart>,
343 {
344 let mut result = Text::new();
345
346 for part in parts {
347 match part.into() {
348 TextPart::Plain(s) => {
349 result.append(&s, None);
350 }
351 TextPart::Styled(s, style) => {
352 result.append(&s, Some(style));
353 }
354 TextPart::Text(t) => {
355 result.append_text(&t);
356 }
357 }
358 }
359
360 result
361 }
362
363 pub fn plain_text(&self) -> &str {
365 &self.text
366 }
367
368 pub fn cell_len(&self) -> usize {
370 cell_len(&self.text)
371 }
372
373 pub fn len(&self) -> usize {
375 self.text.chars().count()
376 }
377
378 pub fn is_empty(&self) -> bool {
380 self.text.is_empty()
381 }
382
383 pub fn append(&mut self, text: impl Into<String>, style: Option<Style>) {
385 let text = text.into();
386 let start = self.len();
387 let end = start + text.chars().count();
388
389 self.text.push_str(&text);
390
391 if let Some(s) = style {
392 self.spans.push(Span::new(start, end, s));
393 }
394 }
395
396 pub fn append_text(&mut self, other: &Text) {
398 let offset = self.len();
399 let other_len = other.len();
400 self.text.push_str(&other.text);
401
402 let other_base_style = other.style.unwrap_or_default();
405 let other_base_meta = other.meta.clone().unwrap_or_default();
406 if !other_base_style.is_null() || !other_base_meta.is_empty() {
407 self.spans.push(Span::new_with_meta(
408 offset,
409 offset + other_len,
410 other_base_style,
411 Some(other_base_meta),
412 ));
413 }
414
415 for span in &other.spans {
417 self.spans.push(Span::new_with_meta(
418 span.start + offset,
419 span.end + offset,
420 span.style,
421 span.meta.clone(),
422 ));
423 }
424 }
425
426 pub fn stylize(&mut self, start: usize, end: usize, style: Style) {
431 let length = self.len();
432 if start >= length || end <= start {
433 return;
434 }
435 let clamped_end = end.min(length);
436 self.spans.push(Span::new(start, clamped_end, style));
437 }
438
439 pub fn stylize_range(&mut self, style: Style, start: isize, end: Option<isize>) {
460 if style.is_null() {
461 return;
462 }
463
464 let length = self.len() as isize;
465
466 let start = if start < 0 {
468 (length + start).max(0) as usize
469 } else {
470 start as usize
471 };
472
473 let end = match end {
474 None => self.len(),
475 Some(e) if e < 0 => (length + e).max(0) as usize,
476 Some(e) => e as usize,
477 };
478
479 if start >= self.len() || end <= start {
481 return;
482 }
483
484 self.spans
485 .push(Span::new(start, end.min(self.len()), style));
486 }
487
488 pub fn stylize_before(&mut self, style: Style, start: isize, end: Option<isize>) {
499 if style.is_null() {
500 return;
501 }
502
503 let length = self.len() as isize;
504
505 let start = if start < 0 {
507 (length + start).max(0) as usize
508 } else {
509 start as usize
510 };
511
512 let end = match end {
513 None => self.len(),
514 Some(e) if e < 0 => (length + e).max(0) as usize,
515 Some(e) => e as usize,
516 };
517
518 if start >= self.len() || end <= start {
520 return;
521 }
522
523 self.spans
525 .insert(0, Span::new(start, end.min(self.len()), style));
526 }
527
528 pub fn apply_meta(
533 &mut self,
534 meta: BTreeMap<String, crate::style::MetaValue>,
535 start: isize,
536 end: Option<isize>,
537 ) {
538 if meta.is_empty() {
539 return;
540 }
541
542 let length = self.len() as isize;
543
544 let start = if start < 0 {
545 (length + start).max(0) as usize
546 } else {
547 start as usize
548 };
549
550 let end = match end {
551 None => self.len(),
552 Some(e) if e < 0 => (length + e).max(0) as usize,
553 Some(e) => e as usize,
554 };
555
556 if start >= self.len() || end <= start {
557 return;
558 }
559
560 let meta = StyleMeta {
561 link: None,
562 link_id: None,
563 meta: Some(Arc::new(meta)),
564 };
565
566 self.spans.push(Span::new_with_meta(
567 start,
568 end.min(self.len()),
569 Style::new(),
570 Some(meta),
571 ));
572 }
573
574 pub fn highlight_regex(&mut self, pattern: &str, style: Style) -> usize {
595 let re = match Regex::new(pattern) {
596 Ok(r) => r,
597 Err(_) => return 0,
598 };
599
600 let mut count = 0;
601 let plain = self.plain_text().to_string();
602
603 for mat in re.find_iter(&plain) {
604 let start_char = plain[..mat.start()].chars().count();
606 let end_char = start_char + plain[mat.start()..mat.end()].chars().count();
607
608 if end_char > start_char {
609 self.spans.push(Span::new(start_char, end_char, style));
610 count += 1;
611 }
612 }
613
614 count
615 }
616
617 pub fn highlight_words(&mut self, words: &[&str], style: Style, case_sensitive: bool) -> usize {
639 if words.is_empty() {
640 return 0;
641 }
642
643 let pattern = words
645 .iter()
646 .map(|w| regex::escape(w))
647 .collect::<Vec<_>>()
648 .join("|");
649
650 let pattern = if case_sensitive {
651 pattern
652 } else {
653 format!("(?i){}", pattern)
654 };
655
656 let re = match Regex::new(&pattern) {
657 Ok(r) => r,
658 Err(_) => return 0,
659 };
660
661 let mut count = 0;
662 let plain = self.plain_text().to_string();
663
664 for mat in re.find_iter(&plain) {
665 let start_char = plain[..mat.start()].chars().count();
667 let end_char = start_char + plain[mat.start()..mat.end()].chars().count();
668
669 if end_char > start_char {
670 self.spans.push(Span::new(start_char, end_char, style));
671 count += 1;
672 }
673 }
674
675 count
676 }
677
678 pub fn divide(&self, offsets: impl IntoIterator<Item = usize>) -> Vec<Text> {
705 let plain = self.plain_text();
706 let text_length = self.len();
707
708 let mut offsets: Vec<usize> = offsets
710 .into_iter()
711 .map(|o| o.min(text_length)) .collect();
713 offsets.sort_unstable();
714 offsets.dedup();
715
716 let offsets: Vec<usize> = offsets
718 .into_iter()
719 .filter(|&o| o > 0 && o < text_length)
720 .collect();
721
722 if offsets.is_empty() {
723 return vec![self.clone()];
724 }
725
726 let mut divide_offsets = vec![0];
728 divide_offsets.extend(offsets.iter().copied());
729 divide_offsets.push(text_length);
730
731 let line_ranges: Vec<(usize, usize)> = divide_offsets
733 .windows(2)
734 .map(|w| (w[0], w[1]))
735 .filter(|(start, end)| start < end) .collect();
737
738 if line_ranges.is_empty() {
739 return vec![self.clone()];
740 }
741
742 let chars: Vec<char> = plain.chars().collect();
744 let new_lines: Vec<Text> = line_ranges
745 .iter()
746 .map(|&(start, end)| {
747 let clamped_end = end.min(chars.len());
748 let clamped_start = start.min(clamped_end);
749 let substring: String = chars[clamped_start..clamped_end].iter().collect();
750 Text {
751 text: substring,
752 spans: Vec::new(),
753 style: self.style,
754 meta: self.meta.clone(),
755 }
756 })
757 .collect();
758
759 if self.spans.is_empty() {
761 return new_lines;
762 }
763
764 let mut result = new_lines;
766 let line_count = line_ranges.len();
767
768 for span in &self.spans {
769 if span.start >= span.end || span.start >= text_length {
771 continue;
772 }
773 let span_start = span.start;
774 let span_end = span.end.min(text_length);
775
776 let mut lower_bound = 0;
778 let mut upper_bound = line_count;
779 let mut start_line_no = (lower_bound + upper_bound) / 2;
780
781 loop {
782 if start_line_no >= line_count {
783 break;
784 }
785 let (line_start, line_end) = line_ranges[start_line_no];
786 if span_start < line_start {
787 if start_line_no == 0 {
788 break;
789 }
790 upper_bound = start_line_no - 1;
791 } else if span_start > line_end {
792 lower_bound = start_line_no + 1;
793 } else {
794 break;
795 }
796 start_line_no = (lower_bound + upper_bound) / 2;
797 }
798
799 let end_line_no = if span_end < line_ranges[start_line_no].1 {
801 start_line_no
802 } else {
803 lower_bound = start_line_no;
804 upper_bound = line_count;
805 let mut end_line_no = (lower_bound + upper_bound) / 2;
806
807 loop {
808 if end_line_no >= line_count {
809 end_line_no = line_count - 1;
810 break;
811 }
812 let (line_start, line_end) = line_ranges[end_line_no];
813 if span_end < line_start {
814 if end_line_no == 0 {
815 break;
816 }
817 upper_bound = end_line_no - 1;
818 } else if span_end > line_end {
819 lower_bound = end_line_no + 1;
820 } else {
821 break;
822 }
823 end_line_no = (lower_bound + upper_bound) / 2;
824 }
825 end_line_no
826 };
827
828 for line_no in start_line_no..=end_line_no.min(line_count - 1) {
830 let (line_start, line_end) = line_ranges[line_no];
831 let new_start = span_start.saturating_sub(line_start);
832 let new_end = span_end
833 .saturating_sub(line_start)
834 .min(line_end - line_start);
835
836 if new_end > new_start {
837 result[line_no].spans.push(Span::new_with_meta(
838 new_start,
839 new_end,
840 span.style,
841 span.meta.clone(),
842 ));
843 }
844 }
845 }
846
847 result
848 }
849
850 pub fn spans(&self) -> &[Span] {
852 &self.spans
853 }
854
855 pub fn spans_mut(&mut self) -> &mut Vec<Span> {
857 &mut self.spans
858 }
859
860 pub fn base_style(&self) -> Option<Style> {
862 self.style
863 }
864
865 pub fn set_base_style(&mut self, style: Option<Style>) {
867 self.style = style;
868 }
869
870 pub fn copy(&self) -> Text {
872 self.clone()
873 }
874
875 pub fn blank_copy(&self, plain: &str) -> Text {
877 Text {
878 text: plain.to_string(),
879 spans: Vec::new(),
880 style: self.style,
881 meta: self.meta.clone(),
882 }
883 }
884
885 pub fn join<I>(&self, texts: I) -> Text
887 where
888 I: IntoIterator<Item = Text>,
889 {
890 let mut result = self.blank_copy("");
891 let mut first = true;
892
893 for text in texts {
894 if !first && !self.is_empty() {
895 result.append_text(self);
896 }
897 result.append_text(&text);
898 first = false;
899 }
900
901 result
902 }
903
904 pub fn pad_right(&self, width: usize) -> Text {
924 let current_width = self.cell_len();
925 if current_width >= width {
926 return self.clone();
927 }
928
929 let mut result = self.clone();
930 let spaces = " ".repeat(width - current_width);
931 result.text.push_str(&spaces);
932 result
933 }
934
935 pub fn pad_left(&self, width: usize) -> Text {
951 let current_width = self.cell_len();
952 if current_width >= width {
953 return self.clone();
954 }
955
956 let pad_count = width - current_width;
957 let spaces = " ".repeat(pad_count);
958
959 let shifted_spans: Vec<Span> = self
961 .spans
962 .iter()
963 .map(|span| {
964 Span::new_with_meta(
965 span.start + pad_count,
966 span.end + pad_count,
967 span.style,
968 span.meta.clone(),
969 )
970 })
971 .collect();
972
973 Text {
974 text: format!("{}{}", spaces, self.text),
975 spans: shifted_spans,
976 style: self.style,
977 meta: self.meta.clone(),
978 }
979 }
980
981 pub fn center(&self, width: usize) -> Text {
997 let current_width = self.cell_len();
998 if current_width >= width {
999 return self.clone();
1000 }
1001
1002 let total_pad = width - current_width;
1003 let left_pad = total_pad / 2;
1004 let right_pad = total_pad - left_pad;
1005
1006 let left_spaces = " ".repeat(left_pad);
1007 let right_spaces = " ".repeat(right_pad);
1008
1009 let shifted_spans: Vec<Span> = self
1011 .spans
1012 .iter()
1013 .map(|span| {
1014 Span::new_with_meta(
1015 span.start + left_pad,
1016 span.end + left_pad,
1017 span.style,
1018 span.meta.clone(),
1019 )
1020 })
1021 .collect();
1022
1023 Text {
1024 text: format!("{}{}{}", left_spaces, self.text, right_spaces),
1025 spans: shifted_spans,
1026 style: self.style,
1027 meta: self.meta.clone(),
1028 }
1029 }
1030
1031 pub fn expand_tabs(&self, tab_size: usize) -> Text {
1049 if !self.text.contains('\t') {
1050 return self.clone();
1051 }
1052
1053 let tab_size = if tab_size == 0 { 8 } else { tab_size };
1054
1055 let mut result_text = String::new();
1056 let mut result_spans: Vec<Span> = Vec::new();
1057 let mut cell_position: usize = 0;
1058
1059 let chars: Vec<char> = self.text.chars().collect();
1060
1061 for &c in &chars {
1062 if c == '\t' {
1063 let tab_remainder = cell_position % tab_size;
1065 let spaces = if tab_remainder == 0 {
1066 tab_size
1067 } else {
1068 tab_size - tab_remainder
1069 };
1070
1071 result_text.push_str(&" ".repeat(spaces));
1072 cell_position += spaces;
1073 } else if c == '\n' {
1074 result_text.push(c);
1075 cell_position = 0; } else {
1077 result_text.push(c);
1078 cell_position += crate::cells::char_width(c);
1079 }
1080 }
1081
1082 let mut old_to_new: Vec<usize> = Vec::with_capacity(chars.len() + 1);
1085 old_to_new.push(0);
1086
1087 let mut new_pos: usize = 0;
1088 cell_position = 0;
1089
1090 for &c in &chars {
1091 if c == '\t' {
1092 let tab_remainder = cell_position % tab_size;
1093 let spaces = if tab_remainder == 0 {
1094 tab_size
1095 } else {
1096 tab_size - tab_remainder
1097 };
1098 new_pos += spaces;
1099 cell_position += spaces;
1100 } else if c == '\n' {
1101 new_pos += 1;
1102 cell_position = 0;
1103 } else {
1104 new_pos += 1;
1105 cell_position += crate::cells::char_width(c);
1106 }
1107 old_to_new.push(new_pos);
1108 }
1109
1110 for span in &self.spans {
1111 let new_start = if span.start < old_to_new.len() {
1112 old_to_new[span.start]
1113 } else {
1114 old_to_new.last().copied().unwrap_or(0)
1115 };
1116 let new_end = if span.end < old_to_new.len() {
1117 old_to_new[span.end]
1118 } else {
1119 old_to_new.last().copied().unwrap_or(0)
1120 };
1121
1122 if new_end > new_start {
1123 result_spans.push(Span::new_with_meta(
1124 new_start,
1125 new_end,
1126 span.style,
1127 span.meta.clone(),
1128 ));
1129 }
1130 }
1131
1132 Text {
1133 text: result_text,
1134 spans: result_spans,
1135 style: self.style,
1136 meta: self.meta.clone(),
1137 }
1138 }
1139
1140 pub fn with_indent_guides(self, indent_size: usize, style: Option<crate::Style>) -> Text {
1154 let guide_style = style.unwrap_or_else(|| {
1155 Style::new()
1156 .with_dim(true)
1157 .with_color(crate::color::SimpleColor::Standard(2))
1158 });
1159 self.with_indent_guides_full(Some(indent_size), "│", guide_style)
1160 }
1161
1162 pub fn rstrip(&self) -> Text {
1167 let trimmed = self.text.trim_end();
1168 let new_len = trimmed.chars().count();
1169
1170 let adjusted_spans: Vec<Span> = self
1171 .spans
1172 .iter()
1173 .filter_map(|span| {
1174 if span.start >= new_len {
1175 None
1176 } else {
1177 Some(Span::new_with_meta(
1178 span.start,
1179 span.end.min(new_len),
1180 span.style,
1181 span.meta.clone(),
1182 ))
1183 }
1184 })
1185 .filter(|span| !span.is_empty())
1186 .collect();
1187
1188 Text {
1189 text: trimmed.to_string(),
1190 spans: adjusted_spans,
1191 style: self.style,
1192 meta: self.meta.clone(),
1193 }
1194 }
1195
1196 pub fn rstrip_end(&self, size: usize) -> Text {
1205 let text_width = self.cell_len();
1206 if text_width <= size {
1207 return self.clone();
1208 }
1209
1210 let excess = text_width - size;
1211
1212 let mut trailing_ws_width = 0;
1214 for c in self.text.chars().rev() {
1215 if c.is_whitespace() {
1216 trailing_ws_width += crate::cells::char_width(c);
1217 } else {
1218 break;
1219 }
1220 }
1221
1222 if trailing_ws_width == 0 {
1223 return self.clone();
1224 }
1225
1226 let cells_to_remove = trailing_ws_width.min(excess);
1228
1229 let mut chars: Vec<char> = self.text.chars().collect();
1231 let mut removed = 0;
1232 while !chars.is_empty() && removed < cells_to_remove {
1233 if let Some(&c) = chars.last() {
1234 if c.is_whitespace() {
1235 removed += crate::cells::char_width(c);
1236 chars.pop();
1237 } else {
1238 break;
1239 }
1240 } else {
1241 break;
1242 }
1243 }
1244
1245 let new_text: String = chars.iter().collect();
1246 let new_len = chars.len();
1247
1248 let adjusted_spans: Vec<Span> = self
1249 .spans
1250 .iter()
1251 .filter_map(|span| {
1252 if span.start >= new_len {
1253 None
1254 } else {
1255 Some(Span::new_with_meta(
1256 span.start,
1257 span.end.min(new_len),
1258 span.style,
1259 span.meta.clone(),
1260 ))
1261 }
1262 })
1263 .filter(|span| !span.is_empty())
1264 .collect();
1265
1266 Text {
1267 text: new_text,
1268 spans: adjusted_spans,
1269 style: self.style,
1270 meta: self.meta.clone(),
1271 }
1272 }
1273
1274 pub fn truncate(
1282 &self,
1283 max_width: usize,
1284 overflow: crate::console::OverflowMethod,
1285 pad: bool,
1286 ) -> Text {
1287 use crate::cells::set_cell_size;
1288 use crate::console::OverflowMethod;
1289
1290 if overflow == OverflowMethod::Ignore {
1291 if pad && self.cell_len() < max_width {
1292 return self.pad_right(max_width);
1293 }
1294 return self.clone();
1295 }
1296
1297 let current_width = self.cell_len();
1298
1299 if current_width <= max_width {
1300 if pad {
1301 return self.pad_right(max_width);
1302 }
1303 return self.clone();
1304 }
1305
1306 let new_plain = if overflow == OverflowMethod::Ellipsis && max_width > 0 {
1308 let truncated = set_cell_size(&self.text, max_width.saturating_sub(1));
1309 format!("{}…", truncated)
1310 } else {
1311 set_cell_size(&self.text, max_width)
1312 };
1313
1314 let new_char_len = new_plain.chars().count();
1315
1316 let adjusted_spans: Vec<Span> = self
1318 .spans
1319 .iter()
1320 .filter_map(|span| {
1321 if span.start >= new_char_len {
1322 None
1323 } else {
1324 Some(Span::new_with_meta(
1325 span.start,
1326 span.end.min(new_char_len),
1327 span.style,
1328 span.meta.clone(),
1329 ))
1330 }
1331 })
1332 .filter(|span| !span.is_empty())
1333 .collect();
1334
1335 Text {
1336 text: new_plain,
1337 spans: adjusted_spans,
1338 style: self.style,
1339 meta: self.meta.clone(),
1340 }
1341 }
1342
1343 pub fn split(&self, separator: &str, include_separator: bool, allow_blank: bool) -> Vec<Text> {
1355 if separator.is_empty() {
1356 return vec![self.clone()];
1357 }
1358
1359 if !self.text.contains(separator) {
1360 return vec![self.clone()];
1361 }
1362
1363 let chars: Vec<char> = self.text.chars().collect();
1365 let sep_chars: Vec<char> = separator.chars().collect();
1366 let sep_len = sep_chars.len();
1367 let text_len = chars.len();
1368
1369 let mut sep_ranges: Vec<(usize, usize)> = Vec::new();
1371 let mut i = 0;
1372 while i + sep_len <= text_len {
1373 if &chars[i..i + sep_len] == sep_chars.as_slice() {
1374 sep_ranges.push((i, i + sep_len));
1375 i += sep_len;
1376 } else {
1377 i += 1;
1378 }
1379 }
1380
1381 if sep_ranges.is_empty() {
1382 return vec![self.clone()];
1383 }
1384
1385 let mut result: Vec<Text> = Vec::new();
1387 let mut pos = 0;
1388
1389 for (sep_start, sep_end) in &sep_ranges {
1390 if include_separator {
1392 if *sep_end > pos {
1394 let segment_text: String = chars[pos..*sep_end].iter().collect();
1395 result.push(self.slice_at_offsets(pos, *sep_end, &segment_text));
1396 }
1397 } else {
1398 let segment_text: String = chars[pos..*sep_start].iter().collect();
1400 if allow_blank || !segment_text.is_empty() {
1401 result.push(self.slice_at_offsets(pos, *sep_start, &segment_text));
1402 }
1403 }
1404 pos = *sep_end;
1405 }
1406
1407 if pos < text_len {
1409 let segment_text: String = chars[pos..].iter().collect();
1410 if allow_blank || !segment_text.is_empty() {
1411 result.push(self.slice_at_offsets(pos, text_len, &segment_text));
1412 }
1413 } else if include_separator {
1414 if allow_blank {
1417 result.push(self.blank_copy(""));
1418 }
1419 } else {
1420 if allow_blank {
1423 result.push(self.blank_copy(""));
1424 }
1425 }
1426
1427 result
1428 }
1429
1430 fn slice_at_offsets(&self, start: usize, end: usize, text: &str) -> Text {
1432 let adjusted_spans: Vec<Span> = self
1433 .spans
1434 .iter()
1435 .filter_map(|span| {
1436 if span.end <= start || span.start >= end {
1437 None
1438 } else {
1439 let new_start = span.start.saturating_sub(start);
1440 let new_end = span.end.min(end).saturating_sub(start);
1441 if new_start < new_end {
1442 Some(Span::new_with_meta(
1443 new_start,
1444 new_end,
1445 span.style,
1446 span.meta.clone(),
1447 ))
1448 } else {
1449 None
1450 }
1451 }
1452 })
1453 .collect();
1454
1455 Text {
1456 text: text.to_string(),
1457 spans: adjusted_spans,
1458 style: self.style,
1459 meta: self.meta.clone(),
1460 }
1461 }
1462
1463 fn justify_full(&self, width: usize) -> Text {
1472 let current_width = self.cell_len();
1473 if current_width >= width {
1474 return self.clone();
1475 }
1476
1477 let words = self.split(" ", false, false);
1483 if words.len() <= 1 {
1484 return self.pad_right(width);
1486 }
1487
1488 let words_width: usize = words.iter().map(|w| w.cell_len()).sum();
1490 let num_gaps = words.len().saturating_sub(1);
1491 if num_gaps == 0 {
1492 return self.pad_right(width);
1493 }
1494
1495 let mut spaces: Vec<usize> = vec![1; num_gaps];
1498 let mut num_spaces = num_gaps;
1499 let mut index = 0usize;
1500 while words_width + num_spaces < width {
1501 let pos = num_gaps.saturating_sub(index).saturating_sub(1);
1502 spaces[pos] += 1;
1503 num_spaces += 1;
1504 index = (index + 1) % num_gaps;
1505 }
1506
1507 let mut result = Text::new();
1508 result.style = self.style;
1509
1510 for (i, word) in words.iter().enumerate() {
1511 result.append_text(word);
1512
1513 if i < num_gaps {
1514 result.append(" ".repeat(spaces[i]), None);
1516 }
1517 }
1518
1519 result
1520 }
1521
1522 pub fn wrap(
1553 &self,
1554 width: usize,
1555 justify: Option<crate::console::JustifyMethod>,
1556 overflow: Option<crate::console::OverflowMethod>,
1557 tab_size: usize,
1558 no_wrap: bool,
1559 ) -> Vec<Text> {
1560 use crate::console::{JustifyMethod, OverflowMethod};
1561 use crate::wrap::divide_line;
1562
1563 let wrap_justify = justify.unwrap_or(JustifyMethod::Default);
1564 let wrap_overflow = overflow.unwrap_or(OverflowMethod::Fold);
1565
1566 let no_wrap = no_wrap || wrap_overflow == OverflowMethod::Ignore;
1568
1569 let mut all_lines: Vec<Text> = Vec::new();
1570
1571 let source_lines = self.split("\n", false, true);
1573
1574 for line in source_lines {
1575 let line = if line.plain_text().contains('\t') {
1577 line.expand_tabs(tab_size)
1578 } else {
1579 line
1580 };
1581
1582 let wrapped_lines = if no_wrap {
1583 vec![line.clone()]
1584 } else {
1585 let fold = wrap_overflow == OverflowMethod::Fold;
1587 let offsets = divide_line(line.plain_text(), width, fold);
1588
1589 if offsets.is_empty() {
1590 vec![line.clone()]
1591 } else {
1592 let char_offsets: Vec<usize> = offsets
1594 .iter()
1595 .map(|&byte_offset| line.plain_text()[..byte_offset].chars().count())
1596 .collect();
1597 line.divide(char_offsets)
1598 }
1599 };
1600
1601 for wrapped_line in wrapped_lines {
1603 all_lines.push(wrapped_line);
1604 }
1605 }
1606
1607 let num_lines = all_lines.len();
1609 for (i, wrapped_line) in all_lines.iter_mut().enumerate() {
1610 let is_last_line = i == num_lines - 1;
1611
1612 if !no_wrap {
1614 *wrapped_line = wrapped_line.rstrip_end(width);
1615 }
1616
1617 *wrapped_line = match wrap_justify {
1619 JustifyMethod::Left => wrapped_line.pad_right(width),
1620 JustifyMethod::Right => {
1621 let stripped = wrapped_line.rstrip();
1622 stripped.pad_left(width)
1623 }
1624 JustifyMethod::Center => {
1625 let stripped = wrapped_line.rstrip();
1626 stripped.center(width)
1627 }
1628 JustifyMethod::Full => {
1629 if is_last_line {
1631 wrapped_line.rstrip().pad_right(width)
1632 } else {
1633 wrapped_line.justify_full(width)
1634 }
1635 }
1636 JustifyMethod::Default => wrapped_line.clone(),
1637 };
1638
1639 if !no_wrap {
1641 *wrapped_line = wrapped_line.truncate(width, wrap_overflow, false);
1642 }
1643 }
1644
1645 all_lines
1646 }
1647}
1648
1649impl Text {
1654 pub fn slice(&self, start: usize, end: usize) -> Text {
1659 let text_len = self.len();
1660 let start = start.min(text_len);
1661 let end = end.min(text_len);
1662 if start >= end {
1663 return self.blank_copy("");
1664 }
1665 let lines = self.divide([start, end]);
1666 if start == 0 {
1669 lines
1670 .into_iter()
1671 .next()
1672 .unwrap_or_else(|| self.blank_copy(""))
1673 } else if lines.len() > 1 {
1674 lines
1675 .into_iter()
1676 .nth(1)
1677 .unwrap_or_else(|| self.blank_copy(""))
1678 } else {
1679 self.blank_copy("")
1680 }
1681 }
1682
1683 pub fn align(&mut self, align: JustifyMethod, width: usize) {
1686 let current_width = self.cell_len();
1687 if current_width >= width {
1688 *self = self.truncate(width, crate::console::OverflowMethod::Crop, false);
1689 return;
1690 }
1691 let excess_space = width - current_width;
1692 if excess_space == 0 {
1693 return;
1694 }
1695 match align {
1696 JustifyMethod::Left | JustifyMethod::Default => {
1697 *self = self.pad_right(width);
1698 }
1699 JustifyMethod::Center => {
1700 *self = self.center(width);
1701 }
1702 JustifyMethod::Right => {
1703 *self = self.pad_left(width);
1704 }
1705 JustifyMethod::Full => {
1706 *self = self.justify_full(width);
1707 }
1708 }
1709 }
1710
1711 pub fn contains(&self, text: &str) -> bool {
1713 self.text.contains(text)
1714 }
1715
1716 pub fn to_markup(&self) -> String {
1720 let plain = &self.text;
1721 if self.spans.is_empty() && self.style.is_none_or(|s| s.is_null()) {
1722 return markup::escape(plain);
1723 }
1724
1725 let mut events: Vec<(usize, bool, String)> = Vec::new();
1727
1728 if let Some(style) = self.style {
1730 if !style.is_null() {
1731 let style_str = style.to_markup_string();
1732 if !style_str.is_empty() {
1733 events.push((0, false, style_str.clone()));
1734 events.push((self.len(), true, style_str));
1735 }
1736 }
1737 }
1738
1739 for span in &self.spans {
1741 let style_str = span.style.to_markup_string();
1742 if !style_str.is_empty() {
1743 events.push((span.start, false, style_str.clone()));
1744 events.push((span.end, true, style_str));
1745 }
1746 }
1747
1748 events.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
1750
1751 let mut output = String::new();
1752 let chars: Vec<char> = plain.chars().collect();
1753 let mut position = 0;
1754
1755 for (offset, closing, style_str) in &events {
1756 let offset = (*offset).min(chars.len());
1757 if offset > position {
1758 let text_slice: String = chars[position..offset].iter().collect();
1759 output.push_str(&markup::escape(&text_slice));
1760 position = offset;
1761 }
1762 if *closing {
1763 output.push_str(&format!("[/{}]", style_str));
1764 } else {
1765 output.push_str(&format!("[{}]", style_str));
1766 }
1767 }
1768
1769 if position < chars.len() {
1771 let text_slice: String = chars[position..].iter().collect();
1772 output.push_str(&markup::escape(&text_slice));
1773 }
1774
1775 output
1776 }
1777
1778 pub fn get_style_at_offset(&self, offset: usize) -> Style {
1780 let mut style = self.style.unwrap_or_default();
1781 for span in &self.spans {
1782 if span.start <= offset && offset < span.end {
1783 style = style.combine(&span.style);
1784 }
1785 }
1786 style
1787 }
1788
1789 pub fn set_length(&mut self, length: usize) {
1791 let current = self.len();
1792 match current.cmp(&length) {
1793 Ordering::Less => {
1794 *self = self.pad_right(length);
1796 }
1797 Ordering::Greater => {
1798 self.right_crop(current - length);
1800 }
1801 Ordering::Equal => {}
1802 }
1803 }
1804
1805 pub fn right_crop(&mut self, amount: usize) {
1807 if amount == 0 {
1808 return;
1809 }
1810 let chars: Vec<char> = self.text.chars().collect();
1811 if amount >= chars.len() {
1812 self.text.clear();
1813 self.spans.clear();
1814 return;
1815 }
1816 let new_len = chars.len() - amount;
1817 self.text = chars[..new_len].iter().collect();
1818 self.spans = self
1819 .spans
1820 .iter()
1821 .filter(|span| span.start < new_len)
1822 .map(|span| {
1823 if span.end <= new_len {
1824 span.clone()
1825 } else {
1826 Span::new_with_meta(span.start, new_len, span.style, span.meta.clone())
1827 }
1828 })
1829 .collect();
1830 }
1831
1832 pub fn fit(&self, width: usize) -> Vec<Text> {
1834 let mut lines = Vec::new();
1835 for mut line in self.split("\n", false, true) {
1836 line.set_length(width);
1837 lines.push(line);
1838 }
1839 lines
1840 }
1841
1842 pub fn remove_suffix(&mut self, suffix: &str) {
1844 if self.text.ends_with(suffix) {
1845 let suffix_chars = suffix.chars().count();
1846 self.right_crop(suffix_chars);
1847 }
1848 }
1849
1850 pub fn extend_style(&mut self, count: usize) {
1852 if count == 0 {
1853 return;
1854 }
1855 let end_offset = self.len();
1856 if !self.spans.is_empty() {
1857 for span in &mut self.spans {
1858 if span.end >= end_offset {
1859 span.end += count;
1860 }
1861 }
1862 }
1863 self.text.push_str(&" ".repeat(count));
1864 }
1865
1866 pub fn detect_indentation(&self) -> usize {
1868 let re = Regex::new(r"(?m)^( *)(.*)$").unwrap_or_else(|_| Regex::new("$^").unwrap());
1869 let mut indentations: Vec<usize> = Vec::new();
1870 for cap in re.captures_iter(&self.text) {
1871 let indent_len = cap.get(1).map_or(0, |m| m.as_str().len());
1872 let content = cap.get(2).map_or("", |m| m.as_str());
1873 if !content.is_empty() && indent_len > 0 {
1874 indentations.push(indent_len);
1875 }
1876 }
1877
1878 if indentations.is_empty() {
1879 return 1;
1880 }
1881
1882 let even_indents: Vec<usize> = indentations.into_iter().filter(|i| i % 2 == 0).collect();
1884 if even_indents.is_empty() {
1885 return 1;
1886 }
1887
1888 even_indents.iter().copied().reduce(gcd).unwrap_or(1).max(1)
1889 }
1890
1891 pub fn copy_styles(&mut self, other: &Text) {
1893 self.spans.extend(other.spans.iter().cloned());
1894 }
1895
1896 pub fn append_tokens<I, S>(&mut self, tokens: I)
1900 where
1901 I: IntoIterator<Item = (S, Option<Style>)>,
1902 S: AsRef<str>,
1903 {
1904 for (content, style) in tokens {
1905 let sanitized = strip_control_codes(content.as_ref());
1906 self.append(sanitized, style);
1907 }
1908 }
1909
1910 pub fn with_indent_guides_full(
1915 &self,
1916 indent_size: Option<usize>,
1917 character: &str,
1918 style: Style,
1919 ) -> Text {
1920 let indent_size = indent_size.unwrap_or_else(|| self.detect_indentation());
1921 if indent_size == 0 {
1922 return self.clone();
1923 }
1924
1925 let expanded = self.expand_tabs(indent_size);
1926 let indent_line = format!("{}{}", character, " ".repeat(indent_size.saturating_sub(1)));
1927 let re_indent = Regex::new(r"^( *)(.*)$").unwrap();
1928
1929 let mut new_lines: Vec<Text> = Vec::new();
1930 let mut blank_lines: usize = 0;
1931
1932 for line in expanded.split("\n", false, true) {
1933 let plain = line.plain_text().to_string();
1934 if let Some(caps) = re_indent.captures(&plain) {
1935 let indent = caps.get(1).map_or("", |m| m.as_str());
1936 let content = caps.get(2).map_or("", |m| m.as_str());
1937
1938 if content.is_empty() {
1939 blank_lines += 1;
1940 continue;
1941 }
1942
1943 let indent_len = indent.len();
1944 let full_indents = indent_len / indent_size;
1945 let remaining_space = indent_len % indent_size;
1946 let new_indent = format!(
1947 "{}{}",
1948 indent_line.repeat(full_indents),
1949 " ".repeat(remaining_space)
1950 );
1951
1952 let mut result_line = line.clone();
1953 let new_indent_len = new_indent.chars().count();
1955 let chars: Vec<char> = result_line.text.chars().collect();
1956 let replace_len = indent_len.min(new_indent_len).min(chars.len());
1957 let mut new_text = new_indent.clone();
1958 if replace_len < chars.len() {
1959 let rest: String = chars[replace_len..].iter().collect();
1960 new_text.push_str(&rest);
1961 }
1962 result_line.text = new_text;
1963 result_line.stylize(0, new_indent_len.min(indent_len), style);
1964
1965 if blank_lines > 0 {
1967 for _ in 0..blank_lines {
1968 let blank_guide = Text::styled(new_indent.clone(), style);
1969 new_lines.push(blank_guide);
1970 }
1971 blank_lines = 0;
1972 }
1973
1974 new_lines.push(result_line);
1975 } else {
1976 blank_lines += 1;
1977 }
1978 }
1979
1980 for _ in 0..blank_lines {
1982 new_lines.push(Text::new());
1983 }
1984
1985 let joiner = expanded.blank_copy("\n");
1986 joiner.join(new_lines)
1987 }
1988}
1989
1990impl std::ops::Add for Text {
1991 type Output = Text;
1992
1993 fn add(self, rhs: Text) -> Text {
1994 let mut result = self.clone();
1995 result.append_text(&rhs);
1996 result
1997 }
1998}
1999
2000impl std::ops::Add<&str> for Text {
2001 type Output = Text;
2002
2003 fn add(self, rhs: &str) -> Text {
2004 let mut result = self.clone();
2005 result.append(rhs, None);
2006 result
2007 }
2008}
2009
2010fn gcd(a: usize, b: usize) -> usize {
2012 if b == 0 { a } else { gcd(b, a % b) }
2013}
2014
2015impl From<&str> for Text {
2016 fn from(s: &str) -> Self {
2017 Text::plain(s)
2018 }
2019}
2020
2021impl From<String> for Text {
2022 fn from(s: String) -> Self {
2023 Text::plain(s)
2024 }
2025}
2026
2027impl Renderable for Text {
2031 fn render(&self, _console: &Console, options: &ConsoleOptions) -> Segments {
2032 let text = self.plain_text();
2033 let width = options.max_width;
2034
2035 let needs_processing = width > 0
2038 && (options.justify.is_some()
2039 || options.overflow.is_some()
2040 || text.lines().any(|line| cell_len(line) > width));
2041
2042 if !needs_processing {
2043 return self.render_unwrapped();
2044 }
2045
2046 let lines = self.wrap(
2047 width,
2048 options.justify,
2049 options.overflow,
2050 options.tab_size,
2051 options.no_wrap,
2052 );
2053
2054 if lines.len() == 1 {
2055 return lines[0].render_unwrapped();
2056 }
2057
2058 let mut segments = Segments::new();
2062 for (i, line) in lines.iter().enumerate() {
2063 segments.extend(line.render_unwrapped());
2064 if i + 1 < lines.len() {
2065 segments.push(Segment::line());
2066 }
2067 }
2068
2069 segments
2070 }
2071
2072 fn measure(&self, _console: &Console, options: &ConsoleOptions) -> Measurement {
2073 let text = self.plain_text();
2074 let lines: Vec<&str> = text.lines().collect();
2075
2076 let mut max_width = lines.iter().map(|line| cell_len(line)).max().unwrap_or(0);
2077
2078 let words: Vec<&str> = text.split_whitespace().collect();
2079 let mut min_width = words
2080 .iter()
2081 .map(|word| cell_len(word))
2082 .max()
2083 .unwrap_or(max_width);
2084
2085 if options.max_width > 0 {
2086 if max_width > options.max_width {
2087 max_width = options.max_width;
2088 }
2089 if min_width > options.max_width {
2090 min_width = options.max_width;
2091 }
2092 }
2093
2094 Measurement::new(min_width, max_width)
2095 }
2096}
2097
2098impl Text {
2099 fn render_unwrapped(&self) -> Segments {
2100 let text = self.plain_text();
2101
2102 if self.spans.is_empty() {
2104 let base_style = self.style.unwrap_or_default();
2105 let base_meta = self.meta.clone().unwrap_or_default();
2106 let segment = match (base_style.is_null(), base_meta.is_empty()) {
2107 (true, true) => Segment::new(text.to_string()),
2108 (true, false) => Segment::new_with_meta(text.to_string(), base_meta),
2109 (false, true) => Segment::styled(text.to_string(), base_style),
2110 (false, false) => {
2111 Segment::styled_with_meta(text.to_string(), base_style, base_meta)
2112 }
2113 };
2114 return Segments::from(segment);
2115 }
2116
2117 let enumerated_spans: Vec<(usize, &Span)> = self
2120 .spans
2121 .iter()
2122 .enumerate()
2123 .map(|(i, s)| (i + 1, s))
2124 .collect();
2125
2126 let mut style_map: std::collections::HashMap<usize, Style> =
2128 std::collections::HashMap::new();
2129 style_map.insert(0, self.style.unwrap_or_default());
2130 for (index, span) in &enumerated_spans {
2131 style_map.insert(*index, span.style);
2132 }
2133
2134 let mut meta_map: std::collections::HashMap<usize, StyleMeta> =
2136 std::collections::HashMap::new();
2137 meta_map.insert(0, self.meta.clone().unwrap_or_default());
2138 for (index, span) in &enumerated_spans {
2139 meta_map.insert(*index, span.meta.clone().unwrap_or_default());
2140 }
2141
2142 let mut events: Vec<(usize, bool, usize)> = Vec::new();
2144 events.push((0, false, 0)); for (index, span) in &enumerated_spans {
2146 events.push((span.start, false, *index)); events.push((span.end, true, *index)); }
2149 events.push((self.len(), true, 0)); events.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
2153
2154 let mut segments = Segments::new();
2156 let mut stack: Vec<usize> = Vec::new();
2157
2158 let chars: Vec<char> = text.chars().collect();
2159
2160 for i in 0..events.len() - 1 {
2161 let (offset, leaving, style_id) = events[i];
2162 let (next_offset, _, _) = events[i + 1];
2163
2164 if leaving {
2165 if let Some(pos) = stack.iter().position(|&x| x == style_id) {
2167 stack.remove(pos);
2168 }
2169 } else {
2170 stack.push(style_id);
2172 }
2173
2174 if next_offset > offset && offset < chars.len() {
2176 let end = next_offset.min(chars.len());
2177 let segment_text: String = chars[offset..end].iter().collect();
2178
2179 let mut combined_style = Style::new();
2181 let mut combined_meta = StyleMeta::new();
2182 let mut sorted_stack = stack.clone();
2183 sorted_stack.sort();
2184 for &style_id in &sorted_stack {
2185 if let Some(&style) = style_map.get(&style_id) {
2186 combined_style = combined_style.combine(&style);
2187 }
2188 if let Some(meta) = meta_map.get(&style_id) {
2189 combined_meta = combined_meta.combine(meta);
2190 }
2191 }
2192
2193 match (combined_style.is_null(), combined_meta.is_empty()) {
2194 (true, true) => segments.push(Segment::new(segment_text)),
2195 (true, false) => {
2196 segments.push(Segment::new_with_meta(segment_text, combined_meta))
2197 }
2198 (false, true) => segments.push(Segment::styled(segment_text, combined_style)),
2199 (false, false) => segments.push(Segment::styled_with_meta(
2200 segment_text,
2201 combined_style,
2202 combined_meta,
2203 )),
2204 }
2205 }
2206 }
2207
2208 segments
2209 }
2210}
2211
2212#[cfg(test)]
2213mod tests {
2214 use super::*;
2215
2216 #[test]
2219 fn test_span_new() {
2220 let style = Style::new().with_bold(true);
2221 let span = Span::new(0, 10, style);
2222 assert_eq!(span.start, 0);
2223 assert_eq!(span.end, 10);
2224 assert_eq!(span.style, style);
2225 }
2226
2227 #[test]
2228 fn test_span_is_empty() {
2229 let style = Style::new();
2230 assert!(Span::new(5, 5, style).is_empty());
2231 assert!(Span::new(10, 5, style).is_empty());
2232 assert!(!Span::new(0, 5, style).is_empty());
2233 }
2234
2235 #[test]
2236 fn test_span_split_middle() {
2237 let style = Style::new().with_bold(true);
2238 let span = Span::new(0, 10, style);
2239 let (first, second) = span.split(5);
2240
2241 assert_eq!(first.start, 0);
2242 assert_eq!(first.end, 5);
2243 assert!(second.is_some());
2244 let second = second.unwrap();
2245 assert_eq!(second.start, 5);
2246 assert_eq!(second.end, 10);
2247 }
2248
2249 #[test]
2250 fn test_span_split_before_start() {
2251 let style = Style::new();
2252 let span = Span::new(5, 10, style);
2253 let (first, second) = span.split(3);
2254
2255 assert_eq!(first.start, 5);
2256 assert_eq!(first.end, 10);
2257 assert!(second.is_none());
2258 }
2259
2260 #[test]
2261 fn test_span_split_after_end() {
2262 let style = Style::new();
2263 let span = Span::new(0, 5, style);
2264 let (first, second) = span.split(10);
2265
2266 assert_eq!(first.start, 0);
2267 assert_eq!(first.end, 5);
2268 assert!(second.is_none());
2269 }
2270
2271 #[test]
2272 fn test_span_split_at_end() {
2273 let style = Style::new();
2274 let span = Span::new(0, 5, style);
2275 let (first, second) = span.split(5);
2276
2277 assert_eq!(first.start, 0);
2278 assert_eq!(first.end, 5);
2279 assert!(second.is_none());
2280 }
2281
2282 #[test]
2283 fn test_span_move_positive() {
2284 let style = Style::new();
2285 let span = Span::new(5, 10, style);
2286 let moved = span.move_by(3);
2287
2288 assert_eq!(moved.start, 8);
2289 assert_eq!(moved.end, 13);
2290 }
2291
2292 #[test]
2293 fn test_span_move_negative() {
2294 let style = Style::new();
2295 let span = Span::new(5, 10, style);
2296 let moved = span.move_by(-3);
2297
2298 assert_eq!(moved.start, 2);
2299 assert_eq!(moved.end, 7);
2300 }
2301
2302 #[test]
2303 fn test_span_move_negative_clamp() {
2304 let style = Style::new();
2305 let span = Span::new(2, 5, style);
2306 let moved = span.move_by(-10);
2307
2308 assert_eq!(moved.start, 0);
2309 assert_eq!(moved.end, 0);
2310 }
2311
2312 #[test]
2313 fn test_span_right_crop() {
2314 let style = Style::new();
2315 let span = Span::new(0, 10, style);
2316
2317 let cropped = span.right_crop(5);
2318 assert_eq!(cropped.start, 0);
2319 assert_eq!(cropped.end, 5);
2320
2321 let uncropped = span.right_crop(15);
2322 assert_eq!(uncropped.start, 0);
2323 assert_eq!(uncropped.end, 10);
2324 }
2325
2326 #[test]
2327 fn test_span_extend() {
2328 let style = Style::new();
2329 let span = Span::new(0, 5, style);
2330
2331 let extended = span.extend(3);
2332 assert_eq!(extended.start, 0);
2333 assert_eq!(extended.end, 8);
2334
2335 let unchanged = span.extend(0);
2336 assert_eq!(unchanged.start, 0);
2337 assert_eq!(unchanged.end, 5);
2338 }
2339
2340 #[test]
2343 fn test_text_plain() {
2344 let text = Text::plain("hello");
2345 assert_eq!(text.plain_text(), "hello");
2346 assert_eq!(text.len(), 5);
2347 }
2348
2349 #[test]
2350 fn test_text_styled() {
2351 let style = Style::new().with_bold(true);
2352 let text = Text::styled("hello", style);
2353 assert_eq!(text.spans().len(), 0);
2355 assert_eq!(text.base_style(), Some(style));
2356 }
2357
2358 #[test]
2359 fn test_text_append() {
2360 let mut text = Text::new();
2361 text.append("hello ", None);
2362 text.append("world", Some(Style::new().with_bold(true)));
2363 assert_eq!(text.plain_text(), "hello world");
2364 assert_eq!(text.spans().len(), 1);
2365 }
2366
2367 #[test]
2368 fn test_text_append_text() {
2369 let mut text = Text::plain("Hello ");
2370 let other = Text::styled("World", Style::new().with_bold(true));
2371 text.append_text(&other);
2372
2373 assert_eq!(text.plain_text(), "Hello World");
2374 assert_eq!(text.spans().len(), 1);
2375 assert_eq!(text.spans()[0].start, 6);
2376 assert_eq!(text.spans()[0].end, 11);
2377 }
2378
2379 #[test]
2382 fn test_text_assemble() {
2383 let bold = Style::new().with_bold(true);
2384 let text = Text::assemble([
2385 TextPart::Plain("Hello, ".to_string()),
2386 TextPart::Styled("World".to_string(), bold),
2387 TextPart::Plain("!".to_string()),
2388 ]);
2389
2390 assert_eq!(text.plain_text(), "Hello, World!");
2391 assert_eq!(text.spans().len(), 1);
2392 assert_eq!(text.spans()[0].start, 7);
2393 assert_eq!(text.spans()[0].end, 12);
2394 }
2395
2396 #[test]
2397 fn test_text_assemble_with_text() {
2398 let inner = Text::styled("styled", Style::new().with_italic(true));
2399 let text = Text::assemble([
2400 TextPart::Plain("prefix ".to_string()),
2401 TextPart::Text(inner),
2402 TextPart::Plain(" suffix".to_string()),
2403 ]);
2404
2405 assert_eq!(text.plain_text(), "prefix styled suffix");
2406 }
2407
2408 #[test]
2411 fn test_stylize_range_basic() {
2412 let mut text = Text::plain("Hello World");
2413 text.stylize_range(Style::new().with_bold(true), 0, Some(5));
2414
2415 assert_eq!(text.spans().len(), 1);
2416 assert_eq!(text.spans()[0].start, 0);
2417 assert_eq!(text.spans()[0].end, 5);
2418 }
2419
2420 #[test]
2421 fn test_stylize_range_negative_start() {
2422 let mut text = Text::plain("Hello World");
2423 text.stylize_range(Style::new().with_bold(true), -5, None);
2424
2425 assert_eq!(text.spans().len(), 1);
2426 assert_eq!(text.spans()[0].start, 6); assert_eq!(text.spans()[0].end, 11);
2428 }
2429
2430 #[test]
2431 fn test_stylize_range_negative_end() {
2432 let mut text = Text::plain("Hello World");
2433 text.stylize_range(Style::new().with_bold(true), 0, Some(-6));
2434
2435 assert_eq!(text.spans().len(), 1);
2436 assert_eq!(text.spans()[0].start, 0);
2437 assert_eq!(text.spans()[0].end, 5); }
2439
2440 #[test]
2441 fn test_stylize_range_none_end() {
2442 let mut text = Text::plain("Hello World");
2443 text.stylize_range(Style::new().with_bold(true), 6, None);
2444
2445 assert_eq!(text.spans().len(), 1);
2446 assert_eq!(text.spans()[0].start, 6);
2447 assert_eq!(text.spans()[0].end, 11);
2448 }
2449
2450 #[test]
2451 fn test_stylize_range_invalid() {
2452 let mut text = Text::plain("Hello");
2453 text.stylize_range(Style::new().with_bold(true), 10, Some(5));
2455 assert!(text.spans().is_empty());
2456
2457 text.stylize_range(Style::new().with_bold(true), 10, None);
2459 assert!(text.spans().is_empty());
2460 }
2461
2462 #[test]
2465 fn test_stylize_before() {
2466 let mut text = Text::plain("Hello World");
2467 text.stylize_range(Style::new().with_bold(true), 0, None);
2468 text.stylize_before(Style::new().with_italic(true), 0, None);
2469
2470 assert_eq!(text.spans().len(), 2);
2472 assert_eq!(text.spans()[0].style.italic, Some(true));
2473 assert_eq!(text.spans()[1].style.bold, Some(true));
2474 }
2475
2476 #[test]
2479 fn test_highlight_regex_basic() {
2480 let mut text = Text::plain("foo bar foo baz");
2481 let count = text.highlight_regex(r"foo", Style::new().with_bold(true));
2482
2483 assert_eq!(count, 2);
2484 assert_eq!(text.spans().len(), 2);
2485 }
2486
2487 #[test]
2488 fn test_highlight_regex_no_match() {
2489 let mut text = Text::plain("hello world");
2490 let count = text.highlight_regex(r"xyz", Style::new().with_bold(true));
2491
2492 assert_eq!(count, 0);
2493 assert!(text.spans().is_empty());
2494 }
2495
2496 #[test]
2497 fn test_highlight_regex_invalid() {
2498 let mut text = Text::plain("hello world");
2499 let count = text.highlight_regex(r"[invalid", Style::new().with_bold(true));
2500
2501 assert_eq!(count, 0);
2502 }
2503
2504 #[test]
2507 fn test_highlight_words_basic() {
2508 let mut text = Text::plain("Hello World Hello");
2509 let count = text.highlight_words(&["Hello"], Style::new().with_bold(true), true);
2510
2511 assert_eq!(count, 2);
2512 assert_eq!(text.spans().len(), 2);
2513 }
2514
2515 #[test]
2516 fn test_highlight_words_case_insensitive() {
2517 let mut text = Text::plain("Hello HELLO hello");
2518 let count = text.highlight_words(&["hello"], Style::new().with_bold(true), false);
2519
2520 assert_eq!(count, 3);
2521 }
2522
2523 #[test]
2524 fn test_highlight_words_case_sensitive() {
2525 let mut text = Text::plain("Hello HELLO hello");
2526 let count = text.highlight_words(&["Hello"], Style::new().with_bold(true), true);
2527
2528 assert_eq!(count, 1);
2529 }
2530
2531 #[test]
2532 fn test_highlight_words_multiple() {
2533 let mut text = Text::plain("foo bar baz foo");
2534 let count = text.highlight_words(&["foo", "bar"], Style::new().with_bold(true), true);
2535
2536 assert_eq!(count, 3); }
2538
2539 #[test]
2540 fn test_highlight_words_empty() {
2541 let mut text = Text::plain("hello");
2542 let count = text.highlight_words(&[], Style::new().with_bold(true), true);
2543
2544 assert_eq!(count, 0);
2545 }
2546
2547 #[test]
2550 fn test_divide_empty_offsets() {
2551 let text = Text::plain("Hello World");
2552 let divided = text.divide([]);
2553
2554 assert_eq!(divided.len(), 1);
2555 assert_eq!(divided[0].plain_text(), "Hello World");
2556 }
2557
2558 #[test]
2559 fn test_divide_single_offset() {
2560 let text = Text::plain("Hello World");
2561 let divided = text.divide([5]);
2562
2563 assert_eq!(divided.len(), 2);
2564 assert_eq!(divided[0].plain_text(), "Hello");
2565 assert_eq!(divided[1].plain_text(), " World");
2566 }
2567
2568 #[test]
2569 fn test_divide_multiple_offsets() {
2570 let text = Text::plain("Hello World!");
2571 let divided = text.divide([5, 6]);
2572
2573 assert_eq!(divided.len(), 3);
2574 assert_eq!(divided[0].plain_text(), "Hello");
2575 assert_eq!(divided[1].plain_text(), " ");
2576 assert_eq!(divided[2].plain_text(), "World!");
2577 }
2578
2579 #[test]
2580 fn test_divide_with_spans() {
2581 let mut text = Text::plain("Hello World");
2582 text.stylize(0, 5, Style::new().with_bold(true));
2583
2584 let divided = text.divide([5]);
2585
2586 assert_eq!(divided.len(), 2);
2587 assert_eq!(divided[0].plain_text(), "Hello");
2588 assert_eq!(divided[0].spans().len(), 1);
2589 assert_eq!(divided[0].spans()[0].start, 0);
2590 assert_eq!(divided[0].spans()[0].end, 5);
2591
2592 assert_eq!(divided[1].plain_text(), " World");
2593 assert!(divided[1].spans().is_empty());
2594 }
2595
2596 #[test]
2597 fn test_divide_span_crosses_boundary() {
2598 let mut text = Text::plain("Hello World");
2599 text.stylize(3, 9, Style::new().with_bold(true));
2601
2602 let divided = text.divide([5]);
2603
2604 assert_eq!(divided[0].plain_text(), "Hello");
2606 assert_eq!(divided[0].spans().len(), 1);
2607 assert_eq!(divided[0].spans()[0].start, 3);
2608 assert_eq!(divided[0].spans()[0].end, 5);
2609
2610 assert_eq!(divided[1].plain_text(), " World");
2612 assert_eq!(divided[1].spans().len(), 1);
2613 assert_eq!(divided[1].spans()[0].start, 0);
2614 assert_eq!(divided[1].spans()[0].end, 4);
2615 }
2616
2617 #[test]
2620 fn test_text_render_plain() {
2621 let text = Text::plain("Hello World");
2622 let console = Console::new();
2623 let options = ConsoleOptions::default();
2624
2625 let segments = text.render(&console, &options);
2626 assert_eq!(segments.len(), 1);
2627 assert_eq!(&*segments.iter().next().unwrap().text, "Hello World");
2628 }
2629
2630 #[test]
2631 fn test_text_render_styled() {
2632 let mut text = Text::plain("Hello World");
2633 text.stylize(0, 5, Style::new().with_bold(true));
2634
2635 let console = Console::new();
2636 let options = ConsoleOptions::default();
2637
2638 let segments = text.render(&console, &options);
2639 assert!(segments.len() >= 2);
2640 }
2641
2642 #[test]
2643 fn test_text_measure() {
2644 let text = Text::plain("Hello\nWorld!");
2645 let console = Console::new();
2646 let options = ConsoleOptions::default();
2647
2648 let measurement = text.measure(&console, &options);
2649 assert_eq!(measurement.maximum, 6); assert_eq!(measurement.minimum, 6); }
2652
2653 #[test]
2656 fn test_from_markup() {
2657 let text = Text::from_markup("[bold]Hello[/] World", false).unwrap();
2658 assert_eq!(text.plain_text(), "Hello World");
2659 assert!(!text.spans().is_empty());
2660 }
2661
2662 #[test]
2665 fn test_text_join() {
2666 let separator = Text::plain(", ");
2667 let texts = vec![Text::plain("a"), Text::plain("b"), Text::plain("c")];
2668
2669 let joined = separator.join(texts);
2670 assert_eq!(joined.plain_text(), "a, b, c");
2671 }
2672
2673 #[test]
2674 fn test_text_join_empty_separator() {
2675 let separator = Text::plain("");
2676 let texts = vec![Text::plain("a"), Text::plain("b")];
2677
2678 let joined = separator.join(texts);
2679 assert_eq!(joined.plain_text(), "ab");
2680 }
2681
2682 #[test]
2685 fn test_text_unicode_len() {
2686 let text = Text::plain("你好");
2687 assert_eq!(text.len(), 2); assert_eq!(text.cell_len(), 4); }
2690
2691 #[test]
2692 fn test_divide_unicode() {
2693 let text = Text::plain("你好世界");
2694 let divided = text.divide([2]);
2695
2696 assert_eq!(divided.len(), 2);
2697 assert_eq!(divided[0].plain_text(), "你好");
2698 assert_eq!(divided[1].plain_text(), "世界");
2699 }
2700
2701 #[test]
2704 fn test_pad_right_basic() {
2705 let text = Text::plain("hello");
2706 let padded = text.pad_right(10);
2707 assert_eq!(padded.plain_text(), "hello ");
2708 assert_eq!(padded.cell_len(), 10);
2709 }
2710
2711 #[test]
2712 fn test_pad_right_already_wide() {
2713 let text = Text::plain("hello world");
2714 let padded = text.pad_right(5);
2715 assert_eq!(padded.plain_text(), "hello world");
2716 }
2717
2718 #[test]
2719 fn test_pad_right_preserves_spans() {
2720 let mut text = Text::plain("hello");
2721 text.stylize(0, 5, Style::new().with_bold(true));
2722 let padded = text.pad_right(10);
2723 assert_eq!(padded.spans().len(), 1);
2724 assert_eq!(padded.spans()[0].start, 0);
2725 assert_eq!(padded.spans()[0].end, 5);
2726 }
2727
2728 #[test]
2731 fn test_pad_left_basic() {
2732 let text = Text::plain("hello");
2733 let padded = text.pad_left(10);
2734 assert_eq!(padded.plain_text(), " hello");
2735 assert_eq!(padded.cell_len(), 10);
2736 }
2737
2738 #[test]
2739 fn test_pad_left_shifts_spans() {
2740 let mut text = Text::plain("hello");
2741 text.stylize(0, 5, Style::new().with_bold(true));
2742 let padded = text.pad_left(10);
2743 assert_eq!(padded.spans().len(), 1);
2744 assert_eq!(padded.spans()[0].start, 5); assert_eq!(padded.spans()[0].end, 10);
2746 }
2747
2748 #[test]
2751 fn test_center_basic() {
2752 let text = Text::plain("hi");
2753 let centered = text.center(6);
2754 assert_eq!(centered.plain_text(), " hi ");
2755 assert_eq!(centered.cell_len(), 6);
2756 }
2757
2758 #[test]
2759 fn test_center_odd_padding() {
2760 let text = Text::plain("hi");
2761 let centered = text.center(7);
2762 assert_eq!(centered.plain_text(), " hi ");
2764 }
2765
2766 #[test]
2767 fn test_center_shifts_spans() {
2768 let mut text = Text::plain("hi");
2769 text.stylize(0, 2, Style::new().with_bold(true));
2770 let centered = text.center(6);
2771 assert_eq!(centered.spans().len(), 1);
2772 assert_eq!(centered.spans()[0].start, 2);
2773 assert_eq!(centered.spans()[0].end, 4);
2774 }
2775
2776 #[test]
2779 fn test_expand_tabs_basic() {
2780 let text = Text::plain("a\tb");
2781 let expanded = text.expand_tabs(4);
2782 assert_eq!(expanded.plain_text(), "a b");
2783 }
2784
2785 #[test]
2786 fn test_expand_tabs_multiple() {
2787 let text = Text::plain("a\tbc\td");
2788 let expanded = text.expand_tabs(4);
2789 assert_eq!(expanded.plain_text(), "a bc d");
2793 }
2794
2795 #[test]
2796 fn test_expand_tabs_no_tabs() {
2797 let text = Text::plain("hello");
2798 let expanded = text.expand_tabs(4);
2799 assert_eq!(expanded.plain_text(), "hello");
2800 }
2801
2802 #[test]
2803 fn test_expand_tabs_preserves_spans() {
2804 let mut text = Text::plain("a\tb");
2805 text.stylize(0, 1, Style::new().with_bold(true)); text.stylize(2, 3, Style::new().with_italic(true)); let expanded = text.expand_tabs(4);
2808
2809 assert_eq!(expanded.spans().len(), 2);
2812 assert_eq!(expanded.spans()[0].start, 0);
2813 assert_eq!(expanded.spans()[0].end, 1);
2814 assert_eq!(expanded.spans()[1].start, 4);
2815 assert_eq!(expanded.spans()[1].end, 5);
2816 }
2817
2818 #[test]
2821 fn test_rstrip_basic() {
2822 let text = Text::plain("hello ");
2823 let stripped = text.rstrip();
2824 assert_eq!(stripped.plain_text(), "hello");
2825 }
2826
2827 #[test]
2828 fn test_rstrip_no_whitespace() {
2829 let text = Text::plain("hello");
2830 let stripped = text.rstrip();
2831 assert_eq!(stripped.plain_text(), "hello");
2832 }
2833
2834 #[test]
2835 fn test_rstrip_adjusts_spans() {
2836 let mut text = Text::plain("hello ");
2837 text.stylize(0, 8, Style::new().with_bold(true));
2838 let stripped = text.rstrip();
2839 assert_eq!(stripped.spans().len(), 1);
2840 assert_eq!(stripped.spans()[0].end, 5); }
2842
2843 #[test]
2846 fn test_rstrip_end_basic() {
2847 let text = Text::plain("hello ");
2848 let stripped = text.rstrip_end(5);
2849 assert_eq!(stripped.plain_text(), "hello");
2850 }
2851
2852 #[test]
2853 fn test_rstrip_end_partial() {
2854 let text = Text::plain("hello ");
2855 let stripped = text.rstrip_end(7);
2856 assert_eq!(stripped.plain_text(), "hello ");
2858 }
2859
2860 #[test]
2861 fn test_rstrip_end_already_short() {
2862 let text = Text::plain("hello");
2863 let stripped = text.rstrip_end(10);
2864 assert_eq!(stripped.plain_text(), "hello");
2865 }
2866
2867 #[test]
2870 fn test_truncate_crop() {
2871 use crate::console::OverflowMethod;
2872 let text = Text::plain("hello world");
2873 let truncated = text.truncate(5, OverflowMethod::Crop, false);
2874 assert_eq!(truncated.plain_text(), "hello");
2875 }
2876
2877 #[test]
2878 fn test_truncate_ellipsis() {
2879 use crate::console::OverflowMethod;
2880 let text = Text::plain("hello world");
2881 let truncated = text.truncate(6, OverflowMethod::Ellipsis, false);
2882 assert_eq!(truncated.plain_text(), "hello…");
2883 }
2884
2885 #[test]
2886 fn test_truncate_with_pad() {
2887 use crate::console::OverflowMethod;
2888 let text = Text::plain("hi");
2889 let truncated = text.truncate(5, OverflowMethod::Crop, true);
2890 assert_eq!(truncated.plain_text(), "hi ");
2891 }
2892
2893 #[test]
2896 fn test_split_newlines() {
2897 let text = Text::plain("hello\nworld");
2898 let lines = text.split("\n", false, false);
2899 assert_eq!(lines.len(), 2);
2900 assert_eq!(lines[0].plain_text(), "hello");
2901 assert_eq!(lines[1].plain_text(), "world");
2902 }
2903
2904 #[test]
2905 fn test_split_include_separator() {
2906 let text = Text::plain("hello\nworld");
2907 let lines = text.split("\n", true, false);
2908 assert_eq!(lines.len(), 2);
2909 assert_eq!(lines[0].plain_text(), "hello\n");
2910 assert_eq!(lines[1].plain_text(), "world");
2911 }
2912
2913 #[test]
2914 fn test_split_allow_blank() {
2915 let text = Text::plain("hello\n");
2916 let lines = text.split("\n", false, true);
2917 assert_eq!(lines.len(), 2);
2918 assert_eq!(lines[0].plain_text(), "hello");
2919 assert_eq!(lines[1].plain_text(), "");
2920 }
2921
2922 #[test]
2925 fn test_wrap_basic() {
2926 let text = Text::plain("hello world test");
2927 let lines = text.wrap(6, None, None, 8, false);
2928 assert_eq!(lines.len(), 3);
2929 assert_eq!(lines[0].plain_text(), "hello ");
2930 assert_eq!(lines[1].plain_text(), "world ");
2931 assert_eq!(lines[2].plain_text(), "test");
2932 }
2933
2934 #[test]
2935 fn test_wrap_existing_newlines() {
2936 let text = Text::plain("hello\nworld");
2937 let lines = text.wrap(20, None, None, 8, false);
2938 assert_eq!(lines.len(), 2);
2939 assert_eq!(lines[0].plain_text(), "hello");
2940 assert_eq!(lines[1].plain_text(), "world");
2941 }
2942
2943 #[test]
2944 fn test_wrap_left_justify() {
2945 use crate::console::JustifyMethod;
2946 let text = Text::plain("hi");
2947 let lines = text.wrap(5, Some(JustifyMethod::Left), None, 8, false);
2948 assert_eq!(lines.len(), 1);
2949 assert_eq!(lines[0].plain_text(), "hi ");
2950 }
2951
2952 #[test]
2953 fn test_wrap_right_justify() {
2954 use crate::console::JustifyMethod;
2955 let text = Text::plain("hi");
2956 let lines = text.wrap(5, Some(JustifyMethod::Right), None, 8, false);
2957 assert_eq!(lines.len(), 1);
2958 assert_eq!(lines[0].plain_text(), " hi");
2959 }
2960
2961 #[test]
2962 fn test_wrap_center_justify() {
2963 use crate::console::JustifyMethod;
2964 let text = Text::plain("hi");
2965 let lines = text.wrap(6, Some(JustifyMethod::Center), None, 8, false);
2966 assert_eq!(lines.len(), 1);
2967 assert_eq!(lines[0].plain_text(), " hi ");
2968 }
2969
2970 #[test]
2971 fn test_wrap_fold_long_word() {
2972 use crate::console::OverflowMethod;
2973 let text = Text::plain("abcdefghij");
2974 let lines = text.wrap(4, None, Some(OverflowMethod::Fold), 8, false);
2975 assert_eq!(lines.len(), 3);
2976 assert_eq!(lines[0].plain_text(), "abcd");
2977 assert_eq!(lines[1].plain_text(), "efgh");
2978 assert_eq!(lines[2].plain_text(), "ij");
2979 }
2980
2981 #[test]
2982 fn test_wrap_no_wrap() {
2983 let text = Text::plain("hello world");
2984 let lines = text.wrap(5, None, None, 8, true);
2985 assert_eq!(lines.len(), 1);
2986 assert_eq!(lines[0].plain_text(), "hello world");
2987 }
2988
2989 #[test]
2990 fn test_render_no_wrap_still_applies_justify() {
2991 use crate::Console;
2992 use crate::console::{ConsoleOptions, JustifyMethod};
2993
2994 let console = Console::new();
2995 let options = ConsoleOptions {
2996 max_width: 6,
2997 justify: Some(JustifyMethod::Center),
2998 no_wrap: true,
2999 ..Default::default()
3000 };
3001
3002 let text = Text::plain("hi");
3003 let segments = text.render(&console, &options);
3004 let rendered: String = segments.iter().map(|s| s.text.as_ref()).collect();
3005 assert_eq!(rendered, " hi ");
3006 }
3007
3008 #[test]
3009 fn test_justify_full_distributes_extra_spaces_right_to_left() {
3010 let text = Text::plain("a b c");
3015 let justified = text.justify_full(8);
3016 assert_eq!(justified.plain_text(), "a b c");
3017 }
3018
3019 #[test]
3020 fn test_wrap_with_tabs() {
3021 let text = Text::plain("a\tb");
3022 let lines = text.wrap(20, None, None, 4, false);
3023 assert_eq!(lines.len(), 1);
3024 assert_eq!(lines[0].plain_text(), "a b");
3025 }
3026
3027 #[test]
3028 fn test_wrap_preserves_spans() {
3029 let mut text = Text::plain("hello world");
3030 text.stylize(0, 5, Style::new().with_bold(true));
3031 let lines = text.wrap(6, None, None, 8, false);
3032
3033 assert_eq!(lines.len(), 2);
3034 assert!(!lines[0].spans().is_empty());
3036 assert_eq!(lines[0].spans()[0].style.bold, Some(true));
3037 }
3038
3039 #[test]
3040 fn test_wrap_cjk() {
3041 let text = Text::plain("你好世界");
3042 let lines = text.wrap(5, None, None, 8, false);
3044 assert_eq!(lines.len(), 2);
3045 assert_eq!(lines[0].plain_text(), "你好");
3046 assert_eq!(lines[1].plain_text(), "世界");
3047 }
3048
3049 #[test]
3050 fn test_wrap_full_justify() {
3051 use crate::console::JustifyMethod;
3052 let text = Text::plain("a b c");
3053 let lines = text.wrap(7, Some(JustifyMethod::Full), None, 8, false);
3054 assert_eq!(lines.len(), 1);
3055 assert_eq!(lines[0].plain_text(), "a b c ");
3057 }
3058
3059 #[test]
3060 fn test_wrap_full_justify_multiline() {
3061 use crate::console::JustifyMethod;
3062 let text = Text::plain("a b c d e");
3063 let lines = text.wrap(5, Some(JustifyMethod::Full), None, 8, false);
3064 assert!(lines.len() >= 2);
3066 }
3067
3068 #[test]
3071 fn test_slice_basic() {
3072 let text = Text::plain("Hello World");
3073 let sliced = text.slice(0, 5);
3074 assert_eq!(sliced.plain_text(), "Hello");
3075 }
3076
3077 #[test]
3078 fn test_slice_middle() {
3079 let text = Text::plain("Hello World");
3080 let sliced = text.slice(6, 11);
3081 assert_eq!(sliced.plain_text(), "World");
3082 }
3083
3084 #[test]
3085 fn test_slice_preserves_spans() {
3086 let mut text = Text::plain("Hello World");
3087 text.stylize(0, 5, Style::new().with_bold(true));
3088
3089 let sliced = text.slice(0, 5);
3090 assert_eq!(sliced.plain_text(), "Hello");
3091 assert!(!sliced.spans().is_empty());
3092 assert_eq!(sliced.spans()[0].start, 0);
3093 assert_eq!(sliced.spans()[0].end, 5);
3094 }
3095
3096 #[test]
3097 fn test_slice_clips_span() {
3098 let mut text = Text::plain("Hello World");
3099 text.stylize(3, 8, Style::new().with_bold(true));
3100
3101 let sliced = text.slice(0, 5);
3102 assert_eq!(sliced.plain_text(), "Hello");
3103 assert!(!sliced.spans().is_empty());
3104 assert_eq!(sliced.spans()[0].start, 3);
3105 assert_eq!(sliced.spans()[0].end, 5);
3106 }
3107
3108 #[test]
3109 fn test_slice_empty_range() {
3110 let text = Text::plain("Hello World");
3111 let sliced = text.slice(5, 5);
3112 assert_eq!(sliced.plain_text(), "");
3113 }
3114
3115 #[test]
3118 fn test_align_left() {
3119 use crate::console::JustifyMethod;
3120 let mut text = Text::plain("hi");
3121 text.align(JustifyMethod::Left, 6);
3122 assert_eq!(text.plain_text(), "hi ");
3123 }
3124
3125 #[test]
3126 fn test_align_right() {
3127 use crate::console::JustifyMethod;
3128 let mut text = Text::plain("hi");
3129 text.align(JustifyMethod::Right, 6);
3130 assert_eq!(text.plain_text(), " hi");
3131 }
3132
3133 #[test]
3134 fn test_align_center() {
3135 use crate::console::JustifyMethod;
3136 let mut text = Text::plain("hi");
3137 text.align(JustifyMethod::Center, 6);
3138 assert_eq!(text.plain_text(), " hi ");
3139 }
3140
3141 #[test]
3144 fn test_contains_true() {
3145 let text = Text::plain("Hello World");
3146 assert!(text.contains("World"));
3147 }
3148
3149 #[test]
3150 fn test_contains_false() {
3151 let text = Text::plain("Hello World");
3152 assert!(!text.contains("xyz"));
3153 }
3154
3155 #[test]
3158 fn test_get_style_at_offset() {
3159 let mut text = Text::plain("Hello World");
3160 let bold = Style::new().with_bold(true);
3161 text.stylize(0, 5, bold);
3162
3163 let style_at_0 = text.get_style_at_offset(0);
3164 assert_eq!(style_at_0.bold, Some(true));
3165
3166 let style_at_6 = text.get_style_at_offset(6);
3167 assert_ne!(style_at_6.bold, Some(true));
3168 }
3169
3170 #[test]
3173 fn test_set_length_pad() {
3174 let mut text = Text::plain("hi");
3175 text.set_length(5);
3176 assert_eq!(text.cell_len(), 5);
3177 assert!(text.plain_text().starts_with("hi"));
3178 }
3179
3180 #[test]
3181 fn test_set_length_crop() {
3182 let mut text = Text::plain("hello world");
3183 text.set_length(5);
3184 assert_eq!(text.plain_text(), "hello");
3185 }
3186
3187 #[test]
3190 fn test_text_right_crop() {
3191 let mut text = Text::plain("Hello World");
3192 text.right_crop(6);
3193 assert_eq!(text.plain_text(), "Hello");
3194 }
3195
3196 #[test]
3197 fn test_text_right_crop_adjusts_spans() {
3198 let mut text = Text::plain("Hello World");
3199 text.stylize(0, 11, Style::new().with_bold(true));
3200 text.right_crop(6);
3201 assert_eq!(text.spans()[0].end, 5);
3202 }
3203
3204 #[test]
3207 fn test_fit_basic() {
3208 let text = Text::plain("Hello\nWorld");
3209 let lines = text.fit(10);
3210 assert_eq!(lines.len(), 2);
3211 assert_eq!(lines[0].cell_len(), 10);
3212 assert_eq!(lines[1].cell_len(), 10);
3213 }
3214
3215 #[test]
3218 fn test_remove_suffix_found() {
3219 let mut text = Text::plain("Hello World");
3220 text.remove_suffix(" World");
3221 assert_eq!(text.plain_text(), "Hello");
3222 }
3223
3224 #[test]
3225 fn test_remove_suffix_not_found() {
3226 let mut text = Text::plain("Hello World");
3227 text.remove_suffix("xyz");
3228 assert_eq!(text.plain_text(), "Hello World");
3229 }
3230
3231 #[test]
3234 fn test_extend_style() {
3235 let mut text = Text::plain("hello");
3236 text.stylize(0, 5, Style::new().with_bold(true));
3237 text.extend_style(3);
3238 assert_eq!(text.len(), 8);
3239 assert_eq!(text.spans()[0].end, 8);
3240 }
3241
3242 #[test]
3245 fn test_detect_indentation() {
3246 let text = Text::plain(" foo\n bar\n baz");
3247 let indent = text.detect_indentation();
3248 assert_eq!(indent, 2);
3249 }
3250
3251 #[test]
3254 fn test_copy_styles() {
3255 let mut text = Text::plain("Hello World");
3256 let mut other = Text::plain("Hello World");
3257 other.stylize(0, 5, Style::new().with_bold(true));
3258
3259 text.copy_styles(&other);
3260 assert_eq!(text.spans().len(), 1);
3261 }
3262
3263 #[test]
3266 fn test_apply_meta_adds_metadata_span() {
3267 let mut text = Text::plain("Hello World");
3268 let mut meta = BTreeMap::new();
3269 meta.insert("foo".to_string(), crate::style::MetaValue::str("bar"));
3270
3271 text.apply_meta(meta, 0, Some(5));
3272
3273 assert_eq!(text.spans().len(), 1);
3274 let span = &text.spans()[0];
3275 assert_eq!(span.start, 0);
3276 assert_eq!(span.end, 5);
3277 assert!(span.style.is_null());
3278 let span_meta = span.meta.as_ref().and_then(|m| m.meta.as_ref());
3279 assert!(span_meta.is_some());
3280 assert_eq!(
3281 span_meta.and_then(|m| m.get("foo")),
3282 Some(&crate::style::MetaValue::str("bar"))
3283 );
3284 }
3285
3286 #[test]
3287 fn test_apply_meta_supports_negative_offsets() {
3288 let mut text = Text::plain("abcdef");
3289 let mut meta = BTreeMap::new();
3290 meta.insert(
3291 "@click".to_string(),
3292 crate::style::MetaValue::str("handler"),
3293 );
3294
3295 text.apply_meta(meta, -3, None);
3296
3297 assert_eq!(text.spans().len(), 1);
3298 assert_eq!(text.spans()[0].start, 3);
3299 assert_eq!(text.spans()[0].end, 6);
3300 }
3301
3302 #[test]
3305 fn test_append_tokens_appends_content_and_styles() {
3306 let mut text = Text::plain("A");
3307 let bold = Style::new().with_bold(true);
3308 let italic = Style::new().with_italic(true);
3309
3310 text.append_tokens(vec![("B", Some(bold)), ("C", None), ("D", Some(italic))]);
3311
3312 assert_eq!(text.plain_text(), "ABCD");
3313 assert_eq!(text.spans().len(), 2);
3314 assert_eq!(text.spans()[0].start, 1);
3315 assert_eq!(text.spans()[0].end, 2);
3316 assert_eq!(text.spans()[0].style.bold, Some(true));
3317 assert_eq!(text.spans()[1].start, 3);
3318 assert_eq!(text.spans()[1].end, 4);
3319 assert_eq!(text.spans()[1].style.italic, Some(true));
3320 }
3321
3322 #[test]
3323 fn test_append_tokens_strips_control_codes() {
3324 let mut text = Text::new();
3325 let bold = Style::new().with_bold(true);
3326
3327 text.append_tokens(vec![("a\x07b", Some(bold))]);
3328
3329 assert_eq!(text.plain_text(), "ab");
3330 assert_eq!(text.spans().len(), 1);
3331 assert_eq!(text.spans()[0].start, 0);
3332 assert_eq!(text.spans()[0].end, 2);
3333 }
3334
3335 #[test]
3338 fn test_add_text() {
3339 let a = Text::plain("Hello ");
3340 let b = Text::plain("World");
3341 let result = a + b;
3342 assert_eq!(result.plain_text(), "Hello World");
3343 }
3344
3345 #[test]
3346 fn test_add_str() {
3347 let a = Text::plain("Hello ");
3348 let result = a + "World";
3349 assert_eq!(result.plain_text(), "Hello World");
3350 }
3351
3352 #[test]
3355 fn test_to_markup_plain() {
3356 let text = Text::plain("Hello World");
3357 assert_eq!(text.to_markup(), "Hello World");
3358 }
3359
3360 #[test]
3361 fn test_to_markup_with_style() {
3362 let mut text = Text::plain("Hello");
3363 text.stylize(0, 5, Style::new().with_bold(true));
3364 let markup = text.to_markup();
3365 assert!(markup.contains("[bold]"));
3366 assert!(markup.contains("[/bold]"));
3367 assert!(markup.contains("Hello"));
3368 }
3369}