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