markdown_renderer.rs

  1use std::{ops::Range, sync::Arc};
  2
  3use gpui::{
  4    div, px, rems, AnyElement, DefiniteLength, Div, ElementId, Hsla, ParentElement, SharedString,
  5    Styled, StyledText, WindowContext,
  6};
  7use language::LanguageRegistry;
  8use pulldown_cmark::{Alignment, CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag};
  9use rich_text::render_rich_text;
 10use theme::{ActiveTheme, Theme};
 11use ui::{h_flex, v_flex};
 12
 13enum TableState {
 14    Header,
 15    Body,
 16}
 17
 18struct MarkdownTable {
 19    column_alignments: Vec<Alignment>,
 20    header: Vec<Div>,
 21    body: Vec<Vec<Div>>,
 22    current_row: Vec<Div>,
 23    state: TableState,
 24    border_color: Hsla,
 25}
 26
 27impl MarkdownTable {
 28    fn new(border_color: Hsla, column_alignments: Vec<Alignment>) -> Self {
 29        Self {
 30            column_alignments,
 31            header: Vec::new(),
 32            body: Vec::new(),
 33            current_row: Vec::new(),
 34            state: TableState::Header,
 35            border_color,
 36        }
 37    }
 38
 39    fn finish_row(&mut self) {
 40        match self.state {
 41            TableState::Header => {
 42                self.header.extend(self.current_row.drain(..));
 43                self.state = TableState::Body;
 44            }
 45            TableState::Body => {
 46                self.body.push(self.current_row.drain(..).collect());
 47            }
 48        }
 49    }
 50
 51    fn add_cell(&mut self, contents: AnyElement) {
 52        let container = match self.alignment_for_next_cell() {
 53            Alignment::Left | Alignment::None => div(),
 54            Alignment::Center => v_flex().items_center(),
 55            Alignment::Right => v_flex().items_end(),
 56        };
 57
 58        let cell = container
 59            .w_full()
 60            .child(contents)
 61            .px_2()
 62            .py_1()
 63            .border_color(self.border_color);
 64
 65        let cell = match self.state {
 66            TableState::Header => cell.border_2(),
 67            TableState::Body => cell.border_1(),
 68        };
 69
 70        self.current_row.push(cell);
 71    }
 72
 73    fn finish(self) -> Div {
 74        let mut table = v_flex().w_full();
 75        let mut header = h_flex();
 76
 77        for cell in self.header {
 78            header = header.child(cell);
 79        }
 80        table = table.child(header);
 81        for row in self.body {
 82            let mut row_div = h_flex();
 83            for cell in row {
 84                row_div = row_div.child(cell);
 85            }
 86            table = table.child(row_div);
 87        }
 88        table
 89    }
 90
 91    fn alignment_for_next_cell(&self) -> Alignment {
 92        self.column_alignments
 93            .get(self.current_row.len())
 94            .copied()
 95            .unwrap_or(Alignment::None)
 96    }
 97}
 98
 99struct Renderer<I> {
100    source_contents: String,
101    iter: I,
102    theme: Arc<Theme>,
103    finished: Vec<Div>,
104    language_registry: Arc<LanguageRegistry>,
105    table: Option<MarkdownTable>,
106    list_depth: usize,
107    block_quote_depth: usize,
108}
109
110impl<'a, I> Renderer<I>
111where
112    I: Iterator<Item = (Event<'a>, Range<usize>)>,
113{
114    fn new(
115        iter: I,
116        source_contents: String,
117        language_registry: &Arc<LanguageRegistry>,
118        theme: Arc<Theme>,
119    ) -> Self {
120        Self {
121            iter,
122            source_contents,
123            theme,
124            table: None,
125            finished: vec![],
126            language_registry: language_registry.clone(),
127            list_depth: 0,
128            block_quote_depth: 0,
129        }
130    }
131
132    fn run(mut self, cx: &WindowContext) -> Self {
133        while let Some((event, source_range)) = self.iter.next() {
134            match event {
135                Event::Start(tag) => {
136                    self.start_tag(tag);
137                }
138                Event::End(tag) => {
139                    self.end_tag(tag, source_range, cx);
140                }
141                Event::Rule => {
142                    let rule = div().w_full().h(px(2.)).bg(self.theme.colors().border);
143                    self.finished.push(div().mb_4().child(rule));
144                }
145                _ => {}
146            }
147        }
148        self
149    }
150
151    fn start_tag(&mut self, tag: Tag<'a>) {
152        match tag {
153            Tag::List(_) => {
154                self.list_depth += 1;
155            }
156            Tag::BlockQuote => {
157                self.block_quote_depth += 1;
158            }
159            Tag::Table(column_alignments) => {
160                self.table = Some(MarkdownTable::new(
161                    self.theme.colors().border,
162                    column_alignments,
163                ));
164            }
165            _ => {}
166        }
167    }
168
169    fn end_tag(&mut self, tag: Tag, source_range: Range<usize>, cx: &WindowContext) {
170        match tag {
171            Tag::Paragraph => {
172                if self.list_depth > 0 || self.block_quote_depth > 0 {
173                    return;
174                }
175
176                let element = self.render_md_from_range(source_range.clone(), cx);
177                let paragraph = div().mb_3().child(element);
178
179                self.finished.push(paragraph);
180            }
181            Tag::Heading(level, _, _) => {
182                let mut headline = self.headline(level);
183                if source_range.start > 0 {
184                    headline = headline.mt_4();
185                }
186
187                let element = self.render_md_from_range(source_range.clone(), cx);
188                let headline = headline.child(element);
189
190                self.finished.push(headline);
191            }
192            Tag::List(_) => {
193                if self.list_depth == 1 {
194                    let element = self.render_md_from_range(source_range.clone(), cx);
195                    let list = div().mb_3().child(element);
196
197                    self.finished.push(list);
198                }
199
200                self.list_depth -= 1;
201            }
202            Tag::BlockQuote => {
203                let element = self.render_md_from_range(source_range.clone(), cx);
204
205                let block_quote = h_flex()
206                    .mb_3()
207                    .child(
208                        div()
209                            .w(px(4.))
210                            .bg(self.theme.colors().border)
211                            .h_full()
212                            .mr_2()
213                            .mt_1(),
214                    )
215                    .text_color(self.theme.colors().text_muted)
216                    .child(element);
217
218                self.finished.push(block_quote);
219
220                self.block_quote_depth -= 1;
221            }
222            Tag::CodeBlock(kind) => {
223                let contents = self.source_contents[source_range.clone()].trim();
224                let contents = contents.trim_start_matches("```");
225                let contents = contents.trim_end_matches("```");
226                let contents = match kind {
227                    CodeBlockKind::Fenced(language) => {
228                        contents.trim_start_matches(&language.to_string())
229                    }
230                    CodeBlockKind::Indented => contents,
231                };
232                let contents: String = contents.into();
233                let contents = SharedString::from(contents);
234
235                let code_block = div()
236                    .mb_3()
237                    .px_4()
238                    .py_0()
239                    .bg(self.theme.colors().surface_background)
240                    .child(StyledText::new(contents));
241
242                self.finished.push(code_block);
243            }
244            Tag::Table(_alignment) => {
245                if self.table.is_none() {
246                    log::error!("Table end without table ({:?})", source_range);
247                    return;
248                }
249
250                let table = self.table.take().unwrap();
251                let table = table.finish().mb_4();
252                self.finished.push(table);
253            }
254            Tag::TableHead => {
255                if self.table.is_none() {
256                    log::error!("Table head without table ({:?})", source_range);
257                    return;
258                }
259
260                self.table.as_mut().unwrap().finish_row();
261            }
262            Tag::TableRow => {
263                if self.table.is_none() {
264                    log::error!("Table row without table ({:?})", source_range);
265                    return;
266                }
267
268                self.table.as_mut().unwrap().finish_row();
269            }
270            Tag::TableCell => {
271                if self.table.is_none() {
272                    log::error!("Table cell without table ({:?})", source_range);
273                    return;
274                }
275
276                let contents = self.render_md_from_range(source_range.clone(), cx);
277                self.table.as_mut().unwrap().add_cell(contents);
278            }
279            _ => {}
280        }
281    }
282
283    fn render_md_from_range(
284        &self,
285        source_range: Range<usize>,
286        cx: &WindowContext,
287    ) -> gpui::AnyElement {
288        let mentions = &[];
289        let language = None;
290        let paragraph = &self.source_contents[source_range.clone()];
291        let rich_text = render_rich_text(
292            paragraph.into(),
293            mentions,
294            &self.language_registry,
295            language,
296        );
297        let id: ElementId = source_range.start.into();
298        rich_text.element(id, cx)
299    }
300
301    fn headline(&self, level: HeadingLevel) -> Div {
302        let size = match level {
303            HeadingLevel::H1 => rems(2.),
304            HeadingLevel::H2 => rems(1.5),
305            HeadingLevel::H3 => rems(1.25),
306            HeadingLevel::H4 => rems(1.),
307            HeadingLevel::H5 => rems(0.875),
308            HeadingLevel::H6 => rems(0.85),
309        };
310
311        let color = match level {
312            HeadingLevel::H6 => self.theme.colors().text_muted,
313            _ => self.theme.colors().text,
314        };
315
316        let line_height = DefiniteLength::from(rems(1.25));
317
318        let headline = h_flex()
319            .w_full()
320            .line_height(line_height)
321            .text_size(size)
322            .text_color(color)
323            .mb_4()
324            .pb(rems(0.15));
325
326        headline
327    }
328}
329
330pub fn render_markdown(
331    markdown_input: &str,
332    language_registry: &Arc<LanguageRegistry>,
333    cx: &WindowContext,
334) -> Vec<Div> {
335    let theme = cx.theme().clone();
336    let options = Options::all();
337    let parser = Parser::new_ext(markdown_input, options);
338    let renderer = Renderer::new(
339        parser.into_offset_iter(),
340        markdown_input.to_owned(),
341        language_registry,
342        theme,
343    );
344    let renderer = renderer.run(cx);
345    return renderer.finished;
346}