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}