use std::collections::{BTreeMap, HashMap};
use std::fmt;
use std::hash::{Hash, Hasher};
use std::ops::{Add, AddAssign};
use crate::ansi::AnsiDecoder;
use crate::cells::cell_len;
use crate::console::{Console, ConsoleOptions};
use crate::renderables::Renderable;
use crate::segment::Segment;
use crate::style::Style;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum JustifyMethod {
#[default]
Default,
Left,
Center,
Right,
Full,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OverflowMethod {
#[default]
Fold,
Crop,
Ellipsis,
Ignore,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Span {
pub start: usize,
pub end: usize,
pub style: Style,
}
impl Span {
#[must_use]
pub fn new(start: usize, end: usize, style: Style) -> Self {
Self {
start: start.min(end),
end: end.max(start),
style,
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.start >= self.end
}
#[must_use]
pub fn len(&self) -> usize {
self.end.saturating_sub(self.start)
}
#[must_use]
pub fn move_right(&self, offset: usize, max: usize) -> Self {
Self {
start: (self.start + offset).min(max),
end: (self.end + offset).min(max),
style: self.style.clone(),
}
}
#[must_use]
pub fn split(&self, offset: usize) -> (Self, Self) {
let split_point = self.start + offset;
(
Self {
start: self.start,
end: split_point.min(self.end),
style: self.style.clone(),
},
Self {
start: split_point.min(self.end),
end: self.end,
style: self.style.clone(),
},
)
}
#[must_use]
pub fn adjust(&self, offset: usize) -> Self {
Self {
start: self.start.saturating_sub(offset),
end: self.end.saturating_sub(offset),
style: self.style.clone(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Text {
plain: String,
spans: Vec<Span>,
length: usize,
style: Style,
pub justify: JustifyMethod,
pub overflow: OverflowMethod,
pub no_wrap: bool,
pub end: String,
pub tab_size: usize,
}
#[derive(Debug, Clone)]
pub struct FromAnsiOptions {
pub style: Style,
pub justify: Option<JustifyMethod>,
pub overflow: Option<OverflowMethod>,
pub no_wrap: Option<bool>,
pub end: String,
pub tab_size: Option<usize>,
}
impl Default for FromAnsiOptions {
fn default() -> Self {
Self {
style: Style::null(),
justify: None,
overflow: None,
no_wrap: None,
end: "\n".to_string(),
tab_size: Some(8),
}
}
}
impl Text {
#[must_use]
pub fn from_ansi(text: &str) -> Self {
Self::from_ansi_with_options(text, &FromAnsiOptions::default())
}
#[must_use]
pub fn from_ansi_with_options(text: &str, options: &FromAnsiOptions) -> Self {
let mut decoder = AnsiDecoder::new();
let lines = decoder.decode(text);
let mut result = Self::new("");
result.set_style(options.style.clone());
if let Some(justify) = options.justify {
result.justify = justify;
}
if let Some(overflow) = options.overflow {
result.overflow = overflow;
}
if let Some(no_wrap) = options.no_wrap {
result.no_wrap = no_wrap;
}
result.end.clone_from(&options.end);
result.tab_size = options.tab_size.unwrap_or(8);
for (idx, line) in lines.iter().enumerate() {
if idx > 0 {
result.append("\n");
}
result.append_text(line);
}
result
}
#[must_use]
pub fn new(text: impl Into<String>) -> Self {
let plain: String = text.into();
let length = plain.chars().count();
Self {
plain,
spans: Vec::new(),
length,
style: Style::default(),
justify: JustifyMethod::Default,
overflow: OverflowMethod::Fold,
no_wrap: false,
end: String::from("\n"),
tab_size: 8,
}
}
#[must_use]
pub fn styled(text: impl Into<String>, style: Style) -> Self {
let plain: String = text.into();
let length = plain.chars().count();
let span = if length > 0 {
vec![Span::new(0, length, style.clone())]
} else {
Vec::new()
};
Self {
plain,
spans: span,
length,
style,
justify: JustifyMethod::Default,
overflow: OverflowMethod::Fold,
no_wrap: false,
end: String::from("\n"),
tab_size: 8,
}
}
#[must_use]
pub fn assemble(pieces: &[(&str, Option<Style>)]) -> Self {
let mut text = Self::new("");
for (content, style) in pieces {
if let Some(s) = style {
text.append_styled(content, s.clone());
} else {
text.append(content);
}
}
text
}
#[must_use]
pub fn plain(&self) -> &str {
&self.plain
}
#[must_use]
pub fn spans(&self) -> &[Span] {
&self.spans
}
#[must_use]
pub fn len(&self) -> usize {
self.length
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.plain.is_empty()
}
#[must_use]
pub fn cell_len(&self) -> usize {
cell_len(&self.plain)
}
#[must_use]
pub fn style(&self) -> &Style {
&self.style
}
pub fn set_style(&mut self, style: Style) {
self.style = style;
}
pub fn append(&mut self, text: &str) {
self.plain.push_str(text);
self.length += text.chars().count();
}
pub fn append_styled(&mut self, text: &str, style: Style) {
let start = self.length;
let text_len = text.chars().count();
self.plain.push_str(text);
self.length += text_len;
if text_len > 0 {
self.spans.push(Span::new(start, self.length, style));
}
}
pub fn append_text(&mut self, other: &Text) {
let offset = self.length;
self.plain.push_str(&other.plain);
self.length += other.length;
for span in &other.spans {
self.spans.push(span.move_right(offset, self.length));
}
}
pub fn stylize(&mut self, start: usize, end: usize, style: Style) {
let clamped_start = start.min(self.length);
let clamped_end = end.min(self.length);
if clamped_start < clamped_end {
self.spans
.push(Span::new(clamped_start, clamped_end, style));
}
}
pub fn stylize_all(&mut self, style: Style) {
if self.length > 0 {
self.spans.push(Span::new(0, self.length, style));
}
}
pub fn highlight_regex(&mut self, pattern: &str, style: &Style) -> Result<(), regex::Error> {
let re = regex::Regex::new(pattern)?;
let char_starts: Vec<usize> = self.plain.char_indices().map(|(i, _)| i).collect();
let total_chars = char_starts.len();
let total_bytes = self.plain.len();
for mat in re.find_iter(&self.plain) {
let byte_start = mat.start();
let byte_end = mat.end();
let char_start = char_starts.binary_search(&byte_start).unwrap_or_else(|x| x);
let char_end = if byte_end == total_bytes {
total_chars
} else {
char_starts.binary_search(&byte_end).unwrap_or_else(|x| x)
};
if char_start < char_end {
self.spans
.push(Span::new(char_start, char_end, style.clone()));
}
}
Ok(())
}
pub fn highlight_words(&mut self, words: &[&str], style: &Style, case_sensitive: bool) {
if words.is_empty() {
return;
}
if case_sensitive {
let char_starts: Vec<usize> = self.plain.char_indices().map(|(i, _)| i).collect();
let total_chars = char_starts.len();
let total_bytes = self.plain.len();
for word in words {
if word.is_empty() {
continue;
}
let mut search_start = 0;
while let Some(pos) = self.plain[search_start..].find(word) {
let byte_start = search_start + pos;
let byte_end = byte_start + word.len();
let char_start = char_starts.binary_search(&byte_start).unwrap_or_else(|x| x);
let char_end = if byte_end == total_bytes {
total_chars
} else {
char_starts.binary_search(&byte_end).unwrap_or_else(|x| x)
};
if char_start < char_end {
self.spans
.push(Span::new(char_start, char_end, style.clone()));
}
search_start = byte_end;
}
}
return;
}
let mut lowered = String::new();
let mut lower_to_original: Vec<usize> = Vec::new();
for (orig_idx, c) in self.plain.chars().enumerate() {
for lower in c.to_lowercase() {
lowered.push(lower);
lower_to_original.push(orig_idx);
}
}
let lowered_char_starts: Vec<usize> = lowered.char_indices().map(|(i, _)| i).collect();
let total_lowered_chars = lowered_char_starts.len();
let total_lowered_bytes = lowered.len();
for word in words {
let search_word = word.to_lowercase();
if search_word.is_empty() {
continue;
}
let mut search_start = 0;
while let Some(pos) = lowered[search_start..].find(&search_word) {
let byte_start = search_start + pos;
let byte_end = byte_start + search_word.len();
let char_start_lowered = lowered_char_starts
.binary_search(&byte_start)
.unwrap_or_else(|x| x);
let char_end_lowered = if byte_end == total_lowered_bytes {
total_lowered_chars
} else {
lowered_char_starts
.binary_search(&byte_end)
.unwrap_or_else(|x| x)
};
if char_start_lowered < char_end_lowered
&& char_end_lowered <= lower_to_original.len()
{
let orig_start = lower_to_original[char_start_lowered];
let orig_end = lower_to_original[char_end_lowered - 1] + 1;
if orig_start < orig_end {
self.spans
.push(Span::new(orig_start, orig_end, style.clone()));
}
}
search_start = byte_end;
}
}
}
#[must_use]
pub fn slice(&self, start: usize, end: usize) -> Self {
let clamped_start = start.min(self.length);
let clamped_end = end.min(self.length).max(clamped_start);
if clamped_start >= clamped_end {
return Self::new("");
}
let mut indices = self.plain.char_indices();
let byte_start = indices
.nth(clamped_start)
.map_or(self.plain.len(), |(i, _)| i);
let byte_end = if clamped_end > clamped_start {
indices
.nth(clamped_end - clamped_start - 1)
.map_or(self.plain.len(), |(i, _)| i)
} else {
byte_start
};
let plain = self.plain[byte_start..byte_end].to_string();
let mut spans = Vec::new();
for span in &self.spans {
if span.end <= clamped_start || span.start >= clamped_end {
continue; }
let new_start = span.start.max(clamped_start) - clamped_start;
let new_end = span.end.min(clamped_end) - clamped_start;
if new_start < new_end {
spans.push(Span::new(new_start, new_end, span.style.clone()));
}
}
Self {
plain,
spans,
length: clamped_end - clamped_start,
style: self.style.clone(),
justify: self.justify,
overflow: self.overflow,
no_wrap: self.no_wrap,
end: self.end.clone(),
tab_size: self.tab_size,
}
}
#[must_use]
pub fn join<'a, I>(&self, items: I) -> Self
where
I: IntoIterator<Item = &'a Self>,
{
let mut result = Self::new("");
let mut first = true;
for item in items {
if first {
first = false;
} else {
result.append_text(self);
}
result.append_text(item);
}
result
}
#[must_use]
pub fn split_lines(&self) -> Vec<Self> {
let mut lines = Vec::new();
let mut start_byte = 0;
let mut start_char = 0;
for (char_idx, (byte_idx, c)) in self.plain.char_indices().enumerate() {
if c == '\n' {
let plain = self.plain[start_byte..byte_idx].to_string();
let length = char_idx - start_char;
let mut spans = Vec::new();
for span in &self.spans {
if span.end <= start_char || span.start >= char_idx {
continue;
}
let new_start = span.start.max(start_char) - start_char;
let new_end = span.end.min(char_idx) - start_char;
if new_start < new_end {
spans.push(Span::new(new_start, new_end, span.style.clone()));
}
}
lines.push(Self {
plain,
spans,
length,
style: self.style.clone(),
justify: self.justify,
overflow: self.overflow,
no_wrap: self.no_wrap,
end: self.end.clone(),
tab_size: self.tab_size,
});
start_byte = byte_idx + c.len_utf8();
start_char = char_idx + 1;
}
}
if start_byte <= self.plain.len() {
let plain = self.plain[start_byte..].to_string();
let length = self.length - start_char;
let mut spans = Vec::new();
for span in &self.spans {
if span.end <= start_char || span.start >= self.length {
continue;
}
let new_start = span.start.max(start_char) - start_char;
let new_end = span.end.min(self.length) - start_char;
if new_start < new_end {
spans.push(Span::new(new_start, new_end, span.style.clone()));
}
}
lines.push(Self {
plain,
spans,
length,
style: self.style.clone(),
justify: self.justify,
overflow: self.overflow,
no_wrap: self.no_wrap,
end: self.end.clone(),
tab_size: self.tab_size,
});
}
if lines.is_empty() {
lines.push(Self::new(""));
}
lines
}
#[must_use]
pub fn divide(&self, offsets: &[usize]) -> Vec<Self> {
if offsets.is_empty() {
return vec![self.clone()];
}
let mut result = Vec::new();
let mut prev = 0;
for &offset in offsets {
let clamped = offset.min(self.length);
result.push(self.slice(prev, clamped));
prev = clamped;
}
if prev < self.length {
result.push(self.slice(prev, self.length));
} else {
result.push(Self::new(""));
}
result
}
#[must_use]
pub fn expand_tabs(&self, tab_size: usize) -> Self {
if tab_size == 0 || !self.plain.contains('\t') {
return self.clone();
}
let mut new_plain = String::new();
let mut char_map: Vec<usize> = Vec::new(); let mut new_len = 0;
let mut col = 0;
for (old_idx, c) in self.plain.chars().enumerate() {
if c == '\t' {
let spaces = tab_size - (col % tab_size);
for _ in 0..spaces {
new_plain.push(' ');
char_map.push(old_idx);
new_len += 1;
col += 1;
}
} else {
new_plain.push(c);
char_map.push(old_idx);
new_len += 1;
if c == '\n' {
col = 0;
} else {
col += 1;
}
}
}
let mut new_spans = Vec::new();
for span in &self.spans {
let new_start = char_map.partition_point(|&old| old < span.start);
let new_end = char_map.partition_point(|&old| old < span.end);
if new_start < new_end {
new_spans.push(Span::new(new_start, new_end, span.style.clone()));
}
}
Self {
plain: new_plain,
spans: new_spans,
length: new_len,
style: self.style.clone(),
justify: self.justify,
overflow: self.overflow,
no_wrap: self.no_wrap,
end: self.end.clone(),
tab_size: self.tab_size,
}
}
pub fn truncate(&mut self, max_width: usize, overflow: OverflowMethod, pad: bool) {
let current_width = self.cell_len();
if current_width <= max_width {
if pad && current_width < max_width {
let padding = " ".repeat(max_width - current_width);
self.append(&padding);
}
return;
}
match overflow {
OverflowMethod::Crop | OverflowMethod::Fold => {
let (cut_pos, width) = self.find_truncation_point(max_width);
*self = self.slice(0, cut_pos);
if pad && width < max_width {
let padding = " ".repeat(max_width - width);
self.append(&padding);
}
}
OverflowMethod::Ellipsis => {
if max_width < 3 {
let (cut_pos, _) = self.find_truncation_point(max_width);
*self = self.slice(0, cut_pos);
return;
}
let target_width = max_width - 3;
let (cut_pos, _) = self.find_truncation_point(target_width);
*self = self.slice(0, cut_pos);
self.append("...");
if pad {
let final_width = self.cell_len();
if final_width < max_width {
let padding = " ".repeat(max_width - final_width);
self.append(&padding);
}
}
}
OverflowMethod::Ignore => {
}
}
}
fn find_truncation_point(&self, max_width: usize) -> (usize, usize) {
let mut width = 0;
let mut cut_pos = 0;
for (i, c) in self.plain.chars().enumerate() {
let char_width = crate::cells::get_character_cell_size(c);
if width + char_width > max_width {
break;
}
width += char_width;
cut_pos = i + 1;
}
(cut_pos, width)
}
pub fn pad(&mut self, width: usize, align: JustifyMethod) {
let current_width = self.cell_len();
if current_width >= width {
return;
}
let padding = width - current_width;
match align {
JustifyMethod::Left | JustifyMethod::Default => {
self.append(&" ".repeat(padding));
}
JustifyMethod::Right => {
let mut new_text = Self::new(" ".repeat(padding));
new_text.append_text(self);
*self = new_text;
}
JustifyMethod::Center => {
let left_pad = padding / 2;
let right_pad = padding - left_pad;
let mut new_text = Self::new(" ".repeat(left_pad));
new_text.append_text(self);
new_text.append(&" ".repeat(right_pad));
*self = new_text;
}
JustifyMethod::Full => {
self.append(&" ".repeat(padding));
}
}
}
#[must_use]
pub fn strip(&self) -> Self {
let chars: Vec<char> = self.plain.chars().collect();
let start = chars
.iter()
.position(|c| !c.is_whitespace())
.unwrap_or(chars.len());
let end = chars
.iter()
.rposition(|c| !c.is_whitespace())
.map_or(0, |p| p + 1);
if start >= end {
Self::new("")
} else {
self.slice(start, end)
}
}
#[must_use]
pub fn to_lowercase(&self) -> Self {
self.map_case(char::to_lowercase)
}
#[must_use]
pub fn to_uppercase(&self) -> Self {
self.map_case(char::to_uppercase)
}
fn map_case<I, F>(&self, mut mapper: F) -> Self
where
I: Iterator<Item = char>,
F: FnMut(char) -> I,
{
let old_len = self.plain.chars().count();
let mut positions = Vec::with_capacity(old_len + 1);
let mut new_plain = String::new();
let mut new_len = 0usize;
positions.push(0);
for c in self.plain.chars() {
for mapped in mapper(c) {
new_plain.push(mapped);
new_len += 1;
}
positions.push(new_len);
}
let mut new_spans = Vec::new();
for span in &self.spans {
let start = span.start.min(old_len);
let end = span.end.min(old_len);
let new_start = positions[start];
let new_end = positions[end];
if new_start < new_end {
new_spans.push(Span::new(new_start, new_end, span.style.clone()));
}
}
Self {
plain: new_plain,
spans: new_spans,
length: new_len,
style: self.style.clone(),
justify: self.justify,
overflow: self.overflow,
no_wrap: self.no_wrap,
end: self.end.clone(),
tab_size: self.tab_size,
}
}
#[must_use]
pub fn render<'a>(&'a self, end: &'a str) -> Vec<Segment<'a>> {
if self.plain.is_empty() {
return if end.is_empty() {
Vec::new()
} else {
vec![Segment::new(end, None)]
};
}
let mut events: BTreeMap<usize, Vec<(usize, bool)>> = BTreeMap::new();
for (idx, span) in self.spans.iter().enumerate() {
events.entry(span.start).or_default().push((idx, true));
events.entry(span.end).or_default().push((idx, false));
}
let mut byte_indices: Vec<usize> = Vec::with_capacity(self.length + 1);
for (i, _) in self.plain.char_indices() {
byte_indices.push(i);
}
byte_indices.push(self.plain.len());
let mut result = Vec::with_capacity(self.spans.len() + 2);
let mut active_spans: Vec<usize> = Vec::with_capacity(self.spans.len());
let mut style_cache: HashMap<u64, Style> = HashMap::with_capacity(self.spans.len() + 1);
let mut pos = 0;
for (event_pos, span_events) in events {
if event_pos > pos && pos < self.length {
let start_byte = byte_indices[pos];
let end_char_idx = event_pos.min(self.length);
let end_byte = byte_indices[end_char_idx];
let text_slice = &self.plain[start_byte..end_byte];
let style = self.compute_style(&active_spans, &mut style_cache);
result.push(Segment::new(text_slice, Some(style)));
pos = event_pos;
}
let mut ends: Vec<usize> = Vec::new();
let mut starts: Vec<usize> = Vec::new();
for (span_idx, is_start) in span_events {
if is_start {
starts.push(span_idx);
} else {
ends.push(span_idx);
}
}
for span_idx in ends {
active_spans.retain(|&x| x != span_idx);
}
active_spans.extend(starts);
}
if pos < self.length {
let start_byte = byte_indices[pos];
let end_byte = byte_indices[self.length];
let text_slice = &self.plain[start_byte..end_byte];
let style = self.compute_style(&active_spans, &mut style_cache);
result.push(Segment::new(text_slice, Some(style)));
}
if !end.is_empty() {
result.push(Segment::new(end, None));
}
result
}
fn compute_style(&self, active_spans: &[usize], cache: &mut HashMap<u64, Style>) -> Style {
let cache_key = self.hash_spans(active_spans);
if let Some(cached) = cache.get(&cache_key) {
return cached.clone();
}
let mut combined = self.style.clone();
for &span_idx in active_spans {
if let Some(span) = self.spans.get(span_idx) {
combined = combined.combine(&span.style);
}
}
cache.insert(cache_key, combined.clone());
combined
}
fn hash_spans(&self, spans: &[usize]) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
spans.hash(&mut hasher);
hasher.finish()
}
#[must_use]
pub fn wrap(&self, width: usize) -> Vec<Self> {
if width == 0 {
return vec![Self::new("")];
}
let expanded = self.expand_tabs(self.tab_size);
if expanded.no_wrap || expanded.cell_len() <= width {
return vec![expanded];
}
let mut lines = Vec::new();
for line in expanded.split_lines() {
if line.cell_len() <= width {
lines.push(line);
} else {
lines.extend(self.wrap_line(&line, width));
}
}
lines
}
fn wrap_line(&self, line: &Text, width: usize) -> Vec<Self> {
let mut result = Vec::new();
let chars: Vec<char> = line.plain.chars().collect();
if chars.is_empty() {
return vec![Self::new("")];
}
match line.overflow {
OverflowMethod::Fold => {
let mut current_line_start = 0;
let mut current_width = 0;
let mut last_space = None;
for (i, c) in chars.iter().enumerate() {
let char_width = crate::cells::get_character_cell_size(*c);
if c.is_whitespace() && *c != '\n' {
last_space = Some(i);
}
if current_width + char_width > width {
let (wrap_at, next_start) = if let Some(space_pos) = last_space {
if space_pos > current_line_start && space_pos < i {
(space_pos + 1, space_pos + 1)
} else {
(i, i)
}
} else {
(i, i)
};
if wrap_at > current_line_start {
result.push(line.slice(current_line_start, wrap_at));
}
current_line_start = next_start;
while current_line_start < chars.len()
&& chars[current_line_start].is_whitespace()
{
current_line_start += 1;
}
current_width = 0;
last_space = None;
for j in current_line_start..=i {
if j < chars.len() {
current_width += crate::cells::get_character_cell_size(chars[j]);
}
}
} else {
current_width += char_width;
}
}
if current_line_start < chars.len() {
result.push(line.slice(current_line_start, chars.len()));
}
}
OverflowMethod::Crop => {
result.push(line.slice(0, self.char_pos_for_width(line, width)));
}
OverflowMethod::Ellipsis => {
if width >= 3 {
let mut truncated = line.slice(0, self.char_pos_for_width(line, width - 3));
truncated.append("...");
result.push(truncated);
} else {
result.push(line.slice(0, self.char_pos_for_width(line, width)));
}
}
OverflowMethod::Ignore => {
result.push(line.clone());
}
}
if result.is_empty() {
result.push(Self::new(""));
}
result
}
fn char_pos_for_width(&self, text: &Text, target_width: usize) -> usize {
let mut width = 0;
for (i, c) in text.plain.chars().enumerate() {
let char_width = crate::cells::get_character_cell_size(c);
if width + char_width > target_width {
return i;
}
width += char_width;
}
text.length
}
}
impl Renderable for Text {
fn render<'a>(&'a self, _console: &Console, _options: &ConsoleOptions) -> Vec<Segment<'a>> {
self.render("")
}
}
impl fmt::Display for Text {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.plain)
}
}
impl PartialEq for Text {
fn eq(&self, other: &Self) -> bool {
self.plain == other.plain && self.spans == other.spans
}
}
impl Eq for Text {}
impl Add for Text {
type Output = Self;
fn add(mut self, rhs: Self) -> Self::Output {
self.append_text(&rhs);
self
}
}
impl AddAssign for Text {
fn add_assign(&mut self, rhs: Self) {
self.append_text(&rhs);
}
}
impl From<&str> for Text {
fn from(s: &str) -> Self {
Self::new(s)
}
}
impl From<String> for Text {
fn from(s: String) -> Self {
Self::new(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_new() {
let text = Text::new("hello");
assert_eq!(text.plain(), "hello");
assert_eq!(text.len(), 5);
assert!(!text.is_empty());
}
#[test]
fn test_text_styled() {
let style = Style::new().bold();
let text = Text::styled("hello", style);
assert_eq!(text.spans().len(), 1);
assert_eq!(text.spans()[0].start, 0);
assert_eq!(text.spans()[0].end, 5);
}
#[test]
fn test_text_append() {
let mut text = Text::new("hello");
text.append(" world");
assert_eq!(text.plain(), "hello world");
assert_eq!(text.len(), 11);
}
#[test]
fn test_text_append_styled() {
let mut text = Text::new("hello ");
text.append_styled("world", Style::new().bold());
assert_eq!(text.plain(), "hello world");
assert_eq!(text.spans().len(), 1);
assert_eq!(text.spans()[0].start, 6);
assert_eq!(text.spans()[0].end, 11);
}
#[test]
fn test_text_slice() {
let mut text = Text::new("hello world");
text.stylize(0, 5, Style::new().bold());
text.stylize(6, 11, Style::new().italic());
let slice = text.slice(3, 8);
assert_eq!(slice.plain(), "lo wo");
assert_eq!(slice.len(), 5);
assert_eq!(slice.spans().len(), 2);
}
#[test]
fn test_text_split_lines() {
let text = Text::new("line1\nline2\nline3");
let lines = text.split_lines();
assert_eq!(lines.len(), 3);
assert_eq!(lines[0].plain(), "line1");
assert_eq!(lines[1].plain(), "line2");
assert_eq!(lines[2].plain(), "line3");
}
#[test]
fn test_text_divide() {
let text = Text::new("hello world");
let parts = text.divide(&[5]);
assert_eq!(parts.len(), 2);
assert_eq!(parts[0].plain(), "hello");
assert_eq!(parts[1].plain(), " world");
}
#[test]
fn test_text_expand_tabs() {
let text = Text::new("a\tb");
let expanded = text.expand_tabs(8);
assert_eq!(expanded.plain(), "a b");
}
#[test]
fn test_text_truncate() {
let mut text = Text::new("hello world");
text.truncate(8, OverflowMethod::Ellipsis, false);
assert_eq!(text.plain(), "hello...");
}
#[test]
fn test_text_truncate_ellipsis_small_width_respects_cells() {
let mut text = Text::new("日本");
text.truncate(1, OverflowMethod::Ellipsis, false);
assert!(
text.cell_len() <= 1,
"truncate should respect cell width for small max"
);
}
#[test]
fn test_text_pad() {
let mut text = Text::new("hi");
text.pad(5, JustifyMethod::Center);
assert_eq!(text.cell_len(), 5);
}
#[test]
fn test_text_strip() {
let text = Text::new(" hello ");
let stripped = text.strip();
assert_eq!(stripped.plain(), "hello");
}
#[test]
fn test_text_render() {
let mut text = Text::new("hello world");
text.stylize(0, 5, Style::new().bold());
let segments = text.render("");
assert!(segments.len() >= 2);
}
#[test]
fn test_text_add() {
let a = Text::new("hello ");
let b = Text::new("world");
let combined = a + b;
assert_eq!(combined.plain(), "hello world");
}
#[test]
fn test_span_split() {
let span = Span::new(0, 10, Style::new().bold());
let (left, right) = span.split(5);
assert_eq!(left.start, 0);
assert_eq!(left.end, 5);
assert_eq!(right.start, 5);
assert_eq!(right.end, 10);
}
#[test]
fn test_span_move_right() {
let span = Span::new(0, 5, Style::new().bold());
let moved = span.move_right(10, 20);
assert_eq!(moved.start, 10);
assert_eq!(moved.end, 15);
}
#[test]
fn test_cell_len_cjk() {
let text = Text::new("Hello\u{4e2d}\u{6587}");
assert_eq!(text.cell_len(), 9);
}
#[test]
fn test_assemble() {
let text = Text::assemble(&[("hello ", None), ("world", Some(Style::new().bold()))]);
assert_eq!(text.plain(), "hello world");
assert_eq!(text.spans().len(), 1);
}
#[test]
fn test_text_from_str() {
let text: Text = "hello".into();
assert_eq!(text.plain(), "hello");
assert_eq!(text.len(), 5);
}
#[test]
fn test_text_from_string() {
let text: Text = String::from("hello").into();
assert_eq!(text.plain(), "hello");
assert_eq!(text.len(), 5);
}
#[test]
fn test_text_empty() {
let text = Text::new("");
assert!(text.is_empty());
assert_eq!(text.len(), 0);
assert_eq!(text.plain(), "");
}
#[test]
fn test_text_styled_empty() {
let text = Text::styled("", Style::new().bold());
assert!(text.is_empty());
assert!(text.spans().is_empty());
}
#[test]
fn test_overlapping_spans() {
let mut text = Text::new("hello world");
text.stylize(0, 11, Style::new().bold());
text.stylize(2, 9, Style::new().italic());
assert_eq!(text.spans().len(), 2);
let segments = text.render("");
assert!(!segments.is_empty());
}
#[test]
fn test_adjacent_spans() {
let mut text = Text::new("helloworld");
text.stylize(0, 5, Style::new().bold());
text.stylize(5, 10, Style::new().italic());
assert_eq!(text.spans().len(), 2);
let segments = text.render("");
assert!(segments.len() >= 2);
}
#[test]
fn test_nested_spans() {
let mut text = Text::new("hello world");
text.stylize(0, 11, Style::new().bold());
text.stylize(6, 11, Style::new().color_str("red").unwrap_or_default());
let segments = text.render("");
assert!(!segments.is_empty());
}
#[test]
fn test_render_empty() {
let text = Text::new("");
let segments = text.render("");
assert!(segments.is_empty());
}
#[test]
fn test_render_with_end() {
let text = Text::new("hello");
let segments = text.render("\n");
assert!(segments.len() >= 2);
assert_eq!(segments.last().unwrap().text, "\n");
}
#[test]
fn test_render_base_style() {
let mut text = Text::new("hello");
text.set_style(Style::new().bold());
let segments = text.render("");
assert!(!segments.is_empty());
let style = segments[0].style.as_ref().unwrap();
assert!(style.attributes.contains(crate::style::Attributes::BOLD));
}
#[test]
fn test_divide_with_span_crossing_boundary() {
let mut text = Text::new("hello world");
text.stylize(3, 9, Style::new().bold());
let parts = text.divide(&[5]);
assert_eq!(parts.len(), 2);
assert_eq!(parts[0].plain(), "hello");
assert_eq!(parts[1].plain(), " world");
assert_eq!(parts[0].spans().len(), 1);
assert_eq!(parts[0].spans()[0].start, 3);
assert_eq!(parts[0].spans()[0].end, 5);
assert_eq!(parts[1].spans().len(), 1);
assert_eq!(parts[1].spans()[0].start, 0);
assert_eq!(parts[1].spans()[0].end, 4);
}
#[test]
fn test_divide_span_starts_at_cut() {
let mut text = Text::new("hello world");
text.stylize(5, 11, Style::new().bold());
let parts = text.divide(&[5]);
assert_eq!(parts[0].spans().len(), 0); assert_eq!(parts[1].spans().len(), 1); assert_eq!(parts[1].spans()[0].start, 0);
assert_eq!(parts[1].spans()[0].end, 6);
}
#[test]
fn test_divide_span_ends_at_cut() {
let mut text = Text::new("hello world");
text.stylize(0, 5, Style::new().bold());
let parts = text.divide(&[5]);
assert_eq!(parts[0].spans().len(), 1); assert_eq!(parts[0].spans()[0].start, 0);
assert_eq!(parts[0].spans()[0].end, 5);
assert_eq!(parts[1].spans().len(), 0); }
#[test]
fn test_divide_multiple_spans() {
let mut text = Text::new("hello world!");
text.stylize(0, 5, Style::new().bold()); text.stylize(6, 11, Style::new().italic());
let parts = text.divide(&[6]);
assert_eq!(parts[0].plain(), "hello ");
assert_eq!(parts[1].plain(), "world!");
assert_eq!(parts[0].spans().len(), 1);
assert_eq!(parts[1].spans().len(), 1);
assert_eq!(parts[1].spans()[0].start, 0);
assert_eq!(parts[1].spans()[0].end, 5);
}
#[test]
fn test_divide_empty_offsets() {
let text = Text::new("hello");
let parts = text.divide(&[]);
assert_eq!(parts.len(), 1);
assert_eq!(parts[0].plain(), "hello");
}
#[test]
fn test_divide_multiple_cuts() {
let text = Text::new("hello world!");
let parts = text.divide(&[5, 6, 11]);
assert_eq!(parts.len(), 4);
assert_eq!(parts[0].plain(), "hello");
assert_eq!(parts[1].plain(), " ");
assert_eq!(parts[2].plain(), "world");
assert_eq!(parts[3].plain(), "!");
}
#[test]
fn test_divide_cut_at_end() {
let text = Text::new("hello");
let parts = text.divide(&[5]);
assert_eq!(parts.len(), 2);
assert_eq!(parts[0].plain(), "hello");
assert_eq!(parts[1].plain(), "");
}
#[test]
fn test_divide_cut_beyond_length() {
let text = Text::new("hello");
let parts = text.divide(&[10]);
assert_eq!(parts.len(), 2);
assert_eq!(parts[0].plain(), "hello");
assert_eq!(parts[1].plain(), "");
}
#[test]
fn test_wrap_basic() {
let text = Text::new("hello world foo bar");
let lines = text.wrap(10);
assert!(lines.len() >= 2);
for line in &lines {
assert!(line.cell_len() <= 10);
}
}
#[test]
fn test_wrap_preserves_break_space() {
let text = Text::new("a b");
let lines = text.wrap(2);
assert_eq!(lines.len(), 2);
assert_eq!(lines[0].plain(), "a ");
assert_eq!(lines[1].plain(), "b");
}
#[test]
fn test_wrap_long_word() {
let text = Text::new("supercalifragilistic");
let lines = text.wrap(10);
assert!(lines.len() >= 2);
for line in &lines {
assert!(line.cell_len() <= 10);
}
}
#[test]
fn test_wrap_zero_width() {
let text = Text::new("hello");
let lines = text.wrap(0);
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].plain(), "");
}
#[test]
fn test_wrap_no_wrap_flag() {
let mut text = Text::new("hello world this is long");
text.no_wrap = true;
let lines = text.wrap(10);
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].plain(), "hello world this is long");
}
#[test]
fn test_wrap_fits_width() {
let text = Text::new("hello");
let lines = text.wrap(20);
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].plain(), "hello");
}
#[test]
fn test_wrap_with_wide_chars() {
let text = Text::new("你好世界"); let lines = text.wrap(6);
assert!(lines.len() >= 2);
}
#[test]
fn test_wrap_preserves_spans() {
let mut text = Text::new("hello world");
text.stylize(0, 5, Style::new().bold());
let lines = text.wrap(6);
assert!(!lines[0].spans().is_empty());
}
#[test]
fn test_wrap_overflow_crop() {
let mut text = Text::new("hello world this is too long");
text.overflow = OverflowMethod::Crop;
let lines = text.wrap(10);
assert_eq!(lines.len(), 1);
assert!(lines[0].cell_len() <= 10);
}
#[test]
fn test_wrap_overflow_ellipsis() {
let mut text = Text::new("hello world this is too long");
text.overflow = OverflowMethod::Ellipsis;
let lines = text.wrap(10);
assert_eq!(lines.len(), 1);
assert!(lines[0].plain().ends_with("..."));
}
#[test]
fn test_wrap_overflow_ellipsis_narrow_respects_cells() {
let mut text = Text::new("你");
text.overflow = OverflowMethod::Ellipsis;
let lines = text.wrap(1);
assert_eq!(lines.len(), 1);
assert!(lines[0].cell_len() <= 1);
}
#[test]
fn test_pad_left() {
let mut text = Text::new("hi");
text.pad(5, JustifyMethod::Left);
assert_eq!(text.cell_len(), 5);
assert_eq!(text.plain(), "hi ");
}
#[test]
fn test_pad_right() {
let mut text = Text::new("hi");
text.pad(5, JustifyMethod::Right);
assert_eq!(text.cell_len(), 5);
assert_eq!(text.plain(), " hi");
}
#[test]
fn test_pad_center() {
let mut text = Text::new("hi");
text.pad(6, JustifyMethod::Center);
assert_eq!(text.cell_len(), 6);
assert_eq!(text.plain(), " hi ");
}
#[test]
fn test_pad_full() {
let mut text = Text::new("hi");
text.pad(5, JustifyMethod::Full);
assert_eq!(text.cell_len(), 5);
}
#[test]
fn test_pad_already_wide() {
let mut text = Text::new("hello");
text.pad(3, JustifyMethod::Center);
assert_eq!(text.plain(), "hello");
}
#[test]
fn test_slice_empty_range() {
let text = Text::new("hello");
let slice = text.slice(3, 3);
assert!(slice.is_empty());
}
#[test]
fn test_slice_out_of_bounds() {
let text = Text::new("hello");
let slice = text.slice(10, 20);
assert!(slice.is_empty());
}
#[test]
fn test_slice_reversed_range() {
let text = Text::new("hello");
let slice = text.slice(4, 2);
assert!(slice.is_empty());
}
#[test]
fn test_slice_preserves_span() {
let mut text = Text::new("hello world");
text.stylize(0, 5, Style::new().bold());
let slice = text.slice(0, 3);
assert_eq!(slice.plain(), "hel");
assert_eq!(slice.spans().len(), 1);
assert_eq!(slice.spans()[0].start, 0);
assert_eq!(slice.spans()[0].end, 3);
}
#[test]
fn test_span_is_empty() {
let empty = Span::new(5, 5, Style::new());
assert!(empty.is_empty());
let non_empty = Span::new(0, 5, Style::new());
assert!(!non_empty.is_empty());
}
#[test]
fn test_span_len() {
let span = Span::new(3, 10, Style::new());
assert_eq!(span.len(), 7);
}
#[test]
fn test_span_adjust() {
let span = Span::new(10, 15, Style::new());
let adjusted = span.adjust(5);
assert_eq!(adjusted.start, 5);
assert_eq!(adjusted.end, 10);
}
#[test]
fn test_span_new_swaps_if_needed() {
let span = Span::new(10, 5, Style::new());
assert_eq!(span.start, 5);
assert_eq!(span.end, 10);
}
#[test]
fn test_expand_tabs_multiple() {
let text = Text::new("a\tb\tc");
let expanded = text.expand_tabs(4);
assert!(!expanded.plain().contains('\t'));
}
#[test]
fn test_expand_tabs_preserves_spans() {
let mut text = Text::new("a\tb");
text.stylize(0, 1, Style::new().bold()); let expanded = text.expand_tabs(4);
assert!(!expanded.spans().is_empty());
}
#[test]
fn test_expand_tabs_zero_size() {
let text = Text::new("a\tb");
let expanded = text.expand_tabs(0);
assert_eq!(expanded.plain(), "a\tb");
}
#[test]
fn test_text_display() {
let text = Text::new("hello");
assert_eq!(format!("{text}"), "hello");
}
#[test]
fn test_text_equality() {
let a = Text::new("hello");
let b = Text::new("hello");
assert_eq!(a, b);
let c = Text::new("world");
assert_ne!(a, c);
}
#[test]
fn test_text_add_assign() {
let mut text = Text::new("hello ");
text += Text::new("world");
assert_eq!(text.plain(), "hello world");
}
#[test]
fn test_stylize_all() {
let mut text = Text::new("hello");
text.stylize_all(Style::new().bold());
assert_eq!(text.spans().len(), 1);
assert_eq!(text.spans()[0].start, 0);
assert_eq!(text.spans()[0].end, 5);
}
#[test]
fn test_stylize_clamps() {
let mut text = Text::new("hello");
text.stylize(3, 100, Style::new().bold());
assert_eq!(text.spans().len(), 1);
assert_eq!(text.spans()[0].end, 5); }
#[test]
fn test_to_lowercase() {
let text = Text::new("Hello WORLD");
let lower = text.to_lowercase();
assert_eq!(lower.plain(), "hello world");
}
#[test]
fn test_to_uppercase() {
let text = Text::new("Hello World");
let upper = text.to_uppercase();
assert_eq!(upper.plain(), "HELLO WORLD");
}
#[test]
fn test_to_uppercase_updates_length_and_clamps_spans() {
let mut text = Text::new("ß");
text.stylize_all(Style::new().bold());
let upper = text.to_uppercase();
assert_eq!(upper.plain(), "SS");
assert_eq!(upper.len(), 2);
assert!(upper.spans().iter().all(|span| span.end <= upper.len()));
}
#[test]
fn test_to_uppercase_remaps_spans_for_expansion() {
let mut text = Text::new("aßb");
text.stylize(1, 2, Style::new().bold());
let upper = text.to_uppercase();
assert_eq!(upper.plain(), "ASSB");
assert_eq!(upper.spans().len(), 1);
assert_eq!(upper.spans()[0].start, 1);
assert_eq!(upper.spans()[0].end, 3);
}
#[test]
fn test_append_text_merges_spans() {
let mut a = Text::new("hello");
a.stylize(0, 5, Style::new().bold());
let mut b = Text::new("world");
b.stylize(0, 5, Style::new().italic());
a.append_text(&b);
assert_eq!(a.plain(), "helloworld");
assert_eq!(a.spans().len(), 2);
assert_eq!(a.spans()[1].start, 5);
assert_eq!(a.spans()[1].end, 10);
}
#[test]
fn test_highlight_regex() {
let mut text = Text::new("hello world hello");
text.highlight_regex("hello", &Style::new().bold()).unwrap();
assert_eq!(text.spans().len(), 2);
}
#[test]
fn test_highlight_words() {
let mut text = Text::new("Hello World HELLO");
text.highlight_words(&["hello"], &Style::new().bold(), false);
assert_eq!(text.spans().len(), 2);
}
#[test]
fn test_highlight_words_empty_word_ignored() {
let mut text = Text::new("Hello");
text.highlight_words(&[""], &Style::new().bold(), false);
assert!(text.spans().is_empty());
}
#[test]
fn test_highlight_words_case_insensitive_unicode() {
let mut text = Text::new("Ångström ångström");
text.highlight_words(&["ÅNGSTRÖM"], &Style::new().bold(), false);
assert_eq!(text.spans().len(), 2);
}
#[test]
fn test_split_lines_empty() {
let text = Text::new("");
let lines = text.split_lines();
assert_eq!(lines.len(), 1);
assert!(lines[0].is_empty());
}
#[test]
fn test_split_lines_trailing_newline() {
let text = Text::new("hello\n");
let lines = text.split_lines();
assert_eq!(lines.len(), 2);
assert_eq!(lines[0].plain(), "hello");
assert_eq!(lines[1].plain(), "");
}
#[test]
fn test_truncate_crop() {
let mut text = Text::new("hello world");
text.truncate(5, OverflowMethod::Crop, false);
assert_eq!(text.plain(), "hello");
}
#[test]
fn test_truncate_with_pad() {
let mut text = Text::new("hi");
text.truncate(5, OverflowMethod::Crop, true);
assert_eq!(text.cell_len(), 5);
assert_eq!(text.plain(), "hi ");
}
#[test]
fn test_join_basic() {
let separator = Text::new(", ");
let items = vec![Text::new("a"), Text::new("b"), Text::new("c")];
let joined = separator.join(&items);
assert_eq!(joined.plain(), "a, b, c");
}
#[test]
fn test_join_empty() {
let separator = Text::new(", ");
let items: Vec<Text> = vec![];
let joined = separator.join(&items);
assert_eq!(joined.plain(), "");
}
#[test]
fn test_join_single() {
let separator = Text::new(", ");
let items = vec![Text::new("only")];
let joined = separator.join(&items);
assert_eq!(joined.plain(), "only");
}
#[test]
fn test_join_preserves_styles() {
let mut separator = Text::new(" | ");
separator.stylize_all(Style::new().bold());
let mut item1 = Text::new("a");
item1.stylize_all(Style::new().italic());
let item2 = Text::new("b");
let items = vec![item1, item2];
let joined = separator.join(&items);
assert_eq!(joined.plain(), "a | b");
assert!(joined.spans().len() >= 2);
}
}