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