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 Default for ParsedMarkdownTableRow {
114 fn default() -> Self {
115 Self::new()
116 }
117}
118
119impl ParsedMarkdownTableRow {
120 pub fn new() -> Self {
121 Self {
122 children: Vec::new(),
123 }
124 }
125
126 pub fn with_children(children: Vec<ParsedMarkdownText>) -> Self {
127 Self { children }
128 }
129}
130
131#[derive(Debug)]
132#[cfg_attr(test, derive(PartialEq))]
133pub struct ParsedMarkdownBlockQuote {
134 pub source_range: Range<usize>,
135 pub children: Vec<ParsedMarkdownElement>,
136}
137
138#[derive(Debug)]
139pub struct ParsedMarkdownText {
140 /// Where the text is located in the source Markdown document.
141 pub source_range: Range<usize>,
142 /// The text content stripped of any formatting symbols.
143 pub contents: String,
144 /// The list of highlights contained in the Markdown document.
145 pub highlights: Vec<(Range<usize>, MarkdownHighlight)>,
146 /// The regions of the various ranges in the Markdown document.
147 pub region_ranges: Vec<Range<usize>>,
148 /// The regions of the Markdown document.
149 pub regions: Vec<ParsedRegion>,
150}
151
152/// A run of highlighted Markdown text.
153#[derive(Debug, Clone, PartialEq, Eq)]
154pub enum MarkdownHighlight {
155 /// A styled Markdown highlight.
156 Style(MarkdownHighlightStyle),
157 /// A highlighted code block.
158 Code(HighlightId),
159}
160
161impl MarkdownHighlight {
162 /// Converts this [`MarkdownHighlight`] to a [`HighlightStyle`].
163 pub fn to_highlight_style(&self, theme: &theme::SyntaxTheme) -> Option<HighlightStyle> {
164 match self {
165 MarkdownHighlight::Style(style) => {
166 let mut highlight = HighlightStyle::default();
167
168 if style.italic {
169 highlight.font_style = Some(FontStyle::Italic);
170 }
171
172 if style.underline {
173 highlight.underline = Some(UnderlineStyle {
174 thickness: px(1.),
175 ..Default::default()
176 });
177 }
178
179 if style.strikethrough {
180 highlight.strikethrough = Some(StrikethroughStyle {
181 thickness: px(1.),
182 ..Default::default()
183 });
184 }
185
186 if style.weight != FontWeight::default() {
187 highlight.font_weight = Some(style.weight);
188 }
189
190 Some(highlight)
191 }
192
193 MarkdownHighlight::Code(id) => id.style(theme),
194 }
195 }
196}
197
198/// The style for a Markdown highlight.
199#[derive(Debug, Clone, Default, PartialEq, Eq)]
200pub struct MarkdownHighlightStyle {
201 /// Whether the text should be italicized.
202 pub italic: bool,
203 /// Whether the text should be underlined.
204 pub underline: bool,
205 /// Whether the text should be struck through.
206 pub strikethrough: bool,
207 /// The weight of the text.
208 pub weight: FontWeight,
209}
210
211/// A parsed region in a Markdown document.
212#[derive(Debug, Clone)]
213#[cfg_attr(test, derive(PartialEq))]
214pub struct ParsedRegion {
215 /// Whether the region is a code block.
216 pub code: bool,
217 /// The link contained in this region, if it has one.
218 pub link: Option<Link>,
219}
220
221/// A Markdown link.
222#[derive(Debug, Clone)]
223#[cfg_attr(test, derive(PartialEq))]
224pub enum Link {
225 /// A link to a webpage.
226 Web {
227 /// The URL of the webpage.
228 url: String,
229 },
230 /// A link to a path on the filesystem.
231 Path {
232 /// The path as provided in the Markdown document.
233 display_path: PathBuf,
234 /// The absolute path to the item.
235 path: PathBuf,
236 },
237}
238
239impl Link {
240 pub fn identify(file_location_directory: Option<PathBuf>, text: String) -> Option<Link> {
241 if text.starts_with("http") {
242 return Some(Link::Web { url: text });
243 }
244
245 let path = PathBuf::from(&text);
246 if path.is_absolute() && path.exists() {
247 return Some(Link::Path {
248 display_path: path.clone(),
249 path,
250 });
251 }
252
253 if let Some(file_location_directory) = file_location_directory {
254 let display_path = path;
255 let path = file_location_directory.join(text);
256 if path.exists() {
257 return Some(Link::Path { display_path, path });
258 }
259 }
260
261 None
262 }
263}
264
265impl Display for Link {
266 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267 match self {
268 Link::Web { url } => write!(f, "{}", url),
269 Link::Path {
270 display_path,
271 path: _,
272 } => write!(f, "{}", display_path.display()),
273 }
274 }
275}