use super::{
Line, Style, TextError,
ansi::{self, ParseMode},
};
use unicode_width::UnicodeWidthStr;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Span {
content: String,
style: Style,
}
impl Span {
pub fn new(content: impl Into<String>, style: Style) -> Result<Self, TextError> {
let content = content.into();
Self::validate_content(&content)?;
Ok(Self { content, style })
}
pub fn plain(content: impl Into<String>) -> Result<Self, TextError> {
Self::new(content, Style::new())
}
pub fn from_raw_lossy(content: impl AsRef<str>) -> Result<Self, TextError> {
single_span(ansi::parse_text(content.as_ref(), ParseMode::Raw)?)
}
pub fn from_ansi(content: impl AsRef<str>) -> Result<Self, TextError> {
single_span(ansi::parse_text(content.as_ref(), ParseMode::Ansi)?)
}
pub(crate) fn validate_content(content: &str) -> Result<(), TextError> {
if content
.chars()
.any(|ch| ch == '\n' || ch == '\r' || ch == '\t' || ch == '\x1b' || ch.is_control())
{
return Err(TextError::StructuralContent);
}
Ok(())
}
pub(crate) fn from_trusted_content(content: impl Into<String>, style: Style) -> Self {
Self::new(content, style)
.expect("trusted span content must not contain structural terminal content")
}
pub fn content(&self) -> &str {
&self.content
}
pub fn style(&self) -> Style {
self.style
}
pub fn display_width(&self) -> usize {
UnicodeWidthStr::width(self.content.as_str())
}
}
fn single_span(lines: Vec<Line>) -> Result<Span, TextError> {
let [line] = lines.try_into().map_err(|_| TextError::MultipleLines)?;
let mut spans = line.into_spans().into_iter();
let Some(span) = spans.next() else {
return Span::plain("");
};
if spans.next().is_some() {
return Err(TextError::MultipleStyles);
}
Ok(span)
}