basalt_widgets/markdown/
view.rs1use ratatui::{
42 buffer::Buffer,
43 layout::Rect,
44 style::{Color, Modifier, Stylize},
45 text::{Line, Span},
46 widgets::{
47 self, Block, BorderType, Paragraph, ScrollbarOrientation, StatefulWidget,
48 StatefulWidgetRef, Widget,
49 },
50};
51
52use basalt_core::markdown::{self, HeadingLevel, ItemKind};
53
54use super::state::MarkdownViewState;
55
56#[derive(Clone, Debug, PartialEq)]
91pub struct MarkdownView;
92
93impl MarkdownView {
94 fn heading(level: HeadingLevel, content: Vec<Span>) -> Line {
95 let prefix = match level {
96 HeadingLevel::H1 => Span::from("█ ").blue(),
97 HeadingLevel::H2 => Span::from("██ ").cyan(),
98 HeadingLevel::H3 => Span::from("▓▓▓ ").green(),
99 HeadingLevel::H4 => Span::from("▓▓▓▓ ").yellow(),
100 HeadingLevel::H5 => Span::from("▓▓▓▓▓ ").red(),
101 HeadingLevel::H6 => Span::from("░░░░░░ ").red(),
102 };
103 Line::from([prefix].into_iter().chain(content).collect::<Vec<_>>()).bold()
104 }
105
106 fn item<'a>(kind: Option<ItemKind>, content: Vec<Span<'a>>, prefix: Span<'a>) -> Line<'a> {
107 match kind {
108 Some(kind) => match kind {
109 ItemKind::Unchecked => Line::from(
110 [prefix, " ".black()]
111 .into_iter()
112 .chain(content)
113 .collect::<Vec<_>>(),
114 ),
115 ItemKind::Checked => Line::from(
116 [prefix, " ".magenta()]
117 .into_iter()
118 .chain(content)
119 .collect::<Vec<_>>(),
120 ),
121 ItemKind::HardChecked => Line::from(
122 [prefix, " ".magenta()]
123 .into_iter()
124 .chain(content)
125 .collect::<Vec<_>>(),
126 )
127 .black()
128 .add_modifier(Modifier::CROSSED_OUT),
129 ItemKind::Ordered(num) => Line::from(
130 [prefix, num.to_string().black(), ". ".into()]
131 .into_iter()
132 .chain(content)
133 .collect::<Vec<_>>(),
134 ),
135 ItemKind::Unordered => Line::from(
136 [prefix, "- ".black()]
137 .into_iter()
138 .chain(content)
139 .collect::<Vec<_>>(),
140 ),
141 },
142 None => Line::from(
143 [prefix, "- ".black()]
144 .into_iter()
145 .chain(content)
146 .collect::<Vec<_>>(),
147 ),
148 }
149 }
150
151 fn text_to_spans<'a>(text: markdown::Text) -> Vec<Span<'a>> {
152 text.into_iter()
153 .map(|text| Span::from(text.content))
154 .collect()
155 }
156
157 fn code_block<'a>(text: markdown::Text) -> Vec<Line<'a>> {
158 text.into_iter()
159 .flat_map(|text| {
160 text.content
161 .clone()
162 .split("\n")
163 .map(String::from)
164 .collect::<Vec<String>>()
165 })
166 .map(|text| Line::from(text).red().bg(Color::Rgb(10, 10, 10)))
167 .collect()
168 }
169
170 fn render_markdown<'a>(node: markdown::Node, prefix: Span<'a>) -> Vec<Line<'a>> {
171 match node.markdown_node {
172 markdown::MarkdownNode::Paragraph { text } => {
173 let mut spans = MarkdownView::text_to_spans(text);
174 spans.insert(0, prefix.clone());
175 vec![spans.into(), Line::from(prefix)]
176 }
177 markdown::MarkdownNode::Heading { level, text } => [
178 MarkdownView::heading(level, MarkdownView::text_to_spans(text)),
179 Line::default(),
180 ]
181 .to_vec(),
182 markdown::MarkdownNode::Item { kind, text } => [
183 MarkdownView::item(kind, MarkdownView::text_to_spans(text), prefix),
184 Line::default(),
185 ]
186 .to_vec(),
187 markdown::MarkdownNode::CodeBlock { text, .. } => {
189 let mut lines = MarkdownView::code_block(text);
190 lines.insert(0, Line::default());
191 lines
192 }
193 markdown::MarkdownNode::BlockQuote { nodes, .. } => {
195 let mut lines = nodes
196 .into_iter()
197 .flat_map(|child| {
198 MarkdownView::render_markdown(child, Span::from("┃ ").magenta())
199 })
200 .map(|line| line.dark_gray())
201 .collect::<Vec<Line<'a>>>();
202
203 lines.push(Line::default());
204
205 lines
206 }
207 }
208 }
209}
210
211impl StatefulWidgetRef for MarkdownView {
212 type State = MarkdownViewState;
213
214 fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
215 let nodes = markdown::from_str(&state.text)
216 .into_iter()
217 .flat_map(|node| MarkdownView::render_markdown(node, Span::default()))
218 .collect::<Vec<Line<'_>>>();
219
220 let mut scroll_state = state.scrollbar.state.content_length(nodes.len());
221
222 let root_node = Paragraph::new(nodes)
223 .block(Block::bordered().border_type(BorderType::Rounded))
224 .scroll((state.scrollbar.position as u16, 0));
225
226 Widget::render(root_node, area, buf);
227
228 StatefulWidget::render(
229 widgets::Scrollbar::new(ScrollbarOrientation::VerticalRight),
230 area,
231 buf,
232 &mut scroll_state,
233 );
234 }
235}