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