markdown_elements.rs

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