1use ratatui::{
42 buffer::Buffer,
43 layout::Rect,
44 style::{Color, Modifier, Stylize},
45 text::{Line, Span},
46 widgets::{
47 self, Block, BorderType, Padding, Paragraph, ScrollbarOrientation, StatefulWidget,
48 StatefulWidgetRef, Widget,
49 },
50};
51
52use super::parser;
53
54use super::state::MarkdownViewState;
55
56#[derive(Clone, Debug, PartialEq)]
91pub struct MarkdownView;
92
93impl MarkdownView {
94 fn heading(level: parser::HeadingLevel, content: Vec<Span>) -> Line {
95 let prefix = match level {
96 parser::HeadingLevel::H1 => Span::from("█ ").blue(),
97 parser::HeadingLevel::H2 => Span::from("██ ").cyan(),
98 parser::HeadingLevel::H3 => Span::from("▓▓▓ ").green(),
99 parser::HeadingLevel::H4 => Span::from("▓▓▓▓ ").yellow(),
100 parser::HeadingLevel::H5 => Span::from("▓▓▓▓▓ ").red(),
101 parser::HeadingLevel::H6 => Span::from("░░░░░░ ").red(),
102 };
103 Line::from([prefix].into_iter().chain(content).collect::<Vec<_>>()).bold()
104 }
105
106 fn task<'a>(
107 kind: parser::TaskListItemKind,
108 content: Vec<Span<'a>>,
109 prefix: Span<'a>,
110 ) -> Line<'a> {
111 match kind {
112 parser::TaskListItemKind::Unchecked => Line::from(
113 [prefix, "□ ".black()]
114 .into_iter()
115 .chain(content)
116 .collect::<Vec<_>>(),
117 ),
118 parser::TaskListItemKind::Checked => Line::from(
119 [prefix, "■ ".magenta()]
120 .into_iter()
121 .chain(content)
122 .collect::<Vec<_>>(),
123 )
124 .black()
125 .add_modifier(Modifier::CROSSED_OUT),
126 parser::TaskListItemKind::LooselyChecked => Line::from(
127 [prefix, "■ ".magenta()]
128 .into_iter()
129 .chain(content)
130 .collect::<Vec<_>>(),
131 ),
132 }
133 }
134
135 fn item<'a>(kind: parser::ItemKind, content: Vec<Span<'a>>, prefix: Span<'a>) -> Line<'a> {
136 match kind {
137 parser::ItemKind::Ordered(num) => Line::from(
138 [prefix, num.to_string().black(), ". ".into()]
139 .into_iter()
140 .chain(content)
141 .collect::<Vec<_>>(),
142 ),
143 parser::ItemKind::Unordered => Line::from(
144 [prefix, "- ".black()]
145 .into_iter()
146 .chain(content)
147 .collect::<Vec<_>>(),
148 ),
149 }
150 }
151
152 fn text_to_spans<'a>(text: parser::Text) -> Vec<Span<'a>> {
153 text.into_iter()
154 .map(|text| Span::from(text.content))
155 .collect()
156 }
157
158 fn code_block<'a>(text: parser::Text, width: usize) -> Vec<Line<'a>> {
159 text.into_iter()
160 .flat_map(|text| {
161 text.content
162 .clone()
163 .split("\n")
164 .map(|line| {
165 format!(
166 " {} {}",
167 line,
168 (line.len()..width).map(|_| " ").collect::<String>()
169 )
170 })
171 .collect::<Vec<String>>()
172 })
173 .map(|text| Line::from(text).bold().bg(Color::Black))
174 .collect()
175 }
176
177 fn wrap_with_prefix(text: String, width: usize, prefix: Span) -> Vec<Line> {
178 let options =
179 textwrap::Options::new(width.saturating_sub(prefix.width())).break_words(false);
180
181 textwrap::wrap(&text, &options)
182 .into_iter()
183 .map(|wrapped_line| {
184 Line::from([prefix.clone(), Span::from(wrapped_line.to_string())].to_vec())
185 })
186 .collect()
187 }
188
189 fn render_markdown<'a>(node: parser::Node, area: Rect, prefix: Span<'a>) -> Vec<Line<'a>> {
190 match node.markdown_node {
191 parser::MarkdownNode::Paragraph { text } => {
192 MarkdownView::wrap_with_prefix(text.into(), area.width.into(), prefix.clone())
193 .into_iter()
194 .chain([Line::from(prefix)])
195 .collect::<Vec<_>>()
196 }
197 parser::MarkdownNode::Heading { level, text } => [
198 MarkdownView::heading(level, MarkdownView::text_to_spans(text)),
199 Line::default(),
200 ]
201 .to_vec(),
202 parser::MarkdownNode::Item { text } => [MarkdownView::item(
203 parser::ItemKind::Unordered,
204 MarkdownView::text_to_spans(text),
205 prefix,
206 )]
207 .to_vec(),
208 parser::MarkdownNode::TaskListItem { kind, text } => [MarkdownView::task(
209 kind,
210 MarkdownView::text_to_spans(text),
211 prefix,
212 )]
213 .to_vec(),
214 parser::MarkdownNode::CodeBlock { text, .. } => {
216 [Line::from((0..area.width).map(|_| " ").collect::<String>()).bg(Color::Black)]
217 .into_iter()
218 .chain(MarkdownView::code_block(text, area.width.into()))
219 .chain([Line::default()])
220 .collect::<Vec<_>>()
221 }
222 parser::MarkdownNode::List { nodes, kind } => nodes
223 .into_iter()
224 .enumerate()
225 .flat_map(|(i, child)| {
226 let parser::MarkdownNode::Item { text } = child.markdown_node else {
227 return MarkdownView::render_markdown(child, area, prefix.clone());
228 };
229
230 let item = match kind {
231 parser::ListKind::Ordered(start) => MarkdownView::item(
232 parser::ItemKind::Ordered(start + i as u64),
233 MarkdownView::text_to_spans(text),
234 prefix.clone(),
235 ),
236 _ => MarkdownView::item(
237 parser::ItemKind::Unordered,
238 MarkdownView::text_to_spans(text),
239 prefix.clone(),
240 ),
241 };
242
243 [item].to_vec()
244 })
245 .chain([Line::default()])
246 .collect::<Vec<Line<'a>>>(),
247
248 parser::MarkdownNode::BlockQuote { nodes, .. } => nodes
250 .into_iter()
251 .flat_map(|child| {
252 MarkdownView::render_markdown(child, area, Span::from("┃ ").magenta())
253 .into_iter()
254 .collect::<Vec<_>>()
255 })
256 .map(|line| line.dark_gray())
257 .chain([Line::default()])
258 .collect::<Vec<Line<'a>>>(),
259 }
260 }
261}
262
263impl StatefulWidgetRef for MarkdownView {
264 type State = MarkdownViewState;
265
266 fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
267 let block = Block::bordered()
268 .border_type(BorderType::Rounded)
269 .padding(Padding::horizontal(1));
270
271 let nodes = parser::from_str(&state.text)
272 .into_iter()
273 .flat_map(|node| {
274 MarkdownView::render_markdown(node, block.inner(area), Span::default())
275 })
276 .collect::<Vec<Line<'_>>>();
277
278 let mut scroll_state = state.scrollbar.state.content_length(nodes.len());
279
280 let root_node = Paragraph::new(nodes)
281 .block(block)
282 .scroll((state.scrollbar.position as u16, 0));
283
284 Widget::render(root_node, area, buf);
285
286 StatefulWidget::render(
287 widgets::Scrollbar::new(ScrollbarOrientation::VerticalRight),
288 area,
289 buf,
290 &mut scroll_state,
291 );
292 }
293}