1use crate::theme::current_theme;
6use crate::view::{Align, BoxNode, HStackNode, Justify, LayoutMode, TextNode, VStackNode, View};
7use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd};
8
9pub fn render(markdown: &str) -> View {
25 let parser = Parser::new(markdown);
26 let renderer = MarkdownRenderer::new();
27 renderer.render(parser)
28}
29
30struct MarkdownRenderer {
32 view_stack: Vec<Vec<View>>,
34 current_text: String,
36 bold: bool,
38 italic: bool,
39 in_code_block: bool,
41 code_block_content: String,
43 code_block_lang: Option<String>,
45 in_list_item: bool,
47 list_markers: Vec<ListMarker>,
49 blockquote_depth: usize,
51}
52
53#[derive(Clone)]
54enum ListMarker {
55 Unordered,
56 Ordered(u64),
57}
58
59impl MarkdownRenderer {
60 fn new() -> Self {
61 Self {
62 view_stack: vec![vec![]],
63 current_text: String::new(),
64 bold: false,
65 italic: false,
66 in_code_block: false,
67 code_block_content: String::new(),
68 code_block_lang: None,
69 in_list_item: false,
70 list_markers: vec![],
71 blockquote_depth: 0,
72 }
73 }
74
75 fn render(mut self, parser: Parser) -> View {
76 for event in parser {
77 self.handle_event(event);
78 }
79
80 self.flush_text();
82
83 let children = self.view_stack.pop().unwrap_or_default();
85 if children.is_empty() {
86 View::Empty
87 } else if children.len() == 1 {
88 children.into_iter().next().unwrap()
89 } else {
90 View::VStack(VStackNode {
91 children,
92 spacing: 0,
93 justify: Justify::Start,
94 align: Align::Stretch,
95 layout_mode: LayoutMode::Flex,
96 })
97 }
98 }
99
100 fn handle_event(&mut self, event: Event) {
101 match event {
102 Event::Start(Tag::Paragraph) => {
104 self.flush_text();
105 }
106 Event::Start(Tag::Heading { level, .. }) => {
107 self.flush_text();
108 self.bold = true;
110 let _ = level; }
113 Event::Start(Tag::CodeBlock(kind)) => {
114 self.flush_text();
115 self.in_code_block = true;
116 self.code_block_content.clear();
117 self.code_block_lang = match kind {
118 CodeBlockKind::Fenced(lang) => {
119 let lang = lang.to_string();
120 if lang.is_empty() {
121 None
122 } else {
123 Some(lang)
124 }
125 }
126 CodeBlockKind::Indented => None,
127 };
128 }
129 Event::Start(Tag::List(start)) => {
130 self.flush_text();
131 let marker = match start {
132 Some(n) => ListMarker::Ordered(n),
133 None => ListMarker::Unordered,
134 };
135 self.list_markers.push(marker);
136 }
137 Event::Start(Tag::Item) => {
138 self.flush_text();
139 self.in_list_item = true;
140 }
141 Event::Start(Tag::BlockQuote) => {
142 self.flush_text();
143 self.blockquote_depth += 1;
144 }
145
146 Event::Start(Tag::Strong) => {
148 self.bold = true;
151 }
152 Event::Start(Tag::Emphasis) => {
153 self.italic = true;
155 }
156 Event::Code(text) => {
157 self.flush_text();
159 self.push_inline_code(&text);
160 }
161
162 Event::Text(text) => {
164 if self.in_code_block {
165 self.code_block_content.push_str(&text);
166 } else {
167 self.current_text.push_str(&text);
168 }
169 }
170 Event::SoftBreak => {
171 if self.in_code_block {
172 self.code_block_content.push('\n');
173 } else {
174 self.current_text.push('\n');
176 }
177 }
178 Event::HardBreak => {
179 if self.in_code_block {
180 self.code_block_content.push('\n');
181 } else {
182 self.flush_text();
183 }
184 }
185
186 Event::End(TagEnd::Paragraph) => {
188 self.flush_text();
189 self.push_spacing();
190 }
191 Event::End(TagEnd::Heading(_)) => {
192 self.flush_text();
193 self.bold = false;
194 self.push_spacing();
195 }
196 Event::End(TagEnd::CodeBlock) => {
197 self.push_code_block();
198 self.in_code_block = false;
199 self.code_block_content.clear();
200 self.code_block_lang = None;
201 self.push_spacing();
202 }
203 Event::End(TagEnd::List(_)) => {
204 self.list_markers.pop();
205 }
206 Event::End(TagEnd::Item) => {
207 self.flush_list_item();
208 self.in_list_item = false;
209 }
210 Event::End(TagEnd::BlockQuote) => {
211 self.blockquote_depth = self.blockquote_depth.saturating_sub(1);
212 }
213
214 Event::End(TagEnd::Strong) => {
216 self.bold = false;
218 }
219 Event::End(TagEnd::Emphasis) => {
220 self.italic = false;
222 }
223
224 Event::Start(Tag::Link { .. })
226 | Event::End(TagEnd::Link)
227 | Event::Start(Tag::Image { .. })
228 | Event::End(TagEnd::Image) => {
229 }
231
232 _ => {}
233 }
234 }
235
236 fn flush_text(&mut self) {
238 if self.current_text.is_empty() {
239 return;
240 }
241
242 let text = std::mem::take(&mut self.current_text);
243 let theme = current_theme();
244
245 let view = View::Text(TextNode {
246 content: text,
247 color: Some(theme.foreground),
248 bg_color: None,
249 bold: self.bold,
250 italic: self.italic,
251 underline: false,
252 dim: false,
253 });
254
255 self.push_view(view);
256 }
257
258 fn push_inline_code(&mut self, text: &str) {
260 let theme = current_theme();
261
262 let view = View::Text(TextNode {
264 content: format!("`{}`", text),
265 color: Some(theme.primary),
266 bg_color: None,
267 bold: false,
268 italic: false,
269 underline: false,
270 dim: false,
271 });
272
273 self.push_view(view);
274 }
275
276 fn push_code_block(&mut self) {
278 let content = self.code_block_content.trim_end().to_string();
279 let theme = current_theme();
280
281 let text_view = View::Text(TextNode {
283 content,
284 color: Some(theme.foreground),
285 bg_color: None,
286 bold: false,
287 italic: false,
288 underline: false,
289 dim: false,
290 });
291
292 let code_box = View::Box(BoxNode {
293 child: Some(Box::new(text_view)),
294 border: true,
295 padding: 1,
296 flex: 0,
297 scroll: false,
298 auto_scroll_bottom: false,
299 focusable: false,
300 min_width: None,
301 max_width: None,
302 min_height: None,
303 max_height: None,
304 });
305
306 self.push_view(code_box);
307 }
308
309 fn flush_list_item(&mut self) {
311 self.flush_text();
312
313 let marker = match self.list_markers.last_mut() {
315 Some(ListMarker::Unordered) => " • ".to_string(),
316 Some(ListMarker::Ordered(n)) => {
317 let marker = format!("{:>2}. ", n);
318 *n += 1;
319 marker
320 }
321 None => " • ".to_string(),
322 };
323
324 let indent = " ".repeat(self.list_markers.len().saturating_sub(1));
326
327 if let Some(views) = self.view_stack.last_mut() {
329 if let Some(last_view) = views.pop() {
330 let content_box = View::Box(BoxNode {
332 child: Some(Box::new(last_view)),
333 border: false,
334 padding: 0,
335 flex: 1,
336 scroll: false,
337 auto_scroll_bottom: false,
338 focusable: false,
339 min_width: None,
340 max_width: None,
341 min_height: None,
342 max_height: None,
343 });
344
345 let marked_view = View::HStack(HStackNode {
347 children: vec![View::text(format!("{}{}", indent, marker)), content_box],
348 spacing: 0,
349 justify: Justify::Start,
350 align: Align::Start,
351 layout_mode: LayoutMode::Flex,
352 });
353 views.push(marked_view);
354 }
355 }
356 }
357
358 fn push_spacing(&mut self) {
360 self.push_view(View::text(""));
362 }
363
364 fn push_view(&mut self, view: View) {
366 if let Some(views) = self.view_stack.last_mut() {
367 views.push(view);
368 }
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375
376 #[test]
377 fn test_plain_text() {
378 let view = render("Hello world");
379 assert!(matches!(view, View::VStack(_) | View::Text(_)));
380 }
381
382 #[test]
383 fn test_code_block() {
384 let view = render("```rust\nfn main() {}\n```");
385 assert!(matches!(view, View::VStack(_)));
387 }
388
389 #[test]
390 fn test_bold_text() {
391 let view = render("This is **bold** text");
392 assert!(matches!(view, View::VStack(_)));
393 }
394}