1use gpui::{px, FontStyle, FontWeight, HighlightStyle, SharedString, UnderlineStyle};
2use language::HighlightId;
3use std::{ops::Range, path::PathBuf};
4
5#[derive(Debug)]
6#[cfg_attr(test, derive(PartialEq))]
7pub enum ParsedMarkdownElement {
8 Heading(ParsedMarkdownHeading),
9 /// An ordered or unordered list of items.
10 List(ParsedMarkdownList),
11 Table(ParsedMarkdownTable),
12 BlockQuote(ParsedMarkdownBlockQuote),
13 CodeBlock(ParsedMarkdownCodeBlock),
14 /// A paragraph of text and other inline elements.
15 Paragraph(ParsedMarkdownText),
16 HorizontalRule(Range<usize>),
17}
18
19impl ParsedMarkdownElement {
20 pub fn source_range(&self) -> Range<usize> {
21 match self {
22 Self::Heading(heading) => heading.source_range.clone(),
23 Self::List(list) => list.source_range.clone(),
24 Self::Table(table) => table.source_range.clone(),
25 Self::BlockQuote(block_quote) => block_quote.source_range.clone(),
26 Self::CodeBlock(code_block) => code_block.source_range.clone(),
27 Self::Paragraph(text) => text.source_range.clone(),
28 Self::HorizontalRule(range) => range.clone(),
29 }
30 }
31}
32
33#[derive(Debug)]
34#[cfg_attr(test, derive(PartialEq))]
35pub struct ParsedMarkdown {
36 pub children: Vec<ParsedMarkdownElement>,
37}
38
39#[derive(Debug)]
40#[cfg_attr(test, derive(PartialEq))]
41pub struct ParsedMarkdownList {
42 pub source_range: Range<usize>,
43 pub children: Vec<ParsedMarkdownListItem>,
44}
45
46#[derive(Debug)]
47#[cfg_attr(test, derive(PartialEq))]
48pub struct ParsedMarkdownListItem {
49 /// How many indentations deep this item is.
50 pub depth: u16,
51 pub item_type: ParsedMarkdownListItemType,
52 pub contents: Vec<Box<ParsedMarkdownElement>>,
53}
54
55#[derive(Debug)]
56#[cfg_attr(test, derive(PartialEq))]
57pub enum ParsedMarkdownListItemType {
58 Ordered(u64),
59 Task(bool),
60 Unordered,
61}
62
63#[derive(Debug)]
64#[cfg_attr(test, derive(PartialEq))]
65pub struct ParsedMarkdownCodeBlock {
66 pub source_range: Range<usize>,
67 pub language: Option<String>,
68 pub contents: SharedString,
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<Box<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.weight != FontWeight::default() {
174 highlight.font_weight = Some(style.weight);
175 }
176
177 Some(highlight)
178 }
179
180 MarkdownHighlight::Code(id) => id.style(theme),
181 }
182 }
183}
184
185/// The style for a Markdown highlight.
186#[derive(Debug, Clone, Default, PartialEq, Eq)]
187pub struct MarkdownHighlightStyle {
188 /// Whether the text should be italicized.
189 pub italic: bool,
190 /// Whether the text should be underlined.
191 pub underline: bool,
192 /// The weight of the text.
193 pub weight: FontWeight,
194}
195
196/// A parsed region in a Markdown document.
197#[derive(Debug, Clone)]
198#[cfg_attr(test, derive(PartialEq))]
199pub struct ParsedRegion {
200 /// Whether the region is a code block.
201 pub code: bool,
202 /// The link contained in this region, if it has one.
203 pub link: Option<Link>,
204}
205
206/// A Markdown link.
207#[derive(Debug, Clone)]
208#[cfg_attr(test, derive(PartialEq))]
209pub enum Link {
210 /// A link to a webpage.
211 Web {
212 /// The URL of the webpage.
213 url: String,
214 },
215 /// A link to a path on the filesystem.
216 Path {
217 /// The path to the item.
218 path: PathBuf,
219 },
220}
221
222impl Link {
223 pub fn identify(file_location_directory: Option<PathBuf>, text: String) -> Option<Link> {
224 if text.starts_with("http") {
225 return Some(Link::Web { url: text });
226 }
227
228 let path = PathBuf::from(&text);
229 if path.is_absolute() && path.exists() {
230 return Some(Link::Path { path });
231 }
232
233 if let Some(file_location_directory) = file_location_directory {
234 let path = file_location_directory.join(text);
235 if path.exists() {
236 return Some(Link::Path { path });
237 }
238 }
239
240 None
241 }
242}