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