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