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