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