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 Markdown document.
175 pub regions: Vec<(Range<usize>, ParsedRegion)>,
176}
177
178/// A run of highlighted Markdown text.
179#[derive(Debug, Clone, PartialEq, Eq)]
180pub enum MarkdownHighlight {
181 /// A styled Markdown highlight.
182 Style(MarkdownHighlightStyle),
183 /// A highlighted code block.
184 Code(HighlightId),
185}
186
187impl MarkdownHighlight {
188 /// Converts this [`MarkdownHighlight`] to a [`HighlightStyle`].
189 pub fn to_highlight_style(&self, theme: &theme::SyntaxTheme) -> Option<HighlightStyle> {
190 match self {
191 MarkdownHighlight::Style(style) => {
192 let mut highlight = HighlightStyle::default();
193
194 if style.italic {
195 highlight.font_style = Some(FontStyle::Italic);
196 }
197
198 if style.underline {
199 highlight.underline = Some(UnderlineStyle {
200 thickness: px(1.),
201 ..Default::default()
202 });
203 }
204
205 if style.strikethrough {
206 highlight.strikethrough = Some(StrikethroughStyle {
207 thickness: px(1.),
208 ..Default::default()
209 });
210 }
211
212 if style.weight != FontWeight::default() {
213 highlight.font_weight = Some(style.weight);
214 }
215
216 if style.link {
217 highlight.underline = Some(UnderlineStyle {
218 thickness: px(1.),
219 ..Default::default()
220 });
221 }
222
223 if style.oblique {
224 highlight.font_style = Some(FontStyle::Oblique)
225 }
226
227 Some(highlight)
228 }
229
230 MarkdownHighlight::Code(id) => id.style(theme),
231 }
232 }
233}
234
235/// The style for a Markdown highlight.
236#[derive(Debug, Clone, Default, PartialEq, Eq)]
237pub struct MarkdownHighlightStyle {
238 /// Whether the text should be italicized.
239 pub italic: bool,
240 /// Whether the text should be underlined.
241 pub underline: bool,
242 /// Whether the text should be struck through.
243 pub strikethrough: bool,
244 /// The weight of the text.
245 pub weight: FontWeight,
246 /// Whether the text should be stylized as link.
247 pub link: bool,
248 // Whether the text should be obliqued.
249 pub oblique: bool,
250}
251
252/// A parsed region in a Markdown document.
253#[derive(Debug, Clone)]
254#[cfg_attr(test, derive(PartialEq))]
255pub struct ParsedRegion {
256 /// Whether the region is a code block.
257 pub code: bool,
258 /// The link contained in this region, if it has one.
259 pub link: Option<Link>,
260}
261
262/// A Markdown link.
263#[derive(Debug, Clone)]
264#[cfg_attr(test, derive(PartialEq))]
265pub enum Link {
266 /// A link to a webpage.
267 Web {
268 /// The URL of the webpage.
269 url: String,
270 },
271 /// A link to a path on the filesystem.
272 Path {
273 /// The path as provided in the Markdown document.
274 display_path: PathBuf,
275 /// The absolute path to the item.
276 path: PathBuf,
277 },
278}
279
280impl Link {
281 pub fn identify(file_location_directory: Option<PathBuf>, text: String) -> Option<Link> {
282 if text.starts_with("http") {
283 return Some(Link::Web { url: text });
284 }
285
286 // URL decode the text to handle spaces and other special characters
287 let decoded_text = urlencoding::decode(&text)
288 .map(|s| s.into_owned())
289 .unwrap_or(text);
290
291 let path = PathBuf::from(&decoded_text);
292 if path.is_absolute() && path.exists() {
293 return Some(Link::Path {
294 display_path: path.clone(),
295 path,
296 });
297 }
298
299 if let Some(file_location_directory) = file_location_directory {
300 let display_path = path;
301 let path = file_location_directory.join(decoded_text);
302 if path.exists() {
303 return Some(Link::Path { display_path, path });
304 }
305 }
306
307 None
308 }
309}
310
311impl Display for Link {
312 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
313 match self {
314 Link::Web { url } => write!(f, "{}", url),
315 Link::Path { display_path, .. } => write!(f, "{}", display_path.display()),
316 }
317 }
318}
319
320/// A Markdown Image
321#[derive(Debug, Clone)]
322#[cfg_attr(test, derive(PartialEq))]
323pub struct Image {
324 pub link: Link,
325 pub source_range: Range<usize>,
326 pub alt_text: Option<SharedString>,
327 pub width: Option<DefiniteLength>,
328 pub height: Option<DefiniteLength>,
329}
330
331impl Image {
332 pub fn identify(
333 text: String,
334 source_range: Range<usize>,
335 file_location_directory: Option<PathBuf>,
336 ) -> Option<Self> {
337 let link = Link::identify(file_location_directory, text)?;
338 Some(Self {
339 source_range,
340 link,
341 alt_text: None,
342 width: None,
343 height: None,
344 })
345 }
346
347 pub fn set_alt_text(&mut self, alt_text: SharedString) {
348 self.alt_text = Some(alt_text);
349 }
350
351 pub fn set_width(&mut self, width: DefiniteLength) {
352 self.width = Some(width);
353 }
354
355 pub fn set_height(&mut self, height: DefiniteLength) {
356 self.height = Some(height);
357 }
358}