markdown.rs

  1use std::ops::Range;
  2use std::sync::Arc;
  3
  4use crate::{HighlightId, Language, LanguageRegistry};
  5use gpui::fonts::Weight;
  6use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
  7
  8#[derive(Debug, Clone)]
  9pub struct ParsedMarkdown {
 10    pub text: String,
 11    pub highlights: Vec<(Range<usize>, MarkdownHighlight)>,
 12    pub region_ranges: Vec<Range<usize>>,
 13    pub regions: Vec<ParsedRegion>,
 14}
 15
 16#[derive(Debug, Clone, PartialEq, Eq)]
 17pub enum MarkdownHighlight {
 18    Style(MarkdownHighlightStyle),
 19    Code(HighlightId),
 20}
 21
 22#[derive(Debug, Clone, Default, PartialEq, Eq)]
 23pub struct MarkdownHighlightStyle {
 24    pub italic: bool,
 25    pub underline: bool,
 26    pub weight: Weight,
 27}
 28
 29#[derive(Debug, Clone)]
 30pub struct ParsedRegion {
 31    pub code: bool,
 32    pub link_url: Option<String>,
 33}
 34
 35pub async fn parse_markdown(
 36    markdown: &str,
 37    language_registry: &Arc<LanguageRegistry>,
 38    language: Option<Arc<Language>>,
 39) -> ParsedMarkdown {
 40    let mut text = String::new();
 41    let mut highlights = Vec::new();
 42    let mut region_ranges = Vec::new();
 43    let mut regions = Vec::new();
 44
 45    parse_markdown_block(
 46        markdown,
 47        language_registry,
 48        language,
 49        &mut text,
 50        &mut highlights,
 51        &mut region_ranges,
 52        &mut regions,
 53    )
 54    .await;
 55
 56    ParsedMarkdown {
 57        text,
 58        highlights,
 59        region_ranges,
 60        regions,
 61    }
 62}
 63
 64pub async fn parse_markdown_block(
 65    markdown: &str,
 66    language_registry: &Arc<LanguageRegistry>,
 67    language: Option<Arc<Language>>,
 68    text: &mut String,
 69    highlights: &mut Vec<(Range<usize>, MarkdownHighlight)>,
 70    region_ranges: &mut Vec<Range<usize>>,
 71    regions: &mut Vec<ParsedRegion>,
 72) {
 73    let mut bold_depth = 0;
 74    let mut italic_depth = 0;
 75    let mut link_url = None;
 76    let mut current_language = None;
 77    let mut list_stack = Vec::new();
 78
 79    for event in Parser::new_ext(&markdown, Options::all()) {
 80        let prev_len = text.len();
 81        match event {
 82            Event::Text(t) => {
 83                if let Some(language) = &current_language {
 84                    highlight_code(text, highlights, t.as_ref(), language);
 85                } else {
 86                    text.push_str(t.as_ref());
 87
 88                    let mut style = MarkdownHighlightStyle::default();
 89                    if bold_depth > 0 {
 90                        style.weight = Weight::BOLD;
 91                    }
 92                    if italic_depth > 0 {
 93                        style.italic = true;
 94                    }
 95                    if let Some(link_url) = link_url.clone() {
 96                        region_ranges.push(prev_len..text.len());
 97                        regions.push(ParsedRegion {
 98                            link_url: Some(link_url),
 99                            code: false,
100                        });
101                        style.underline = true;
102                    }
103
104                    if style != MarkdownHighlightStyle::default() {
105                        let mut new_highlight = true;
106                        if let Some((last_range, MarkdownHighlight::Style(last_style))) =
107                            highlights.last_mut()
108                        {
109                            if last_range.end == prev_len && last_style == &style {
110                                last_range.end = text.len();
111                                new_highlight = false;
112                            }
113                        }
114                        if new_highlight {
115                            let range = prev_len..text.len();
116                            highlights.push((range, MarkdownHighlight::Style(style)));
117                        }
118                    }
119                }
120            }
121
122            Event::Code(t) => {
123                text.push_str(t.as_ref());
124                region_ranges.push(prev_len..text.len());
125                if link_url.is_some() {
126                    highlights.push((
127                        prev_len..text.len(),
128                        MarkdownHighlight::Style(MarkdownHighlightStyle {
129                            underline: true,
130                            ..Default::default()
131                        }),
132                    ));
133                }
134                regions.push(ParsedRegion {
135                    code: true,
136                    link_url: link_url.clone(),
137                });
138            }
139
140            Event::Start(tag) => match tag {
141                Tag::Paragraph => new_paragraph(text, &mut list_stack),
142
143                Tag::Heading(_, _, _) => {
144                    new_paragraph(text, &mut list_stack);
145                    bold_depth += 1;
146                }
147
148                Tag::CodeBlock(kind) => {
149                    new_paragraph(text, &mut list_stack);
150                    current_language = if let CodeBlockKind::Fenced(language) = kind {
151                        language_registry
152                            .language_for_name(language.as_ref())
153                            .await
154                            .ok()
155                    } else {
156                        language.clone()
157                    }
158                }
159
160                Tag::Emphasis => italic_depth += 1,
161
162                Tag::Strong => bold_depth += 1,
163
164                Tag::Link(_, url, _) => link_url = Some(url.to_string()),
165
166                Tag::List(number) => {
167                    list_stack.push((number, false));
168                }
169
170                Tag::Item => {
171                    let len = list_stack.len();
172                    if let Some((list_number, has_content)) = list_stack.last_mut() {
173                        *has_content = false;
174                        if !text.is_empty() && !text.ends_with('\n') {
175                            text.push('\n');
176                        }
177                        for _ in 0..len - 1 {
178                            text.push_str("  ");
179                        }
180                        if let Some(number) = list_number {
181                            text.push_str(&format!("{}. ", number));
182                            *number += 1;
183                            *has_content = false;
184                        } else {
185                            text.push_str("- ");
186                        }
187                    }
188                }
189
190                _ => {}
191            },
192
193            Event::End(tag) => match tag {
194                Tag::Heading(_, _, _) => bold_depth -= 1,
195                Tag::CodeBlock(_) => current_language = None,
196                Tag::Emphasis => italic_depth -= 1,
197                Tag::Strong => bold_depth -= 1,
198                Tag::Link(_, _, _) => link_url = None,
199                Tag::List(_) => drop(list_stack.pop()),
200                _ => {}
201            },
202
203            Event::HardBreak => text.push('\n'),
204
205            Event::SoftBreak => text.push(' '),
206
207            _ => {}
208        }
209    }
210}
211
212pub fn highlight_code(
213    text: &mut String,
214    highlights: &mut Vec<(Range<usize>, MarkdownHighlight)>,
215    content: &str,
216    language: &Arc<Language>,
217) {
218    let prev_len = text.len();
219    text.push_str(content);
220    for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) {
221        let highlight = MarkdownHighlight::Code(highlight_id);
222        highlights.push((prev_len + range.start..prev_len + range.end, highlight));
223    }
224}
225
226pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option<u64>, bool)>) {
227    let mut is_subsequent_paragraph_of_list = false;
228    if let Some((_, has_content)) = list_stack.last_mut() {
229        if *has_content {
230            is_subsequent_paragraph_of_list = true;
231        } else {
232            *has_content = true;
233            return;
234        }
235    }
236
237    if !text.is_empty() {
238        if !text.ends_with('\n') {
239            text.push('\n');
240        }
241        text.push('\n');
242    }
243    for _ in 0..list_stack.len().saturating_sub(1) {
244        text.push_str("  ");
245    }
246    if is_subsequent_paragraph_of_list {
247        text.push_str("  ");
248    }
249}