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,
},
Paragraph(Vec<StyledInline>),
BulletList(Vec<Vec<StyledInline>>),
NumberedList(Vec<Vec<StyledInline>>),
CodeBlock {
language: Option<String>,
content: String,
},
HorizontalRule,
BlankLine,
Raw(Vec<RatLine<'static>>),
}
#[derive(Clone, Debug, PartialEq)]
pub enum StyledInline {
Plain(String),
Bold(String),
Italic(String),
Underline(String),
Strikethrough(String),
Colored {
text: String,
fg: Option<Color>,
bg: Option<Color>,
},
Code(String),
}
#[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 paragraph(mut self, inlines: Vec<StyledInline>) -> Self {
self.blocks.push(StyledBlock::Paragraph(inlines));
self
}
pub fn text(self, text: impl Into<String>) -> Self {
self.paragraph(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::Paragraph(inlines) => {
render_paragraph(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_paragraph(
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),
),
StyledInline::Colored { text, fg, bg } => {
let mut style = base_style;
if let Some(fg) = fg {
style = style.fg(*fg);
}
if let Some(bg) = bg {
style = style.bg(*bg);
}
RatSpan::styled(text.clone(), style)
}
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::Bold(text) => {
RatSpan::styled(text.clone(), base_style.add_modifier(Modifier::BOLD))
}
StyledInline::Italic(text) => {
RatSpan::styled(text.clone(), base_style.add_modifier(Modifier::ITALIC))
}
StyledInline::Underline(text) => {
RatSpan::styled(text.clone(), base_style.add_modifier(Modifier::UNDERLINED))
}
StyledInline::Strikethrough(text) => {
RatSpan::styled(text.clone(), base_style.add_modifier(Modifier::CROSSED_OUT))
}
StyledInline::Colored { text, fg, bg } => {
let mut style = base_style;
if let Some(fg) = fg {
style = style.fg(*fg);
}
if let Some(bg) = bg {
style = style.bg(*bg);
}
RatSpan::styled(text.clone(), style)
}
StyledInline::Code(text) => {
RatSpan::styled(text.clone(), base_style.add_modifier(Modifier::BOLD))
}
}
}