use crate::core::style::Style;
use crate::core::width::visible_width;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Span {
pub content: String,
pub style: Style,
pub link: Option<String>,
}
impl Span {
pub fn raw(content: impl Into<String>) -> Self {
Self {
content: content.into(),
style: Style::new(),
link: None,
}
}
pub fn styled(content: impl Into<String>, style: Style) -> Self {
Self {
content: content.into(),
style,
link: None,
}
}
#[must_use]
pub fn link(mut self, url: impl Into<String>) -> Self {
self.link = Some(url.into());
self
}
pub fn width(&self) -> usize {
visible_width(&self.content)
}
}
impl From<&str> for Span {
fn from(value: &str) -> Self {
Span::raw(value)
}
}
impl From<String> for Span {
fn from(value: String) -> Self {
Span::raw(value)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Line {
pub spans: Vec<Span>,
}
impl Line {
pub fn new(spans: Vec<Span>) -> Self {
Self { spans }
}
pub fn raw(content: impl Into<String>) -> Self {
Self::new(vec![Span::raw(content)])
}
pub fn styled(content: impl Into<String>, style: Style) -> Self {
Self::new(vec![Span::styled(content, style)])
}
pub fn width(&self) -> usize {
self.spans.iter().map(Span::width).sum()
}
pub fn plain(&self) -> String {
self.spans.iter().map(|s| s.content.as_str()).collect()
}
}
impl From<&str> for Line {
fn from(value: &str) -> Self {
Line::raw(value)
}
}
impl From<String> for Line {
fn from(value: String) -> Self {
Line::raw(value)
}
}
impl From<Span> for Line {
fn from(span: Span) -> Self {
Line::new(vec![span])
}
}
impl From<Vec<Span>> for Line {
fn from(spans: Vec<Span>) -> Self {
Line::new(spans)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Text {
pub lines: Vec<Line>,
}
impl Text {
pub fn new(lines: Vec<Line>) -> Self {
Self { lines }
}
pub fn raw(content: impl Into<String>) -> Self {
let content = content.into();
let lines = content.split('\n').map(Line::raw).collect();
Self { lines }
}
pub fn styled(content: impl Into<String>, style: Style) -> Self {
let content = content.into();
let lines = content
.split('\n')
.map(|l| Line::styled(l, style))
.collect();
Self { lines }
}
pub fn push_line(&mut self, line: impl Into<Line>) {
self.lines.push(line.into());
}
pub fn width(&self) -> usize {
self.lines.iter().map(Line::width).max().unwrap_or(0)
}
pub fn height(&self) -> usize {
self.lines.len()
}
}
impl From<&str> for Text {
fn from(value: &str) -> Self {
Text::raw(value)
}
}
impl From<String> for Text {
fn from(value: String) -> Self {
Text::raw(value)
}
}
impl From<Span> for Text {
fn from(span: Span) -> Self {
Text::new(vec![Line::from(span)])
}
}
impl From<Line> for Text {
fn from(line: Line) -> Self {
Text::new(vec![line])
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::style::Color;
#[test]
fn line_width_sums_span_widths() {
let line = Line::new(vec![Span::raw("ab"), Span::raw("δΈ")]);
assert_eq!(line.width(), 4);
}
#[test]
fn text_raw_splits_on_newlines() {
let text = Text::raw("a\nbb\nccc");
assert_eq!(text.height(), 3);
assert_eq!(text.width(), 3);
}
#[test]
fn span_link_is_attached() {
let span = Span::raw("x").link("http://example.com");
assert_eq!(span.link.as_deref(), Some("http://example.com"));
}
#[test]
fn styled_helpers_carry_style() {
let line = Line::styled("hi", Style::new().fg(Color::Red));
assert_eq!(line.spans[0].style.fg, Some(Color::Red));
}
}