markdown.rs

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