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