1use gpui::{
2 DefiniteLength, FontStyle, FontWeight, HighlightStyle, SharedString, StrikethroughStyle,
3 UnderlineStyle, px,
4};
5use language::HighlightId;
6use std::{fmt::Display, ops::Range, path::PathBuf};
7
8#[derive(Debug)]
9#[cfg_attr(test, derive(PartialEq))]
10pub enum ParsedMarkdownElement {
11 Heading(ParsedMarkdownHeading),
12 ListItem(ParsedMarkdownListItem),
13 Table(ParsedMarkdownTable),
14 BlockQuote(ParsedMarkdownBlockQuote),
15 CodeBlock(ParsedMarkdownCodeBlock),
16 /// A paragraph of text and other inline elements.
17 Paragraph(MarkdownParagraph),
18 HorizontalRule(Range<usize>),
19 Image(Image),
20}
21
22impl ParsedMarkdownElement {
23 pub fn source_range(&self) -> Option<Range<usize>> {
24 Some(match self {
25 Self::Heading(heading) => heading.source_range.clone(),
26 Self::ListItem(list_item) => list_item.source_range.clone(),
27 Self::Table(table) => table.source_range.clone(),
28 Self::BlockQuote(block_quote) => block_quote.source_range.clone(),
29 Self::CodeBlock(code_block) => code_block.source_range.clone(),
30 Self::Paragraph(text) => match text.get(0)? {
31 MarkdownParagraphChunk::Text(t) => t.source_range.clone(),
32 MarkdownParagraphChunk::Image(image) => image.source_range.clone(),
33 },
34 Self::HorizontalRule(range) => range.clone(),
35 Self::Image(image) => image.source_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: Vec<ParsedMarkdownTableRow>,
108 pub body: Vec<ParsedMarkdownTableRow>,
109 pub column_alignments: Vec<ParsedMarkdownTableAlignment>,
110}
111
112#[derive(Debug, Clone, Copy, Default)]
113#[cfg_attr(test, derive(PartialEq))]
114pub enum ParsedMarkdownTableAlignment {
115 #[default]
116 None,
117 Left,
118 Center,
119 Right,
120}
121
122#[derive(Debug)]
123#[cfg_attr(test, derive(PartialEq))]
124pub struct ParsedMarkdownTableColumn {
125 pub col_span: usize,
126 pub row_span: usize,
127 pub is_header: bool,
128 pub children: MarkdownParagraph,
129}
130
131#[derive(Debug)]
132#[cfg_attr(test, derive(PartialEq))]
133pub struct ParsedMarkdownTableRow {
134 pub columns: Vec<ParsedMarkdownTableColumn>,
135}
136
137impl Default for ParsedMarkdownTableRow {
138 fn default() -> Self {
139 Self::new()
140 }
141}
142
143impl ParsedMarkdownTableRow {
144 pub fn new() -> Self {
145 Self {
146 columns: Vec::new(),
147 }
148 }
149
150 pub fn with_columns(columns: Vec<ParsedMarkdownTableColumn>) -> Self {
151 Self { columns }
152 }
153}
154
155#[derive(Debug)]
156#[cfg_attr(test, derive(PartialEq))]
157pub struct ParsedMarkdownBlockQuote {
158 pub source_range: Range<usize>,
159 pub children: Vec<ParsedMarkdownElement>,
160}
161
162#[derive(Debug, Clone)]
163pub struct ParsedMarkdownText {
164 /// Where the text is located in the source Markdown document.
165 pub source_range: Range<usize>,
166 /// The text content stripped of any formatting symbols.
167 pub contents: SharedString,
168 /// The list of highlights contained in the Markdown document.
169 pub highlights: Vec<(Range<usize>, MarkdownHighlight)>,
170 /// The regions of the various ranges in the Markdown document.
171 pub region_ranges: Vec<Range<usize>>,
172 /// The regions of the Markdown document.
173 pub regions: Vec<ParsedRegion>,
174}
175
176/// A run of highlighted Markdown text.
177#[derive(Debug, Clone, PartialEq, Eq)]
178pub enum MarkdownHighlight {
179 /// A styled Markdown highlight.
180 Style(MarkdownHighlightStyle),
181 /// A highlighted code block.
182 Code(HighlightId),
183}
184
185impl MarkdownHighlight {
186 /// Converts this [`MarkdownHighlight`] to a [`HighlightStyle`].
187 pub fn to_highlight_style(&self, theme: &theme::SyntaxTheme) -> Option<HighlightStyle> {
188 match self {
189 MarkdownHighlight::Style(style) => {
190 let mut highlight = HighlightStyle::default();
191
192 if style.italic {
193 highlight.font_style = Some(FontStyle::Italic);
194 }
195
196 if style.underline {
197 highlight.underline = Some(UnderlineStyle {
198 thickness: px(1.),
199 ..Default::default()
200 });
201 }
202
203 if style.strikethrough {
204 highlight.strikethrough = Some(StrikethroughStyle {
205 thickness: px(1.),
206 ..Default::default()
207 });
208 }
209
210 if style.weight != FontWeight::default() {
211 highlight.font_weight = Some(style.weight);
212 }
213
214 if style.link {
215 highlight.underline = Some(UnderlineStyle {
216 thickness: px(1.),
217 ..Default::default()
218 });
219 }
220
221 Some(highlight)
222 }
223
224 MarkdownHighlight::Code(id) => id.style(theme),
225 }
226 }
227}
228
229/// The style for a Markdown highlight.
230#[derive(Debug, Clone, Default, PartialEq, Eq)]
231pub struct MarkdownHighlightStyle {
232 /// Whether the text should be italicized.
233 pub italic: bool,
234 /// Whether the text should be underlined.
235 pub underline: bool,
236 /// Whether the text should be struck through.
237 pub strikethrough: bool,
238 /// The weight of the text.
239 pub weight: FontWeight,
240 /// Whether the text should be stylized as link.
241 pub link: bool,
242}
243
244/// A parsed region in a Markdown document.
245#[derive(Debug, Clone)]
246#[cfg_attr(test, derive(PartialEq))]
247pub struct ParsedRegion {
248 /// Whether the region is a code block.
249 pub code: bool,
250 /// The link contained in this region, if it has one.
251 pub link: Option<Link>,
252}
253
254/// A Markdown link.
255#[derive(Debug, Clone)]
256#[cfg_attr(test, derive(PartialEq))]
257pub enum Link {
258 /// A link to a webpage.
259 Web {
260 /// The URL of the webpage.
261 url: String,
262 },
263 /// A link to a path on the filesystem.
264 Path {
265 /// The path as provided in the Markdown document.
266 display_path: PathBuf,
267 /// The absolute path to the item.
268 path: PathBuf,
269 },
270}
271
272impl Link {
273 pub fn identify(file_location_directory: Option<PathBuf>, text: String) -> Option<Link> {
274 if text.starts_with("http") {
275 return Some(Link::Web { url: text });
276 }
277
278 let path = PathBuf::from(&text);
279 if path.is_absolute() && path.exists() {
280 return Some(Link::Path {
281 display_path: path.clone(),
282 path,
283 });
284 }
285
286 if let Some(file_location_directory) = file_location_directory {
287 let display_path = path;
288 let path = file_location_directory.join(text);
289 if path.exists() {
290 return Some(Link::Path { display_path, path });
291 }
292 }
293
294 None
295 }
296}
297
298impl Display for Link {
299 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300 match self {
301 Link::Web { url } => write!(f, "{}", url),
302 Link::Path { display_path, .. } => write!(f, "{}", display_path.display()),
303 }
304 }
305}
306
307/// A Markdown Image
308#[derive(Debug, Clone)]
309#[cfg_attr(test, derive(PartialEq))]
310pub struct Image {
311 pub link: Link,
312 pub source_range: Range<usize>,
313 pub alt_text: Option<SharedString>,
314 pub width: Option<DefiniteLength>,
315 pub height: Option<DefiniteLength>,
316}
317
318impl Image {
319 pub fn identify(
320 text: String,
321 source_range: Range<usize>,
322 file_location_directory: Option<PathBuf>,
323 ) -> Option<Self> {
324 let link = Link::identify(file_location_directory, text)?;
325 Some(Self {
326 source_range,
327 link,
328 alt_text: None,
329 width: None,
330 height: None,
331 })
332 }
333
334 pub fn set_alt_text(&mut self, alt_text: SharedString) {
335 self.alt_text = Some(alt_text);
336 }
337
338 pub fn set_width(&mut self, width: DefiniteLength) {
339 self.width = Some(width);
340 }
341
342 pub fn set_height(&mut self, height: DefiniteLength) {
343 self.height = Some(height);
344 }
345}