use ratatui::{
style::{Color, Modifier, Style},
text::{Line, Span as RatatuiSpan},
};
use crate::{
Line as MdLine, LineKind, Span, link_tracker::TrackedUrl, mapper::Mapper,
markdown::Modifier as MdModifier,
};
pub trait Theme: Mapper {
fn blockquote_color(&self, depth: usize) -> Color {
const COLORS: [Color; 6] = [
Color::Indexed(202),
Color::Indexed(203),
Color::Indexed(204),
Color::Indexed(205),
Color::Indexed(206),
Color::Indexed(207),
];
COLORS[depth % COLORS.len()]
}
fn link_bg(&self) -> Color {
Color::Indexed(237)
}
fn link_fg(&self) -> Color {
Color::Indexed(4)
}
fn prefix_color(&self) -> Color {
Color::Indexed(222)
}
fn emphasis_color(&self) -> Color {
Color::Indexed(220)
}
fn code_bg(&self) -> Color {
Color::Indexed(236)
}
fn code_fg(&self) -> Color {
Color::Indexed(203)
}
fn hr_color(&self) -> Color {
Color::Indexed(240)
}
fn table_border_color(&self) -> Color {
Color::Indexed(240)
}
fn table_header_color(&self) -> Color {
Color::Indexed(255)
}
fn code_style(&self) -> Style {
Style::default().fg(self.code_fg()).bg(self.code_bg())
}
fn emphasis_style(&self) -> Style {
Style::default()
.add_modifier(Modifier::ITALIC)
.fg(self.emphasis_color())
}
fn strong_emphasis_style(&self) -> Style {
Style::default()
.add_modifier(Modifier::BOLD)
.fg(self.emphasis_color())
}
fn link_url_style(&self) -> Style {
Style::default()
.fg(self.link_fg())
.bg(self.link_bg())
.underlined()
}
fn link_description_style(&self) -> Style {
Style::default()
.fg(self.link_fg())
.bg(self.link_bg())
.underlined()
}
fn link_wrapper_style(&self) -> Style {
Style::default().fg(self.link_bg())
}
fn hr_style(&self) -> Style {
Style::default().fg(self.hr_color())
}
fn table_border_style(&self) -> Style {
Style::default().fg(self.table_border_color())
}
fn table_header_style(&self) -> Style {
Style::default()
.add_modifier(Modifier::BOLD)
.fg(self.table_header_color())
}
fn prefix_style(&self) -> Style {
Style::default().fg(self.prefix_color())
}
fn blockquote_style(&self, depth: usize) -> Style {
Style::default().fg(self.blockquote_color(depth))
}
fn strikethrough_color(&self) -> Color {
Color::Indexed(245)
}
fn strikethrough_style(&self) -> Style {
Style::default()
.add_modifier(Modifier::CROSSED_OUT | Modifier::DIM)
.fg(self.strikethrough_color())
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct DefaultTheme;
const STYLED: crate::mapper::StyledMapper = crate::mapper::StyledMapper;
impl Mapper for DefaultTheme {
fn link_desc_open(&self) -> &str {
STYLED.link_desc_open()
}
fn link_desc_close(&self) -> &str {
STYLED.link_desc_close()
}
fn link_url_open(&self) -> &str {
STYLED.link_url_open()
}
fn link_url_close(&self) -> &str {
STYLED.link_url_close()
}
fn blockquote_bar(&self) -> &str {
STYLED.blockquote_bar()
}
fn horizontal_rule_char(&self) -> &str {
STYLED.horizontal_rule_char()
}
fn task_checked(&self) -> &str {
STYLED.task_checked()
}
fn table_vertical(&self) -> &str {
STYLED.table_vertical()
}
fn table_horizontal(&self) -> &str {
STYLED.table_horizontal()
}
fn table_top_left(&self) -> &str {
STYLED.table_top_left()
}
fn table_top_right(&self) -> &str {
STYLED.table_top_right()
}
fn table_bottom_left(&self) -> &str {
STYLED.table_bottom_left()
}
fn table_bottom_right(&self) -> &str {
STYLED.table_bottom_right()
}
fn table_top_junction(&self) -> &str {
STYLED.table_top_junction()
}
fn table_bottom_junction(&self) -> &str {
STYLED.table_bottom_junction()
}
fn table_left_junction(&self) -> &str {
STYLED.table_left_junction()
}
fn table_right_junction(&self) -> &str {
STYLED.table_right_junction()
}
fn table_cross(&self) -> &str {
STYLED.table_cross()
}
fn emphasis_open(&self) -> &str {
STYLED.emphasis_open()
}
fn emphasis_close(&self) -> &str {
STYLED.emphasis_close()
}
fn strong_open(&self) -> &str {
STYLED.strong_open()
}
fn strong_close(&self) -> &str {
STYLED.strong_close()
}
fn code_open(&self) -> &str {
STYLED.code_open()
}
fn code_close(&self) -> &str {
STYLED.code_close()
}
fn strikethrough_open(&self) -> &str {
STYLED.strikethrough_open()
}
fn strikethrough_close(&self) -> &str {
STYLED.strikethrough_close()
}
}
impl Theme for DefaultTheme {}
pub fn render_line<T: Theme>(md_line: MdLine, theme: &T) -> (Line<'static>, Vec<TrackedUrl>) {
let MdLine { spans, kind, .. } = md_line;
let mut bq_depth = 0;
let is_table_header = matches!(kind, LineKind::TableRow { is_header: true });
let is_code_block = matches!(kind, LineKind::CodeBlock { .. });
let line_spans: Vec<RatatuiSpan<'static>> = spans
.into_iter()
.map(|node| {
node_to_span(
node,
bq_depth,
is_table_header,
is_code_block,
theme,
&mut bq_depth,
)
})
.collect();
(Line::from(line_spans), md_line.urls)
}
fn node_to_span<T: Theme>(
node: Span,
current_bq_depth: usize,
is_table_header: bool,
is_code_block: bool,
theme: &T,
bq_depth_out: &mut usize,
) -> RatatuiSpan<'static> {
let Span {
content,
modifiers,
} = node;
if theme.hide_urls() && modifiers.is_link_url() {
return RatatuiSpan::default();
}
if modifiers.contains(MdModifier::BlockquoteBar) {
let style = theme.blockquote_style(current_bq_depth);
*bq_depth_out = current_bq_depth + 1;
return RatatuiSpan::styled(content, style);
}
if modifiers.contains(MdModifier::ListMarker) {
return RatatuiSpan::styled(content, theme.prefix_style());
}
if modifiers.contains(MdModifier::TableBorder) {
return RatatuiSpan::styled(content, theme.table_border_style());
}
if modifiers.contains(MdModifier::HorizontalRule) {
return RatatuiSpan::styled(content, theme.hr_style());
}
if modifiers.contains(MdModifier::Code) || is_code_block {
return RatatuiSpan::styled(content, theme.code_style());
}
if modifiers.contains(MdModifier::LinkDescriptionWrapper)
|| modifiers.contains(MdModifier::LinkURLWrapper)
{
return RatatuiSpan::styled(content, theme.link_wrapper_style());
}
if modifiers.contains(MdModifier::LinkURL) {
return RatatuiSpan::styled(content, theme.link_url_style());
}
let mut style = if is_table_header {
theme.table_header_style()
} else {
Style::default()
};
if modifiers.contains(MdModifier::LinkDescription) {
style = style.patch(theme.link_description_style());
}
if modifiers.contains(MdModifier::Emphasis) {
style = style.patch(theme.emphasis_style());
}
if modifiers.contains(MdModifier::StrongEmphasis) {
style = style.patch(theme.strong_emphasis_style());
}
if modifiers.contains(MdModifier::Strikethrough) {
style = style.patch(theme.strikethrough_style());
}
if modifiers.contains(MdModifier::Link) {
style = style.patch(theme.link_description_style());
}
if style == Style::default() {
RatatuiSpan::from(content)
} else {
RatatuiSpan::styled(content, style)
}
}