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("───────────────", Style::fg(self.theme.muted())));
88 self.lines.push(Line::default());
89 }
90 _ => {}
91 }
92 }
93
94 fn handle_start(&mut self, tag: Tag<'_>) {
95 match tag {
96 Tag::Heading { .. } | Tag::BlockQuote(_) | Tag::List(_) | Tag::Item | Tag::Paragraph => {
97 self.handle_block_start(&tag);
98 }
99
100 Tag::Strong | Tag::Emphasis | Tag::Strikethrough | Tag::Link { .. } => {
101 self.handle_inline_start(tag);
102 }
103
104 Tag::CodeBlock(_) => self.handle_code_block_start(tag),
105
106 Tag::Table(_) | Tag::TableRow | Tag::TableCell => self.handle_table_start(tag),
107
108 _ => {}
109 }
110 }
111
112 fn handle_end(&mut self, tag_end: TagEnd) {
113 match tag_end {
114 TagEnd::Paragraph | TagEnd::Heading(_) | TagEnd::BlockQuote(_) | TagEnd::List(_) | TagEnd::Item => {
115 self.handle_block_end(tag_end);
116 }
117
118 TagEnd::Strong | TagEnd::Emphasis | TagEnd::Strikethrough | TagEnd::Link => {
119 self.handle_inline_end(tag_end);
120 }
121
122 TagEnd::CodeBlock => self.handle_code_block_end(),
123
124 TagEnd::Table | TagEnd::TableRow | TagEnd::TableHead | TagEnd::TableCell => {
125 self.handle_table_end(tag_end);
126 }
127
128 _ => {}
129 }
130 }
131
132 fn handle_block_start(&mut self, tag: &Tag<'_>) {
133 match tag {
134 Tag::Heading { level, .. } => {
135 self.finish_current_line();
136 let prefix = "#".repeat(*level as usize);
137 self.push_styled_text(&format!("{prefix} "), Style::fg(self.theme.heading()).bold());
138 self.style_stack.push(Style::fg(self.theme.heading()).bold());
139 }
140 Tag::BlockQuote(_) => {
141 self.finish_current_line();
142 self.blockquote_depth += 1;
143 self.style_stack.push(Style::fg(self.theme.blockquote()));
144 }
145 Tag::List(start) => {
146 if self.list_stack.is_empty() {
147 self.finish_current_line();
148 }
149 self.list_stack.push(*start);
150 }
151 Tag::Item => {
152 self.flush_line();
153 let indent = " ".repeat(self.list_stack.len().saturating_sub(1));
154 let marker = match self.list_stack.last_mut() {
155 Some(Some(n)) => {
156 let marker = format!("{n}. ");
157 *n += 1;
158 marker
159 }
160 _ => "- ".to_string(),
161 };
162 self.push_styled_text(&format!("{indent}{marker}"), Style::fg(self.theme.muted()));
163 }
164 _ => {}
165 }
166 }
167
168 fn handle_block_end(&mut self, tag_end: TagEnd) {
169 match tag_end {
170 TagEnd::Paragraph => {
171 self.flush_line();
172 self.lines.push(Line::default());
173 }
174 TagEnd::Heading(_) => {
175 self.style_stack.pop();
176 self.flush_line();
177 self.lines.push(Line::default());
178 }
179 TagEnd::BlockQuote(_) => {
180 self.style_stack.pop();
181 self.blockquote_depth -= 1;
182 self.flush_line();
183 if self.blockquote_depth == 0 {
184 self.lines.push(Line::default());
185 }
186 }
187 TagEnd::List(_) => {
188 self.list_stack.pop();
189 if self.list_stack.is_empty() {
190 self.flush_line();
191 self.lines.push(Line::default());
192 }
193 }
194 TagEnd::Item => {
195 self.flush_line();
196 }
197 _ => {}
198 }
199 }
200
201 fn handle_inline_start(&mut self, tag: Tag<'_>) {
202 match tag {
203 Tag::Strong => {
204 self.style_stack.push(Style::default().bold());
205 }
206 Tag::Emphasis => {
207 self.style_stack.push(Style::default().italic());
208 }
209 Tag::Strikethrough => {
210 self.style_stack.push(Style::default().strikethrough());
211 }
212 Tag::Link { dest_url, .. } => {
213 self.style_stack.push(Style::fg(self.theme.link()).underline());
214 let _ = dest_url;
216 }
217 _ => {}
218 }
219 }
220
221 fn handle_inline_end(&mut self, _tag_end: TagEnd) {
222 self.style_stack.pop();
223 }
224
225 fn handle_code_block_start(&mut self, tag: Tag<'_>) {
226 if let Tag::CodeBlock(kind) = tag {
227 self.finish_current_line();
228 self.in_code_block = true;
229 self.code_buffer.clear();
230 self.code_lang = match kind {
231 CodeBlockKind::Fenced(lang) => lang.split(',').next().unwrap_or("").trim().to_string(),
232 CodeBlockKind::Indented => String::new(),
233 };
234 }
235 }
236
237 fn handle_code_block_end(&mut self) {
238 self.in_code_block = false;
239 let code = std::mem::take(&mut self.code_buffer);
240 let lang = std::mem::take(&mut self.code_lang);
241 let code_lines = self.context.highlighter().highlight(&code, &lang, self.theme);
242 self.lines.extend(code_lines);
243 self.lines.push(Line::default());
244 }
245
246 fn handle_table_start(&mut self, tag: Tag<'_>) {
247 match tag {
248 Tag::Table(alignments) => {
249 self.finish_current_line();
250 self.table_state = Some(TableState::new(&alignments));
251 }
252 Tag::TableRow => {
253 if let Some(ref mut table) = self.table_state {
254 table.start_row();
255 }
256 }
257 Tag::TableCell => {
258 self.active_cell = Some(CellBuilder::default());
259 }
260 _ => {}
261 }
262 }
263
264 fn handle_table_end(&mut self, tag_end: TagEnd) {
265 match tag_end {
266 TagEnd::Table => {
267 if let Some(table) = self.table_state.take() {
268 let rendered = table.render(self.theme);
269 self.lines.extend(rendered);
270 self.lines.push(Line::default());
271 }
272 }
273 TagEnd::TableRow | TagEnd::TableHead => {
274 if let Some(ref mut table) = self.table_state {
275 table.finish_row();
276 }
277 }
278 TagEnd::TableCell => {
279 if let Some(builder) = self.active_cell.take()
280 && let Some(ref mut table) = self.table_state
281 {
282 let col_idx = table.current_row.len();
283 let alignment = table.alignments.get(col_idx).copied().unwrap_or(Alignment::None);
284 let lines = builder.finish();
285 let max_width = lines.iter().map(line_display_width).max().unwrap_or(0);
286 let cell = TableCell { lines, alignment, max_width };
287 table.add_cell(cell);
288 }
289 }
290 _ => {}
291 }
292 }
293
294 fn current_style(&self) -> Style {
295 self.style_stack.iter().copied().fold(Style::default(), Style::merge)
296 }
297
298 fn push_text(&mut self, text: &str) {
299 let style = self.current_style();
300 let prefix = self.blockquote_prefix();
301
302 for (i, chunk) in text.split('\n').enumerate() {
303 if i > 0 {
304 self.flush_line();
305 }
306 if self.current_line.is_empty() && !prefix.is_empty() {
307 self.current_line.push_with_style(&*prefix, Style::fg(self.theme.blockquote()));
308 }
309 if !chunk.is_empty() {
310 self.current_line.push_span(Span::with_style(chunk, style));
311 }
312 }
313 }
314
315 fn push_styled_text(&mut self, text: &str, style: Style) {
316 self.current_line.push_span(Span::with_style(text, style));
317 }
318
319 fn push_inline_text(&mut self, text: &str) {
320 if self.in_code_block {
321 self.code_buffer.push_str(text);
322 return;
323 }
324
325 let style = self.current_style();
326 if let Some(cell) = self.active_cell.as_mut() {
327 cell.push_text(text, style);
328 return;
329 }
330
331 self.push_text(text);
332 }
333
334 fn push_inline_code(&mut self, code: &str) {
335 if self.in_code_block {
336 self.code_buffer.push_str(code);
337 return;
338 }
339
340 let style = Style::fg(self.theme.code_fg());
341 if let Some(cell) = self.active_cell.as_mut() {
342 cell.push_code(code, style);
343 } else {
344 self.current_line.push_span(Span::with_style(code, style));
345 }
346 }
347
348 fn push_soft_break(&mut self) {
349 if self.in_code_block {
350 self.code_buffer.push('\n');
351 return;
352 }
353
354 let style = self.current_style();
355 if let Some(cell) = self.active_cell.as_mut() {
356 cell.soft_break(style);
357 return;
358 }
359
360 self.push_text(" ");
361 }
362
363 fn push_hard_break(&mut self) {
364 if self.in_code_block {
365 self.code_buffer.push('\n');
366 return;
367 }
368
369 if let Some(cell) = self.active_cell.as_mut() {
370 cell.hard_break();
371 return;
372 }
373
374 self.flush_line();
375 }
376
377 fn finish_current_line(&mut self) {
380 if !self.current_line.is_empty() {
381 self.flush_line();
382 }
383 }
384
385 fn flush_line(&mut self) {
386 let prefix = self.blockquote_prefix();
387 if !prefix.is_empty() && self.current_line.is_empty() {
388 self.current_line.push_with_style(&*prefix, Style::fg(self.theme.blockquote()));
389 }
390 let line = std::mem::take(&mut self.current_line);
391 self.lines.push(line);
392 }
393
394 fn blockquote_prefix(&self) -> Cow<'static, str> {
395 if self.blockquote_depth == 0 { Cow::Borrowed("") } else { Cow::Owned(" ".repeat(self.blockquote_depth)) }
396 }
397}