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