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