markdown_elements.rs

  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}