markdown_elements.rs

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