1use gpui::{
2 px, FontStyle, FontWeight, HighlightStyle, SharedString, StrikethroughStyle, UnderlineStyle,
3};
4use language::HighlightId;
5use std::{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}
72
73#[derive(Debug)]
74#[cfg_attr(test, derive(PartialEq))]
75pub struct ParsedMarkdownHeading {
76 pub source_range: Range<usize>,
77 pub level: HeadingLevel,
78 pub contents: ParsedMarkdownText,
79}
80
81#[derive(Debug, PartialEq)]
82pub enum HeadingLevel {
83 H1,
84 H2,
85 H3,
86 H4,
87 H5,
88 H6,
89}
90
91#[derive(Debug)]
92pub struct ParsedMarkdownTable {
93 pub source_range: Range<usize>,
94 pub header: ParsedMarkdownTableRow,
95 pub body: Vec<ParsedMarkdownTableRow>,
96 pub column_alignments: Vec<ParsedMarkdownTableAlignment>,
97}
98
99#[derive(Debug, Clone, Copy)]
100#[cfg_attr(test, derive(PartialEq))]
101pub enum ParsedMarkdownTableAlignment {
102 /// Default text alignment.
103 None,
104 Left,
105 Center,
106 Right,
107}
108
109#[derive(Debug)]
110#[cfg_attr(test, derive(PartialEq))]
111pub struct ParsedMarkdownTableRow {
112 pub children: Vec<ParsedMarkdownText>,
113}
114
115impl ParsedMarkdownTableRow {
116 pub fn new() -> Self {
117 Self {
118 children: Vec::new(),
119 }
120 }
121
122 pub fn with_children(children: Vec<ParsedMarkdownText>) -> Self {
123 Self { children }
124 }
125}
126
127#[derive(Debug)]
128#[cfg_attr(test, derive(PartialEq))]
129pub struct ParsedMarkdownBlockQuote {
130 pub source_range: Range<usize>,
131 pub children: Vec<Box<ParsedMarkdownElement>>,
132}
133
134#[derive(Debug)]
135pub struct ParsedMarkdownText {
136 /// Where the text is located in the source Markdown document.
137 pub source_range: Range<usize>,
138 /// The text content stripped of any formatting symbols.
139 pub contents: String,
140 /// The list of highlights contained in the Markdown document.
141 pub highlights: Vec<(Range<usize>, MarkdownHighlight)>,
142 /// The regions of the various ranges in the Markdown document.
143 pub region_ranges: Vec<Range<usize>>,
144 /// The regions of the Markdown document.
145 pub regions: Vec<ParsedRegion>,
146}
147
148/// A run of highlighted Markdown text.
149#[derive(Debug, Clone, PartialEq, Eq)]
150pub enum MarkdownHighlight {
151 /// A styled Markdown highlight.
152 Style(MarkdownHighlightStyle),
153 /// A highlighted code block.
154 Code(HighlightId),
155}
156
157impl MarkdownHighlight {
158 /// Converts this [`MarkdownHighlight`] to a [`HighlightStyle`].
159 pub fn to_highlight_style(&self, theme: &theme::SyntaxTheme) -> Option<HighlightStyle> {
160 match self {
161 MarkdownHighlight::Style(style) => {
162 let mut highlight = HighlightStyle::default();
163
164 if style.italic {
165 highlight.font_style = Some(FontStyle::Italic);
166 }
167
168 if style.underline {
169 highlight.underline = Some(UnderlineStyle {
170 thickness: px(1.),
171 ..Default::default()
172 });
173 }
174
175 if style.strikethrough {
176 highlight.strikethrough = Some(StrikethroughStyle {
177 thickness: px(1.),
178 ..Default::default()
179 });
180 }
181
182 if style.weight != FontWeight::default() {
183 highlight.font_weight = Some(style.weight);
184 }
185
186 Some(highlight)
187 }
188
189 MarkdownHighlight::Code(id) => id.style(theme),
190 }
191 }
192}
193
194/// The style for a Markdown highlight.
195#[derive(Debug, Clone, Default, PartialEq, Eq)]
196pub struct MarkdownHighlightStyle {
197 /// Whether the text should be italicized.
198 pub italic: bool,
199 /// Whether the text should be underlined.
200 pub underline: bool,
201 /// Whether the text should be struck through.
202 pub strikethrough: bool,
203 /// The weight of the text.
204 pub weight: FontWeight,
205}
206
207/// A parsed region in a Markdown document.
208#[derive(Debug, Clone)]
209#[cfg_attr(test, derive(PartialEq))]
210pub struct ParsedRegion {
211 /// Whether the region is a code block.
212 pub code: bool,
213 /// The link contained in this region, if it has one.
214 pub link: Option<Link>,
215}
216
217/// A Markdown link.
218#[derive(Debug, Clone)]
219#[cfg_attr(test, derive(PartialEq))]
220pub enum Link {
221 /// A link to a webpage.
222 Web {
223 /// The URL of the webpage.
224 url: String,
225 },
226 /// A link to a path on the filesystem.
227 Path {
228 /// The 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 { path });
242 }
243
244 if let Some(file_location_directory) = file_location_directory {
245 let path = file_location_directory.join(text);
246 if path.exists() {
247 return Some(Link::Path { path });
248 }
249 }
250
251 None
252 }
253}