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 various ranges in the Markdown document.
175    pub region_ranges: Vec<Range<usize>>,
176    /// The regions of the Markdown document.
177    pub regions: Vec<ParsedRegion>,
178}
179
180/// A run of highlighted Markdown text.
181#[derive(Debug, Clone, PartialEq, Eq)]
182pub enum MarkdownHighlight {
183    /// A styled Markdown highlight.
184    Style(MarkdownHighlightStyle),
185    /// A highlighted code block.
186    Code(HighlightId),
187}
188
189impl MarkdownHighlight {
190    /// Converts this [`MarkdownHighlight`] to a [`HighlightStyle`].
191    pub fn to_highlight_style(&self, theme: &theme::SyntaxTheme) -> Option<HighlightStyle> {
192        match self {
193            MarkdownHighlight::Style(style) => {
194                let mut highlight = HighlightStyle::default();
195
196                if style.italic {
197                    highlight.font_style = Some(FontStyle::Italic);
198                }
199
200                if style.underline {
201                    highlight.underline = Some(UnderlineStyle {
202                        thickness: px(1.),
203                        ..Default::default()
204                    });
205                }
206
207                if style.strikethrough {
208                    highlight.strikethrough = Some(StrikethroughStyle {
209                        thickness: px(1.),
210                        ..Default::default()
211                    });
212                }
213
214                if style.weight != FontWeight::default() {
215                    highlight.font_weight = Some(style.weight);
216                }
217
218                if style.link {
219                    highlight.underline = Some(UnderlineStyle {
220                        thickness: px(1.),
221                        ..Default::default()
222                    });
223                }
224
225                if style.oblique {
226                    highlight.font_style = Some(FontStyle::Oblique)
227                }
228
229                Some(highlight)
230            }
231
232            MarkdownHighlight::Code(id) => id.style(theme),
233        }
234    }
235}
236
237/// The style for a Markdown highlight.
238#[derive(Debug, Clone, Default, PartialEq, Eq)]
239pub struct MarkdownHighlightStyle {
240    /// Whether the text should be italicized.
241    pub italic: bool,
242    /// Whether the text should be underlined.
243    pub underline: bool,
244    /// Whether the text should be struck through.
245    pub strikethrough: bool,
246    /// The weight of the text.
247    pub weight: FontWeight,
248    /// Whether the text should be stylized as link.
249    pub link: bool,
250    // Whether the text should be obliqued.
251    pub oblique: bool,
252}
253
254/// A parsed region in a Markdown document.
255#[derive(Debug, Clone)]
256#[cfg_attr(test, derive(PartialEq))]
257pub struct ParsedRegion {
258    /// Whether the region is a code block.
259    pub code: bool,
260    /// The link contained in this region, if it has one.
261    pub link: Option<Link>,
262}
263
264/// A Markdown link.
265#[derive(Debug, Clone)]
266#[cfg_attr(test, derive(PartialEq))]
267pub enum Link {
268    /// A link to a webpage.
269    Web {
270        /// The URL of the webpage.
271        url: String,
272    },
273    /// A link to a path on the filesystem.
274    Path {
275        /// The path as provided in the Markdown document.
276        display_path: PathBuf,
277        /// The absolute path to the item.
278        path: PathBuf,
279    },
280}
281
282impl Link {
283    pub fn identify(file_location_directory: Option<PathBuf>, text: String) -> Option<Link> {
284        if text.starts_with("http") {
285            return Some(Link::Web { url: text });
286        }
287
288        // URL decode the text to handle spaces and other special characters
289        let decoded_text = urlencoding::decode(&text)
290            .map(|s| s.into_owned())
291            .unwrap_or(text);
292
293        let path = PathBuf::from(&decoded_text);
294        if path.is_absolute() && path.exists() {
295            return Some(Link::Path {
296                display_path: path.clone(),
297                path,
298            });
299        }
300
301        if let Some(file_location_directory) = file_location_directory {
302            let display_path = path;
303            let path = file_location_directory.join(decoded_text);
304            if path.exists() {
305                return Some(Link::Path { display_path, path });
306            }
307        }
308
309        None
310    }
311}
312
313impl Display for Link {
314    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315        match self {
316            Link::Web { url } => write!(f, "{}", url),
317            Link::Path { display_path, .. } => write!(f, "{}", display_path.display()),
318        }
319    }
320}
321
322/// A Markdown Image
323#[derive(Debug, Clone)]
324#[cfg_attr(test, derive(PartialEq))]
325pub struct Image {
326    pub link: Link,
327    pub source_range: Range<usize>,
328    pub alt_text: Option<SharedString>,
329    pub width: Option<DefiniteLength>,
330    pub height: Option<DefiniteLength>,
331}
332
333impl Image {
334    pub fn identify(
335        text: String,
336        source_range: Range<usize>,
337        file_location_directory: Option<PathBuf>,
338    ) -> Option<Self> {
339        let link = Link::identify(file_location_directory, text)?;
340        Some(Self {
341            source_range,
342            link,
343            alt_text: None,
344            width: None,
345            height: None,
346        })
347    }
348
349    pub fn set_alt_text(&mut self, alt_text: SharedString) {
350        self.alt_text = Some(alt_text);
351    }
352
353    pub fn set_width(&mut self, width: DefiniteLength) {
354        self.width = Some(width);
355    }
356
357    pub fn set_height(&mut self, height: DefiniteLength) {
358        self.height = Some(height);
359    }
360}