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