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) = ¤t_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}