1mod table;
2
3use pulldown_cmark::{Alignment, CodeBlockKind, Event, Options, Parser, Tag, TagEnd};
4use std::borrow::Cow;
5
6use crate::line::Line;
7use crate::rendering::render_context::ViewContext;
8use crate::span::Span;
9use crate::style::Style;
10use crate::theme::Theme;
11
12use table::{CellBuilder, TableCell, TableState, line_display_width};
13
14pub fn render_markdown(text: &str, context: &ViewContext) -> Vec<Line> {
15 let renderer = MarkdownRenderer::new(context);
16 renderer.render(text)
17}
18
19struct MarkdownRenderer<'a> {
20 context: &'a ViewContext,
21 theme: &'a Theme,
22 lines: Vec<Line>,
23 current_line: Line,
24 style_stack: Vec<Style>,
25 list_stack: Vec<Option<u64>>,
27 code_buffer: String,
29 code_lang: String,
31 in_code_block: bool,
33 blockquote_depth: usize,
35 table_state: Option<TableState>,
37 active_cell: Option<CellBuilder>,
39}
40
41impl<'a> MarkdownRenderer<'a> {
42 fn new(context: &'a ViewContext) -> Self {
43 Self {
44 context,
45 theme: &context.theme,
46 lines: Vec::new(),
47 current_line: Line::default(),
48 style_stack: Vec::new(),
49 list_stack: Vec::new(),
50 code_buffer: String::new(),
51 code_lang: String::new(),
52 in_code_block: false,
53 blockquote_depth: 0,
54 table_state: None,
55 active_cell: None,
56 }
57 }
58
59 fn render(mut self, text: &str) -> Vec<Line> {
60 let options = Options::ENABLE_STRIKETHROUGH | Options::ENABLE_TABLES;
61 let parser = Parser::new_ext(text, options);
62
63 for event in parser {
64 self.handle_event(event);
65 }
66
67 self.flush_line();
68
69 while self.lines.last().is_some_and(Line::is_empty) {
71 self.lines.pop();
72 }
73
74 self.lines
75 }
76
77 fn handle_event(&mut self, event: Event<'_>) {
78 match event {
79 Event::Start(tag) => self.handle_start(tag),
80 Event::End(tag_end) => self.handle_end(tag_end),
81 Event::Text(text) => self.push_inline_text(&text),
82 Event::Code(code) => self.push_inline_code(&code),
83 Event::SoftBreak => self.push_soft_break(),
84 Event::HardBreak => self.push_hard_break(),
85 Event::Rule => {
86 self.finish_current_line();
87 self.lines.push(Line::with_style(
88 "───────────────",
89 Style::fg(self.theme.muted()),
90 ));
91 self.lines.push(Line::default());
92 }
93 _ => {}
94 }
95 }
96
97 fn handle_start(&mut self, tag: Tag<'_>) {
98 match tag {
99 Tag::Heading { .. }
100 | Tag::BlockQuote(_)
101 | Tag::List(_)
102 | Tag::Item
103 | Tag::Paragraph => self.handle_block_start(&tag),
104
105 Tag::Strong | Tag::Emphasis | Tag::Strikethrough | Tag::Link { .. } => {
106 self.handle_inline_start(tag);
107 }
108
109 Tag::CodeBlock(_) => self.handle_code_block_start(tag),
110
111 Tag::Table(_) | Tag::TableRow | Tag::TableCell => self.handle_table_start(tag),
112
113 _ => {}
114 }
115 }
116
117 fn handle_end(&mut self, tag_end: TagEnd) {
118 match tag_end {
119 TagEnd::Paragraph
120 | TagEnd::Heading(_)
121 | TagEnd::BlockQuote(_)
122 | TagEnd::List(_)
123 | TagEnd::Item => self.handle_block_end(tag_end),
124
125 TagEnd::Strong | TagEnd::Emphasis | TagEnd::Strikethrough | TagEnd::Link => {
126 self.handle_inline_end(tag_end);
127 }
128
129 TagEnd::CodeBlock => self.handle_code_block_end(),
130
131 TagEnd::Table | TagEnd::TableRow | TagEnd::TableHead | TagEnd::TableCell => {
132 self.handle_table_end(tag_end);
133 }
134
135 _ => {}
136 }
137 }
138
139 fn handle_block_start(&mut self, tag: &Tag<'_>) {
140 match tag {
141 Tag::Heading { level, .. } => {
142 self.finish_current_line();
143 let prefix = "#".repeat(*level as usize);
144 self.push_styled_text(
145 &format!("{prefix} "),
146 Style::fg(self.theme.heading()).bold(),
147 );
148 self.style_stack
149 .push(Style::fg(self.theme.heading()).bold());
150 }
151 Tag::BlockQuote(_) => {
152 self.finish_current_line();
153 self.blockquote_depth += 1;
154 self.style_stack.push(Style::fg(self.theme.blockquote()));
155 }
156 Tag::List(start) => {
157 if self.list_stack.is_empty() {
158 self.finish_current_line();
159 }
160 self.list_stack.push(*start);
161 }
162 Tag::Item => {
163 self.flush_line();
164 let indent = " ".repeat(self.list_stack.len().saturating_sub(1));
165 let marker = match self.list_stack.last_mut() {
166 Some(Some(n)) => {
167 let marker = format!("{n}. ");
168 *n += 1;
169 marker
170 }
171 _ => "- ".to_string(),
172 };
173 self.push_styled_text(&format!("{indent}{marker}"), Style::fg(self.theme.muted()));
174 }
175 _ => {}
176 }
177 }
178
179 fn handle_block_end(&mut self, tag_end: TagEnd) {
180 match tag_end {
181 TagEnd::Paragraph => {
182 self.flush_line();
183 self.lines.push(Line::default());
184 }
185 TagEnd::Heading(_) => {
186 self.style_stack.pop();
187 self.flush_line();
188 self.lines.push(Line::default());
189 }
190 TagEnd::BlockQuote(_) => {
191 self.style_stack.pop();
192 self.blockquote_depth -= 1;
193 self.flush_line();
194 if self.blockquote_depth == 0 {
195 self.lines.push(Line::default());
196 }
197 }
198 TagEnd::List(_) => {
199 self.list_stack.pop();
200 if self.list_stack.is_empty() {
201 self.flush_line();
202 self.lines.push(Line::default());
203 }
204 }
205 TagEnd::Item => {
206 self.flush_line();
207 }
208 _ => {}
209 }
210 }
211
212 fn handle_inline_start(&mut self, tag: Tag<'_>) {
213 match tag {
214 Tag::Strong => {
215 self.style_stack.push(Style::default().bold());
216 }
217 Tag::Emphasis => {
218 self.style_stack.push(Style::default().italic());
219 }
220 Tag::Strikethrough => {
221 self.style_stack.push(Style::default().strikethrough());
222 }
223 Tag::Link { dest_url, .. } => {
224 self.style_stack
225 .push(Style::fg(self.theme.link()).underline());
226 let _ = dest_url;
228 }
229 _ => {}
230 }
231 }
232
233 fn handle_inline_end(&mut self, _tag_end: TagEnd) {
234 self.style_stack.pop();
235 }
236
237 fn handle_code_block_start(&mut self, tag: Tag<'_>) {
238 if let Tag::CodeBlock(kind) = tag {
239 self.finish_current_line();
240 self.in_code_block = true;
241 self.code_buffer.clear();
242 self.code_lang = match kind {
243 CodeBlockKind::Fenced(lang) => {
244 lang.split(',').next().unwrap_or("").trim().to_string()
245 }
246 CodeBlockKind::Indented => String::new(),
247 };
248 }
249 }
250
251 fn handle_code_block_end(&mut self) {
252 self.in_code_block = false;
253 let code = std::mem::take(&mut self.code_buffer);
254 let lang = std::mem::take(&mut self.code_lang);
255 let code_lines = self
256 .context
257 .highlighter()
258 .highlight(&code, &lang, self.theme);
259 self.lines.extend(code_lines);
260 self.lines.push(Line::default());
261 }
262
263 fn handle_table_start(&mut self, tag: Tag<'_>) {
264 match tag {
265 Tag::Table(alignments) => {
266 self.finish_current_line();
267 self.table_state = Some(TableState::new(&alignments));
268 }
269 Tag::TableRow => {
270 if let Some(ref mut table) = self.table_state {
271 table.start_row();
272 }
273 }
274 Tag::TableCell => {
275 self.active_cell = Some(CellBuilder::default());
276 }
277 _ => {}
278 }
279 }
280
281 fn handle_table_end(&mut self, tag_end: TagEnd) {
282 match tag_end {
283 TagEnd::Table => {
284 if let Some(table) = self.table_state.take() {
285 let rendered = table.render(self.theme);
286 self.lines.extend(rendered);
287 self.lines.push(Line::default());
288 }
289 }
290 TagEnd::TableRow | TagEnd::TableHead => {
291 if let Some(ref mut table) = self.table_state {
292 table.finish_row();
293 }
294 }
295 TagEnd::TableCell => {
296 if let Some(builder) = self.active_cell.take()
297 && let Some(ref mut table) = self.table_state
298 {
299 let col_idx = table.current_row.len();
300 let alignment = table
301 .alignments
302 .get(col_idx)
303 .copied()
304 .unwrap_or(Alignment::None);
305 let lines = builder.finish();
306 let max_width = lines.iter().map(line_display_width).max().unwrap_or(0);
307 let cell = TableCell {
308 lines,
309 alignment,
310 max_width,
311 };
312 table.add_cell(cell);
313 }
314 }
315 _ => {}
316 }
317 }
318
319 fn current_style(&self) -> Style {
320 self.style_stack
321 .iter()
322 .copied()
323 .fold(Style::default(), Style::merge)
324 }
325
326 fn push_text(&mut self, text: &str) {
327 let style = self.current_style();
328 let prefix = self.blockquote_prefix();
329
330 for (i, chunk) in text.split('\n').enumerate() {
331 if i > 0 {
332 self.flush_line();
333 }
334 if self.current_line.is_empty() && !prefix.is_empty() {
335 self.current_line
336 .push_with_style(&*prefix, Style::fg(self.theme.blockquote()));
337 }
338 if !chunk.is_empty() {
339 self.current_line.push_span(Span::with_style(chunk, style));
340 }
341 }
342 }
343
344 fn push_styled_text(&mut self, text: &str, style: Style) {
345 self.current_line.push_span(Span::with_style(text, style));
346 }
347
348 fn push_inline_text(&mut self, text: &str) {
349 if self.in_code_block {
350 self.code_buffer.push_str(text);
351 return;
352 }
353
354 let style = self.current_style();
355 if let Some(cell) = self.active_cell.as_mut() {
356 cell.push_text(text, style);
357 return;
358 }
359
360 self.push_text(text);
361 }
362
363 fn push_inline_code(&mut self, code: &str) {
364 if self.in_code_block {
365 self.code_buffer.push_str(code);
366 return;
367 }
368
369 let style = Style::fg(self.theme.code_fg());
370 if let Some(cell) = self.active_cell.as_mut() {
371 cell.push_code(code, style);
372 } else {
373 self.current_line.push_span(Span::with_style(code, style));
374 }
375 }
376
377 fn push_soft_break(&mut self) {
378 if self.in_code_block {
379 self.code_buffer.push('\n');
380 return;
381 }
382
383 let style = self.current_style();
384 if let Some(cell) = self.active_cell.as_mut() {
385 cell.soft_break(style);
386 return;
387 }
388
389 self.push_text(" ");
390 }
391
392 fn push_hard_break(&mut self) {
393 if self.in_code_block {
394 self.code_buffer.push('\n');
395 return;
396 }
397
398 if let Some(cell) = self.active_cell.as_mut() {
399 cell.hard_break();
400 return;
401 }
402
403 self.flush_line();
404 }
405
406 fn finish_current_line(&mut self) {
409 if !self.current_line.is_empty() {
410 self.flush_line();
411 }
412 }
413
414 fn flush_line(&mut self) {
415 let prefix = self.blockquote_prefix();
416 if !prefix.is_empty() && self.current_line.is_empty() {
417 self.current_line
418 .push_with_style(&*prefix, Style::fg(self.theme.blockquote()));
419 }
420 let line = std::mem::take(&mut self.current_line);
421 self.lines.push(line);
422 }
423
424 fn blockquote_prefix(&self) -> Cow<'static, str> {
425 if self.blockquote_depth == 0 {
426 Cow::Borrowed("")
427 } else {
428 Cow::Owned(" ".repeat(self.blockquote_depth))
429 }
430 }
431}