markdown.rs

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