markdown_parser.rs

   1use crate::{
   2    markdown_elements::*,
   3    markdown_minifier::{Minifier, MinifierOptions},
   4};
   5use async_recursion::async_recursion;
   6use collections::FxHashMap;
   7use gpui::{DefiniteLength, FontWeight, px, relative};
   8use html5ever::{ParseOpts, local_name, parse_document, tendril::TendrilSink};
   9use language::LanguageRegistry;
  10use markup5ever_rcdom::RcDom;
  11use pulldown_cmark::{Alignment, Event, Options, Parser, Tag, TagEnd};
  12use std::{
  13    cell::RefCell, collections::HashMap, mem, ops::Range, path::PathBuf, rc::Rc, sync::Arc, vec,
  14};
  15
  16pub async fn parse_markdown(
  17    markdown_input: &str,
  18    file_location_directory: Option<PathBuf>,
  19    language_registry: Option<Arc<LanguageRegistry>>,
  20) -> ParsedMarkdown {
  21    let mut options = Options::all();
  22    options.remove(pulldown_cmark::Options::ENABLE_DEFINITION_LIST);
  23
  24    let parser = Parser::new_ext(markdown_input, options);
  25    let parser = MarkdownParser::new(
  26        parser.into_offset_iter().collect(),
  27        file_location_directory,
  28        language_registry,
  29    );
  30    let renderer = parser.parse_document().await;
  31    ParsedMarkdown {
  32        children: renderer.parsed,
  33    }
  34}
  35
  36fn cleanup_html(source: &str) -> Vec<u8> {
  37    let mut writer = std::io::Cursor::new(Vec::new());
  38    let mut reader = std::io::Cursor::new(source);
  39    let mut minify = Minifier::new(
  40        &mut writer,
  41        MinifierOptions {
  42            omit_doctype: true,
  43            collapse_whitespace: true,
  44            ..Default::default()
  45        },
  46    );
  47    if let Ok(()) = minify.minify(&mut reader) {
  48        writer.into_inner()
  49    } else {
  50        source.bytes().collect()
  51    }
  52}
  53
  54struct MarkdownParser<'a> {
  55    tokens: Vec<(Event<'a>, Range<usize>)>,
  56    /// The current index in the tokens array
  57    cursor: usize,
  58    /// The blocks that we have successfully parsed so far
  59    parsed: Vec<ParsedMarkdownElement>,
  60    file_location_directory: Option<PathBuf>,
  61    language_registry: Option<Arc<LanguageRegistry>>,
  62}
  63
  64struct MarkdownListItem {
  65    content: Vec<ParsedMarkdownElement>,
  66    item_type: ParsedMarkdownListItemType,
  67}
  68
  69impl Default for MarkdownListItem {
  70    fn default() -> Self {
  71        Self {
  72            content: Vec::new(),
  73            item_type: ParsedMarkdownListItemType::Unordered,
  74        }
  75    }
  76}
  77
  78impl<'a> MarkdownParser<'a> {
  79    fn new(
  80        tokens: Vec<(Event<'a>, Range<usize>)>,
  81        file_location_directory: Option<PathBuf>,
  82        language_registry: Option<Arc<LanguageRegistry>>,
  83    ) -> Self {
  84        Self {
  85            tokens,
  86            file_location_directory,
  87            language_registry,
  88            cursor: 0,
  89            parsed: vec![],
  90        }
  91    }
  92
  93    fn eof(&self) -> bool {
  94        if self.tokens.is_empty() {
  95            return true;
  96        }
  97        self.cursor >= self.tokens.len() - 1
  98    }
  99
 100    fn peek(&self, steps: usize) -> Option<&(Event<'_>, Range<usize>)> {
 101        if self.eof() || (steps + self.cursor) >= self.tokens.len() {
 102            return self.tokens.last();
 103        }
 104        self.tokens.get(self.cursor + steps)
 105    }
 106
 107    fn previous(&self) -> Option<&(Event<'_>, Range<usize>)> {
 108        if self.cursor == 0 || self.cursor > self.tokens.len() {
 109            return None;
 110        }
 111        self.tokens.get(self.cursor - 1)
 112    }
 113
 114    fn current(&self) -> Option<&(Event<'_>, Range<usize>)> {
 115        self.peek(0)
 116    }
 117
 118    fn current_event(&self) -> Option<&Event<'_>> {
 119        self.current().map(|(event, _)| event)
 120    }
 121
 122    fn is_text_like(event: &Event) -> bool {
 123        match event {
 124            Event::Text(_)
 125            // Represent an inline code block
 126            | Event::Code(_)
 127            | Event::Html(_)
 128            | Event::InlineHtml(_)
 129            | Event::FootnoteReference(_)
 130            | Event::Start(Tag::Link { .. })
 131            | Event::Start(Tag::Emphasis)
 132            | Event::Start(Tag::Strong)
 133            | Event::Start(Tag::Strikethrough)
 134            | Event::Start(Tag::Image { .. }) => {
 135                true
 136            }
 137            _ => false,
 138        }
 139    }
 140
 141    async fn parse_document(mut self) -> Self {
 142        while !self.eof() {
 143            if let Some(block) = self.parse_block().await {
 144                self.parsed.extend(block);
 145            } else {
 146                self.cursor += 1;
 147            }
 148        }
 149        self
 150    }
 151
 152    #[async_recursion]
 153    async fn parse_block(&mut self) -> Option<Vec<ParsedMarkdownElement>> {
 154        let (current, source_range) = self.current().unwrap();
 155        let source_range = source_range.clone();
 156        match current {
 157            Event::Start(tag) => match tag {
 158                Tag::Paragraph => {
 159                    self.cursor += 1;
 160                    let text = self.parse_text(false, Some(source_range));
 161                    Some(vec![ParsedMarkdownElement::Paragraph(text)])
 162                }
 163                Tag::Heading { level, .. } => {
 164                    let level = *level;
 165                    self.cursor += 1;
 166                    let heading = self.parse_heading(level);
 167                    Some(vec![ParsedMarkdownElement::Heading(heading)])
 168                }
 169                Tag::Table(alignment) => {
 170                    let alignment = alignment.clone();
 171                    self.cursor += 1;
 172                    let table = self.parse_table(alignment);
 173                    Some(vec![ParsedMarkdownElement::Table(table)])
 174                }
 175                Tag::List(order) => {
 176                    let order = *order;
 177                    self.cursor += 1;
 178                    let list = self.parse_list(order).await;
 179                    Some(list)
 180                }
 181                Tag::BlockQuote(_kind) => {
 182                    self.cursor += 1;
 183                    let block_quote = self.parse_block_quote().await;
 184                    Some(vec![ParsedMarkdownElement::BlockQuote(block_quote)])
 185                }
 186                Tag::CodeBlock(kind) => {
 187                    let language = match kind {
 188                        pulldown_cmark::CodeBlockKind::Indented => None,
 189                        pulldown_cmark::CodeBlockKind::Fenced(language) => {
 190                            if language.is_empty() {
 191                                None
 192                            } else {
 193                                Some(language.to_string())
 194                            }
 195                        }
 196                    };
 197
 198                    self.cursor += 1;
 199
 200                    let code_block = self.parse_code_block(language).await?;
 201                    Some(vec![ParsedMarkdownElement::CodeBlock(code_block)])
 202                }
 203                Tag::HtmlBlock => {
 204                    self.cursor += 1;
 205
 206                    Some(self.parse_html_block().await)
 207                }
 208                _ => None,
 209            },
 210            Event::Rule => {
 211                self.cursor += 1;
 212                Some(vec![ParsedMarkdownElement::HorizontalRule(source_range)])
 213            }
 214            _ => None,
 215        }
 216    }
 217
 218    fn parse_text(
 219        &mut self,
 220        should_complete_on_soft_break: bool,
 221        source_range: Option<Range<usize>>,
 222    ) -> MarkdownParagraph {
 223        let source_range = source_range.unwrap_or_else(|| {
 224            self.current()
 225                .map(|(_, range)| range.clone())
 226                .unwrap_or_default()
 227        });
 228
 229        let mut markdown_text_like = Vec::new();
 230        let mut text = String::new();
 231        let mut bold_depth = 0;
 232        let mut italic_depth = 0;
 233        let mut strikethrough_depth = 0;
 234        let mut link: Option<Link> = None;
 235        let mut image: Option<Image> = None;
 236        let mut region_ranges: Vec<Range<usize>> = vec![];
 237        let mut regions: Vec<ParsedRegion> = vec![];
 238        let mut highlights: Vec<(Range<usize>, MarkdownHighlight)> = vec![];
 239        let mut link_urls: Vec<String> = vec![];
 240        let mut link_ranges: Vec<Range<usize>> = vec![];
 241
 242        loop {
 243            if self.eof() {
 244                break;
 245            }
 246
 247            let (current, _) = self.current().unwrap();
 248            let prev_len = text.len();
 249            match current {
 250                Event::SoftBreak => {
 251                    if should_complete_on_soft_break {
 252                        break;
 253                    }
 254                    text.push(' ');
 255                }
 256
 257                Event::HardBreak => {
 258                    text.push('\n');
 259                }
 260
 261                // We want to ignore any inline HTML tags in the text but keep
 262                // the text between them
 263                Event::InlineHtml(_) => {}
 264
 265                Event::Text(t) => {
 266                    text.push_str(t.as_ref());
 267                    let mut style = MarkdownHighlightStyle::default();
 268
 269                    if bold_depth > 0 {
 270                        style.weight = FontWeight::BOLD;
 271                    }
 272
 273                    if italic_depth > 0 {
 274                        style.italic = true;
 275                    }
 276
 277                    if strikethrough_depth > 0 {
 278                        style.strikethrough = true;
 279                    }
 280
 281                    let last_run_len = if let Some(link) = link.clone() {
 282                        region_ranges.push(prev_len..text.len());
 283                        regions.push(ParsedRegion {
 284                            code: false,
 285                            link: Some(link),
 286                        });
 287                        style.link = true;
 288                        prev_len
 289                    } else {
 290                        // Manually scan for links
 291                        let mut finder = linkify::LinkFinder::new();
 292                        finder.kinds(&[linkify::LinkKind::Url]);
 293                        let mut last_link_len = prev_len;
 294                        for link in finder.links(t) {
 295                            let start = prev_len + link.start();
 296                            let end = prev_len + link.end();
 297                            let range = start..end;
 298                            link_ranges.push(range.clone());
 299                            link_urls.push(link.as_str().to_string());
 300
 301                            // If there is a style before we match a link, we have to add this to the highlighted ranges
 302                            if style != MarkdownHighlightStyle::default() && last_link_len < start {
 303                                highlights.push((
 304                                    last_link_len..start,
 305                                    MarkdownHighlight::Style(style.clone()),
 306                                ));
 307                            }
 308
 309                            highlights.push((
 310                                range.clone(),
 311                                MarkdownHighlight::Style(MarkdownHighlightStyle {
 312                                    underline: true,
 313                                    ..style
 314                                }),
 315                            ));
 316                            region_ranges.push(range.clone());
 317                            regions.push(ParsedRegion {
 318                                code: false,
 319                                link: Some(Link::Web {
 320                                    url: link.as_str().to_string(),
 321                                }),
 322                            });
 323                            last_link_len = end;
 324                        }
 325                        last_link_len
 326                    };
 327
 328                    if style != MarkdownHighlightStyle::default() && last_run_len < text.len() {
 329                        let mut new_highlight = true;
 330                        if let Some((last_range, last_style)) = highlights.last_mut()
 331                            && last_range.end == last_run_len
 332                            && last_style == &MarkdownHighlight::Style(style.clone())
 333                        {
 334                            last_range.end = text.len();
 335                            new_highlight = false;
 336                        }
 337                        if new_highlight {
 338                            highlights.push((
 339                                last_run_len..text.len(),
 340                                MarkdownHighlight::Style(style.clone()),
 341                            ));
 342                        }
 343                    }
 344                }
 345                Event::Code(t) => {
 346                    text.push_str(t.as_ref());
 347                    region_ranges.push(prev_len..text.len());
 348
 349                    if link.is_some() {
 350                        highlights.push((
 351                            prev_len..text.len(),
 352                            MarkdownHighlight::Style(MarkdownHighlightStyle {
 353                                link: true,
 354                                ..Default::default()
 355                            }),
 356                        ));
 357                    }
 358                    regions.push(ParsedRegion {
 359                        code: true,
 360                        link: link.clone(),
 361                    });
 362                }
 363                Event::Start(tag) => match tag {
 364                    Tag::Emphasis => italic_depth += 1,
 365                    Tag::Strong => bold_depth += 1,
 366                    Tag::Strikethrough => strikethrough_depth += 1,
 367                    Tag::Link { dest_url, .. } => {
 368                        link = Link::identify(
 369                            self.file_location_directory.clone(),
 370                            dest_url.to_string(),
 371                        );
 372                    }
 373                    Tag::Image { dest_url, .. } => {
 374                        if !text.is_empty() {
 375                            let parsed_regions = MarkdownParagraphChunk::Text(ParsedMarkdownText {
 376                                source_range: source_range.clone(),
 377                                contents: mem::take(&mut text).into(),
 378                                highlights: mem::take(&mut highlights),
 379                                region_ranges: mem::take(&mut region_ranges),
 380                                regions: mem::take(&mut regions),
 381                            });
 382                            markdown_text_like.push(parsed_regions);
 383                        }
 384                        image = Image::identify(
 385                            dest_url.to_string(),
 386                            source_range.clone(),
 387                            self.file_location_directory.clone(),
 388                        );
 389                    }
 390                    _ => {
 391                        break;
 392                    }
 393                },
 394
 395                Event::End(tag) => match tag {
 396                    TagEnd::Emphasis => italic_depth -= 1,
 397                    TagEnd::Strong => bold_depth -= 1,
 398                    TagEnd::Strikethrough => strikethrough_depth -= 1,
 399                    TagEnd::Link => {
 400                        link = None;
 401                    }
 402                    TagEnd::Image => {
 403                        if let Some(mut image) = image.take() {
 404                            if !text.is_empty() {
 405                                image.set_alt_text(std::mem::take(&mut text).into());
 406                                mem::take(&mut highlights);
 407                                mem::take(&mut region_ranges);
 408                                mem::take(&mut regions);
 409                            }
 410                            markdown_text_like.push(MarkdownParagraphChunk::Image(image));
 411                        }
 412                    }
 413                    TagEnd::Paragraph => {
 414                        self.cursor += 1;
 415                        break;
 416                    }
 417                    _ => {
 418                        break;
 419                    }
 420                },
 421                _ => {
 422                    break;
 423                }
 424            }
 425
 426            self.cursor += 1;
 427        }
 428        if !text.is_empty() {
 429            markdown_text_like.push(MarkdownParagraphChunk::Text(ParsedMarkdownText {
 430                source_range,
 431                contents: text.into(),
 432                highlights,
 433                regions,
 434                region_ranges,
 435            }));
 436        }
 437        markdown_text_like
 438    }
 439
 440    fn parse_heading(&mut self, level: pulldown_cmark::HeadingLevel) -> ParsedMarkdownHeading {
 441        let (_event, source_range) = self.previous().unwrap();
 442        let source_range = source_range.clone();
 443        let text = self.parse_text(true, None);
 444
 445        // Advance past the heading end tag
 446        self.cursor += 1;
 447
 448        ParsedMarkdownHeading {
 449            source_range,
 450            level: match level {
 451                pulldown_cmark::HeadingLevel::H1 => HeadingLevel::H1,
 452                pulldown_cmark::HeadingLevel::H2 => HeadingLevel::H2,
 453                pulldown_cmark::HeadingLevel::H3 => HeadingLevel::H3,
 454                pulldown_cmark::HeadingLevel::H4 => HeadingLevel::H4,
 455                pulldown_cmark::HeadingLevel::H5 => HeadingLevel::H5,
 456                pulldown_cmark::HeadingLevel::H6 => HeadingLevel::H6,
 457            },
 458            contents: text,
 459        }
 460    }
 461
 462    fn parse_table(&mut self, alignment: Vec<Alignment>) -> ParsedMarkdownTable {
 463        let (_event, source_range) = self.previous().unwrap();
 464        let source_range = source_range.clone();
 465        let mut header = vec![];
 466        let mut body = vec![];
 467        let mut row_columns = vec![];
 468        let mut in_header = true;
 469        let column_alignments = alignment
 470            .iter()
 471            .map(Self::convert_alignment)
 472            .collect::<Vec<_>>();
 473
 474        loop {
 475            if self.eof() {
 476                break;
 477            }
 478
 479            let (current, source_range) = self.current().unwrap();
 480            let source_range = source_range.clone();
 481            match current {
 482                Event::Start(Tag::TableHead)
 483                | Event::Start(Tag::TableRow)
 484                | Event::End(TagEnd::TableCell) => {
 485                    self.cursor += 1;
 486                }
 487                Event::Start(Tag::TableCell) => {
 488                    self.cursor += 1;
 489                    let cell_contents = self.parse_text(false, Some(source_range));
 490                    row_columns.push(ParsedMarkdownTableColumn {
 491                        col_span: 1,
 492                        row_span: 1,
 493                        is_header: in_header,
 494                        children: cell_contents,
 495                        alignment: column_alignments
 496                            .get(row_columns.len())
 497                            .copied()
 498                            .unwrap_or_default(),
 499                    });
 500                }
 501                Event::End(TagEnd::TableHead) | Event::End(TagEnd::TableRow) => {
 502                    self.cursor += 1;
 503                    let columns = std::mem::take(&mut row_columns);
 504                    if in_header {
 505                        header.push(ParsedMarkdownTableRow { columns: columns });
 506                        in_header = false;
 507                    } else {
 508                        body.push(ParsedMarkdownTableRow::with_columns(columns));
 509                    }
 510                }
 511                Event::End(TagEnd::Table) => {
 512                    self.cursor += 1;
 513                    break;
 514                }
 515                _ => {
 516                    break;
 517                }
 518            }
 519        }
 520
 521        ParsedMarkdownTable {
 522            source_range,
 523            header,
 524            body,
 525        }
 526    }
 527
 528    fn convert_alignment(alignment: &Alignment) -> ParsedMarkdownTableAlignment {
 529        match alignment {
 530            Alignment::None => ParsedMarkdownTableAlignment::None,
 531            Alignment::Left => ParsedMarkdownTableAlignment::Left,
 532            Alignment::Center => ParsedMarkdownTableAlignment::Center,
 533            Alignment::Right => ParsedMarkdownTableAlignment::Right,
 534        }
 535    }
 536
 537    async fn parse_list(&mut self, order: Option<u64>) -> Vec<ParsedMarkdownElement> {
 538        let (_, list_source_range) = self.previous().unwrap();
 539
 540        let mut items = Vec::new();
 541        let mut items_stack = vec![MarkdownListItem::default()];
 542        let mut depth = 1;
 543        let mut order = order;
 544        let mut order_stack = Vec::new();
 545
 546        let mut insertion_indices = FxHashMap::default();
 547        let mut source_ranges = FxHashMap::default();
 548        let mut start_item_range = list_source_range.clone();
 549
 550        while !self.eof() {
 551            let (current, source_range) = self.current().unwrap();
 552            match current {
 553                Event::Start(Tag::List(new_order)) => {
 554                    if items_stack.last().is_some() && !insertion_indices.contains_key(&depth) {
 555                        insertion_indices.insert(depth, items.len());
 556                    }
 557
 558                    // We will use the start of the nested list as the end for the current item's range,
 559                    // because we don't care about the hierarchy of list items
 560                    if let collections::hash_map::Entry::Vacant(e) = source_ranges.entry(depth) {
 561                        e.insert(start_item_range.start..source_range.start);
 562                    }
 563
 564                    order_stack.push(order);
 565                    order = *new_order;
 566                    self.cursor += 1;
 567                    depth += 1;
 568                }
 569                Event::End(TagEnd::List(_)) => {
 570                    order = order_stack.pop().flatten();
 571                    self.cursor += 1;
 572                    depth -= 1;
 573
 574                    if depth == 0 {
 575                        break;
 576                    }
 577                }
 578                Event::Start(Tag::Item) => {
 579                    start_item_range = source_range.clone();
 580
 581                    self.cursor += 1;
 582                    items_stack.push(MarkdownListItem::default());
 583
 584                    let mut task_list = None;
 585                    // Check for task list marker (`- [ ]` or `- [x]`)
 586                    if let Some(event) = self.current_event() {
 587                        // If there is a linebreak in between two list items the task list marker will actually be the first element of the paragraph
 588                        if event == &Event::Start(Tag::Paragraph) {
 589                            self.cursor += 1;
 590                        }
 591
 592                        if let Some((Event::TaskListMarker(checked), range)) = self.current() {
 593                            task_list = Some((*checked, range.clone()));
 594                            self.cursor += 1;
 595                        }
 596                    }
 597
 598                    if let Some((event, range)) = self.current() {
 599                        // This is a plain list item.
 600                        // For example `- some text` or `1. [Docs](./docs.md)`
 601                        if MarkdownParser::is_text_like(event) {
 602                            let text = self.parse_text(false, Some(range.clone()));
 603                            let block = ParsedMarkdownElement::Paragraph(text);
 604                            if let Some(content) = items_stack.last_mut() {
 605                                let item_type = if let Some((checked, range)) = task_list {
 606                                    ParsedMarkdownListItemType::Task(checked, range)
 607                                } else if let Some(order) = order {
 608                                    ParsedMarkdownListItemType::Ordered(order)
 609                                } else {
 610                                    ParsedMarkdownListItemType::Unordered
 611                                };
 612                                content.item_type = item_type;
 613                                content.content.push(block);
 614                            }
 615                        } else {
 616                            let block = self.parse_block().await;
 617                            if let Some(block) = block
 618                                && let Some(list_item) = items_stack.last_mut()
 619                            {
 620                                list_item.content.extend(block);
 621                            }
 622                        }
 623                    }
 624
 625                    // If there is a linebreak in between two list items the task list marker will actually be the first element of the paragraph
 626                    if self.current_event() == Some(&Event::End(TagEnd::Paragraph)) {
 627                        self.cursor += 1;
 628                    }
 629                }
 630                Event::End(TagEnd::Item) => {
 631                    self.cursor += 1;
 632
 633                    if let Some(current) = order {
 634                        order = Some(current + 1);
 635                    }
 636
 637                    if let Some(list_item) = items_stack.pop() {
 638                        let source_range = source_ranges
 639                            .remove(&depth)
 640                            .unwrap_or(start_item_range.clone());
 641
 642                        // We need to remove the last character of the source range, because it includes the newline character
 643                        let source_range = source_range.start..source_range.end - 1;
 644                        let item = ParsedMarkdownElement::ListItem(ParsedMarkdownListItem {
 645                            source_range,
 646                            content: list_item.content,
 647                            depth,
 648                            item_type: list_item.item_type,
 649                        });
 650
 651                        if let Some(index) = insertion_indices.get(&depth) {
 652                            items.insert(*index, item);
 653                            insertion_indices.remove(&depth);
 654                        } else {
 655                            items.push(item);
 656                        }
 657                    }
 658                }
 659                _ => {
 660                    if depth == 0 {
 661                        break;
 662                    }
 663                    // This can only happen if a list item starts with more then one paragraph,
 664                    // or the list item contains blocks that should be rendered after the nested list items
 665                    let block = self.parse_block().await;
 666                    if let Some(block) = block {
 667                        if let Some(list_item) = items_stack.last_mut() {
 668                            // If we did not insert any nested items yet (in this case insertion index is set), we can append the block to the current list item
 669                            if !insertion_indices.contains_key(&depth) {
 670                                list_item.content.extend(block);
 671                                continue;
 672                            }
 673                        }
 674
 675                        // Otherwise we need to insert the block after all the nested items
 676                        // that have been parsed so far
 677                        items.extend(block);
 678                    } else {
 679                        self.cursor += 1;
 680                    }
 681                }
 682            }
 683        }
 684
 685        items
 686    }
 687
 688    #[async_recursion]
 689    async fn parse_block_quote(&mut self) -> ParsedMarkdownBlockQuote {
 690        let (_event, source_range) = self.previous().unwrap();
 691        let source_range = source_range.clone();
 692        let mut nested_depth = 1;
 693
 694        let mut children: Vec<ParsedMarkdownElement> = vec![];
 695
 696        while !self.eof() {
 697            let block = self.parse_block().await;
 698
 699            if let Some(block) = block {
 700                children.extend(block);
 701            } else {
 702                break;
 703            }
 704
 705            if self.eof() {
 706                break;
 707            }
 708
 709            let (current, _source_range) = self.current().unwrap();
 710            match current {
 711                // This is a nested block quote.
 712                // Record that we're in a nested block quote and continue parsing.
 713                // We don't need to advance the cursor since the next
 714                // call to `parse_block` will handle it.
 715                Event::Start(Tag::BlockQuote(_kind)) => {
 716                    nested_depth += 1;
 717                }
 718                Event::End(TagEnd::BlockQuote(_kind)) => {
 719                    nested_depth -= 1;
 720                    if nested_depth == 0 {
 721                        self.cursor += 1;
 722                        break;
 723                    }
 724                }
 725                _ => {}
 726            };
 727        }
 728
 729        ParsedMarkdownBlockQuote {
 730            source_range,
 731            children,
 732        }
 733    }
 734
 735    async fn parse_code_block(
 736        &mut self,
 737        language: Option<String>,
 738    ) -> Option<ParsedMarkdownCodeBlock> {
 739        let Some((_event, source_range)) = self.previous() else {
 740            return None;
 741        };
 742
 743        let source_range = source_range.clone();
 744        let mut code = String::new();
 745
 746        while !self.eof() {
 747            let Some((current, _source_range)) = self.current() else {
 748                break;
 749            };
 750
 751            match current {
 752                Event::Text(text) => {
 753                    code.push_str(text);
 754                    self.cursor += 1;
 755                }
 756                Event::End(TagEnd::CodeBlock) => {
 757                    self.cursor += 1;
 758                    break;
 759                }
 760                _ => {
 761                    break;
 762                }
 763            }
 764        }
 765
 766        code = code.strip_suffix('\n').unwrap_or(&code).to_string();
 767
 768        let highlights = if let Some(language) = &language {
 769            if let Some(registry) = &self.language_registry {
 770                let rope: language::Rope = code.as_str().into();
 771                registry
 772                    .language_for_name_or_extension(language)
 773                    .await
 774                    .map(|l| l.highlight_text(&rope, 0..code.len()))
 775                    .ok()
 776            } else {
 777                None
 778            }
 779        } else {
 780            None
 781        };
 782
 783        Some(ParsedMarkdownCodeBlock {
 784            source_range,
 785            contents: code.into(),
 786            language,
 787            highlights,
 788        })
 789    }
 790
 791    async fn parse_html_block(&mut self) -> Vec<ParsedMarkdownElement> {
 792        let mut elements = Vec::new();
 793        let Some((_event, _source_range)) = self.previous() else {
 794            return elements;
 795        };
 796
 797        let mut html_source_range_start = None;
 798        let mut html_source_range_end = None;
 799        let mut html_buffer = String::new();
 800
 801        while !self.eof() {
 802            let Some((current, source_range)) = self.current() else {
 803                break;
 804            };
 805            let source_range = source_range.clone();
 806            match current {
 807                Event::Html(html) => {
 808                    html_source_range_start.get_or_insert(source_range.start);
 809                    html_source_range_end = Some(source_range.end);
 810                    html_buffer.push_str(html);
 811                    self.cursor += 1;
 812                }
 813                Event::End(TagEnd::CodeBlock) => {
 814                    self.cursor += 1;
 815                    break;
 816                }
 817                _ => {
 818                    break;
 819                }
 820            }
 821        }
 822
 823        let bytes = cleanup_html(&html_buffer);
 824
 825        let mut cursor = std::io::Cursor::new(bytes);
 826        if let Ok(dom) = parse_document(RcDom::default(), ParseOpts::default())
 827            .from_utf8()
 828            .read_from(&mut cursor)
 829            && let Some((start, end)) = html_source_range_start.zip(html_source_range_end)
 830        {
 831            self.parse_html_node(start..end, &dom.document, &mut elements);
 832        }
 833
 834        elements
 835    }
 836
 837    fn parse_html_node(
 838        &self,
 839        source_range: Range<usize>,
 840        node: &Rc<markup5ever_rcdom::Node>,
 841        elements: &mut Vec<ParsedMarkdownElement>,
 842    ) {
 843        match &node.data {
 844            markup5ever_rcdom::NodeData::Document => {
 845                self.consume_children(source_range, node, elements);
 846            }
 847            markup5ever_rcdom::NodeData::Text { contents } => {
 848                elements.push(ParsedMarkdownElement::Paragraph(vec![
 849                    MarkdownParagraphChunk::Text(ParsedMarkdownText {
 850                        source_range,
 851                        regions: Vec::default(),
 852                        region_ranges: Vec::default(),
 853                        highlights: Vec::default(),
 854                        contents: contents.borrow().to_string().into(),
 855                    }),
 856                ]));
 857            }
 858            markup5ever_rcdom::NodeData::Comment { .. } => {}
 859            markup5ever_rcdom::NodeData::Element { name, attrs, .. } => {
 860                if local_name!("img") == name.local {
 861                    if let Some(image) = self.extract_image(source_range, attrs) {
 862                        elements.push(ParsedMarkdownElement::Image(image));
 863                    }
 864                } else if local_name!("p") == name.local {
 865                    let mut paragraph = MarkdownParagraph::new();
 866                    self.parse_paragraph(source_range, node, &mut paragraph);
 867
 868                    if !paragraph.is_empty() {
 869                        elements.push(ParsedMarkdownElement::Paragraph(paragraph));
 870                    }
 871                } else if matches!(
 872                    name.local,
 873                    local_name!("h1")
 874                        | local_name!("h2")
 875                        | local_name!("h3")
 876                        | local_name!("h4")
 877                        | local_name!("h5")
 878                        | local_name!("h6")
 879                ) {
 880                    let mut paragraph = MarkdownParagraph::new();
 881                    self.consume_paragraph(source_range.clone(), node, &mut paragraph);
 882
 883                    if !paragraph.is_empty() {
 884                        elements.push(ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
 885                            source_range,
 886                            level: match name.local {
 887                                local_name!("h1") => HeadingLevel::H1,
 888                                local_name!("h2") => HeadingLevel::H2,
 889                                local_name!("h3") => HeadingLevel::H3,
 890                                local_name!("h4") => HeadingLevel::H4,
 891                                local_name!("h5") => HeadingLevel::H5,
 892                                local_name!("h6") => HeadingLevel::H6,
 893                                _ => unreachable!(),
 894                            },
 895                            contents: paragraph,
 896                        }));
 897                    }
 898                } else if local_name!("blockquote") == name.local {
 899                    if let Some(blockquote) = self.extract_html_blockquote(node, source_range) {
 900                        elements.push(ParsedMarkdownElement::BlockQuote(blockquote));
 901                    }
 902                } else if local_name!("table") == name.local {
 903                    if let Some(table) = self.extract_html_table(node, source_range) {
 904                        elements.push(ParsedMarkdownElement::Table(table));
 905                    }
 906                } else {
 907                    self.consume_children(source_range, node, elements);
 908                }
 909            }
 910            _ => {}
 911        }
 912    }
 913
 914    fn parse_paragraph(
 915        &self,
 916        source_range: Range<usize>,
 917        node: &Rc<markup5ever_rcdom::Node>,
 918        paragraph: &mut MarkdownParagraph,
 919    ) {
 920        match &node.data {
 921            markup5ever_rcdom::NodeData::Text { contents } => {
 922                paragraph.push(MarkdownParagraphChunk::Text(ParsedMarkdownText {
 923                    source_range,
 924                    regions: Vec::default(),
 925                    region_ranges: Vec::default(),
 926                    highlights: Vec::default(),
 927                    contents: contents.borrow().to_string().into(),
 928                }));
 929            }
 930            markup5ever_rcdom::NodeData::Element { name, attrs, .. } => {
 931                if local_name!("img") == name.local {
 932                    if let Some(image) = self.extract_image(source_range, attrs) {
 933                        paragraph.push(MarkdownParagraphChunk::Image(image));
 934                    }
 935                } else {
 936                    self.consume_paragraph(source_range, node, paragraph);
 937                }
 938            }
 939            _ => {}
 940        }
 941    }
 942
 943    fn consume_paragraph(
 944        &self,
 945        source_range: Range<usize>,
 946        node: &Rc<markup5ever_rcdom::Node>,
 947        paragraph: &mut MarkdownParagraph,
 948    ) {
 949        for node in node.children.borrow().iter() {
 950            self.parse_paragraph(source_range.clone(), node, paragraph);
 951        }
 952    }
 953
 954    fn parse_table_row(
 955        &self,
 956        source_range: Range<usize>,
 957        node: &Rc<markup5ever_rcdom::Node>,
 958    ) -> Option<ParsedMarkdownTableRow> {
 959        let mut columns = Vec::new();
 960
 961        match &node.data {
 962            markup5ever_rcdom::NodeData::Element { name, .. } => {
 963                if local_name!("tr") != name.local {
 964                    return None;
 965                }
 966
 967                for node in node.children.borrow().iter() {
 968                    if let Some(column) = self.parse_table_column(source_range.clone(), node) {
 969                        columns.push(column);
 970                    }
 971                }
 972            }
 973            _ => {}
 974        }
 975
 976        if columns.is_empty() {
 977            None
 978        } else {
 979            Some(ParsedMarkdownTableRow { columns })
 980        }
 981    }
 982
 983    fn parse_table_column(
 984        &self,
 985        source_range: Range<usize>,
 986        node: &Rc<markup5ever_rcdom::Node>,
 987    ) -> Option<ParsedMarkdownTableColumn> {
 988        match &node.data {
 989            markup5ever_rcdom::NodeData::Element { name, attrs, .. } => {
 990                if !matches!(name.local, local_name!("th") | local_name!("td")) {
 991                    return None;
 992                }
 993
 994                let mut children = MarkdownParagraph::new();
 995                self.consume_paragraph(source_range, node, &mut children);
 996
 997                let is_header = matches!(name.local, local_name!("th"));
 998
 999                Some(ParsedMarkdownTableColumn {
1000                    col_span: std::cmp::max(
1001                        Self::attr_value(attrs, local_name!("colspan"))
1002                            .and_then(|span| span.parse().ok())
1003                            .unwrap_or(1),
1004                        1,
1005                    ),
1006                    row_span: std::cmp::max(
1007                        Self::attr_value(attrs, local_name!("rowspan"))
1008                            .and_then(|span| span.parse().ok())
1009                            .unwrap_or(1),
1010                        1,
1011                    ),
1012                    is_header,
1013                    children,
1014                    alignment: Self::attr_value(attrs, local_name!("align"))
1015                        .and_then(|align| match align.as_str() {
1016                            "left" => Some(ParsedMarkdownTableAlignment::Left),
1017                            "center" => Some(ParsedMarkdownTableAlignment::Center),
1018                            "right" => Some(ParsedMarkdownTableAlignment::Right),
1019                            _ => None,
1020                        })
1021                        .unwrap_or_else(|| {
1022                            if is_header {
1023                                ParsedMarkdownTableAlignment::Center
1024                            } else {
1025                                ParsedMarkdownTableAlignment::default()
1026                            }
1027                        }),
1028                })
1029            }
1030            _ => None,
1031        }
1032    }
1033
1034    fn consume_children(
1035        &self,
1036        source_range: Range<usize>,
1037        node: &Rc<markup5ever_rcdom::Node>,
1038        elements: &mut Vec<ParsedMarkdownElement>,
1039    ) {
1040        for node in node.children.borrow().iter() {
1041            self.parse_html_node(source_range.clone(), node, elements);
1042        }
1043    }
1044
1045    fn attr_value(
1046        attrs: &RefCell<Vec<html5ever::Attribute>>,
1047        name: html5ever::LocalName,
1048    ) -> Option<String> {
1049        attrs.borrow().iter().find_map(|attr| {
1050            if attr.name.local == name {
1051                Some(attr.value.to_string())
1052            } else {
1053                None
1054            }
1055        })
1056    }
1057
1058    fn extract_styles_from_attributes(
1059        attrs: &RefCell<Vec<html5ever::Attribute>>,
1060    ) -> HashMap<String, String> {
1061        let mut styles = HashMap::new();
1062
1063        if let Some(style) = Self::attr_value(attrs, local_name!("style")) {
1064            for decl in style.split(';') {
1065                let mut parts = decl.splitn(2, ':');
1066                if let Some((key, value)) = parts.next().zip(parts.next()) {
1067                    styles.insert(
1068                        key.trim().to_lowercase().to_string(),
1069                        value.trim().to_string(),
1070                    );
1071                }
1072            }
1073        }
1074
1075        styles
1076    }
1077
1078    fn extract_image(
1079        &self,
1080        source_range: Range<usize>,
1081        attrs: &RefCell<Vec<html5ever::Attribute>>,
1082    ) -> Option<Image> {
1083        let src = Self::attr_value(attrs, local_name!("src"))?;
1084
1085        let mut image = Image::identify(src, source_range, self.file_location_directory.clone())?;
1086
1087        if let Some(alt) = Self::attr_value(attrs, local_name!("alt")) {
1088            image.set_alt_text(alt.into());
1089        }
1090
1091        let styles = Self::extract_styles_from_attributes(attrs);
1092
1093        if let Some(width) = Self::attr_value(attrs, local_name!("width"))
1094            .or_else(|| styles.get("width").cloned())
1095            .and_then(|width| Self::parse_html_element_dimension(&width))
1096        {
1097            image.set_width(width);
1098        }
1099
1100        if let Some(height) = Self::attr_value(attrs, local_name!("height"))
1101            .or_else(|| styles.get("height").cloned())
1102            .and_then(|height| Self::parse_html_element_dimension(&height))
1103        {
1104            image.set_height(height);
1105        }
1106
1107        Some(image)
1108    }
1109
1110    fn parse_html_element_dimension(value: &str) -> Option<DefiniteLength> {
1111        if value.ends_with("%") {
1112            value
1113                .trim_end_matches("%")
1114                .parse::<f32>()
1115                .ok()
1116                .map(|value| relative(value / 100.))
1117        } else {
1118            value
1119                .trim_end_matches("px")
1120                .parse()
1121                .ok()
1122                .map(|value| px(value).into())
1123        }
1124    }
1125
1126    fn extract_html_blockquote(
1127        &self,
1128        node: &Rc<markup5ever_rcdom::Node>,
1129        source_range: Range<usize>,
1130    ) -> Option<ParsedMarkdownBlockQuote> {
1131        let mut children = Vec::new();
1132        self.consume_children(source_range.clone(), node, &mut children);
1133
1134        if children.is_empty() {
1135            None
1136        } else {
1137            Some(ParsedMarkdownBlockQuote {
1138                children,
1139                source_range,
1140            })
1141        }
1142    }
1143
1144    fn extract_html_table(
1145        &self,
1146        node: &Rc<markup5ever_rcdom::Node>,
1147        source_range: Range<usize>,
1148    ) -> Option<ParsedMarkdownTable> {
1149        let mut header_rows = Vec::new();
1150        let mut body_rows = Vec::new();
1151
1152        // node should be a thead or tbody element
1153        for node in node.children.borrow().iter() {
1154            match &node.data {
1155                markup5ever_rcdom::NodeData::Element { name, .. } => {
1156                    if local_name!("thead") == name.local {
1157                        // node should be a tr element
1158                        for node in node.children.borrow().iter() {
1159                            if let Some(row) = self.parse_table_row(source_range.clone(), node) {
1160                                header_rows.push(row);
1161                            }
1162                        }
1163                    } else if local_name!("tbody") == name.local {
1164                        // node should be a tr element
1165                        for node in node.children.borrow().iter() {
1166                            if let Some(row) = self.parse_table_row(source_range.clone(), node) {
1167                                body_rows.push(row);
1168                            }
1169                        }
1170                    }
1171                }
1172                _ => {}
1173            }
1174        }
1175
1176        if !header_rows.is_empty() || !body_rows.is_empty() {
1177            Some(ParsedMarkdownTable {
1178                source_range,
1179                body: body_rows,
1180                header: header_rows,
1181            })
1182        } else {
1183            None
1184        }
1185    }
1186}
1187
1188#[cfg(test)]
1189mod tests {
1190    use super::*;
1191    use ParsedMarkdownListItemType::*;
1192    use core::panic;
1193    use gpui::{AbsoluteLength, BackgroundExecutor, DefiniteLength};
1194    use language::{
1195        HighlightId, Language, LanguageConfig, LanguageMatcher, LanguageRegistry, tree_sitter_rust,
1196    };
1197    use pretty_assertions::assert_eq;
1198
1199    async fn parse(input: &str) -> ParsedMarkdown {
1200        parse_markdown(input, None, None).await
1201    }
1202
1203    #[gpui::test]
1204    async fn test_headings() {
1205        let parsed = parse("# Heading one\n## Heading two\n### Heading three").await;
1206
1207        assert_eq!(
1208            parsed.children,
1209            vec![
1210                h1(text("Heading one", 2..13), 0..14),
1211                h2(text("Heading two", 17..28), 14..29),
1212                h3(text("Heading three", 33..46), 29..46),
1213            ]
1214        );
1215    }
1216
1217    #[gpui::test]
1218    async fn test_newlines_dont_new_paragraphs() {
1219        let parsed = parse("Some text **that is bolded**\n and *italicized*").await;
1220
1221        assert_eq!(
1222            parsed.children,
1223            vec![p("Some text that is bolded and italicized", 0..46)]
1224        );
1225    }
1226
1227    #[gpui::test]
1228    async fn test_heading_with_paragraph() {
1229        let parsed = parse("# Zed\nThe editor").await;
1230
1231        assert_eq!(
1232            parsed.children,
1233            vec![h1(text("Zed", 2..5), 0..6), p("The editor", 6..16),]
1234        );
1235    }
1236
1237    #[gpui::test]
1238    async fn test_double_newlines_do_new_paragraphs() {
1239        let parsed = parse("Some text **that is bolded**\n\n and *italicized*").await;
1240
1241        assert_eq!(
1242            parsed.children,
1243            vec![
1244                p("Some text that is bolded", 0..29),
1245                p("and italicized", 31..47),
1246            ]
1247        );
1248    }
1249
1250    #[gpui::test]
1251    async fn test_bold_italic_text() {
1252        let parsed = parse("Some text **that is bolded** and *italicized*").await;
1253
1254        assert_eq!(
1255            parsed.children,
1256            vec![p("Some text that is bolded and italicized", 0..45)]
1257        );
1258    }
1259
1260    #[gpui::test]
1261    async fn test_nested_bold_strikethrough_text() {
1262        let parsed = parse("Some **bo~~strikethrough~~ld** text").await;
1263
1264        assert_eq!(parsed.children.len(), 1);
1265        assert_eq!(
1266            parsed.children[0],
1267            ParsedMarkdownElement::Paragraph(vec![MarkdownParagraphChunk::Text(
1268                ParsedMarkdownText {
1269                    source_range: 0..35,
1270                    contents: "Some bostrikethroughld text".into(),
1271                    highlights: Vec::new(),
1272                    region_ranges: Vec::new(),
1273                    regions: Vec::new(),
1274                }
1275            )])
1276        );
1277
1278        let new_text = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
1279            text
1280        } else {
1281            panic!("Expected a paragraph");
1282        };
1283
1284        let paragraph = if let MarkdownParagraphChunk::Text(text) = &new_text[0] {
1285            text
1286        } else {
1287            panic!("Expected a text");
1288        };
1289
1290        assert_eq!(
1291            paragraph.highlights,
1292            vec![
1293                (
1294                    5..7,
1295                    MarkdownHighlight::Style(MarkdownHighlightStyle {
1296                        weight: FontWeight::BOLD,
1297                        ..Default::default()
1298                    }),
1299                ),
1300                (
1301                    7..20,
1302                    MarkdownHighlight::Style(MarkdownHighlightStyle {
1303                        weight: FontWeight::BOLD,
1304                        strikethrough: true,
1305                        ..Default::default()
1306                    }),
1307                ),
1308                (
1309                    20..22,
1310                    MarkdownHighlight::Style(MarkdownHighlightStyle {
1311                        weight: FontWeight::BOLD,
1312                        ..Default::default()
1313                    }),
1314                ),
1315            ]
1316        );
1317    }
1318
1319    #[gpui::test]
1320    async fn test_text_with_inline_html() {
1321        let parsed = parse("This is a paragraph with an inline HTML <sometag>tag</sometag>.").await;
1322
1323        assert_eq!(
1324            parsed.children,
1325            vec![p("This is a paragraph with an inline HTML tag.", 0..63),],
1326        );
1327    }
1328
1329    #[gpui::test]
1330    async fn test_raw_links_detection() {
1331        let parsed = parse("Checkout this https://zed.dev link").await;
1332
1333        assert_eq!(
1334            parsed.children,
1335            vec![p("Checkout this https://zed.dev link", 0..34)]
1336        );
1337    }
1338
1339    #[gpui::test]
1340    async fn test_empty_image() {
1341        let parsed = parse("![]()").await;
1342
1343        let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
1344            text
1345        } else {
1346            panic!("Expected a paragraph");
1347        };
1348        assert_eq!(paragraph.len(), 0);
1349    }
1350
1351    #[gpui::test]
1352    async fn test_image_links_detection() {
1353        let parsed = parse("![test](https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png)").await;
1354
1355        let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
1356            text
1357        } else {
1358            panic!("Expected a paragraph");
1359        };
1360        assert_eq!(
1361                paragraph[0],
1362                MarkdownParagraphChunk::Image(Image {
1363                    source_range: 0..111,
1364                    link: Link::Web {
1365                        url: "https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png".to_string(),
1366                    },
1367                    alt_text: Some("test".into()),
1368                    height: None,
1369                    width: None,
1370                },)
1371            );
1372    }
1373
1374    #[gpui::test]
1375    async fn test_image_alt_text() {
1376        let parsed = parse("[![Zed](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/zed-industries/zed/main/assets/badge/v0.json)](https://zed.dev)\n ").await;
1377
1378        let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
1379            text
1380        } else {
1381            panic!("Expected a paragraph");
1382        };
1383        assert_eq!(
1384                    paragraph[0],
1385                    MarkdownParagraphChunk::Image(Image {
1386                        source_range: 0..142,
1387                        link: Link::Web {
1388                            url: "https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/zed-industries/zed/main/assets/badge/v0.json".to_string(),
1389                        },
1390                        alt_text: Some("Zed".into()),
1391                        height: None,
1392                        width: None,
1393                    },)
1394                );
1395    }
1396
1397    #[gpui::test]
1398    async fn test_image_without_alt_text() {
1399        let parsed = parse("![](http://example.com/foo.png)").await;
1400
1401        let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
1402            text
1403        } else {
1404            panic!("Expected a paragraph");
1405        };
1406        assert_eq!(
1407            paragraph[0],
1408            MarkdownParagraphChunk::Image(Image {
1409                source_range: 0..31,
1410                link: Link::Web {
1411                    url: "http://example.com/foo.png".to_string(),
1412                },
1413                alt_text: None,
1414                height: None,
1415                width: None,
1416            },)
1417        );
1418    }
1419
1420    #[gpui::test]
1421    async fn test_image_with_alt_text_containing_formatting() {
1422        let parsed = parse("![foo *bar* baz](http://example.com/foo.png)").await;
1423
1424        let ParsedMarkdownElement::Paragraph(chunks) = &parsed.children[0] else {
1425            panic!("Expected a paragraph");
1426        };
1427        assert_eq!(
1428            chunks,
1429            &[MarkdownParagraphChunk::Image(Image {
1430                source_range: 0..44,
1431                link: Link::Web {
1432                    url: "http://example.com/foo.png".to_string(),
1433                },
1434                alt_text: Some("foo bar baz".into()),
1435                height: None,
1436                width: None,
1437            }),],
1438        );
1439    }
1440
1441    #[gpui::test]
1442    async fn test_images_with_text_in_between() {
1443        let parsed = parse(
1444            "![foo](http://example.com/foo.png)\nLorem Ipsum\n![bar](http://example.com/bar.png)",
1445        )
1446        .await;
1447
1448        let chunks = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
1449            text
1450        } else {
1451            panic!("Expected a paragraph");
1452        };
1453        assert_eq!(
1454            chunks,
1455            &vec![
1456                MarkdownParagraphChunk::Image(Image {
1457                    source_range: 0..81,
1458                    link: Link::Web {
1459                        url: "http://example.com/foo.png".to_string(),
1460                    },
1461                    alt_text: Some("foo".into()),
1462                    height: None,
1463                    width: None,
1464                }),
1465                MarkdownParagraphChunk::Text(ParsedMarkdownText {
1466                    source_range: 0..81,
1467                    contents: " Lorem Ipsum ".into(),
1468                    highlights: Vec::new(),
1469                    region_ranges: Vec::new(),
1470                    regions: Vec::new(),
1471                }),
1472                MarkdownParagraphChunk::Image(Image {
1473                    source_range: 0..81,
1474                    link: Link::Web {
1475                        url: "http://example.com/bar.png".to_string(),
1476                    },
1477                    alt_text: Some("bar".into()),
1478                    height: None,
1479                    width: None,
1480                })
1481            ]
1482        );
1483    }
1484
1485    #[test]
1486    fn test_parse_html_element_dimension() {
1487        // Test percentage values
1488        assert_eq!(
1489            MarkdownParser::parse_html_element_dimension("50%"),
1490            Some(DefiniteLength::Fraction(0.5))
1491        );
1492        assert_eq!(
1493            MarkdownParser::parse_html_element_dimension("100%"),
1494            Some(DefiniteLength::Fraction(1.0))
1495        );
1496        assert_eq!(
1497            MarkdownParser::parse_html_element_dimension("25%"),
1498            Some(DefiniteLength::Fraction(0.25))
1499        );
1500        assert_eq!(
1501            MarkdownParser::parse_html_element_dimension("0%"),
1502            Some(DefiniteLength::Fraction(0.0))
1503        );
1504
1505        // Test pixel values
1506        assert_eq!(
1507            MarkdownParser::parse_html_element_dimension("100px"),
1508            Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.0))))
1509        );
1510        assert_eq!(
1511            MarkdownParser::parse_html_element_dimension("50px"),
1512            Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(50.0))))
1513        );
1514        assert_eq!(
1515            MarkdownParser::parse_html_element_dimension("0px"),
1516            Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(0.0))))
1517        );
1518
1519        // Test values without units (should be treated as pixels)
1520        assert_eq!(
1521            MarkdownParser::parse_html_element_dimension("100"),
1522            Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.0))))
1523        );
1524        assert_eq!(
1525            MarkdownParser::parse_html_element_dimension("42"),
1526            Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(42.0))))
1527        );
1528
1529        // Test invalid values
1530        assert_eq!(
1531            MarkdownParser::parse_html_element_dimension("invalid"),
1532            None
1533        );
1534        assert_eq!(MarkdownParser::parse_html_element_dimension("px"), None);
1535        assert_eq!(MarkdownParser::parse_html_element_dimension("%"), None);
1536        assert_eq!(MarkdownParser::parse_html_element_dimension(""), None);
1537        assert_eq!(MarkdownParser::parse_html_element_dimension("abc%"), None);
1538        assert_eq!(MarkdownParser::parse_html_element_dimension("abcpx"), None);
1539
1540        // Test decimal values
1541        assert_eq!(
1542            MarkdownParser::parse_html_element_dimension("50.5%"),
1543            Some(DefiniteLength::Fraction(0.505))
1544        );
1545        assert_eq!(
1546            MarkdownParser::parse_html_element_dimension("100.25px"),
1547            Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.25))))
1548        );
1549        assert_eq!(
1550            MarkdownParser::parse_html_element_dimension("42.0"),
1551            Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(42.0))))
1552        );
1553    }
1554
1555    #[gpui::test]
1556    async fn test_inline_html_image_tag() {
1557        let parsed =
1558            parse("<p>Some text<img src=\"http://example.com/foo.png\" /> some more text</p>")
1559                .await;
1560
1561        assert_eq!(
1562            ParsedMarkdown {
1563                children: vec![ParsedMarkdownElement::Paragraph(vec![
1564                    MarkdownParagraphChunk::Text(ParsedMarkdownText {
1565                        source_range: 0..71,
1566                        contents: "Some text".into(),
1567                        highlights: Default::default(),
1568                        region_ranges: Default::default(),
1569                        regions: Default::default()
1570                    }),
1571                    MarkdownParagraphChunk::Image(Image {
1572                        source_range: 0..71,
1573                        link: Link::Web {
1574                            url: "http://example.com/foo.png".to_string(),
1575                        },
1576                        alt_text: None,
1577                        height: None,
1578                        width: None,
1579                    }),
1580                    MarkdownParagraphChunk::Text(ParsedMarkdownText {
1581                        source_range: 0..71,
1582                        contents: " some more text".into(),
1583                        highlights: Default::default(),
1584                        region_ranges: Default::default(),
1585                        regions: Default::default()
1586                    }),
1587                ])]
1588            },
1589            parsed
1590        );
1591    }
1592
1593    #[gpui::test]
1594    async fn test_html_block_quote() {
1595        let parsed = parse(
1596            "<blockquote>
1597              <p>some description</p>
1598            </blockquote>",
1599        )
1600        .await;
1601
1602        assert_eq!(
1603            ParsedMarkdown {
1604                children: vec![block_quote(
1605                    vec![ParsedMarkdownElement::Paragraph(text(
1606                        "some description",
1607                        0..76
1608                    ))],
1609                    0..76,
1610                )]
1611            },
1612            parsed
1613        );
1614    }
1615
1616    #[gpui::test]
1617    async fn test_html_nested_block_quote() {
1618        let parsed = parse(
1619            "<blockquote>
1620              <p>some description</p>
1621              <blockquote>
1622                <p>second description</p>
1623              </blockquote>
1624            </blockquote>",
1625        )
1626        .await;
1627
1628        assert_eq!(
1629            ParsedMarkdown {
1630                children: vec![block_quote(
1631                    vec![
1632                        ParsedMarkdownElement::Paragraph(text("some description", 0..173)),
1633                        block_quote(
1634                            vec![ParsedMarkdownElement::Paragraph(text(
1635                                "second description",
1636                                0..173
1637                            ))],
1638                            0..173,
1639                        )
1640                    ],
1641                    0..173,
1642                )]
1643            },
1644            parsed
1645        );
1646    }
1647
1648    #[gpui::test]
1649    async fn test_html_table() {
1650        let parsed = parse(
1651            "<table>
1652          <thead>
1653            <tr>
1654              <th>Id</th>
1655              <th>Name</th>
1656            </tr>
1657          </thead>
1658          <tbody>
1659            <tr>
1660              <td>1</td>
1661              <td>Chris</td>
1662            </tr>
1663            <tr>
1664              <td>2</td>
1665              <td>Dennis</td>
1666            </tr>
1667          </tbody>
1668        </table>",
1669        )
1670        .await;
1671
1672        assert_eq!(
1673            ParsedMarkdown {
1674                children: vec![ParsedMarkdownElement::Table(table(
1675                    0..366,
1676                    vec![row(vec![
1677                        column(
1678                            1,
1679                            1,
1680                            true,
1681                            text("Id", 0..366),
1682                            ParsedMarkdownTableAlignment::Center
1683                        ),
1684                        column(
1685                            1,
1686                            1,
1687                            true,
1688                            text("Name ", 0..366),
1689                            ParsedMarkdownTableAlignment::Center
1690                        )
1691                    ])],
1692                    vec![
1693                        row(vec![
1694                            column(
1695                                1,
1696                                1,
1697                                false,
1698                                text("1", 0..366),
1699                                ParsedMarkdownTableAlignment::None
1700                            ),
1701                            column(
1702                                1,
1703                                1,
1704                                false,
1705                                text("Chris", 0..366),
1706                                ParsedMarkdownTableAlignment::None
1707                            )
1708                        ]),
1709                        row(vec![
1710                            column(
1711                                1,
1712                                1,
1713                                false,
1714                                text("2", 0..366),
1715                                ParsedMarkdownTableAlignment::None
1716                            ),
1717                            column(
1718                                1,
1719                                1,
1720                                false,
1721                                text("Dennis", 0..366),
1722                                ParsedMarkdownTableAlignment::None
1723                            )
1724                        ]),
1725                    ],
1726                ))],
1727            },
1728            parsed
1729        );
1730    }
1731
1732    #[gpui::test]
1733    async fn test_html_table_without_headings() {
1734        let parsed = parse(
1735            "<table>
1736          <tbody>
1737            <tr>
1738              <td>1</td>
1739              <td>Chris</td>
1740            </tr>
1741            <tr>
1742              <td>2</td>
1743              <td>Dennis</td>
1744            </tr>
1745          </tbody>
1746        </table>",
1747        )
1748        .await;
1749
1750        assert_eq!(
1751            ParsedMarkdown {
1752                children: vec![ParsedMarkdownElement::Table(table(
1753                    0..240,
1754                    vec![],
1755                    vec![
1756                        row(vec![
1757                            column(
1758                                1,
1759                                1,
1760                                false,
1761                                text("1", 0..240),
1762                                ParsedMarkdownTableAlignment::None
1763                            ),
1764                            column(
1765                                1,
1766                                1,
1767                                false,
1768                                text("Chris", 0..240),
1769                                ParsedMarkdownTableAlignment::None
1770                            )
1771                        ]),
1772                        row(vec![
1773                            column(
1774                                1,
1775                                1,
1776                                false,
1777                                text("2", 0..240),
1778                                ParsedMarkdownTableAlignment::None
1779                            ),
1780                            column(
1781                                1,
1782                                1,
1783                                false,
1784                                text("Dennis", 0..240),
1785                                ParsedMarkdownTableAlignment::None
1786                            )
1787                        ]),
1788                    ],
1789                ))],
1790            },
1791            parsed
1792        );
1793    }
1794
1795    #[gpui::test]
1796    async fn test_html_table_without_body() {
1797        let parsed = parse(
1798            "<table>
1799          <thead>
1800            <tr>
1801              <th>Id</th>
1802              <th>Name</th>
1803            </tr>
1804          </thead>
1805        </table>",
1806        )
1807        .await;
1808
1809        assert_eq!(
1810            ParsedMarkdown {
1811                children: vec![ParsedMarkdownElement::Table(table(
1812                    0..150,
1813                    vec![row(vec![
1814                        column(
1815                            1,
1816                            1,
1817                            true,
1818                            text("Id", 0..150),
1819                            ParsedMarkdownTableAlignment::Center
1820                        ),
1821                        column(
1822                            1,
1823                            1,
1824                            true,
1825                            text("Name", 0..150),
1826                            ParsedMarkdownTableAlignment::Center
1827                        )
1828                    ])],
1829                    vec![],
1830                ))],
1831            },
1832            parsed
1833        );
1834    }
1835
1836    #[gpui::test]
1837    async fn test_html_heading_tags() {
1838        let parsed = parse("<h1>Heading</h1><h2>Heading</h2><h3>Heading</h3><h4>Heading</h4><h5>Heading</h5><h6>Heading</h6>").await;
1839
1840        assert_eq!(
1841            ParsedMarkdown {
1842                children: vec![
1843                    ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
1844                        level: HeadingLevel::H1,
1845                        source_range: 0..96,
1846                        contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
1847                            source_range: 0..96,
1848                            contents: "Heading".into(),
1849                            highlights: Vec::default(),
1850                            region_ranges: Vec::default(),
1851                            regions: Vec::default()
1852                        })],
1853                    }),
1854                    ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
1855                        level: HeadingLevel::H2,
1856                        source_range: 0..96,
1857                        contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
1858                            source_range: 0..96,
1859                            contents: "Heading".into(),
1860                            highlights: Vec::default(),
1861                            region_ranges: Vec::default(),
1862                            regions: Vec::default()
1863                        })],
1864                    }),
1865                    ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
1866                        level: HeadingLevel::H3,
1867                        source_range: 0..96,
1868                        contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
1869                            source_range: 0..96,
1870                            contents: "Heading".into(),
1871                            highlights: Vec::default(),
1872                            region_ranges: Vec::default(),
1873                            regions: Vec::default()
1874                        })],
1875                    }),
1876                    ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
1877                        level: HeadingLevel::H4,
1878                        source_range: 0..96,
1879                        contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
1880                            source_range: 0..96,
1881                            contents: "Heading".into(),
1882                            highlights: Vec::default(),
1883                            region_ranges: Vec::default(),
1884                            regions: Vec::default()
1885                        })],
1886                    }),
1887                    ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
1888                        level: HeadingLevel::H5,
1889                        source_range: 0..96,
1890                        contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
1891                            source_range: 0..96,
1892                            contents: "Heading".into(),
1893                            highlights: Vec::default(),
1894                            region_ranges: Vec::default(),
1895                            regions: Vec::default()
1896                        })],
1897                    }),
1898                    ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
1899                        level: HeadingLevel::H6,
1900                        source_range: 0..96,
1901                        contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
1902                            source_range: 0..96,
1903                            contents: "Heading".into(),
1904                            highlights: Vec::default(),
1905                            region_ranges: Vec::default(),
1906                            regions: Vec::default()
1907                        })],
1908                    }),
1909                ],
1910            },
1911            parsed
1912        );
1913    }
1914
1915    #[gpui::test]
1916    async fn test_html_image_tag() {
1917        let parsed = parse("<img src=\"http://example.com/foo.png\" />").await;
1918
1919        assert_eq!(
1920            ParsedMarkdown {
1921                children: vec![ParsedMarkdownElement::Image(Image {
1922                    source_range: 0..40,
1923                    link: Link::Web {
1924                        url: "http://example.com/foo.png".to_string(),
1925                    },
1926                    alt_text: None,
1927                    height: None,
1928                    width: None,
1929                })]
1930            },
1931            parsed
1932        );
1933    }
1934
1935    #[gpui::test]
1936    async fn test_html_image_tag_with_alt_text() {
1937        let parsed = parse("<img src=\"http://example.com/foo.png\" alt=\"Foo\" />").await;
1938
1939        assert_eq!(
1940            ParsedMarkdown {
1941                children: vec![ParsedMarkdownElement::Image(Image {
1942                    source_range: 0..50,
1943                    link: Link::Web {
1944                        url: "http://example.com/foo.png".to_string(),
1945                    },
1946                    alt_text: Some("Foo".into()),
1947                    height: None,
1948                    width: None,
1949                })]
1950            },
1951            parsed
1952        );
1953    }
1954
1955    #[gpui::test]
1956    async fn test_html_image_tag_with_height_and_width() {
1957        let parsed =
1958            parse("<img src=\"http://example.com/foo.png\" height=\"100\" width=\"200\" />").await;
1959
1960        assert_eq!(
1961            ParsedMarkdown {
1962                children: vec![ParsedMarkdownElement::Image(Image {
1963                    source_range: 0..65,
1964                    link: Link::Web {
1965                        url: "http://example.com/foo.png".to_string(),
1966                    },
1967                    alt_text: None,
1968                    height: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.)))),
1969                    width: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(200.)))),
1970                })]
1971            },
1972            parsed
1973        );
1974    }
1975
1976    #[gpui::test]
1977    async fn test_html_image_style_tag_with_height_and_width() {
1978        let parsed = parse(
1979            "<img src=\"http://example.com/foo.png\" style=\"height:100px; width:200px;\" />",
1980        )
1981        .await;
1982
1983        assert_eq!(
1984            ParsedMarkdown {
1985                children: vec![ParsedMarkdownElement::Image(Image {
1986                    source_range: 0..75,
1987                    link: Link::Web {
1988                        url: "http://example.com/foo.png".to_string(),
1989                    },
1990                    alt_text: None,
1991                    height: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.)))),
1992                    width: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(200.)))),
1993                })]
1994            },
1995            parsed
1996        );
1997    }
1998
1999    #[gpui::test]
2000    async fn test_header_only_table() {
2001        let markdown = "\
2002| Header 1 | Header 2 |
2003|----------|----------|
2004
2005Some other content
2006";
2007
2008        let expected_table = table(
2009            0..48,
2010            vec![row(vec![
2011                column(
2012                    1,
2013                    1,
2014                    true,
2015                    text("Header 1", 1..11),
2016                    ParsedMarkdownTableAlignment::None,
2017                ),
2018                column(
2019                    1,
2020                    1,
2021                    true,
2022                    text("Header 2", 12..22),
2023                    ParsedMarkdownTableAlignment::None,
2024                ),
2025            ])],
2026            vec![],
2027        );
2028
2029        assert_eq!(
2030            parse(markdown).await.children[0],
2031            ParsedMarkdownElement::Table(expected_table)
2032        );
2033    }
2034
2035    #[gpui::test]
2036    async fn test_basic_table() {
2037        let markdown = "\
2038| Header 1 | Header 2 |
2039|----------|----------|
2040| Cell 1   | Cell 2   |
2041| Cell 3   | Cell 4   |";
2042
2043        let expected_table = table(
2044            0..95,
2045            vec![row(vec![
2046                column(
2047                    1,
2048                    1,
2049                    true,
2050                    text("Header 1", 1..11),
2051                    ParsedMarkdownTableAlignment::None,
2052                ),
2053                column(
2054                    1,
2055                    1,
2056                    true,
2057                    text("Header 2", 12..22),
2058                    ParsedMarkdownTableAlignment::None,
2059                ),
2060            ])],
2061            vec![
2062                row(vec![
2063                    column(
2064                        1,
2065                        1,
2066                        false,
2067                        text("Cell 1", 49..59),
2068                        ParsedMarkdownTableAlignment::None,
2069                    ),
2070                    column(
2071                        1,
2072                        1,
2073                        false,
2074                        text("Cell 2", 60..70),
2075                        ParsedMarkdownTableAlignment::None,
2076                    ),
2077                ]),
2078                row(vec![
2079                    column(
2080                        1,
2081                        1,
2082                        false,
2083                        text("Cell 3", 73..83),
2084                        ParsedMarkdownTableAlignment::None,
2085                    ),
2086                    column(
2087                        1,
2088                        1,
2089                        false,
2090                        text("Cell 4", 84..94),
2091                        ParsedMarkdownTableAlignment::None,
2092                    ),
2093                ]),
2094            ],
2095        );
2096
2097        assert_eq!(
2098            parse(markdown).await.children[0],
2099            ParsedMarkdownElement::Table(expected_table)
2100        );
2101    }
2102
2103    #[gpui::test]
2104    async fn test_list_basic() {
2105        let parsed = parse(
2106            "\
2107* Item 1
2108* Item 2
2109* Item 3
2110",
2111        )
2112        .await;
2113
2114        assert_eq!(
2115            parsed.children,
2116            vec![
2117                list_item(0..8, 1, Unordered, vec![p("Item 1", 2..8)]),
2118                list_item(9..17, 1, Unordered, vec![p("Item 2", 11..17)]),
2119                list_item(18..26, 1, Unordered, vec![p("Item 3", 20..26)]),
2120            ],
2121        );
2122    }
2123
2124    #[gpui::test]
2125    async fn test_list_with_tasks() {
2126        let parsed = parse(
2127            "\
2128- [ ] TODO
2129- [x] Checked
2130",
2131        )
2132        .await;
2133
2134        assert_eq!(
2135            parsed.children,
2136            vec![
2137                list_item(0..10, 1, Task(false, 2..5), vec![p("TODO", 6..10)]),
2138                list_item(11..24, 1, Task(true, 13..16), vec![p("Checked", 17..24)]),
2139            ],
2140        );
2141    }
2142
2143    #[gpui::test]
2144    async fn test_list_with_indented_task() {
2145        let parsed = parse(
2146            "\
2147- [ ] TODO
2148  - [x] Checked
2149  - Unordered
2150  1. Number 1
2151  1. Number 2
21521. Number A
2153",
2154        )
2155        .await;
2156
2157        assert_eq!(
2158            parsed.children,
2159            vec![
2160                list_item(0..12, 1, Task(false, 2..5), vec![p("TODO", 6..10)]),
2161                list_item(13..26, 2, Task(true, 15..18), vec![p("Checked", 19..26)]),
2162                list_item(29..40, 2, Unordered, vec![p("Unordered", 31..40)]),
2163                list_item(43..54, 2, Ordered(1), vec![p("Number 1", 46..54)]),
2164                list_item(57..68, 2, Ordered(2), vec![p("Number 2", 60..68)]),
2165                list_item(69..80, 1, Ordered(1), vec![p("Number A", 72..80)]),
2166            ],
2167        );
2168    }
2169
2170    #[gpui::test]
2171    async fn test_list_with_linebreak_is_handled_correctly() {
2172        let parsed = parse(
2173            "\
2174- [ ] Task 1
2175
2176- [x] Task 2
2177",
2178        )
2179        .await;
2180
2181        assert_eq!(
2182            parsed.children,
2183            vec![
2184                list_item(0..13, 1, Task(false, 2..5), vec![p("Task 1", 6..12)]),
2185                list_item(14..26, 1, Task(true, 16..19), vec![p("Task 2", 20..26)]),
2186            ],
2187        );
2188    }
2189
2190    #[gpui::test]
2191    async fn test_list_nested() {
2192        let parsed = parse(
2193            "\
2194* Item 1
2195* Item 2
2196* Item 3
2197
21981. Hello
21991. Two
2200   1. Three
22012. Four
22023. Five
2203
2204* First
2205  1. Hello
2206     1. Goodbyte
2207        - Inner
2208        - Inner
2209  2. Goodbyte
2210        - Next item empty
2211        -
2212* Last
2213",
2214        )
2215        .await;
2216
2217        assert_eq!(
2218            parsed.children,
2219            vec![
2220                list_item(0..8, 1, Unordered, vec![p("Item 1", 2..8)]),
2221                list_item(9..17, 1, Unordered, vec![p("Item 2", 11..17)]),
2222                list_item(18..27, 1, Unordered, vec![p("Item 3", 20..26)]),
2223                list_item(28..36, 1, Ordered(1), vec![p("Hello", 31..36)]),
2224                list_item(37..46, 1, Ordered(2), vec![p("Two", 40..43),]),
2225                list_item(47..55, 2, Ordered(1), vec![p("Three", 50..55)]),
2226                list_item(56..63, 1, Ordered(3), vec![p("Four", 59..63)]),
2227                list_item(64..72, 1, Ordered(4), vec![p("Five", 67..71)]),
2228                list_item(73..82, 1, Unordered, vec![p("First", 75..80)]),
2229                list_item(83..96, 2, Ordered(1), vec![p("Hello", 86..91)]),
2230                list_item(97..116, 3, Ordered(1), vec![p("Goodbyte", 100..108)]),
2231                list_item(117..124, 4, Unordered, vec![p("Inner", 119..124)]),
2232                list_item(133..140, 4, Unordered, vec![p("Inner", 135..140)]),
2233                list_item(143..159, 2, Ordered(2), vec![p("Goodbyte", 146..154)]),
2234                list_item(160..180, 3, Unordered, vec![p("Next item empty", 165..180)]),
2235                list_item(186..190, 3, Unordered, vec![]),
2236                list_item(191..197, 1, Unordered, vec![p("Last", 193..197)]),
2237            ]
2238        );
2239    }
2240
2241    #[gpui::test]
2242    async fn test_list_with_nested_content() {
2243        let parsed = parse(
2244            "\
2245*   This is a list item with two paragraphs.
2246
2247    This is the second paragraph in the list item.
2248",
2249        )
2250        .await;
2251
2252        assert_eq!(
2253            parsed.children,
2254            vec![list_item(
2255                0..96,
2256                1,
2257                Unordered,
2258                vec![
2259                    p("This is a list item with two paragraphs.", 4..44),
2260                    p("This is the second paragraph in the list item.", 50..97)
2261                ],
2262            ),],
2263        );
2264    }
2265
2266    #[gpui::test]
2267    async fn test_list_item_with_inline_html() {
2268        let parsed = parse(
2269            "\
2270*   This is a list item with an inline HTML <sometag>tag</sometag>.
2271",
2272        )
2273        .await;
2274
2275        assert_eq!(
2276            parsed.children,
2277            vec![list_item(
2278                0..67,
2279                1,
2280                Unordered,
2281                vec![p("This is a list item with an inline HTML tag.", 4..44),],
2282            ),],
2283        );
2284    }
2285
2286    #[gpui::test]
2287    async fn test_nested_list_with_paragraph_inside() {
2288        let parsed = parse(
2289            "\
22901. a
2291    1. b
2292        1. c
2293
2294    text
2295
2296    1. d
2297",
2298        )
2299        .await;
2300
2301        assert_eq!(
2302            parsed.children,
2303            vec![
2304                list_item(0..7, 1, Ordered(1), vec![p("a", 3..4)],),
2305                list_item(8..20, 2, Ordered(1), vec![p("b", 12..13),],),
2306                list_item(21..27, 3, Ordered(1), vec![p("c", 25..26),],),
2307                p("text", 32..37),
2308                list_item(41..46, 2, Ordered(1), vec![p("d", 45..46),],),
2309            ],
2310        );
2311    }
2312
2313    #[gpui::test]
2314    async fn test_list_with_leading_text() {
2315        let parsed = parse(
2316            "\
2317* `code`
2318* **bold**
2319* [link](https://example.com)
2320",
2321        )
2322        .await;
2323
2324        assert_eq!(
2325            parsed.children,
2326            vec![
2327                list_item(0..8, 1, Unordered, vec![p("code", 2..8)]),
2328                list_item(9..19, 1, Unordered, vec![p("bold", 11..19)]),
2329                list_item(20..49, 1, Unordered, vec![p("link", 22..49)],),
2330            ],
2331        );
2332    }
2333
2334    #[gpui::test]
2335    async fn test_simple_block_quote() {
2336        let parsed = parse("> Simple block quote with **styled text**").await;
2337
2338        assert_eq!(
2339            parsed.children,
2340            vec![block_quote(
2341                vec![p("Simple block quote with styled text", 2..41)],
2342                0..41
2343            )]
2344        );
2345    }
2346
2347    #[gpui::test]
2348    async fn test_simple_block_quote_with_multiple_lines() {
2349        let parsed = parse(
2350            "\
2351> # Heading
2352> More
2353> text
2354>
2355> More text
2356",
2357        )
2358        .await;
2359
2360        assert_eq!(
2361            parsed.children,
2362            vec![block_quote(
2363                vec![
2364                    h1(text("Heading", 4..11), 2..12),
2365                    p("More text", 14..26),
2366                    p("More text", 30..40)
2367                ],
2368                0..40
2369            )]
2370        );
2371    }
2372
2373    #[gpui::test]
2374    async fn test_nested_block_quote() {
2375        let parsed = parse(
2376            "\
2377> A
2378>
2379> > # B
2380>
2381> C
2382
2383More text
2384",
2385        )
2386        .await;
2387
2388        assert_eq!(
2389            parsed.children,
2390            vec![
2391                block_quote(
2392                    vec![
2393                        p("A", 2..4),
2394                        block_quote(vec![h1(text("B", 12..13), 10..14)], 8..14),
2395                        p("C", 18..20)
2396                    ],
2397                    0..20
2398                ),
2399                p("More text", 21..31)
2400            ]
2401        );
2402    }
2403
2404    #[gpui::test]
2405    async fn test_code_block() {
2406        let parsed = parse(
2407            "\
2408```
2409fn main() {
2410    return 0;
2411}
2412```
2413",
2414        )
2415        .await;
2416
2417        assert_eq!(
2418            parsed.children,
2419            vec![code_block(
2420                None,
2421                "fn main() {\n    return 0;\n}",
2422                0..35,
2423                None
2424            )]
2425        );
2426    }
2427
2428    #[gpui::test]
2429    async fn test_code_block_with_language(executor: BackgroundExecutor) {
2430        let language_registry = Arc::new(LanguageRegistry::test(executor.clone()));
2431        language_registry.add(rust_lang());
2432
2433        let parsed = parse_markdown(
2434            "\
2435```rust
2436fn main() {
2437    return 0;
2438}
2439```
2440",
2441            None,
2442            Some(language_registry),
2443        )
2444        .await;
2445
2446        assert_eq!(
2447            parsed.children,
2448            vec![code_block(
2449                Some("rust".to_string()),
2450                "fn main() {\n    return 0;\n}",
2451                0..39,
2452                Some(vec![])
2453            )]
2454        );
2455    }
2456
2457    fn rust_lang() -> Arc<Language> {
2458        Arc::new(Language::new(
2459            LanguageConfig {
2460                name: "Rust".into(),
2461                matcher: LanguageMatcher {
2462                    path_suffixes: vec!["rs".into()],
2463                    ..Default::default()
2464                },
2465                collapsed_placeholder: " /* ... */ ".to_string(),
2466                ..Default::default()
2467            },
2468            Some(tree_sitter_rust::LANGUAGE.into()),
2469        ))
2470    }
2471
2472    fn h1(contents: MarkdownParagraph, source_range: Range<usize>) -> ParsedMarkdownElement {
2473        ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
2474            source_range,
2475            level: HeadingLevel::H1,
2476            contents,
2477        })
2478    }
2479
2480    fn h2(contents: MarkdownParagraph, source_range: Range<usize>) -> ParsedMarkdownElement {
2481        ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
2482            source_range,
2483            level: HeadingLevel::H2,
2484            contents,
2485        })
2486    }
2487
2488    fn h3(contents: MarkdownParagraph, source_range: Range<usize>) -> ParsedMarkdownElement {
2489        ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
2490            source_range,
2491            level: HeadingLevel::H3,
2492            contents,
2493        })
2494    }
2495
2496    fn p(contents: &str, source_range: Range<usize>) -> ParsedMarkdownElement {
2497        ParsedMarkdownElement::Paragraph(text(contents, source_range))
2498    }
2499
2500    fn text(contents: &str, source_range: Range<usize>) -> MarkdownParagraph {
2501        vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
2502            highlights: Vec::new(),
2503            region_ranges: Vec::new(),
2504            regions: Vec::new(),
2505            source_range,
2506            contents: contents.to_string().into(),
2507        })]
2508    }
2509
2510    fn block_quote(
2511        children: Vec<ParsedMarkdownElement>,
2512        source_range: Range<usize>,
2513    ) -> ParsedMarkdownElement {
2514        ParsedMarkdownElement::BlockQuote(ParsedMarkdownBlockQuote {
2515            source_range,
2516            children,
2517        })
2518    }
2519
2520    fn code_block(
2521        language: Option<String>,
2522        code: &str,
2523        source_range: Range<usize>,
2524        highlights: Option<Vec<(Range<usize>, HighlightId)>>,
2525    ) -> ParsedMarkdownElement {
2526        ParsedMarkdownElement::CodeBlock(ParsedMarkdownCodeBlock {
2527            source_range,
2528            language,
2529            contents: code.to_string().into(),
2530            highlights,
2531        })
2532    }
2533
2534    fn list_item(
2535        source_range: Range<usize>,
2536        depth: u16,
2537        item_type: ParsedMarkdownListItemType,
2538        content: Vec<ParsedMarkdownElement>,
2539    ) -> ParsedMarkdownElement {
2540        ParsedMarkdownElement::ListItem(ParsedMarkdownListItem {
2541            source_range,
2542            item_type,
2543            depth,
2544            content,
2545        })
2546    }
2547
2548    fn table(
2549        source_range: Range<usize>,
2550        header: Vec<ParsedMarkdownTableRow>,
2551        body: Vec<ParsedMarkdownTableRow>,
2552    ) -> ParsedMarkdownTable {
2553        ParsedMarkdownTable {
2554            source_range,
2555            header,
2556            body,
2557        }
2558    }
2559
2560    fn row(columns: Vec<ParsedMarkdownTableColumn>) -> ParsedMarkdownTableRow {
2561        ParsedMarkdownTableRow { columns }
2562    }
2563
2564    fn column(
2565        col_span: usize,
2566        row_span: usize,
2567        is_header: bool,
2568        children: MarkdownParagraph,
2569        alignment: ParsedMarkdownTableAlignment,
2570    ) -> ParsedMarkdownTableColumn {
2571        ParsedMarkdownTableColumn {
2572            col_span,
2573            row_span,
2574            is_header,
2575            children,
2576            alignment,
2577        }
2578    }
2579
2580    impl PartialEq for ParsedMarkdownTable {
2581        fn eq(&self, other: &Self) -> bool {
2582            self.source_range == other.source_range
2583                && self.header == other.header
2584                && self.body == other.body
2585        }
2586    }
2587
2588    impl PartialEq for ParsedMarkdownText {
2589        fn eq(&self, other: &Self) -> bool {
2590            self.source_range == other.source_range && self.contents == other.contents
2591        }
2592    }
2593}