markdown_elements.rs

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