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 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}