1use std::borrow::Cow;
2use std::ops::Range;
3
4use pulldown_cmark::{Alignment, CodeBlockKind, Event, Parser, Tag, TagEnd};
5
6use super::headings::MarkdownHeading;
7use super::pulldown_options;
8use super::source_map::SourceMap;
9use super::table::{CellBuilder, TableCell, TableState, line_display_width};
10use crate::line::Line;
11use crate::rendering::render_context::ViewContext;
12use crate::span::Span;
13use crate::style::Style;
14use crate::theme::Theme;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct SourceMappedLine {
18 pub source_line_no: usize,
19 pub line: Line,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct MarkdownBlock {
24 pub anchor_line_no: usize,
25 pub rendered_line_range: Range<usize>,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct MarkdownRenderResult {
30 pub lines: Vec<SourceMappedLine>,
31 pub headings: Vec<MarkdownHeading>,
32 pub blocks: Vec<MarkdownBlock>,
33}
34
35impl MarkdownRenderResult {
36 pub fn to_lines(self) -> Vec<Line> {
37 self.lines.into_iter().map(|line| line.line).collect()
38 }
39}
40
41pub fn render_markdown_result(text: &str, context: &ViewContext) -> MarkdownRenderResult {
42 let source = SourceMap::new(text);
43 MarkdownRenderer::new(context, &source).render()
44}
45
46struct MarkdownRenderer<'a> {
47 context: &'a ViewContext,
48 theme: &'a Theme,
49 source: &'a SourceMap<'a>,
50 style_stack: InlineStyleStack,
51 headings: HeadingCollector,
52 display_lines: Vec<Line>,
53 display_line_sources: Vec<usize>,
54 current_line: Line,
55 current_source_line_no: usize,
56 list_stack: Vec<Option<u64>>,
57 list_item_stack: Vec<ListItemState>,
58 code_buffer: String,
59 code_lang: String,
60 in_code_block: bool,
61 blockquote_depth: usize,
62 table_state: Option<TableState>,
63 active_cell: Option<CellBuilder>,
64 blocks: Vec<MarkdownBlock>,
65 current_block: Option<BlockBuilder>,
66}
67
68struct BlockBuilder {
69 anchor_line_no: usize,
70 first_rendered_line: usize,
71}
72
73#[derive(Default)]
74struct ListItemState {
75 is_loose: bool,
76}
77
78#[derive(Clone, Copy)]
79enum BlockSpacing {
80 None,
81 BlankLineAfter,
82}
83
84impl<'a> MarkdownRenderer<'a> {
85 fn new(context: &'a ViewContext, source: &'a SourceMap<'a>) -> Self {
86 Self {
87 context,
88 theme: &context.theme,
89 source,
90 style_stack: InlineStyleStack::new(),
91 headings: HeadingCollector::new(),
92 display_lines: Vec::new(),
93 display_line_sources: Vec::new(),
94 current_line: Line::default(),
95 current_source_line_no: 1,
96 list_stack: Vec::new(),
97 list_item_stack: Vec::new(),
98 code_buffer: String::new(),
99 code_lang: String::new(),
100 in_code_block: false,
101 blockquote_depth: 0,
102 table_state: None,
103 active_cell: None,
104 blocks: Vec::new(),
105 current_block: None,
106 }
107 }
108
109 fn render(mut self) -> MarkdownRenderResult {
110 let parser = Parser::new_ext(self.source.text(), pulldown_options()).into_offset_iter();
111 for (event, range) in parser {
112 self.handle_event(event, range);
113 }
114
115 self.flush_line();
116 while self.display_lines.last().is_some_and(Line::is_empty) {
117 self.display_lines.pop();
118 self.display_line_sources.pop();
119 }
120
121 self.finalize_current_block();
122
123 let lines: Vec<SourceMappedLine> = self
124 .display_lines
125 .into_iter()
126 .zip(self.display_line_sources)
127 .map(|(line, source_line_no)| SourceMappedLine { source_line_no, line })
128 .collect();
129 MarkdownRenderResult { lines, headings: self.headings.into_headings(), blocks: self.blocks }
130 }
131
132 fn handle_event(&mut self, event: Event<'_>, range: Range<usize>) {
133 self.current_source_line_no = self.source.line_no_for_start(&range);
134
135 match event {
136 Event::Start(tag) => self.handle_start(tag, range),
137 Event::End(tag_end) => self.handle_end(tag_end),
138 Event::Text(text) => {
139 self.headings.append_text(&text);
140 self.push_inline_text(&text);
141 }
142 Event::Code(code) => {
143 self.headings.append_text(&code);
144 self.push_inline_code(&code);
145 }
146 Event::SoftBreak => self.push_soft_break(),
147 Event::HardBreak => self.push_hard_break(),
148 Event::Rule => {
149 self.finish_current_line();
150 self.start_block(self.current_source_line_no);
151 self.push_line(Line::with_style("───────────────", Style::fg(self.theme.muted())));
152 self.finish_rendered_block(BlockSpacing::BlankLineAfter);
153 }
154 _ => {}
155 }
156 }
157
158 fn handle_start(&mut self, tag: Tag<'_>, range: Range<usize>) {
159 match tag {
160 Tag::Heading { level, .. } => {
161 self.headings.begin(level as u8, self.source.line_no_for_start(&range));
162 self.finish_current_line();
163 self.start_block(self.current_source_line_no);
164 let prefix = "#".repeat(level as usize);
165 let style = heading_style(level as u8, self.theme);
166 self.push_styled_text(&format!("{prefix} "), style);
167 self.style_stack.push(style);
168 }
169 Tag::Paragraph if self.list_item_stack.is_empty() => {
170 self.start_block(self.current_source_line_no);
171 }
172 Tag::BlockQuote(_) => {
173 self.finish_current_line();
174 self.blockquote_depth += 1;
175 self.style_stack.push(Style::fg(self.theme.blockquote()));
176 }
177 Tag::List(start) => {
178 if self.list_stack.is_empty() {
179 self.finish_current_line();
180 }
181 self.list_stack.push(start);
182 }
183 Tag::Item => {
184 self.list_item_stack.push(ListItemState::default());
185 self.finish_current_line();
186 self.start_block(self.current_source_line_no);
187 let indent = " ".repeat(self.list_stack.len().saturating_sub(1));
188 let marker = match self.list_stack.last_mut() {
189 Some(Some(n)) => {
190 let marker = format!("{n}. ");
191 *n += 1;
192 marker
193 }
194 _ => "- ".to_string(),
195 };
196 self.push_styled_text(&format!("{indent}{marker}"), Style::fg(self.theme.muted()));
197 }
198 Tag::Strong | Tag::Emphasis | Tag::Strikethrough | Tag::Link { .. } => {
199 self.style_stack.push_inline_tag(&tag, self.theme);
200 }
201 Tag::CodeBlock(kind) => self.handle_code_block_start(kind),
202 Tag::Table(_) | Tag::TableRow | Tag::TableCell => self.handle_table_start(tag),
203 _ => {}
204 }
205 }
206
207 fn handle_end(&mut self, tag_end: TagEnd) {
208 match tag_end {
209 TagEnd::Paragraph => self.finish_block(BlockSpacing::BlankLineAfter),
210 TagEnd::Heading(_) => {
211 self.style_stack.pop();
212 self.headings.finish();
213 self.finish_block(BlockSpacing::BlankLineAfter);
214 }
215 TagEnd::BlockQuote(_) => {
216 self.style_stack.pop();
217 self.blockquote_depth = self.blockquote_depth.saturating_sub(1);
218 let spacing =
219 if self.blockquote_depth == 0 { BlockSpacing::BlankLineAfter } else { BlockSpacing::None };
220 self.finish_block(spacing);
221 }
222 TagEnd::List(_) => {
223 self.list_stack.pop();
224 if self.list_stack.is_empty() {
225 self.finish_block(BlockSpacing::BlankLineAfter);
226 }
227 }
228 TagEnd::Item => {
229 self.finish_current_line();
230 if self.list_item_stack.pop().is_some_and(|state| state.is_loose) {
231 self.push_blank_line();
232 }
233 }
234 TagEnd::Strong | TagEnd::Emphasis | TagEnd::Strikethrough | TagEnd::Link => self.style_stack.pop(),
235 TagEnd::CodeBlock => self.handle_code_block_end(),
236 TagEnd::Table | TagEnd::TableRow | TagEnd::TableHead | TagEnd::TableCell => self.handle_table_end(tag_end),
237 _ => {}
238 }
239 }
240
241 fn handle_code_block_start(&mut self, kind: CodeBlockKind<'_>) {
242 self.finish_current_line();
243 self.start_block(self.current_source_line_no);
244 self.in_code_block = true;
245 self.code_buffer.clear();
246 self.code_lang = match kind {
247 CodeBlockKind::Fenced(lang) => lang.split(',').next().unwrap_or("").trim().to_string(),
248 CodeBlockKind::Indented => String::new(),
249 };
250 }
251
252 fn handle_code_block_end(&mut self) {
253 self.in_code_block = false;
254 let code = std::mem::take(&mut self.code_buffer);
255 let lang = std::mem::take(&mut self.code_lang);
256 let code_lines = self.context.highlighter().highlight(&code, &lang, self.theme);
257 self.extend_lines(code_lines);
258 self.finish_rendered_block(BlockSpacing::BlankLineAfter);
259 }
260
261 fn handle_table_start(&mut self, tag: Tag<'_>) {
262 match tag {
263 Tag::Table(alignments) => {
264 self.finish_current_line();
265 self.start_block(self.current_source_line_no);
266 self.table_state = Some(TableState::new(&alignments));
267 }
268 Tag::TableRow => {
269 if let Some(ref mut table) = self.table_state {
270 table.start_row();
271 }
272 }
273 Tag::TableCell => {
274 self.active_cell = Some(CellBuilder::default());
275 }
276 _ => {}
277 }
278 }
279
280 fn handle_table_end(&mut self, tag_end: TagEnd) {
281 match tag_end {
282 TagEnd::Table => {
283 if let Some(table) = self.table_state.take() {
284 let rendered = table.render(self.theme);
285 self.extend_lines(rendered);
286 self.finish_rendered_block(BlockSpacing::BlankLineAfter);
287 }
288 }
289 TagEnd::TableRow | TagEnd::TableHead => {
290 if let Some(ref mut table) = self.table_state {
291 table.finish_row();
292 }
293 }
294 TagEnd::TableCell => {
295 if let Some(builder) = self.active_cell.take()
296 && let Some(ref mut table) = self.table_state
297 {
298 let col_idx = table.current_row.len();
299 let alignment = table.alignments.get(col_idx).copied().unwrap_or(Alignment::None);
300 let lines = builder.finish();
301 let max_width = lines.iter().map(line_display_width).max().unwrap_or(0);
302 let cell = TableCell { lines, alignment, max_width };
303 table.add_cell(cell);
304 }
305 }
306 _ => {}
307 }
308 }
309
310 fn push_inline_text(&mut self, text: &str) {
311 if self.in_code_block {
312 self.code_buffer.push_str(text);
313 return;
314 }
315
316 let style = self.style_stack.current();
317 if let Some(cell) = self.active_cell.as_mut() {
318 cell.push_text(text, style);
319 return;
320 }
321
322 self.push_text(text);
323 }
324
325 fn push_inline_code(&mut self, code: &str) {
326 if self.in_code_block {
327 self.code_buffer.push_str(code);
328 return;
329 }
330
331 let style = Style::fg(self.theme.code_fg());
332 if let Some(cell) = self.active_cell.as_mut() {
333 cell.push_code(code, style);
334 } else {
335 self.current_line.push_span(Span::with_style(code, style));
336 }
337 }
338
339 fn push_soft_break(&mut self) {
340 if self.in_code_block {
341 self.code_buffer.push('\n');
342 return;
343 }
344
345 let style = self.style_stack.current();
346 if let Some(cell) = self.active_cell.as_mut() {
347 cell.soft_break(style);
348 return;
349 }
350
351 self.push_text(" ");
352 }
353
354 fn push_hard_break(&mut self) {
355 if self.in_code_block {
356 self.code_buffer.push('\n');
357 return;
358 }
359
360 if let Some(cell) = self.active_cell.as_mut() {
361 cell.hard_break();
362 return;
363 }
364
365 self.flush_line();
366 }
367
368 fn push_text(&mut self, text: &str) {
369 let style = self.style_stack.current();
370 let prefix = self.blockquote_prefix();
371
372 for (index, chunk) in text.split('\n').enumerate() {
373 if index > 0 {
374 self.flush_line();
375 }
376 if self.current_line.is_empty() && !prefix.is_empty() {
377 self.current_line.push_with_style(&*prefix, Style::fg(self.theme.blockquote()));
378 }
379 if !chunk.is_empty() {
380 self.current_line.push_span(Span::with_style(chunk, style));
381 }
382 }
383 }
384
385 fn push_styled_text(&mut self, text: &str, style: Style) {
386 self.current_line.push_span(Span::with_style(text, style));
387 }
388
389 fn finish_current_line(&mut self) {
390 if !self.current_line.is_empty() {
391 self.flush_line();
392 }
393 }
394
395 fn finish_block(&mut self, spacing: BlockSpacing) {
396 self.finish_current_line();
397 self.finish_rendered_block(spacing);
398 }
399
400 fn finish_rendered_block(&mut self, spacing: BlockSpacing) {
401 if matches!(spacing, BlockSpacing::BlankLineAfter) {
402 if let Some(item_state) = self.list_item_stack.last_mut() {
403 item_state.is_loose = true;
404 }
405 self.push_blank_line();
406 }
407 }
408
409 fn push_blank_line(&mut self) {
410 if self.display_lines.is_empty() {
411 return;
412 }
413 if self.display_lines.last().is_some_and(Line::is_empty) {
414 return;
415 }
416 self.push_line(Line::default());
417 }
418
419 fn flush_line(&mut self) {
420 let prefix = self.blockquote_prefix();
421 if !prefix.is_empty() && self.current_line.is_empty() {
422 self.current_line.push_with_style(&*prefix, Style::fg(self.theme.blockquote()));
423 }
424 let line = std::mem::take(&mut self.current_line);
425 self.push_line(line);
426 }
427
428 fn push_line(&mut self, line: Line) {
429 self.display_lines.push(line);
430 self.display_line_sources.push(self.current_source_line_no);
431 }
432
433 fn extend_lines(&mut self, lines: Vec<Line>) {
434 for line in lines {
435 self.push_line(line);
436 }
437 }
438
439 fn blockquote_prefix(&self) -> Cow<'static, str> {
440 if self.blockquote_depth == 0 { Cow::Borrowed("") } else { Cow::Owned(" ".repeat(self.blockquote_depth)) }
441 }
442
443 fn start_block(&mut self, anchor_line_no: usize) {
444 self.finalize_current_block();
445 self.current_block = Some(BlockBuilder { anchor_line_no, first_rendered_line: self.display_lines.len() });
446 }
447
448 fn finalize_current_block(&mut self) {
449 let Some(builder) = self.current_block.take() else {
450 return;
451 };
452 let mut end = self.display_lines.len();
453 while end > builder.first_rendered_line && self.display_lines.get(end - 1).is_some_and(Line::is_empty) {
454 end -= 1;
455 }
456 if end > builder.first_rendered_line {
457 self.blocks.push(MarkdownBlock {
458 anchor_line_no: builder.anchor_line_no,
459 rendered_line_range: builder.first_rendered_line..end,
460 });
461 }
462 }
463}
464
465struct InlineStyleStack {
466 stack: Vec<Style>,
467}
468
469impl InlineStyleStack {
470 fn new() -> Self {
471 Self { stack: Vec::new() }
472 }
473
474 fn push(&mut self, style: Style) {
475 self.stack.push(style);
476 }
477
478 fn pop(&mut self) {
479 self.stack.pop();
480 }
481
482 fn current(&self) -> Style {
483 self.stack.iter().copied().fold(Style::default(), Style::merge)
484 }
485
486 fn push_inline_tag(&mut self, tag: &Tag<'_>, theme: &Theme) {
487 match tag {
488 Tag::Heading { level, .. } => self.push(heading_style(*level as u8, theme)),
489 Tag::BlockQuote(_) => self.push(Style::fg(theme.blockquote())),
490 Tag::Strong => self.push(Style::default().bold()),
491 Tag::Emphasis => self.push(Style::default().italic()),
492 Tag::Strikethrough => self.push(Style::default().strikethrough()),
493 Tag::Link { .. } => self.push(Style::fg(theme.link()).underline()),
494 _ => {}
495 }
496 }
497}
498
499struct HeadingCollector {
500 headings: Vec<MarkdownHeading>,
501 active: Option<ActiveHeading>,
502}
503
504impl HeadingCollector {
505 fn new() -> Self {
506 Self { headings: Vec::new(), active: None }
507 }
508
509 fn begin(&mut self, level: u8, source_line_no: usize) {
510 self.active = Some(ActiveHeading { level, source_line_no, title: String::new() });
511 }
512
513 fn append_text(&mut self, text: &str) {
514 if let Some(active) = self.active.as_mut() {
515 active.title.push_str(text);
516 }
517 }
518
519 fn finish(&mut self) {
520 let Some(active) = self.active.take() else {
521 return;
522 };
523 let title = active.title.trim().to_string();
524 if title.is_empty() {
525 return;
526 }
527 self.headings.push(MarkdownHeading { title, level: active.level, source_line_no: active.source_line_no });
528 }
529
530 fn into_headings(self) -> Vec<MarkdownHeading> {
531 self.headings
532 }
533}
534
535fn heading_style(level: u8, theme: &Theme) -> Style {
536 match level {
537 1 => Style::fg(theme.heading()).bold(),
538 2 => Style::fg(theme.text_primary()).bold(),
539 _ => Style::fg(theme.text_secondary()).bold(),
540 }
541}
542
543struct ActiveHeading {
544 level: u8,
545 source_line_no: usize,
546 title: String,
547}