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