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    /// Whether we can expect nested list items inside of this items `content`.
 68    pub nested: bool,
 69}
 70
 71#[derive(Debug)]
 72#[cfg_attr(test, derive(PartialEq))]
 73pub enum ParsedMarkdownListItemType {
 74    Ordered(u64),
 75    Task(bool, Range<usize>),
 76    Unordered,
 77}
 78
 79#[derive(Debug)]
 80#[cfg_attr(test, derive(PartialEq))]
 81pub struct ParsedMarkdownCodeBlock {
 82    pub source_range: Range<usize>,
 83    pub language: Option<String>,
 84    pub contents: SharedString,
 85    pub highlights: Option<Vec<(Range<usize>, HighlightId)>>,
 86}
 87
 88#[derive(Debug)]
 89#[cfg_attr(test, derive(PartialEq))]
 90pub struct ParsedMarkdownHeading {
 91    pub source_range: Range<usize>,
 92    pub level: HeadingLevel,
 93    pub contents: MarkdownParagraph,
 94}
 95
 96#[derive(Debug, PartialEq)]
 97pub enum HeadingLevel {
 98    H1,
 99    H2,
100    H3,
101    H4,
102    H5,
103    H6,
104}
105
106#[derive(Debug)]
107pub struct ParsedMarkdownTable {
108    pub source_range: Range<usize>,
109    pub header: Vec<ParsedMarkdownTableRow>,
110    pub body: Vec<ParsedMarkdownTableRow>,
111    pub caption: Option<MarkdownParagraph>,
112}
113
114#[derive(Debug, Clone, Copy, Default)]
115#[cfg_attr(test, derive(PartialEq))]
116pub enum ParsedMarkdownTableAlignment {
117    #[default]
118    None,
119    Left,
120    Center,
121    Right,
122}
123
124#[derive(Debug)]
125#[cfg_attr(test, derive(PartialEq))]
126pub struct ParsedMarkdownTableColumn {
127    pub col_span: usize,
128    pub row_span: usize,
129    pub is_header: bool,
130    pub children: MarkdownParagraph,
131    pub alignment: ParsedMarkdownTableAlignment,
132}
133
134#[derive(Debug)]
135#[cfg_attr(test, derive(PartialEq))]
136pub struct ParsedMarkdownTableRow {
137    pub columns: Vec<ParsedMarkdownTableColumn>,
138}
139
140impl Default for ParsedMarkdownTableRow {
141    fn default() -> Self {
142        Self::new()
143    }
144}
145
146impl ParsedMarkdownTableRow {
147    pub fn new() -> Self {
148        Self {
149            columns: Vec::new(),
150        }
151    }
152
153    pub fn with_columns(columns: Vec<ParsedMarkdownTableColumn>) -> Self {
154        Self { columns }
155    }
156}
157
158#[derive(Debug)]
159#[cfg_attr(test, derive(PartialEq))]
160pub struct ParsedMarkdownBlockQuote {
161    pub source_range: Range<usize>,
162    pub children: Vec<ParsedMarkdownElement>,
163}
164
165#[derive(Debug, Clone)]
166pub struct ParsedMarkdownText {
167    /// Where the text is located in the source Markdown document.
168    pub source_range: Range<usize>,
169    /// The text content stripped of any formatting symbols.
170    pub contents: SharedString,
171    /// The list of highlights contained in the Markdown document.
172    pub highlights: Vec<(Range<usize>, MarkdownHighlight)>,
173    /// The regions of the various ranges in the Markdown document.
174    pub region_ranges: Vec<Range<usize>>,
175    /// The regions of the Markdown document.
176    pub regions: Vec<ParsedRegion>,
177}
178
179/// A run of highlighted Markdown text.
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub enum MarkdownHighlight {
182    /// A styled Markdown highlight.
183    Style(MarkdownHighlightStyle),
184    /// A highlighted code block.
185    Code(HighlightId),
186}
187
188impl MarkdownHighlight {
189    /// Converts this [`MarkdownHighlight`] to a [`HighlightStyle`].
190    pub fn to_highlight_style(&self, theme: &theme::SyntaxTheme) -> Option<HighlightStyle> {
191        match self {
192            MarkdownHighlight::Style(style) => {
193                let mut highlight = HighlightStyle::default();
194
195                if style.italic {
196                    highlight.font_style = Some(FontStyle::Italic);
197                }
198
199                if style.underline {
200                    highlight.underline = Some(UnderlineStyle {
201                        thickness: px(1.),
202                        ..Default::default()
203                    });
204                }
205
206                if style.strikethrough {
207                    highlight.strikethrough = Some(StrikethroughStyle {
208                        thickness: px(1.),
209                        ..Default::default()
210                    });
211                }
212
213                if style.weight != FontWeight::default() {
214                    highlight.font_weight = Some(style.weight);
215                }
216
217                if style.link {
218                    highlight.underline = Some(UnderlineStyle {
219                        thickness: px(1.),
220                        ..Default::default()
221                    });
222                }
223
224                Some(highlight)
225            }
226
227            MarkdownHighlight::Code(id) => id.style(theme),
228        }
229    }
230}
231
232/// The style for a Markdown highlight.
233#[derive(Debug, Clone, Default, PartialEq, Eq)]
234pub struct MarkdownHighlightStyle {
235    /// Whether the text should be italicized.
236    pub italic: bool,
237    /// Whether the text should be underlined.
238    pub underline: bool,
239    /// Whether the text should be struck through.
240    pub strikethrough: bool,
241    /// The weight of the text.
242    pub weight: FontWeight,
243    /// Whether the text should be stylized as link.
244    pub link: bool,
245}
246
247/// A parsed region in a Markdown document.
248#[derive(Debug, Clone)]
249#[cfg_attr(test, derive(PartialEq))]
250pub struct ParsedRegion {
251    /// Whether the region is a code block.
252    pub code: bool,
253    /// The link contained in this region, if it has one.
254    pub link: Option<Link>,
255}
256
257/// A Markdown link.
258#[derive(Debug, Clone)]
259#[cfg_attr(test, derive(PartialEq))]
260pub enum Link {
261    /// A link to a webpage.
262    Web {
263        /// The URL of the webpage.
264        url: String,
265    },
266    /// A link to a path on the filesystem.
267    Path {
268        /// The path as provided in the Markdown document.
269        display_path: PathBuf,
270        /// The absolute path to the item.
271        path: PathBuf,
272    },
273}
274
275impl Link {
276    pub fn identify(file_location_directory: Option<PathBuf>, text: String) -> Option<Link> {
277        if text.starts_with("http") {
278            return Some(Link::Web { url: text });
279        }
280
281        let path = PathBuf::from(&text);
282        if path.is_absolute() && path.exists() {
283            return Some(Link::Path {
284                display_path: path.clone(),
285                path,
286            });
287        }
288
289        if let Some(file_location_directory) = file_location_directory {
290            let display_path = path;
291            let path = file_location_directory.join(text);
292            if path.exists() {
293                return Some(Link::Path { display_path, path });
294            }
295        }
296
297        None
298    }
299}
300
301impl Display for Link {
302    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303        match self {
304            Link::Web { url } => write!(f, "{}", url),
305            Link::Path { display_path, .. } => write!(f, "{}", display_path.display()),
306        }
307    }
308}
309
310/// A Markdown Image
311#[derive(Debug, Clone)]
312#[cfg_attr(test, derive(PartialEq))]
313pub struct Image {
314    pub link: Link,
315    pub source_range: Range<usize>,
316    pub alt_text: Option<SharedString>,
317    pub width: Option<DefiniteLength>,
318    pub height: Option<DefiniteLength>,
319}
320
321impl Image {
322    pub fn identify(
323        text: String,
324        source_range: Range<usize>,
325        file_location_directory: Option<PathBuf>,
326    ) -> Option<Self> {
327        let link = Link::identify(file_location_directory, text)?;
328        Some(Self {
329            source_range,
330            link,
331            alt_text: None,
332            width: None,
333            height: None,
334        })
335    }
336
337    pub fn set_alt_text(&mut self, alt_text: SharedString) {
338        self.alt_text = Some(alt_text);
339    }
340
341    pub fn set_width(&mut self, width: DefiniteLength) {
342        self.width = Some(width);
343    }
344
345    pub fn set_height(&mut self, height: DefiniteLength) {
346        self.height = Some(height);
347    }
348}