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