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 /// Whether we can expect nested list items inside of this items `content`.
68 pub nested: bool,
69}
70
71#[derive(Debug)]
72#[cfg_attr(test, derive(PartialEq))]
73pub enum ParsedMarkdownListItemType {
74 Ordered(u64),
75 Task(bool, Range<usize>),
76 Unordered,
77}
78
79#[derive(Debug)]
80#[cfg_attr(test, derive(PartialEq))]
81pub struct ParsedMarkdownCodeBlock {
82 pub source_range: Range<usize>,
83 pub language: Option<String>,
84 pub contents: SharedString,
85 pub highlights: Option<Vec<(Range<usize>, HighlightId)>>,
86}
87
88#[derive(Debug)]
89#[cfg_attr(test, derive(PartialEq))]
90pub struct ParsedMarkdownHeading {
91 pub source_range: Range<usize>,
92 pub level: HeadingLevel,
93 pub contents: MarkdownParagraph,
94}
95
96#[derive(Debug, PartialEq)]
97pub enum HeadingLevel {
98 H1,
99 H2,
100 H3,
101 H4,
102 H5,
103 H6,
104}
105
106#[derive(Debug)]
107pub struct ParsedMarkdownTable {
108 pub source_range: Range<usize>,
109 pub header: Vec<ParsedMarkdownTableRow>,
110 pub body: Vec<ParsedMarkdownTableRow>,
111}
112
113#[derive(Debug, Clone, Copy, Default)]
114#[cfg_attr(test, derive(PartialEq))]
115pub enum ParsedMarkdownTableAlignment {
116 #[default]
117 None,
118 Left,
119 Center,
120 Right,
121}
122
123#[derive(Debug)]
124#[cfg_attr(test, derive(PartialEq))]
125pub struct ParsedMarkdownTableColumn {
126 pub col_span: usize,
127 pub row_span: usize,
128 pub is_header: bool,
129 pub children: MarkdownParagraph,
130 pub alignment: ParsedMarkdownTableAlignment,
131}
132
133#[derive(Debug)]
134#[cfg_attr(test, derive(PartialEq))]
135pub struct ParsedMarkdownTableRow {
136 pub columns: Vec<ParsedMarkdownTableColumn>,
137}
138
139impl Default for ParsedMarkdownTableRow {
140 fn default() -> Self {
141 Self::new()
142 }
143}
144
145impl ParsedMarkdownTableRow {
146 pub fn new() -> Self {
147 Self {
148 columns: Vec::new(),
149 }
150 }
151
152 pub fn with_columns(columns: Vec<ParsedMarkdownTableColumn>) -> Self {
153 Self { columns }
154 }
155}
156
157#[derive(Debug)]
158#[cfg_attr(test, derive(PartialEq))]
159pub struct ParsedMarkdownBlockQuote {
160 pub source_range: Range<usize>,
161 pub children: Vec<ParsedMarkdownElement>,
162}
163
164#[derive(Debug, Clone)]
165pub struct ParsedMarkdownText {
166 /// Where the text is located in the source Markdown document.
167 pub source_range: Range<usize>,
168 /// The text content stripped of any formatting symbols.
169 pub contents: SharedString,
170 /// The list of highlights contained in the Markdown document.
171 pub highlights: Vec<(Range<usize>, MarkdownHighlight)>,
172 /// The regions of the various ranges in the Markdown document.
173 pub region_ranges: Vec<Range<usize>>,
174 /// The regions of the Markdown document.
175 pub regions: Vec<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 Some(highlight)
224 }
225
226 MarkdownHighlight::Code(id) => id.style(theme),
227 }
228 }
229}
230
231/// The style for a Markdown highlight.
232#[derive(Debug, Clone, Default, PartialEq, Eq)]
233pub struct MarkdownHighlightStyle {
234 /// Whether the text should be italicized.
235 pub italic: bool,
236 /// Whether the text should be underlined.
237 pub underline: bool,
238 /// Whether the text should be struck through.
239 pub strikethrough: bool,
240 /// The weight of the text.
241 pub weight: FontWeight,
242 /// Whether the text should be stylized as link.
243 pub link: bool,
244}
245
246/// A parsed region in a Markdown document.
247#[derive(Debug, Clone)]
248#[cfg_attr(test, derive(PartialEq))]
249pub struct ParsedRegion {
250 /// Whether the region is a code block.
251 pub code: bool,
252 /// The link contained in this region, if it has one.
253 pub link: Option<Link>,
254}
255
256/// A Markdown link.
257#[derive(Debug, Clone)]
258#[cfg_attr(test, derive(PartialEq))]
259pub enum Link {
260 /// A link to a webpage.
261 Web {
262 /// The URL of the webpage.
263 url: String,
264 },
265 /// A link to a path on the filesystem.
266 Path {
267 /// The path as provided in the Markdown document.
268 display_path: PathBuf,
269 /// The absolute path to the item.
270 path: PathBuf,
271 },
272}
273
274impl Link {
275 pub fn identify(file_location_directory: Option<PathBuf>, text: String) -> Option<Link> {
276 if text.starts_with("http") {
277 return Some(Link::Web { url: text });
278 }
279
280 let path = PathBuf::from(&text);
281 if path.is_absolute() && path.exists() {
282 return Some(Link::Path {
283 display_path: path.clone(),
284 path,
285 });
286 }
287
288 if let Some(file_location_directory) = file_location_directory {
289 let display_path = path;
290 let path = file_location_directory.join(text);
291 if path.exists() {
292 return Some(Link::Path { display_path, path });
293 }
294 }
295
296 None
297 }
298}
299
300impl Display for Link {
301 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302 match self {
303 Link::Web { url } => write!(f, "{}", url),
304 Link::Path { display_path, .. } => write!(f, "{}", display_path.display()),
305 }
306 }
307}
308
309/// A Markdown Image
310#[derive(Debug, Clone)]
311#[cfg_attr(test, derive(PartialEq))]
312pub struct Image {
313 pub link: Link,
314 pub source_range: Range<usize>,
315 pub alt_text: Option<SharedString>,
316 pub width: Option<DefiniteLength>,
317 pub height: Option<DefiniteLength>,
318}
319
320impl Image {
321 pub fn identify(
322 text: String,
323 source_range: Range<usize>,
324 file_location_directory: Option<PathBuf>,
325 ) -> Option<Self> {
326 let link = Link::identify(file_location_directory, text)?;
327 Some(Self {
328 source_range,
329 link,
330 alt_text: None,
331 width: None,
332 height: None,
333 })
334 }
335
336 pub fn set_alt_text(&mut self, alt_text: SharedString) {
337 self.alt_text = Some(alt_text);
338 }
339
340 pub fn set_width(&mut self, width: DefiniteLength) {
341 self.width = Some(width);
342 }
343
344 pub fn set_height(&mut self, height: DefiniteLength) {
345 self.height = Some(height);
346 }
347}