1use gpui::{
2 px, FontStyle, FontWeight, HighlightStyle, SharedString, StrikethroughStyle, UnderlineStyle,
3};
4use language::HighlightId;
5use std::{fmt::Display, ops::Range, path::PathBuf};
6
7#[derive(Debug)]
8#[cfg_attr(test, derive(PartialEq))]
9pub enum ParsedMarkdownElement {
10 Heading(ParsedMarkdownHeading),
11 ListItem(ParsedMarkdownListItem),
12 Table(ParsedMarkdownTable),
13 BlockQuote(ParsedMarkdownBlockQuote),
14 CodeBlock(ParsedMarkdownCodeBlock),
15 /// A paragraph of text and other inline elements.
16 Paragraph(MarkdownParagraph),
17 HorizontalRule(Range<usize>),
18}
19
20impl ParsedMarkdownElement {
21 pub fn source_range(&self) -> Range<usize> {
22 match self {
23 Self::Heading(heading) => heading.source_range.clone(),
24 Self::ListItem(list_item) => list_item.source_range.clone(),
25 Self::Table(table) => table.source_range.clone(),
26 Self::BlockQuote(block_quote) => block_quote.source_range.clone(),
27 Self::CodeBlock(code_block) => code_block.source_range.clone(),
28 Self::Paragraph(text) => match &text[0] {
29 MarkdownParagraphChunk::Text(t) => t.source_range.clone(),
30 MarkdownParagraphChunk::Image(image) => match image {
31 Image::Web { source_range, .. } => source_range.clone(),
32 Image::Path { source_range, .. } => source_range.clone(),
33 },
34 },
35 Self::HorizontalRule(range) => range.clone(),
36 }
37 }
38
39 pub fn is_list_item(&self) -> bool {
40 matches!(self, Self::ListItem(_))
41 }
42}
43
44pub type MarkdownParagraph = Vec<MarkdownParagraphChunk>;
45
46#[derive(Debug)]
47#[cfg_attr(test, derive(PartialEq))]
48pub enum MarkdownParagraphChunk {
49 Text(ParsedMarkdownText),
50 Image(Image),
51}
52
53#[derive(Debug)]
54#[cfg_attr(test, derive(PartialEq))]
55pub struct ParsedMarkdown {
56 pub children: Vec<ParsedMarkdownElement>,
57}
58
59#[derive(Debug)]
60#[cfg_attr(test, derive(PartialEq))]
61pub struct ParsedMarkdownListItem {
62 pub source_range: Range<usize>,
63 /// How many indentations deep this item is.
64 pub depth: u16,
65 pub item_type: ParsedMarkdownListItemType,
66 pub content: Vec<ParsedMarkdownElement>,
67}
68
69#[derive(Debug)]
70#[cfg_attr(test, derive(PartialEq))]
71pub enum ParsedMarkdownListItemType {
72 Ordered(u64),
73 Task(bool, Range<usize>),
74 Unordered,
75}
76
77#[derive(Debug)]
78#[cfg_attr(test, derive(PartialEq))]
79pub struct ParsedMarkdownCodeBlock {
80 pub source_range: Range<usize>,
81 pub language: Option<String>,
82 pub contents: SharedString,
83 pub highlights: Option<Vec<(Range<usize>, HighlightId)>>,
84}
85
86#[derive(Debug)]
87#[cfg_attr(test, derive(PartialEq))]
88pub struct ParsedMarkdownHeading {
89 pub source_range: Range<usize>,
90 pub level: HeadingLevel,
91 pub contents: MarkdownParagraph,
92}
93
94#[derive(Debug, PartialEq)]
95pub enum HeadingLevel {
96 H1,
97 H2,
98 H3,
99 H4,
100 H5,
101 H6,
102}
103
104#[derive(Debug)]
105pub struct ParsedMarkdownTable {
106 pub source_range: Range<usize>,
107 pub header: ParsedMarkdownTableRow,
108 pub body: Vec<ParsedMarkdownTableRow>,
109 pub column_alignments: Vec<ParsedMarkdownTableAlignment>,
110}
111
112#[derive(Debug, Clone, Copy)]
113#[cfg_attr(test, derive(PartialEq))]
114pub enum ParsedMarkdownTableAlignment {
115 /// Default text alignment.
116 None,
117 Left,
118 Center,
119 Right,
120}
121
122#[derive(Debug)]
123#[cfg_attr(test, derive(PartialEq))]
124pub struct ParsedMarkdownTableRow {
125 pub children: Vec<MarkdownParagraph>,
126}
127
128impl Default for ParsedMarkdownTableRow {
129 fn default() -> Self {
130 Self::new()
131 }
132}
133
134impl ParsedMarkdownTableRow {
135 pub fn new() -> Self {
136 Self {
137 children: Vec::new(),
138 }
139 }
140
141 pub fn with_children(children: Vec<MarkdownParagraph>) -> Self {
142 Self { children }
143 }
144}
145
146#[derive(Debug)]
147#[cfg_attr(test, derive(PartialEq))]
148pub struct ParsedMarkdownBlockQuote {
149 pub source_range: Range<usize>,
150 pub children: Vec<ParsedMarkdownElement>,
151}
152
153#[derive(Debug, Clone)]
154pub struct ParsedMarkdownText {
155 /// Where the text is located in the source Markdown document.
156 pub source_range: Range<usize>,
157 /// The text content stripped of any formatting symbols.
158 pub contents: String,
159 /// The list of highlights contained in the Markdown document.
160 pub highlights: Vec<(Range<usize>, MarkdownHighlight)>,
161 /// The regions of the various ranges in the Markdown document.
162 pub region_ranges: Vec<Range<usize>>,
163 /// The regions of the Markdown document.
164 pub regions: Vec<ParsedRegion>,
165}
166
167/// A run of highlighted Markdown text.
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub enum MarkdownHighlight {
170 /// A styled Markdown highlight.
171 Style(MarkdownHighlightStyle),
172 /// A highlighted code block.
173 Code(HighlightId),
174}
175
176impl MarkdownHighlight {
177 /// Converts this [`MarkdownHighlight`] to a [`HighlightStyle`].
178 pub fn to_highlight_style(&self, theme: &theme::SyntaxTheme) -> Option<HighlightStyle> {
179 match self {
180 MarkdownHighlight::Style(style) => {
181 let mut highlight = HighlightStyle::default();
182
183 if style.italic {
184 highlight.font_style = Some(FontStyle::Italic);
185 }
186
187 if style.underline {
188 highlight.underline = Some(UnderlineStyle {
189 thickness: px(1.),
190 ..Default::default()
191 });
192 }
193
194 if style.strikethrough {
195 highlight.strikethrough = Some(StrikethroughStyle {
196 thickness: px(1.),
197 ..Default::default()
198 });
199 }
200
201 if style.weight != FontWeight::default() {
202 highlight.font_weight = Some(style.weight);
203 }
204
205 Some(highlight)
206 }
207
208 MarkdownHighlight::Code(id) => id.style(theme),
209 }
210 }
211}
212
213/// The style for a Markdown highlight.
214#[derive(Debug, Clone, Default, PartialEq, Eq)]
215pub struct MarkdownHighlightStyle {
216 /// Whether the text should be italicized.
217 pub italic: bool,
218 /// Whether the text should be underlined.
219 pub underline: bool,
220 /// Whether the text should be struck through.
221 pub strikethrough: bool,
222 /// The weight of the text.
223 pub weight: FontWeight,
224}
225
226/// A parsed region in a Markdown document.
227#[derive(Debug, Clone)]
228#[cfg_attr(test, derive(PartialEq))]
229pub struct ParsedRegion {
230 /// Whether the region is a code block.
231 pub code: bool,
232 /// The link contained in this region, if it has one.
233 pub link: Option<Link>,
234}
235
236/// A Markdown link.
237#[derive(Debug, Clone)]
238#[cfg_attr(test, derive(PartialEq))]
239pub enum Link {
240 /// A link to a webpage.
241 Web {
242 /// The URL of the webpage.
243 url: String,
244 },
245 /// A link to a path on the filesystem.
246 Path {
247 /// The path as provided in the Markdown document.
248 display_path: PathBuf,
249 /// The absolute path to the item.
250 path: PathBuf,
251 },
252}
253
254impl Link {
255 pub fn identify(file_location_directory: Option<PathBuf>, text: String) -> Option<Link> {
256 if text.starts_with("http") {
257 return Some(Link::Web { url: text });
258 }
259
260 let path = PathBuf::from(&text);
261 if path.is_absolute() && path.exists() {
262 return Some(Link::Path {
263 display_path: path.clone(),
264 path,
265 });
266 }
267
268 if let Some(file_location_directory) = file_location_directory {
269 let display_path = path;
270 let path = file_location_directory.join(text);
271 if path.exists() {
272 return Some(Link::Path { display_path, path });
273 }
274 }
275
276 None
277 }
278}
279
280impl Display for Link {
281 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
282 match self {
283 Link::Web { url } => write!(f, "{}", url),
284 Link::Path { display_path, .. } => write!(f, "{}", display_path.display()),
285 }
286 }
287}
288
289/// A Markdown Image
290#[derive(Debug, Clone)]
291#[cfg_attr(test, derive(PartialEq))]
292pub enum Image {
293 Web {
294 source_range: Range<usize>,
295 /// The URL of the Image.
296 url: String,
297 /// Link URL if exists.
298 link: Option<Link>,
299 /// alt text if it exists
300 alt_text: Option<ParsedMarkdownText>,
301 },
302 /// Image path on the filesystem.
303 Path {
304 source_range: Range<usize>,
305 /// The path as provided in the Markdown document.
306 display_path: PathBuf,
307 /// The absolute path to the item.
308 path: PathBuf,
309 /// Link URL if exists.
310 link: Option<Link>,
311 /// alt text if it exists
312 alt_text: Option<ParsedMarkdownText>,
313 },
314}
315
316impl Image {
317 pub fn identify(
318 source_range: Range<usize>,
319 file_location_directory: Option<PathBuf>,
320 text: String,
321 link: Option<Link>,
322 ) -> Option<Image> {
323 if text.starts_with("http") {
324 return Some(Image::Web {
325 source_range,
326 url: text,
327 link,
328 alt_text: None,
329 });
330 }
331 let path = PathBuf::from(&text);
332 if path.is_absolute() {
333 return Some(Image::Path {
334 source_range,
335 display_path: path.clone(),
336 path,
337 link,
338 alt_text: None,
339 });
340 }
341 if let Some(file_location_directory) = file_location_directory {
342 let display_path = path;
343 let path = file_location_directory.join(text);
344 return Some(Image::Path {
345 source_range,
346 display_path,
347 path,
348 link,
349 alt_text: None,
350 });
351 }
352 None
353 }
354
355 pub fn with_alt_text(&self, alt_text: ParsedMarkdownText) -> Self {
356 match self {
357 Image::Web {
358 ref source_range,
359 ref url,
360 ref link,
361 ..
362 } => Image::Web {
363 source_range: source_range.clone(),
364 url: url.clone(),
365 link: link.clone(),
366 alt_text: Some(alt_text),
367 },
368 Image::Path {
369 ref source_range,
370 ref display_path,
371 ref path,
372 ref link,
373 ..
374 } => Image::Path {
375 source_range: source_range.clone(),
376 display_path: display_path.clone(),
377 path: path.clone(),
378 link: link.clone(),
379 alt_text: Some(alt_text),
380 },
381 }
382 }
383}
384
385impl Display for Image {
386 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
387 match self {
388 Image::Web { url, .. } => write!(f, "{}", url),
389 Image::Path { display_path, .. } => write!(f, "{}", display_path.display()),
390 }
391 }
392}