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