1use gpui::{
  2    DefiniteLength, FontStyle, FontWeight, HighlightStyle, SharedString, StrikethroughStyle,
  3    UnderlineStyle, px,
  4};
  5use language::HighlightId;
  6use std::{fmt::Display, ops::Range, path::PathBuf};
  7
  8#[derive(Debug)]
  9#[cfg_attr(test, derive(PartialEq))]
 10pub enum ParsedMarkdownElement {
 11    Heading(ParsedMarkdownHeading),
 12    ListItem(ParsedMarkdownListItem),
 13    Table(ParsedMarkdownTable),
 14    BlockQuote(ParsedMarkdownBlockQuote),
 15    CodeBlock(ParsedMarkdownCodeBlock),
 16    /// A paragraph of text and other inline elements.
 17    Paragraph(MarkdownParagraph),
 18    HorizontalRule(Range<usize>),
 19    Image(Image),
 20}
 21
 22impl ParsedMarkdownElement {
 23    pub fn source_range(&self) -> Option<Range<usize>> {
 24        Some(match self {
 25            Self::Heading(heading) => heading.source_range.clone(),
 26            Self::ListItem(list_item) => list_item.source_range.clone(),
 27            Self::Table(table) => table.source_range.clone(),
 28            Self::BlockQuote(block_quote) => block_quote.source_range.clone(),
 29            Self::CodeBlock(code_block) => code_block.source_range.clone(),
 30            Self::Paragraph(text) => match text.get(0)? {
 31                MarkdownParagraphChunk::Text(t) => t.source_range.clone(),
 32                MarkdownParagraphChunk::Image(image) => image.source_range.clone(),
 33            },
 34            Self::HorizontalRule(range) => range.clone(),
 35            Self::Image(image) => image.source_range.clone(),
 36        })
 37    }
 38
 39    pub fn is_list_item(&self) -> bool {
 40        matches!(self, Self::ListItem(_))
 41    }
 42}
 43
 44pub type MarkdownParagraph = Vec<MarkdownParagraphChunk>;
 45
 46#[derive(Debug)]
 47#[cfg_attr(test, derive(PartialEq))]
 48pub enum MarkdownParagraphChunk {
 49    Text(ParsedMarkdownText),
 50    Image(Image),
 51}
 52
 53#[derive(Debug)]
 54#[cfg_attr(test, derive(PartialEq))]
 55pub struct ParsedMarkdown {
 56    pub children: Vec<ParsedMarkdownElement>,
 57}
 58
 59#[derive(Debug)]
 60#[cfg_attr(test, derive(PartialEq))]
 61pub struct ParsedMarkdownListItem {
 62    pub source_range: Range<usize>,
 63    /// How many indentations deep this item is.
 64    pub depth: u16,
 65    pub item_type: ParsedMarkdownListItemType,
 66    pub content: Vec<ParsedMarkdownElement>,
 67    /// Whether we can expect nested list items inside of this items `content`.
 68    pub nested: bool,
 69}
 70
 71#[derive(Debug)]
 72#[cfg_attr(test, derive(PartialEq))]
 73pub enum ParsedMarkdownListItemType {
 74    Ordered(u64),
 75    Task(bool, Range<usize>),
 76    Unordered,
 77}
 78
 79#[derive(Debug)]
 80#[cfg_attr(test, derive(PartialEq))]
 81pub struct ParsedMarkdownCodeBlock {
 82    pub source_range: Range<usize>,
 83    pub language: Option<String>,
 84    pub contents: SharedString,
 85    pub highlights: Option<Vec<(Range<usize>, HighlightId)>>,
 86}
 87
 88#[derive(Debug)]
 89#[cfg_attr(test, derive(PartialEq))]
 90pub struct ParsedMarkdownHeading {
 91    pub source_range: Range<usize>,
 92    pub level: HeadingLevel,
 93    pub contents: MarkdownParagraph,
 94}
 95
 96#[derive(Debug, PartialEq)]
 97pub enum HeadingLevel {
 98    H1,
 99    H2,
100    H3,
101    H4,
102    H5,
103    H6,
104}
105
106#[derive(Debug)]
107pub struct ParsedMarkdownTable {
108    pub source_range: Range<usize>,
109    pub header: Vec<ParsedMarkdownTableRow>,
110    pub body: Vec<ParsedMarkdownTableRow>,
111}
112
113#[derive(Debug, Clone, Copy, Default)]
114#[cfg_attr(test, derive(PartialEq))]
115pub enum ParsedMarkdownTableAlignment {
116    #[default]
117    None,
118    Left,
119    Center,
120    Right,
121}
122
123#[derive(Debug)]
124#[cfg_attr(test, derive(PartialEq))]
125pub struct ParsedMarkdownTableColumn {
126    pub col_span: usize,
127    pub row_span: usize,
128    pub is_header: bool,
129    pub children: MarkdownParagraph,
130    pub alignment: ParsedMarkdownTableAlignment,
131}
132
133#[derive(Debug)]
134#[cfg_attr(test, derive(PartialEq))]
135pub struct ParsedMarkdownTableRow {
136    pub columns: Vec<ParsedMarkdownTableColumn>,
137}
138
139impl Default for ParsedMarkdownTableRow {
140    fn default() -> Self {
141        Self::new()
142    }
143}
144
145impl ParsedMarkdownTableRow {
146    pub fn new() -> Self {
147        Self {
148            columns: Vec::new(),
149        }
150    }
151
152    pub fn with_columns(columns: Vec<ParsedMarkdownTableColumn>) -> Self {
153        Self { columns }
154    }
155}
156
157#[derive(Debug)]
158#[cfg_attr(test, derive(PartialEq))]
159pub struct ParsedMarkdownBlockQuote {
160    pub source_range: Range<usize>,
161    pub children: Vec<ParsedMarkdownElement>,
162}
163
164#[derive(Debug, Clone)]
165pub struct ParsedMarkdownText {
166    /// Where the text is located in the source Markdown document.
167    pub source_range: Range<usize>,
168    /// The text content stripped of any formatting symbols.
169    pub contents: SharedString,
170    /// The list of highlights contained in the Markdown document.
171    pub highlights: Vec<(Range<usize>, MarkdownHighlight)>,
172    /// The regions of the various ranges in the Markdown document.
173    pub region_ranges: Vec<Range<usize>>,
174    /// The regions of the Markdown document.
175    pub regions: Vec<ParsedRegion>,
176}
177
178/// A run of highlighted Markdown text.
179#[derive(Debug, Clone, PartialEq, Eq)]
180pub enum MarkdownHighlight {
181    /// A styled Markdown highlight.
182    Style(MarkdownHighlightStyle),
183    /// A highlighted code block.
184    Code(HighlightId),
185}
186
187impl MarkdownHighlight {
188    /// Converts this [`MarkdownHighlight`] to a [`HighlightStyle`].
189    pub fn to_highlight_style(&self, theme: &theme::SyntaxTheme) -> Option<HighlightStyle> {
190        match self {
191            MarkdownHighlight::Style(style) => {
192                let mut highlight = HighlightStyle::default();
193
194                if style.italic {
195                    highlight.font_style = Some(FontStyle::Italic);
196                }
197
198                if style.underline {
199                    highlight.underline = Some(UnderlineStyle {
200                        thickness: px(1.),
201                        ..Default::default()
202                    });
203                }
204
205                if style.strikethrough {
206                    highlight.strikethrough = Some(StrikethroughStyle {
207                        thickness: px(1.),
208                        ..Default::default()
209                    });
210                }
211
212                if style.weight != FontWeight::default() {
213                    highlight.font_weight = Some(style.weight);
214                }
215
216                if style.link {
217                    highlight.underline = Some(UnderlineStyle {
218                        thickness: px(1.),
219                        ..Default::default()
220                    });
221                }
222
223                Some(highlight)
224            }
225
226            MarkdownHighlight::Code(id) => id.style(theme),
227        }
228    }
229}
230
231/// The style for a Markdown highlight.
232#[derive(Debug, Clone, Default, PartialEq, Eq)]
233pub struct MarkdownHighlightStyle {
234    /// Whether the text should be italicized.
235    pub italic: bool,
236    /// Whether the text should be underlined.
237    pub underline: bool,
238    /// Whether the text should be struck through.
239    pub strikethrough: bool,
240    /// The weight of the text.
241    pub weight: FontWeight,
242    /// Whether the text should be stylized as link.
243    pub link: bool,
244}
245
246/// A parsed region in a Markdown document.
247#[derive(Debug, Clone)]
248#[cfg_attr(test, derive(PartialEq))]
249pub struct ParsedRegion {
250    /// Whether the region is a code block.
251    pub code: bool,
252    /// The link contained in this region, if it has one.
253    pub link: Option<Link>,
254}
255
256/// A Markdown link.
257#[derive(Debug, Clone)]
258#[cfg_attr(test, derive(PartialEq))]
259pub enum Link {
260    /// A link to a webpage.
261    Web {
262        /// The URL of the webpage.
263        url: String,
264    },
265    /// A link to a path on the filesystem.
266    Path {
267        /// The path as provided in the Markdown document.
268        display_path: PathBuf,
269        /// The absolute path to the item.
270        path: PathBuf,
271    },
272}
273
274impl Link {
275    pub fn identify(file_location_directory: Option<PathBuf>, text: String) -> Option<Link> {
276        if text.starts_with("http") {
277            return Some(Link::Web { url: text });
278        }
279
280        let path = PathBuf::from(&text);
281        if path.is_absolute() && path.exists() {
282            return Some(Link::Path {
283                display_path: path.clone(),
284                path,
285            });
286        }
287
288        if let Some(file_location_directory) = file_location_directory {
289            let display_path = path;
290            let path = file_location_directory.join(text);
291            if path.exists() {
292                return Some(Link::Path { display_path, path });
293            }
294        }
295
296        None
297    }
298}
299
300impl Display for Link {
301    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302        match self {
303            Link::Web { url } => write!(f, "{}", url),
304            Link::Path { display_path, .. } => write!(f, "{}", display_path.display()),
305        }
306    }
307}
308
309/// A Markdown Image
310#[derive(Debug, Clone)]
311#[cfg_attr(test, derive(PartialEq))]
312pub struct Image {
313    pub link: Link,
314    pub source_range: Range<usize>,
315    pub alt_text: Option<SharedString>,
316    pub width: Option<DefiniteLength>,
317    pub height: Option<DefiniteLength>,
318}
319
320impl Image {
321    pub fn identify(
322        text: String,
323        source_range: Range<usize>,
324        file_location_directory: Option<PathBuf>,
325    ) -> Option<Self> {
326        let link = Link::identify(file_location_directory, text)?;
327        Some(Self {
328            source_range,
329            link,
330            alt_text: None,
331            width: None,
332            height: None,
333        })
334    }
335
336    pub fn set_alt_text(&mut self, alt_text: SharedString) {
337        self.alt_text = Some(alt_text);
338    }
339
340    pub fn set_width(&mut self, width: DefiniteLength) {
341        self.width = Some(width);
342    }
343
344    pub fn set_height(&mut self, height: DefiniteLength) {
345        self.height = Some(height);
346    }
347}