use ratatui::prelude::*;
use ratatui::text::{Line as RatLine, Span as RatSpan};
use crate::theme::Theme;
#[derive(Clone, Debug, PartialEq)]
pub enum StyledBlock {
Heading {
level: u8,
text: String,
},
Line(Vec<StyledInline>),
BulletList(Vec<Vec<StyledInline>>),
NumberedList(Vec<Vec<StyledInline>>),
CodeBlock {
language: Option<String>,
content: String,
},
HorizontalRule,
BlankLine,
Raw(Vec<RatLine<'static>>),
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq)]
pub enum StyledInline {
Plain(String),
Code(String),
Styled {
text: String,
style: InlineStyle,
},
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct InlineStyle {
pub fg: Option<Color>,
pub bg: Option<Color>,
pub bold: bool,
pub italic: bool,
pub underlined: bool,
pub strikethrough: bool,
}
impl InlineStyle {
pub const fn new() -> Self {
Self {
fg: None,
bg: None,
bold: false,
italic: false,
underlined: false,
strikethrough: false,
}
}
pub const fn fg(mut self, c: Color) -> Self {
self.fg = Some(c);
self
}
pub const fn bg(mut self, c: Color) -> Self {
self.bg = Some(c);
self
}
pub const fn bold(mut self) -> Self {
self.bold = true;
self
}
pub const fn italic(mut self) -> Self {
self.italic = true;
self
}
pub const fn underlined(mut self) -> Self {
self.underlined = true;
self
}
pub const fn strikethrough(mut self) -> Self {
self.strikethrough = true;
self
}
}
impl StyledInline {
pub fn styled(text: impl Into<String>, style: InlineStyle) -> Self {
Self::Styled {
text: text.into(),
style,
}
}
pub fn bold(text: impl Into<String>) -> Self {
Self::styled(text, InlineStyle::new().bold())
}
pub fn italic(text: impl Into<String>) -> Self {
Self::styled(text, InlineStyle::new().italic())
}
pub fn underlined(text: impl Into<String>) -> Self {
Self::styled(text, InlineStyle::new().underlined())
}
pub fn strikethrough(text: impl Into<String>) -> Self {
Self::styled(text, InlineStyle::new().strikethrough())
}
pub fn colored(text: impl Into<String>, fg: Color) -> Self {
Self::styled(text, InlineStyle::new().fg(fg))
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct StyledContent {
blocks: Vec<StyledBlock>,
}
impl StyledContent {
pub fn new() -> Self {
Self::default()
}
pub fn from_blocks(blocks: Vec<StyledBlock>) -> Self {
Self { blocks }
}
pub fn heading(mut self, level: u8, text: impl Into<String>) -> Self {
self.blocks.push(StyledBlock::Heading {
level: level.clamp(1, 3),
text: text.into(),
});
self
}
pub fn line(mut self, inlines: Vec<StyledInline>) -> Self {
self.blocks.push(StyledBlock::Line(inlines));
self
}
pub fn text(self, text: impl Into<String>) -> Self {
self.line(vec![StyledInline::Plain(text.into())])
}
pub fn bullet_list(mut self, items: Vec<Vec<StyledInline>>) -> Self {
self.blocks.push(StyledBlock::BulletList(items));
self
}
pub fn numbered_list(mut self, items: Vec<Vec<StyledInline>>) -> Self {
self.blocks.push(StyledBlock::NumberedList(items));
self
}
pub fn code_block(
mut self,
language: Option<impl Into<String>>,
content: impl Into<String>,
) -> Self {
self.blocks.push(StyledBlock::CodeBlock {
language: language.map(|l| l.into()),
content: content.into(),
});
self
}
pub fn horizontal_rule(mut self) -> Self {
self.blocks.push(StyledBlock::HorizontalRule);
self
}
pub fn blank_line(mut self) -> Self {
self.blocks.push(StyledBlock::BlankLine);
self
}
pub fn raw(mut self, lines: Vec<RatLine<'static>>) -> Self {
self.blocks.push(StyledBlock::Raw(lines));
self
}
pub fn push(mut self, block: StyledBlock) -> Self {
self.blocks.push(block);
self
}
pub fn is_empty(&self) -> bool {
self.blocks.is_empty()
}
pub fn len(&self) -> usize {
self.blocks.len()
}
pub fn blocks(&self) -> &[StyledBlock] {
&self.blocks
}
pub(crate) fn render_lines(&self, width: u16, theme: &Theme) -> Vec<RatLine<'static>> {
self.render_lines_styled(width, theme, theme.normal_style())
}
pub(crate) fn render_lines_styled(
&self,
width: u16,
theme: &Theme,
base_style: Style,
) -> Vec<RatLine<'static>> {
let mut lines = Vec::new();
for block in &self.blocks {
render_block(block, width, theme, base_style, &mut lines);
}
lines
}
}
fn render_block(
block: &StyledBlock,
width: u16,
theme: &Theme,
base_style: Style,
lines: &mut Vec<RatLine<'static>>,
) {
match block {
StyledBlock::Heading { level, text } => {
let style = match level {
1 => theme
.focused_style()
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
2 => theme.info_style().add_modifier(Modifier::BOLD),
_ => base_style.add_modifier(Modifier::BOLD | Modifier::ITALIC),
};
lines.push(RatLine::from(RatSpan::styled(text.clone(), style)));
}
StyledBlock::Line(inlines) => {
render_line(inlines, theme, base_style, lines);
}
StyledBlock::BulletList(items) => {
for item in items {
let mut spans = vec![RatSpan::styled(" • ", base_style)];
for inline in item {
spans.push(render_inline(inline, theme, base_style));
}
lines.push(RatLine::from(spans));
}
}
StyledBlock::NumberedList(items) => {
for (i, item) in items.iter().enumerate() {
let prefix = format!(" {}. ", i + 1);
let mut spans = vec![RatSpan::styled(prefix, base_style)];
for inline in item {
spans.push(render_inline(inline, theme, base_style));
}
lines.push(RatLine::from(spans));
}
}
StyledBlock::CodeBlock { language, content } => {
if let Some(lang) = language {
lines.push(RatLine::from(RatSpan::styled(
format!(" [{}]", lang),
theme.disabled_style().add_modifier(Modifier::ITALIC),
)));
}
for line in content.lines() {
lines.push(RatLine::from(RatSpan::styled(
format!(" {}", line),
base_style,
)));
}
if content.is_empty() {
lines.push(RatLine::from(RatSpan::styled(
" ".to_string(),
base_style,
)));
}
}
StyledBlock::HorizontalRule => {
let rule = "─".repeat(width as usize);
lines.push(RatLine::from(RatSpan::styled(rule, theme.border_style())));
}
StyledBlock::BlankLine => {
lines.push(RatLine::from(""));
}
StyledBlock::Raw(raw_lines) => {
lines.extend(raw_lines.iter().cloned());
}
}
}
fn render_line(
inlines: &[StyledInline],
theme: &Theme,
base_style: Style,
lines: &mut Vec<RatLine<'static>>,
) {
let spans: Vec<RatSpan<'static>> = inlines
.iter()
.map(|i| render_inline(i, theme, base_style))
.collect();
lines.push(RatLine::from(spans));
}
fn render_inline(inline: &StyledInline, theme: &Theme, base_style: Style) -> RatSpan<'static> {
match inline {
StyledInline::Code(text) => RatSpan::styled(
text.clone(),
theme.info_style().add_modifier(Modifier::BOLD),
),
other => render_inline_styled(other, base_style),
}
}
fn render_inline_styled(inline: &StyledInline, base_style: Style) -> RatSpan<'static> {
match inline {
StyledInline::Plain(text) => RatSpan::styled(text.clone(), base_style),
StyledInline::Code(text) => {
RatSpan::styled(text.clone(), base_style.add_modifier(Modifier::BOLD))
}
StyledInline::Styled { text, style } => {
let mut s = base_style;
if let Some(fg) = style.fg {
s = s.fg(fg);
}
if let Some(bg) = style.bg {
s = s.bg(bg);
}
if style.bold {
s = s.add_modifier(Modifier::BOLD);
}
if style.italic {
s = s.add_modifier(Modifier::ITALIC);
}
if style.underlined {
s = s.add_modifier(Modifier::UNDERLINED);
}
if style.strikethrough {
s = s.add_modifier(Modifier::CROSSED_OUT);
}
RatSpan::styled(text.clone(), s)
}
}
}
#[cfg(test)]
mod const_builder_test {
use super::InlineStyle;
use ratatui::style::Color;
const _STYLE: InlineStyle = InlineStyle::new()
.fg(Color::Red)
.bg(Color::Black)
.bold()
.italic()
.underlined()
.strikethrough();
}