use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Stylize},
text::{Line, Span},
widgets::{
self, Block, BorderType, Padding, Paragraph, ScrollbarOrientation, StatefulWidget,
StatefulWidgetRef, Widget,
},
};
use super::parser;
use super::state::MarkdownViewState;
#[derive(Clone, Debug, PartialEq)]
pub struct MarkdownView;
impl MarkdownView {
fn heading(level: parser::HeadingLevel, content: Vec<Span>) -> Line {
let prefix = match level {
parser::HeadingLevel::H1 => Span::from("█ ").blue(),
parser::HeadingLevel::H2 => Span::from("██ ").cyan(),
parser::HeadingLevel::H3 => Span::from("▓▓▓ ").green(),
parser::HeadingLevel::H4 => Span::from("▓▓▓▓ ").yellow(),
parser::HeadingLevel::H5 => Span::from("▓▓▓▓▓ ").red(),
parser::HeadingLevel::H6 => Span::from("░░░░░░ ").red(),
};
Line::from([prefix].into_iter().chain(content).collect::<Vec<_>>()).bold()
}
fn task<'a>(
kind: parser::TaskListItemKind,
content: Vec<Span<'a>>,
prefix: Span<'a>,
) -> Line<'a> {
match kind {
parser::TaskListItemKind::Unchecked => Line::from(
[prefix, "□ ".black()]
.into_iter()
.chain(content)
.collect::<Vec<_>>(),
),
parser::TaskListItemKind::Checked => Line::from(
[prefix, "■ ".magenta()]
.into_iter()
.chain(content)
.collect::<Vec<_>>(),
)
.black()
.add_modifier(Modifier::CROSSED_OUT),
parser::TaskListItemKind::LooselyChecked => Line::from(
[prefix, "■ ".magenta()]
.into_iter()
.chain(content)
.collect::<Vec<_>>(),
),
}
}
fn item<'a>(kind: parser::ItemKind, content: Vec<Span<'a>>, prefix: Span<'a>) -> Line<'a> {
match kind {
parser::ItemKind::Ordered(num) => Line::from(
[prefix, num.to_string().black(), ". ".into()]
.into_iter()
.chain(content)
.collect::<Vec<_>>(),
),
parser::ItemKind::Unordered => Line::from(
[prefix, "- ".black()]
.into_iter()
.chain(content)
.collect::<Vec<_>>(),
),
}
}
fn text_to_spans<'a>(text: parser::Text) -> Vec<Span<'a>> {
text.into_iter()
.map(|text| Span::from(text.content))
.collect()
}
fn code_block<'a>(text: parser::Text, width: usize) -> Vec<Line<'a>> {
text.into_iter()
.flat_map(|text| {
text.content
.clone()
.split("\n")
.map(|line| {
format!(
" {} {}",
line,
(line.len()..width).map(|_| " ").collect::<String>()
)
})
.collect::<Vec<String>>()
})
.map(|text| Line::from(text).bold().bg(Color::Black))
.collect()
}
fn wrap_with_prefix(text: String, width: usize, prefix: Span) -> Vec<Line> {
let options =
textwrap::Options::new(width.saturating_sub(prefix.width())).break_words(false);
textwrap::wrap(&text, &options)
.into_iter()
.map(|wrapped_line| {
Line::from([prefix.clone(), Span::from(wrapped_line.to_string())].to_vec())
})
.collect()
}
fn render_markdown<'a>(node: parser::Node, area: Rect, prefix: Span<'a>) -> Vec<Line<'a>> {
match node.markdown_node {
parser::MarkdownNode::Paragraph { text } => {
MarkdownView::wrap_with_prefix(text.into(), area.width.into(), prefix.clone())
.into_iter()
.chain([Line::from(prefix)])
.collect::<Vec<_>>()
}
parser::MarkdownNode::Heading { level, text } => [
MarkdownView::heading(level, MarkdownView::text_to_spans(text)),
Line::default(),
]
.to_vec(),
parser::MarkdownNode::Item { text } => [MarkdownView::item(
parser::ItemKind::Unordered,
MarkdownView::text_to_spans(text),
prefix,
)]
.to_vec(),
parser::MarkdownNode::TaskListItem { kind, text } => [MarkdownView::task(
kind,
MarkdownView::text_to_spans(text),
prefix,
)]
.to_vec(),
parser::MarkdownNode::CodeBlock { text, .. } => {
[Line::from((0..area.width).map(|_| " ").collect::<String>()).bg(Color::Black)]
.into_iter()
.chain(MarkdownView::code_block(text, area.width.into()))
.chain([Line::default()])
.collect::<Vec<_>>()
}
parser::MarkdownNode::List { nodes, kind } => nodes
.into_iter()
.enumerate()
.flat_map(|(i, child)| {
let parser::MarkdownNode::Item { text } = child.markdown_node else {
return MarkdownView::render_markdown(child, area, prefix.clone());
};
let item = match kind {
parser::ListKind::Ordered(start) => MarkdownView::item(
parser::ItemKind::Ordered(start + i as u64),
MarkdownView::text_to_spans(text),
prefix.clone(),
),
_ => MarkdownView::item(
parser::ItemKind::Unordered,
MarkdownView::text_to_spans(text),
prefix.clone(),
),
};
[item].to_vec()
})
.chain([Line::default()])
.collect::<Vec<Line<'a>>>(),
parser::MarkdownNode::BlockQuote { nodes, .. } => nodes
.into_iter()
.flat_map(|child| {
MarkdownView::render_markdown(child, area, Span::from("┃ ").magenta())
.into_iter()
.collect::<Vec<_>>()
})
.map(|line| line.dark_gray())
.chain([Line::default()])
.collect::<Vec<Line<'a>>>(),
}
}
}
impl StatefulWidgetRef for MarkdownView {
type State = MarkdownViewState;
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let block = Block::bordered()
.border_type(BorderType::Rounded)
.padding(Padding::horizontal(1));
let nodes = parser::from_str(&state.text)
.into_iter()
.flat_map(|node| {
MarkdownView::render_markdown(node, block.inner(area), Span::default())
})
.collect::<Vec<Line<'_>>>();
let mut scroll_state = state.scrollbar.state.content_length(nodes.len());
let root_node = Paragraph::new(nodes)
.block(block)
.scroll((state.scrollbar.position as u16, 0));
Widget::render(root_node, area, buf);
StatefulWidget::render(
widgets::Scrollbar::new(ScrollbarOrientation::VerticalRight),
area,
buf,
&mut scroll_state,
);
}
}