markdown_elements.rs

  1use gpui::{
  2    DefiniteLength, FontStyle, FontWeight, HighlightStyle, SharedString, StrikethroughStyle,
  3    UnderlineStyle, px,
  4};
  5use language::HighlightId;
  6use std::{fmt::Display, ops::Range, path::PathBuf};
  7
  8#[derive(Debug)]
  9#[cfg_attr(test, derive(PartialEq))]
 10pub enum ParsedMarkdownElement {
 11    Heading(ParsedMarkdownHeading),
 12    ListItem(ParsedMarkdownListItem),
 13    Table(ParsedMarkdownTable),
 14    BlockQuote(ParsedMarkdownBlockQuote),
 15    CodeBlock(ParsedMarkdownCodeBlock),
 16    /// A paragraph of text and other inline elements.
 17    Paragraph(MarkdownParagraph),
 18    HorizontalRule(Range<usize>),
 19    Image(Image),
 20}
 21
 22impl ParsedMarkdownElement {
 23    pub fn source_range(&self) -> Option<Range<usize>> {
 24        Some(match self {
 25            Self::Heading(heading) => heading.source_range.clone(),
 26            Self::ListItem(list_item) => list_item.source_range.clone(),
 27            Self::Table(table) => table.source_range.clone(),
 28            Self::BlockQuote(block_quote) => block_quote.source_range.clone(),
 29            Self::CodeBlock(code_block) => code_block.source_range.clone(),
 30            Self::Paragraph(text) => match text.get(0)? {
 31                MarkdownParagraphChunk::Text(t) => t.source_range.clone(),
 32                MarkdownParagraphChunk::Image(image) => image.source_range.clone(),
 33            },
 34            Self::HorizontalRule(range) => range.clone(),
 35            Self::Image(image) => image.source_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: Vec<ParsedMarkdownTableRow>,
108    pub body: Vec<ParsedMarkdownTableRow>,
109    pub column_alignments: Vec<ParsedMarkdownTableAlignment>,
110}
111
112#[derive(Debug, Clone, Copy, Default)]
113#[cfg_attr(test, derive(PartialEq))]
114pub enum ParsedMarkdownTableAlignment {
115    #[default]
116    None,
117    Left,
118    Center,
119    Right,
120}
121
122#[derive(Debug)]
123#[cfg_attr(test, derive(PartialEq))]
124pub struct ParsedMarkdownTableColumn {
125    pub col_span: usize,
126    pub row_span: usize,
127    pub is_header: bool,
128    pub children: MarkdownParagraph,
129}
130
131#[derive(Debug)]
132#[cfg_attr(test, derive(PartialEq))]
133pub struct ParsedMarkdownTableRow {
134    pub columns: Vec<ParsedMarkdownTableColumn>,
135}
136
137impl Default for ParsedMarkdownTableRow {
138    fn default() -> Self {
139        Self::new()
140    }
141}
142
143impl ParsedMarkdownTableRow {
144    pub fn new() -> Self {
145        Self {
146            columns: Vec::new(),
147        }
148    }
149
150    pub fn with_columns(columns: Vec<ParsedMarkdownTableColumn>) -> Self {
151        Self { columns }
152    }
153}
154
155#[derive(Debug)]
156#[cfg_attr(test, derive(PartialEq))]
157pub struct ParsedMarkdownBlockQuote {
158    pub source_range: Range<usize>,
159    pub children: Vec<ParsedMarkdownElement>,
160}
161
162#[derive(Debug, Clone)]
163pub struct ParsedMarkdownText {
164    /// Where the text is located in the source Markdown document.
165    pub source_range: Range<usize>,
166    /// The text content stripped of any formatting symbols.
167    pub contents: SharedString,
168    /// The list of highlights contained in the Markdown document.
169    pub highlights: Vec<(Range<usize>, MarkdownHighlight)>,
170    /// The regions of the various ranges in the Markdown document.
171    pub region_ranges: Vec<Range<usize>>,
172    /// The regions of the Markdown document.
173    pub regions: Vec<ParsedRegion>,
174}
175
176/// A run of highlighted Markdown text.
177#[derive(Debug, Clone, PartialEq, Eq)]
178pub enum MarkdownHighlight {
179    /// A styled Markdown highlight.
180    Style(MarkdownHighlightStyle),
181    /// A highlighted code block.
182    Code(HighlightId),
183}
184
185impl MarkdownHighlight {
186    /// Converts this [`MarkdownHighlight`] to a [`HighlightStyle`].
187    pub fn to_highlight_style(&self, theme: &theme::SyntaxTheme) -> Option<HighlightStyle> {
188        match self {
189            MarkdownHighlight::Style(style) => {
190                let mut highlight = HighlightStyle::default();
191
192                if style.italic {
193                    highlight.font_style = Some(FontStyle::Italic);
194                }
195
196                if style.underline {
197                    highlight.underline = Some(UnderlineStyle {
198                        thickness: px(1.),
199                        ..Default::default()
200                    });
201                }
202
203                if style.strikethrough {
204                    highlight.strikethrough = Some(StrikethroughStyle {
205                        thickness: px(1.),
206                        ..Default::default()
207                    });
208                }
209
210                if style.weight != FontWeight::default() {
211                    highlight.font_weight = Some(style.weight);
212                }
213
214                if style.link {
215                    highlight.underline = Some(UnderlineStyle {
216                        thickness: px(1.),
217                        ..Default::default()
218                    });
219                }
220
221                Some(highlight)
222            }
223
224            MarkdownHighlight::Code(id) => id.style(theme),
225        }
226    }
227}
228
229/// The style for a Markdown highlight.
230#[derive(Debug, Clone, Default, PartialEq, Eq)]
231pub struct MarkdownHighlightStyle {
232    /// Whether the text should be italicized.
233    pub italic: bool,
234    /// Whether the text should be underlined.
235    pub underline: bool,
236    /// Whether the text should be struck through.
237    pub strikethrough: bool,
238    /// The weight of the text.
239    pub weight: FontWeight,
240    /// Whether the text should be stylized as link.
241    pub link: bool,
242}
243
244/// A parsed region in a Markdown document.
245#[derive(Debug, Clone)]
246#[cfg_attr(test, derive(PartialEq))]
247pub struct ParsedRegion {
248    /// Whether the region is a code block.
249    pub code: bool,
250    /// The link contained in this region, if it has one.
251    pub link: Option<Link>,
252}
253
254/// A Markdown link.
255#[derive(Debug, Clone)]
256#[cfg_attr(test, derive(PartialEq))]
257pub enum Link {
258    /// A link to a webpage.
259    Web {
260        /// The URL of the webpage.
261        url: String,
262    },
263    /// A link to a path on the filesystem.
264    Path {
265        /// The path as provided in the Markdown document.
266        display_path: PathBuf,
267        /// The absolute path to the item.
268        path: PathBuf,
269    },
270}
271
272impl Link {
273    pub fn identify(file_location_directory: Option<PathBuf>, text: String) -> Option<Link> {
274        if text.starts_with("http") {
275            return Some(Link::Web { url: text });
276        }
277
278        let path = PathBuf::from(&text);
279        if path.is_absolute() && path.exists() {
280            return Some(Link::Path {
281                display_path: path.clone(),
282                path,
283            });
284        }
285
286        if let Some(file_location_directory) = file_location_directory {
287            let display_path = path;
288            let path = file_location_directory.join(text);
289            if path.exists() {
290                return Some(Link::Path { display_path, path });
291            }
292        }
293
294        None
295    }
296}
297
298impl Display for Link {
299    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300        match self {
301            Link::Web { url } => write!(f, "{}", url),
302            Link::Path { display_path, .. } => write!(f, "{}", display_path.display()),
303        }
304    }
305}
306
307/// A Markdown Image
308#[derive(Debug, Clone)]
309#[cfg_attr(test, derive(PartialEq))]
310pub struct Image {
311    pub link: Link,
312    pub source_range: Range<usize>,
313    pub alt_text: Option<SharedString>,
314    pub width: Option<DefiniteLength>,
315    pub height: Option<DefiniteLength>,
316}
317
318impl Image {
319    pub fn identify(
320        text: String,
321        source_range: Range<usize>,
322        file_location_directory: Option<PathBuf>,
323    ) -> Option<Self> {
324        let link = Link::identify(file_location_directory, text)?;
325        Some(Self {
326            source_range,
327            link,
328            alt_text: None,
329            width: None,
330            height: None,
331        })
332    }
333
334    pub fn set_alt_text(&mut self, alt_text: SharedString) {
335        self.alt_text = Some(alt_text);
336    }
337
338    pub fn set_width(&mut self, width: DefiniteLength) {
339        self.width = Some(width);
340    }
341
342    pub fn set_height(&mut self, height: DefiniteLength) {
343        self.height = Some(height);
344    }
345}