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 markdown::parser::PARSE_OPTIONS;
  11use markup5ever_rcdom::RcDom;
  12use pulldown_cmark::{Alignment, Event, Parser, Tag, TagEnd};
  13use std::{
  14    cell::RefCell, collections::HashMap, mem, ops::Range, path::PathBuf, rc::Rc, sync::Arc, vec,
  15};
  16use ui::SharedString;
  17
  18pub async fn parse_markdown(
  19    markdown_input: &str,
  20    file_location_directory: Option<PathBuf>,
  21    language_registry: Option<Arc<LanguageRegistry>>,
  22) -> ParsedMarkdown {
  23    let parser = Parser::new_ext(markdown_input, PARSE_OPTIONS);
  24    let parser = MarkdownParser::new(
  25        parser.into_offset_iter().collect(),
  26        file_location_directory,
  27        language_registry,
  28    );
  29    let renderer = parser.parse_document().await;
  30    ParsedMarkdown {
  31        children: renderer.parsed,
  32    }
  33}
  34
  35fn cleanup_html(source: &str) -> Vec<u8> {
  36    let mut writer = std::io::Cursor::new(Vec::new());
  37    let mut reader = std::io::Cursor::new(source);
  38    let mut minify = Minifier::new(
  39        &mut writer,
  40        MinifierOptions {
  41            omit_doctype: true,
  42            collapse_whitespace: true,
  43            ..Default::default()
  44        },
  45    );
  46    if let Ok(()) = minify.minify(&mut reader) {
  47        writer.into_inner()
  48    } else {
  49        source.bytes().collect()
  50    }
  51}
  52
  53struct MarkdownParser<'a> {
  54    tokens: Vec<(Event<'a>, Range<usize>)>,
  55    /// The current index in the tokens array
  56    cursor: usize,
  57    /// The blocks that we have successfully parsed so far
  58    parsed: Vec<ParsedMarkdownElement>,
  59    file_location_directory: Option<PathBuf>,
  60    language_registry: Option<Arc<LanguageRegistry>>,
  61}
  62
  63#[derive(Debug)]
  64struct ParseHtmlNodeContext {
  65    list_item_depth: u16,
  66}
  67
  68impl Default for ParseHtmlNodeContext {
  69    fn default() -> Self {
  70        Self { list_item_depth: 1 }
  71    }
  72}
  73
  74struct MarkdownListItem {
  75    content: Vec<ParsedMarkdownElement>,
  76    item_type: ParsedMarkdownListItemType,
  77}
  78
  79impl Default for MarkdownListItem {
  80    fn default() -> Self {
  81        Self {
  82            content: Vec::new(),
  83            item_type: ParsedMarkdownListItemType::Unordered,
  84        }
  85    }
  86}
  87
  88impl<'a> MarkdownParser<'a> {
  89    fn new(
  90        tokens: Vec<(Event<'a>, Range<usize>)>,
  91        file_location_directory: Option<PathBuf>,
  92        language_registry: Option<Arc<LanguageRegistry>>,
  93    ) -> Self {
  94        Self {
  95            tokens,
  96            file_location_directory,
  97            language_registry,
  98            cursor: 0,
  99            parsed: vec![],
 100        }
 101    }
 102
 103    fn eof(&self) -> bool {
 104        if self.tokens.is_empty() {
 105            return true;
 106        }
 107        self.cursor >= self.tokens.len() - 1
 108    }
 109
 110    fn peek(&self, steps: usize) -> Option<&(Event<'_>, Range<usize>)> {
 111        if self.eof() || (steps + self.cursor) >= self.tokens.len() {
 112            return self.tokens.last();
 113        }
 114        self.tokens.get(self.cursor + steps)
 115    }
 116
 117    fn previous(&self) -> Option<&(Event<'_>, Range<usize>)> {
 118        if self.cursor == 0 || self.cursor > self.tokens.len() {
 119            return None;
 120        }
 121        self.tokens.get(self.cursor - 1)
 122    }
 123
 124    fn current(&self) -> Option<&(Event<'_>, Range<usize>)> {
 125        self.peek(0)
 126    }
 127
 128    fn current_event(&self) -> Option<&Event<'_>> {
 129        self.current().map(|(event, _)| event)
 130    }
 131
 132    fn is_text_like(event: &Event) -> bool {
 133        match event {
 134            Event::Text(_)
 135            // Represent an inline code block
 136            | Event::Code(_)
 137            | Event::Html(_)
 138            | Event::InlineHtml(_)
 139            | Event::FootnoteReference(_)
 140            | Event::Start(Tag::Link { .. })
 141            | Event::Start(Tag::Emphasis)
 142            | Event::Start(Tag::Strong)
 143            | Event::Start(Tag::Strikethrough)
 144            | Event::Start(Tag::Image { .. }) => {
 145                true
 146            }
 147            _ => false,
 148        }
 149    }
 150
 151    async fn parse_document(mut self) -> Self {
 152        while !self.eof() {
 153            if let Some(block) = self.parse_block().await {
 154                self.parsed.extend(block);
 155            } else {
 156                self.cursor += 1;
 157            }
 158        }
 159        self
 160    }
 161
 162    #[async_recursion]
 163    async fn parse_block(&mut self) -> Option<Vec<ParsedMarkdownElement>> {
 164        let (current, source_range) = self.current().unwrap();
 165        let source_range = source_range.clone();
 166        match current {
 167            Event::Start(tag) => match tag {
 168                Tag::Paragraph => {
 169                    self.cursor += 1;
 170                    let text = self.parse_text(false, Some(source_range));
 171                    Some(vec![ParsedMarkdownElement::Paragraph(text)])
 172                }
 173                Tag::Heading { level, .. } => {
 174                    let level = *level;
 175                    self.cursor += 1;
 176                    let heading = self.parse_heading(level);
 177                    Some(vec![ParsedMarkdownElement::Heading(heading)])
 178                }
 179                Tag::Table(alignment) => {
 180                    let alignment = alignment.clone();
 181                    self.cursor += 1;
 182                    let table = self.parse_table(alignment);
 183                    Some(vec![ParsedMarkdownElement::Table(table)])
 184                }
 185                Tag::List(order) => {
 186                    let order = *order;
 187                    self.cursor += 1;
 188                    let list = self.parse_list(order).await;
 189                    Some(list)
 190                }
 191                Tag::BlockQuote(_kind) => {
 192                    self.cursor += 1;
 193                    let block_quote = self.parse_block_quote().await;
 194                    Some(vec![ParsedMarkdownElement::BlockQuote(block_quote)])
 195                }
 196                Tag::CodeBlock(kind) => {
 197                    let (language, scale) = match kind {
 198                        pulldown_cmark::CodeBlockKind::Indented => (None, None),
 199                        pulldown_cmark::CodeBlockKind::Fenced(language) => {
 200                            if language.is_empty() {
 201                                (None, None)
 202                            } else {
 203                                let parts: Vec<&str> = language.split_whitespace().collect();
 204                                let lang = parts.first().map(|s| s.to_string());
 205                                let scale = parts.get(1).and_then(|s| s.parse::<u32>().ok());
 206                                (lang, scale)
 207                            }
 208                        }
 209                    };
 210
 211                    self.cursor += 1;
 212
 213                    if language.as_deref() == Some("mermaid") {
 214                        let mermaid_diagram = self.parse_mermaid_diagram(scale).await?;
 215                        Some(vec![ParsedMarkdownElement::MermaidDiagram(mermaid_diagram)])
 216                    } else {
 217                        let code_block = self.parse_code_block(language).await?;
 218                        Some(vec![ParsedMarkdownElement::CodeBlock(code_block)])
 219                    }
 220                }
 221                Tag::HtmlBlock => {
 222                    self.cursor += 1;
 223
 224                    Some(self.parse_html_block().await)
 225                }
 226                _ => None,
 227            },
 228            Event::Rule => {
 229                self.cursor += 1;
 230                Some(vec![ParsedMarkdownElement::HorizontalRule(source_range)])
 231            }
 232            _ => None,
 233        }
 234    }
 235
 236    fn parse_text(
 237        &mut self,
 238        should_complete_on_soft_break: bool,
 239        source_range: Option<Range<usize>>,
 240    ) -> MarkdownParagraph {
 241        let source_range = source_range.unwrap_or_else(|| {
 242            self.current()
 243                .map(|(_, range)| range.clone())
 244                .unwrap_or_default()
 245        });
 246
 247        let mut markdown_text_like = Vec::new();
 248        let mut text = String::new();
 249        let mut bold_depth = 0;
 250        let mut italic_depth = 0;
 251        let mut strikethrough_depth = 0;
 252        let mut link: Option<Link> = None;
 253        let mut image: Option<Image> = None;
 254        let mut regions: Vec<(Range<usize>, ParsedRegion)> = vec![];
 255        let mut highlights: Vec<(Range<usize>, MarkdownHighlight)> = vec![];
 256        let mut link_urls: Vec<String> = vec![];
 257        let mut link_ranges: Vec<Range<usize>> = vec![];
 258
 259        loop {
 260            if self.eof() {
 261                break;
 262            }
 263
 264            let (current, _) = self.current().unwrap();
 265            let prev_len = text.len();
 266            match current {
 267                Event::SoftBreak => {
 268                    if should_complete_on_soft_break {
 269                        break;
 270                    }
 271                    text.push(' ');
 272                }
 273
 274                Event::HardBreak => {
 275                    text.push('\n');
 276                }
 277
 278                // We want to ignore any inline HTML tags in the text but keep
 279                // the text between them
 280                Event::InlineHtml(_) => {}
 281
 282                Event::Text(t) => {
 283                    text.push_str(t.as_ref());
 284                    let mut style = MarkdownHighlightStyle::default();
 285
 286                    if bold_depth > 0 {
 287                        style.weight = FontWeight::BOLD;
 288                    }
 289
 290                    if italic_depth > 0 {
 291                        style.italic = true;
 292                    }
 293
 294                    if strikethrough_depth > 0 {
 295                        style.strikethrough = true;
 296                    }
 297
 298                    let last_run_len = if let Some(link) = link.clone() {
 299                        regions.push((
 300                            prev_len..text.len(),
 301                            ParsedRegion {
 302                                code: false,
 303                                link: Some(link),
 304                            },
 305                        ));
 306                        style.link = true;
 307                        prev_len
 308                    } else {
 309                        // Manually scan for links
 310                        let mut finder = linkify::LinkFinder::new();
 311                        finder.kinds(&[linkify::LinkKind::Url]);
 312                        let mut last_link_len = prev_len;
 313                        for link in finder.links(t) {
 314                            let start = prev_len + link.start();
 315                            let end = prev_len + link.end();
 316                            let range = start..end;
 317                            link_ranges.push(range.clone());
 318                            link_urls.push(link.as_str().to_string());
 319
 320                            // If there is a style before we match a link, we have to add this to the highlighted ranges
 321                            if style != MarkdownHighlightStyle::default() && last_link_len < start {
 322                                highlights.push((
 323                                    last_link_len..start,
 324                                    MarkdownHighlight::Style(style.clone()),
 325                                ));
 326                            }
 327
 328                            highlights.push((
 329                                range.clone(),
 330                                MarkdownHighlight::Style(MarkdownHighlightStyle {
 331                                    underline: true,
 332                                    ..style
 333                                }),
 334                            ));
 335
 336                            regions.push((
 337                                range.clone(),
 338                                ParsedRegion {
 339                                    code: false,
 340                                    link: Some(Link::Web {
 341                                        url: link.as_str().to_string(),
 342                                    }),
 343                                },
 344                            ));
 345                            last_link_len = end;
 346                        }
 347                        last_link_len
 348                    };
 349
 350                    if style != MarkdownHighlightStyle::default() && last_run_len < text.len() {
 351                        let mut new_highlight = true;
 352                        if let Some((last_range, last_style)) = highlights.last_mut()
 353                            && last_range.end == last_run_len
 354                            && last_style == &MarkdownHighlight::Style(style.clone())
 355                        {
 356                            last_range.end = text.len();
 357                            new_highlight = false;
 358                        }
 359                        if new_highlight {
 360                            highlights.push((
 361                                last_run_len..text.len(),
 362                                MarkdownHighlight::Style(style.clone()),
 363                            ));
 364                        }
 365                    }
 366                }
 367                Event::Code(t) => {
 368                    text.push_str(t.as_ref());
 369                    let range = prev_len..text.len();
 370
 371                    if link.is_some() {
 372                        highlights.push((
 373                            range.clone(),
 374                            MarkdownHighlight::Style(MarkdownHighlightStyle {
 375                                link: true,
 376                                ..Default::default()
 377                            }),
 378                        ));
 379                    }
 380                    regions.push((
 381                        range,
 382                        ParsedRegion {
 383                            code: true,
 384                            link: link.clone(),
 385                        },
 386                    ));
 387                }
 388                Event::Start(tag) => match tag {
 389                    Tag::Emphasis => italic_depth += 1,
 390                    Tag::Strong => bold_depth += 1,
 391                    Tag::Strikethrough => strikethrough_depth += 1,
 392                    Tag::Link { dest_url, .. } => {
 393                        link = Link::identify(
 394                            self.file_location_directory.clone(),
 395                            dest_url.to_string(),
 396                        );
 397                    }
 398                    Tag::Image { dest_url, .. } => {
 399                        if !text.is_empty() {
 400                            let parsed_regions = MarkdownParagraphChunk::Text(ParsedMarkdownText {
 401                                source_range: source_range.clone(),
 402                                contents: mem::take(&mut text).into(),
 403                                highlights: mem::take(&mut highlights),
 404                                regions: mem::take(&mut regions),
 405                            });
 406                            markdown_text_like.push(parsed_regions);
 407                        }
 408                        image = Image::identify(
 409                            dest_url.to_string(),
 410                            source_range.clone(),
 411                            self.file_location_directory.clone(),
 412                        );
 413                    }
 414                    _ => {
 415                        break;
 416                    }
 417                },
 418
 419                Event::End(tag) => match tag {
 420                    TagEnd::Emphasis => italic_depth -= 1,
 421                    TagEnd::Strong => bold_depth -= 1,
 422                    TagEnd::Strikethrough => strikethrough_depth -= 1,
 423                    TagEnd::Link => {
 424                        link = None;
 425                    }
 426                    TagEnd::Image => {
 427                        if let Some(mut image) = image.take() {
 428                            if !text.is_empty() {
 429                                image.set_alt_text(std::mem::take(&mut text).into());
 430                                mem::take(&mut highlights);
 431                                mem::take(&mut regions);
 432                            }
 433                            markdown_text_like.push(MarkdownParagraphChunk::Image(image));
 434                        }
 435                    }
 436                    TagEnd::Paragraph => {
 437                        self.cursor += 1;
 438                        break;
 439                    }
 440                    _ => {
 441                        break;
 442                    }
 443                },
 444                _ => {
 445                    break;
 446                }
 447            }
 448
 449            self.cursor += 1;
 450        }
 451        if !text.is_empty() {
 452            markdown_text_like.push(MarkdownParagraphChunk::Text(ParsedMarkdownText {
 453                source_range,
 454                contents: text.into(),
 455                highlights,
 456                regions,
 457            }));
 458        }
 459        markdown_text_like
 460    }
 461
 462    fn parse_heading(&mut self, level: pulldown_cmark::HeadingLevel) -> ParsedMarkdownHeading {
 463        let (_event, source_range) = self.previous().unwrap();
 464        let source_range = source_range.clone();
 465        let text = self.parse_text(true, None);
 466
 467        // Advance past the heading end tag
 468        self.cursor += 1;
 469
 470        ParsedMarkdownHeading {
 471            source_range,
 472            level: match level {
 473                pulldown_cmark::HeadingLevel::H1 => HeadingLevel::H1,
 474                pulldown_cmark::HeadingLevel::H2 => HeadingLevel::H2,
 475                pulldown_cmark::HeadingLevel::H3 => HeadingLevel::H3,
 476                pulldown_cmark::HeadingLevel::H4 => HeadingLevel::H4,
 477                pulldown_cmark::HeadingLevel::H5 => HeadingLevel::H5,
 478                pulldown_cmark::HeadingLevel::H6 => HeadingLevel::H6,
 479            },
 480            contents: text,
 481        }
 482    }
 483
 484    fn parse_table(&mut self, alignment: Vec<Alignment>) -> ParsedMarkdownTable {
 485        let (_event, source_range) = self.previous().unwrap();
 486        let source_range = source_range.clone();
 487        let mut header = vec![];
 488        let mut body = vec![];
 489        let mut row_columns = vec![];
 490        let mut in_header = true;
 491        let column_alignments = alignment
 492            .iter()
 493            .map(Self::convert_alignment)
 494            .collect::<Vec<_>>();
 495
 496        loop {
 497            if self.eof() {
 498                break;
 499            }
 500
 501            let (current, source_range) = self.current().unwrap();
 502            let source_range = source_range.clone();
 503            match current {
 504                Event::Start(Tag::TableHead)
 505                | Event::Start(Tag::TableRow)
 506                | Event::End(TagEnd::TableCell) => {
 507                    self.cursor += 1;
 508                }
 509                Event::Start(Tag::TableCell) => {
 510                    self.cursor += 1;
 511                    let cell_contents = self.parse_text(false, Some(source_range));
 512                    row_columns.push(ParsedMarkdownTableColumn {
 513                        col_span: 1,
 514                        row_span: 1,
 515                        is_header: in_header,
 516                        children: cell_contents,
 517                        alignment: column_alignments
 518                            .get(row_columns.len())
 519                            .copied()
 520                            .unwrap_or_default(),
 521                    });
 522                }
 523                Event::End(TagEnd::TableHead) | Event::End(TagEnd::TableRow) => {
 524                    self.cursor += 1;
 525                    let columns = std::mem::take(&mut row_columns);
 526                    if in_header {
 527                        header.push(ParsedMarkdownTableRow { columns: columns });
 528                        in_header = false;
 529                    } else {
 530                        body.push(ParsedMarkdownTableRow::with_columns(columns));
 531                    }
 532                }
 533                Event::End(TagEnd::Table) => {
 534                    self.cursor += 1;
 535                    break;
 536                }
 537                _ => {
 538                    break;
 539                }
 540            }
 541        }
 542
 543        ParsedMarkdownTable {
 544            source_range,
 545            header,
 546            body,
 547            caption: None,
 548        }
 549    }
 550
 551    fn convert_alignment(alignment: &Alignment) -> ParsedMarkdownTableAlignment {
 552        match alignment {
 553            Alignment::None => ParsedMarkdownTableAlignment::None,
 554            Alignment::Left => ParsedMarkdownTableAlignment::Left,
 555            Alignment::Center => ParsedMarkdownTableAlignment::Center,
 556            Alignment::Right => ParsedMarkdownTableAlignment::Right,
 557        }
 558    }
 559
 560    async fn parse_list(&mut self, order: Option<u64>) -> Vec<ParsedMarkdownElement> {
 561        let (_, list_source_range) = self.previous().unwrap();
 562
 563        let mut items = Vec::new();
 564        let mut items_stack = vec![MarkdownListItem::default()];
 565        let mut depth = 1;
 566        let mut order = order;
 567        let mut order_stack = Vec::new();
 568
 569        let mut insertion_indices = FxHashMap::default();
 570        let mut source_ranges = FxHashMap::default();
 571        let mut start_item_range = list_source_range.clone();
 572
 573        while !self.eof() {
 574            let (current, source_range) = self.current().unwrap();
 575            match current {
 576                Event::Start(Tag::List(new_order)) => {
 577                    if items_stack.last().is_some() && !insertion_indices.contains_key(&depth) {
 578                        insertion_indices.insert(depth, items.len());
 579                    }
 580
 581                    // We will use the start of the nested list as the end for the current item's range,
 582                    // because we don't care about the hierarchy of list items
 583                    if let collections::hash_map::Entry::Vacant(e) = source_ranges.entry(depth) {
 584                        e.insert(start_item_range.start..source_range.start);
 585                    }
 586
 587                    order_stack.push(order);
 588                    order = *new_order;
 589                    self.cursor += 1;
 590                    depth += 1;
 591                }
 592                Event::End(TagEnd::List(_)) => {
 593                    order = order_stack.pop().flatten();
 594                    self.cursor += 1;
 595                    depth -= 1;
 596
 597                    if depth == 0 {
 598                        break;
 599                    }
 600                }
 601                Event::Start(Tag::Item) => {
 602                    start_item_range = source_range.clone();
 603
 604                    self.cursor += 1;
 605                    items_stack.push(MarkdownListItem::default());
 606
 607                    let mut task_list = None;
 608                    // Check for task list marker (`- [ ]` or `- [x]`)
 609                    if let Some(event) = self.current_event() {
 610                        // If there is a linebreak in between two list items the task list marker will actually be the first element of the paragraph
 611                        if event == &Event::Start(Tag::Paragraph) {
 612                            self.cursor += 1;
 613                        }
 614
 615                        if let Some((Event::TaskListMarker(checked), range)) = self.current() {
 616                            task_list = Some((*checked, range.clone()));
 617                            self.cursor += 1;
 618                        }
 619                    }
 620
 621                    if let Some((event, range)) = self.current() {
 622                        // This is a plain list item.
 623                        // For example `- some text` or `1. [Docs](./docs.md)`
 624                        if MarkdownParser::is_text_like(event) {
 625                            let text = self.parse_text(false, Some(range.clone()));
 626                            let block = ParsedMarkdownElement::Paragraph(text);
 627                            if let Some(content) = items_stack.last_mut() {
 628                                let item_type = if let Some((checked, range)) = task_list {
 629                                    ParsedMarkdownListItemType::Task(checked, range)
 630                                } else if let Some(order) = order {
 631                                    ParsedMarkdownListItemType::Ordered(order)
 632                                } else {
 633                                    ParsedMarkdownListItemType::Unordered
 634                                };
 635                                content.item_type = item_type;
 636                                content.content.push(block);
 637                            }
 638                        } else {
 639                            let block = self.parse_block().await;
 640                            if let Some(block) = block
 641                                && let Some(list_item) = items_stack.last_mut()
 642                            {
 643                                list_item.content.extend(block);
 644                            }
 645                        }
 646                    }
 647
 648                    // If there is a linebreak in between two list items the task list marker will actually be the first element of the paragraph
 649                    if self.current_event() == Some(&Event::End(TagEnd::Paragraph)) {
 650                        self.cursor += 1;
 651                    }
 652                }
 653                Event::End(TagEnd::Item) => {
 654                    self.cursor += 1;
 655
 656                    if let Some(current) = order {
 657                        order = Some(current + 1);
 658                    }
 659
 660                    if let Some(list_item) = items_stack.pop() {
 661                        let source_range = source_ranges
 662                            .remove(&depth)
 663                            .unwrap_or(start_item_range.clone());
 664
 665                        // We need to remove the last character of the source range, because it includes the newline character
 666                        let source_range = source_range.start..source_range.end - 1;
 667                        let item = ParsedMarkdownElement::ListItem(ParsedMarkdownListItem {
 668                            source_range,
 669                            content: list_item.content,
 670                            depth,
 671                            item_type: list_item.item_type,
 672                            nested: false,
 673                        });
 674
 675                        if let Some(index) = insertion_indices.get(&depth) {
 676                            items.insert(*index, item);
 677                            insertion_indices.remove(&depth);
 678                        } else {
 679                            items.push(item);
 680                        }
 681                    }
 682                }
 683                _ => {
 684                    if depth == 0 {
 685                        break;
 686                    }
 687                    // This can only happen if a list item starts with more then one paragraph,
 688                    // or the list item contains blocks that should be rendered after the nested list items
 689                    let block = self.parse_block().await;
 690                    if let Some(block) = block {
 691                        if let Some(list_item) = items_stack.last_mut() {
 692                            // 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
 693                            if !insertion_indices.contains_key(&depth) {
 694                                list_item.content.extend(block);
 695                                continue;
 696                            }
 697                        }
 698
 699                        // Otherwise we need to insert the block after all the nested items
 700                        // that have been parsed so far
 701                        items.extend(block);
 702                    } else {
 703                        self.cursor += 1;
 704                    }
 705                }
 706            }
 707        }
 708
 709        items
 710    }
 711
 712    #[async_recursion]
 713    async fn parse_block_quote(&mut self) -> ParsedMarkdownBlockQuote {
 714        let (_event, source_range) = self.previous().unwrap();
 715        let source_range = source_range.clone();
 716        let mut nested_depth = 1;
 717
 718        let mut children: Vec<ParsedMarkdownElement> = vec![];
 719
 720        while !self.eof() {
 721            let block = self.parse_block().await;
 722
 723            if let Some(block) = block {
 724                children.extend(block);
 725            } else {
 726                break;
 727            }
 728
 729            if self.eof() {
 730                break;
 731            }
 732
 733            let (current, _source_range) = self.current().unwrap();
 734            match current {
 735                // This is a nested block quote.
 736                // Record that we're in a nested block quote and continue parsing.
 737                // We don't need to advance the cursor since the next
 738                // call to `parse_block` will handle it.
 739                Event::Start(Tag::BlockQuote(_kind)) => {
 740                    nested_depth += 1;
 741                }
 742                Event::End(TagEnd::BlockQuote(_kind)) => {
 743                    nested_depth -= 1;
 744                    if nested_depth == 0 {
 745                        self.cursor += 1;
 746                        break;
 747                    }
 748                }
 749                _ => {}
 750            };
 751        }
 752
 753        ParsedMarkdownBlockQuote {
 754            source_range,
 755            children,
 756        }
 757    }
 758
 759    async fn parse_code_block(
 760        &mut self,
 761        language: Option<String>,
 762    ) -> Option<ParsedMarkdownCodeBlock> {
 763        let Some((_event, source_range)) = self.previous() else {
 764            return None;
 765        };
 766
 767        let source_range = source_range.clone();
 768        let mut code = String::new();
 769
 770        while !self.eof() {
 771            let Some((current, _source_range)) = self.current() else {
 772                break;
 773            };
 774
 775            match current {
 776                Event::Text(text) => {
 777                    code.push_str(text);
 778                    self.cursor += 1;
 779                }
 780                Event::End(TagEnd::CodeBlock) => {
 781                    self.cursor += 1;
 782                    break;
 783                }
 784                _ => {
 785                    break;
 786                }
 787            }
 788        }
 789
 790        code = code.strip_suffix('\n').unwrap_or(&code).to_string();
 791
 792        let highlights = if let Some(language) = &language {
 793            if let Some(registry) = &self.language_registry {
 794                let rope: language::Rope = code.as_str().into();
 795                registry
 796                    .language_for_name_or_extension(language)
 797                    .await
 798                    .map(|l| l.highlight_text(&rope, 0..code.len()))
 799                    .ok()
 800            } else {
 801                None
 802            }
 803        } else {
 804            None
 805        };
 806
 807        Some(ParsedMarkdownCodeBlock {
 808            source_range,
 809            contents: code.into(),
 810            language,
 811            highlights,
 812        })
 813    }
 814
 815    async fn parse_mermaid_diagram(
 816        &mut self,
 817        scale: Option<u32>,
 818    ) -> Option<ParsedMarkdownMermaidDiagram> {
 819        let Some((_event, source_range)) = self.previous() else {
 820            return None;
 821        };
 822
 823        let source_range = source_range.clone();
 824        let mut code = String::new();
 825
 826        while !self.eof() {
 827            let Some((current, _source_range)) = self.current() else {
 828                break;
 829            };
 830
 831            match current {
 832                Event::Text(text) => {
 833                    code.push_str(text);
 834                    self.cursor += 1;
 835                }
 836                Event::End(TagEnd::CodeBlock) => {
 837                    self.cursor += 1;
 838                    break;
 839                }
 840                _ => {
 841                    break;
 842                }
 843            }
 844        }
 845
 846        code = code.strip_suffix('\n').unwrap_or(&code).to_string();
 847
 848        let scale = scale.unwrap_or(100).clamp(10, 500);
 849
 850        Some(ParsedMarkdownMermaidDiagram {
 851            source_range,
 852            contents: ParsedMarkdownMermaidDiagramContents {
 853                contents: code.into(),
 854                scale,
 855            },
 856        })
 857    }
 858
 859    async fn parse_html_block(&mut self) -> Vec<ParsedMarkdownElement> {
 860        let mut elements = Vec::new();
 861        let Some((_event, _source_range)) = self.previous() else {
 862            return elements;
 863        };
 864
 865        let mut html_source_range_start = None;
 866        let mut html_source_range_end = None;
 867        let mut html_buffer = String::new();
 868
 869        while !self.eof() {
 870            let Some((current, source_range)) = self.current() else {
 871                break;
 872            };
 873            let source_range = source_range.clone();
 874            match current {
 875                Event::Html(html) => {
 876                    html_source_range_start.get_or_insert(source_range.start);
 877                    html_source_range_end = Some(source_range.end);
 878                    html_buffer.push_str(html);
 879                    self.cursor += 1;
 880                }
 881                Event::End(TagEnd::CodeBlock) => {
 882                    self.cursor += 1;
 883                    break;
 884                }
 885                _ => {
 886                    break;
 887                }
 888            }
 889        }
 890
 891        let bytes = cleanup_html(&html_buffer);
 892
 893        let mut cursor = std::io::Cursor::new(bytes);
 894        if let Ok(dom) = parse_document(RcDom::default(), ParseOpts::default())
 895            .from_utf8()
 896            .read_from(&mut cursor)
 897            && let Some((start, end)) = html_source_range_start.zip(html_source_range_end)
 898        {
 899            self.parse_html_node(
 900                start..end,
 901                &dom.document,
 902                &mut elements,
 903                &ParseHtmlNodeContext::default(),
 904            );
 905        }
 906
 907        elements
 908    }
 909
 910    fn parse_html_node(
 911        &self,
 912        source_range: Range<usize>,
 913        node: &Rc<markup5ever_rcdom::Node>,
 914        elements: &mut Vec<ParsedMarkdownElement>,
 915        context: &ParseHtmlNodeContext,
 916    ) {
 917        match &node.data {
 918            markup5ever_rcdom::NodeData::Document => {
 919                self.consume_children(source_range, node, elements, context);
 920            }
 921            markup5ever_rcdom::NodeData::Text { contents } => {
 922                elements.push(ParsedMarkdownElement::Paragraph(vec![
 923                    MarkdownParagraphChunk::Text(ParsedMarkdownText {
 924                        source_range,
 925                        regions: Vec::default(),
 926                        highlights: Vec::default(),
 927                        contents: contents.borrow().to_string().into(),
 928                    }),
 929                ]));
 930            }
 931            markup5ever_rcdom::NodeData::Comment { .. } => {}
 932            markup5ever_rcdom::NodeData::Element { name, attrs, .. } => {
 933                let mut styles = if let Some(styles) = Self::markdown_style_from_html_styles(
 934                    Self::extract_styles_from_attributes(attrs),
 935                ) {
 936                    vec![MarkdownHighlight::Style(styles)]
 937                } else {
 938                    Vec::default()
 939                };
 940
 941                if local_name!("img") == name.local {
 942                    if let Some(image) = self.extract_image(source_range, attrs) {
 943                        elements.push(ParsedMarkdownElement::Image(image));
 944                    }
 945                } else if local_name!("p") == name.local {
 946                    let mut paragraph = MarkdownParagraph::new();
 947                    self.parse_paragraph(
 948                        source_range,
 949                        node,
 950                        &mut paragraph,
 951                        &mut styles,
 952                        &mut Vec::new(),
 953                    );
 954
 955                    if !paragraph.is_empty() {
 956                        elements.push(ParsedMarkdownElement::Paragraph(paragraph));
 957                    }
 958                } else if matches!(
 959                    name.local,
 960                    local_name!("h1")
 961                        | local_name!("h2")
 962                        | local_name!("h3")
 963                        | local_name!("h4")
 964                        | local_name!("h5")
 965                        | local_name!("h6")
 966                ) {
 967                    let mut paragraph = MarkdownParagraph::new();
 968                    self.consume_paragraph(
 969                        source_range.clone(),
 970                        node,
 971                        &mut paragraph,
 972                        &mut styles,
 973                        &mut Vec::new(),
 974                    );
 975
 976                    if !paragraph.is_empty() {
 977                        elements.push(ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
 978                            source_range,
 979                            level: match name.local {
 980                                local_name!("h1") => HeadingLevel::H1,
 981                                local_name!("h2") => HeadingLevel::H2,
 982                                local_name!("h3") => HeadingLevel::H3,
 983                                local_name!("h4") => HeadingLevel::H4,
 984                                local_name!("h5") => HeadingLevel::H5,
 985                                local_name!("h6") => HeadingLevel::H6,
 986                                _ => unreachable!(),
 987                            },
 988                            contents: paragraph,
 989                        }));
 990                    }
 991                } else if local_name!("ul") == name.local || local_name!("ol") == name.local {
 992                    if let Some(list_items) = self.extract_html_list(
 993                        node,
 994                        local_name!("ol") == name.local,
 995                        context.list_item_depth,
 996                        source_range,
 997                    ) {
 998                        elements.extend(list_items);
 999                    }
1000                } else if local_name!("blockquote") == name.local {
1001                    if let Some(blockquote) = self.extract_html_blockquote(node, source_range) {
1002                        elements.push(ParsedMarkdownElement::BlockQuote(blockquote));
1003                    }
1004                } else if local_name!("table") == name.local {
1005                    if let Some(table) = self.extract_html_table(node, source_range) {
1006                        elements.push(ParsedMarkdownElement::Table(table));
1007                    }
1008                } else {
1009                    self.consume_children(source_range, node, elements, context);
1010                }
1011            }
1012            _ => {}
1013        }
1014    }
1015
1016    fn parse_paragraph(
1017        &self,
1018        source_range: Range<usize>,
1019        node: &Rc<markup5ever_rcdom::Node>,
1020        paragraph: &mut MarkdownParagraph,
1021        highlights: &mut Vec<MarkdownHighlight>,
1022        regions: &mut Vec<(Range<usize>, ParsedRegion)>,
1023    ) {
1024        fn items_with_range<T>(
1025            range: Range<usize>,
1026            items: impl IntoIterator<Item = T>,
1027        ) -> Vec<(Range<usize>, T)> {
1028            items
1029                .into_iter()
1030                .map(|item| (range.clone(), item))
1031                .collect()
1032        }
1033
1034        match &node.data {
1035            markup5ever_rcdom::NodeData::Text { contents } => {
1036                // append the text to the last chunk, so we can have a hacky version
1037                // of inline text with highlighting
1038                if let Some(text) = paragraph.iter_mut().last().and_then(|p| match p {
1039                    MarkdownParagraphChunk::Text(text) => Some(text),
1040                    _ => None,
1041                }) {
1042                    let mut new_text = text.contents.to_string();
1043                    new_text.push_str(&contents.borrow());
1044
1045                    text.highlights.extend(items_with_range(
1046                        text.contents.len()..new_text.len(),
1047                        std::mem::take(highlights),
1048                    ));
1049                    text.regions.extend(items_with_range(
1050                        text.contents.len()..new_text.len(),
1051                        std::mem::take(regions)
1052                            .into_iter()
1053                            .map(|(_, region)| region),
1054                    ));
1055                    text.contents = SharedString::from(new_text);
1056                } else {
1057                    let contents = contents.borrow().to_string();
1058                    paragraph.push(MarkdownParagraphChunk::Text(ParsedMarkdownText {
1059                        source_range,
1060                        highlights: items_with_range(0..contents.len(), std::mem::take(highlights)),
1061                        regions: items_with_range(
1062                            0..contents.len(),
1063                            std::mem::take(regions)
1064                                .into_iter()
1065                                .map(|(_, region)| region),
1066                        ),
1067                        contents: contents.into(),
1068                    }));
1069                }
1070            }
1071            markup5ever_rcdom::NodeData::Element { name, attrs, .. } => {
1072                if local_name!("img") == name.local {
1073                    if let Some(image) = self.extract_image(source_range, attrs) {
1074                        paragraph.push(MarkdownParagraphChunk::Image(image));
1075                    }
1076                } else if local_name!("b") == name.local || local_name!("strong") == name.local {
1077                    highlights.push(MarkdownHighlight::Style(MarkdownHighlightStyle {
1078                        weight: FontWeight::BOLD,
1079                        ..Default::default()
1080                    }));
1081
1082                    self.consume_paragraph(source_range, node, paragraph, highlights, regions);
1083                } else if local_name!("i") == name.local {
1084                    highlights.push(MarkdownHighlight::Style(MarkdownHighlightStyle {
1085                        italic: true,
1086                        ..Default::default()
1087                    }));
1088
1089                    self.consume_paragraph(source_range, node, paragraph, highlights, regions);
1090                } else if local_name!("em") == name.local {
1091                    highlights.push(MarkdownHighlight::Style(MarkdownHighlightStyle {
1092                        oblique: true,
1093                        ..Default::default()
1094                    }));
1095
1096                    self.consume_paragraph(source_range, node, paragraph, highlights, regions);
1097                } else if local_name!("del") == name.local {
1098                    highlights.push(MarkdownHighlight::Style(MarkdownHighlightStyle {
1099                        strikethrough: true,
1100                        ..Default::default()
1101                    }));
1102
1103                    self.consume_paragraph(source_range, node, paragraph, highlights, regions);
1104                } else if local_name!("ins") == name.local {
1105                    highlights.push(MarkdownHighlight::Style(MarkdownHighlightStyle {
1106                        underline: true,
1107                        ..Default::default()
1108                    }));
1109
1110                    self.consume_paragraph(source_range, node, paragraph, highlights, regions);
1111                } else if local_name!("a") == name.local {
1112                    if let Some(url) = Self::attr_value(attrs, local_name!("href"))
1113                        && let Some(link) =
1114                            Link::identify(self.file_location_directory.clone(), url)
1115                    {
1116                        highlights.push(MarkdownHighlight::Style(MarkdownHighlightStyle {
1117                            link: true,
1118                            ..Default::default()
1119                        }));
1120
1121                        regions.push((
1122                            source_range.clone(),
1123                            ParsedRegion {
1124                                code: false,
1125                                link: Some(link),
1126                            },
1127                        ));
1128                    }
1129
1130                    self.consume_paragraph(source_range, node, paragraph, highlights, regions);
1131                } else {
1132                    self.consume_paragraph(source_range, node, paragraph, highlights, regions);
1133                }
1134            }
1135            _ => {}
1136        }
1137    }
1138
1139    fn consume_paragraph(
1140        &self,
1141        source_range: Range<usize>,
1142        node: &Rc<markup5ever_rcdom::Node>,
1143        paragraph: &mut MarkdownParagraph,
1144        highlights: &mut Vec<MarkdownHighlight>,
1145        regions: &mut Vec<(Range<usize>, ParsedRegion)>,
1146    ) {
1147        for node in node.children.borrow().iter() {
1148            self.parse_paragraph(source_range.clone(), node, paragraph, highlights, regions);
1149        }
1150    }
1151
1152    fn parse_table_row(
1153        &self,
1154        source_range: Range<usize>,
1155        node: &Rc<markup5ever_rcdom::Node>,
1156    ) -> Option<ParsedMarkdownTableRow> {
1157        let mut columns = Vec::new();
1158
1159        match &node.data {
1160            markup5ever_rcdom::NodeData::Element { name, .. } => {
1161                if local_name!("tr") != name.local {
1162                    return None;
1163                }
1164
1165                for node in node.children.borrow().iter() {
1166                    if let Some(column) = self.parse_table_column(source_range.clone(), node) {
1167                        columns.push(column);
1168                    }
1169                }
1170            }
1171            _ => {}
1172        }
1173
1174        if columns.is_empty() {
1175            None
1176        } else {
1177            Some(ParsedMarkdownTableRow { columns })
1178        }
1179    }
1180
1181    fn parse_table_column(
1182        &self,
1183        source_range: Range<usize>,
1184        node: &Rc<markup5ever_rcdom::Node>,
1185    ) -> Option<ParsedMarkdownTableColumn> {
1186        match &node.data {
1187            markup5ever_rcdom::NodeData::Element { name, attrs, .. } => {
1188                if !matches!(name.local, local_name!("th") | local_name!("td")) {
1189                    return None;
1190                }
1191
1192                let mut children = MarkdownParagraph::new();
1193                self.consume_paragraph(
1194                    source_range,
1195                    node,
1196                    &mut children,
1197                    &mut Vec::new(),
1198                    &mut Vec::new(),
1199                );
1200
1201                let is_header = matches!(name.local, local_name!("th"));
1202
1203                Some(ParsedMarkdownTableColumn {
1204                    col_span: std::cmp::max(
1205                        Self::attr_value(attrs, local_name!("colspan"))
1206                            .and_then(|span| span.parse().ok())
1207                            .unwrap_or(1),
1208                        1,
1209                    ),
1210                    row_span: std::cmp::max(
1211                        Self::attr_value(attrs, local_name!("rowspan"))
1212                            .and_then(|span| span.parse().ok())
1213                            .unwrap_or(1),
1214                        1,
1215                    ),
1216                    is_header,
1217                    children,
1218                    alignment: Self::attr_value(attrs, local_name!("align"))
1219                        .and_then(|align| match align.as_str() {
1220                            "left" => Some(ParsedMarkdownTableAlignment::Left),
1221                            "center" => Some(ParsedMarkdownTableAlignment::Center),
1222                            "right" => Some(ParsedMarkdownTableAlignment::Right),
1223                            _ => None,
1224                        })
1225                        .unwrap_or_else(|| {
1226                            if is_header {
1227                                ParsedMarkdownTableAlignment::Center
1228                            } else {
1229                                ParsedMarkdownTableAlignment::default()
1230                            }
1231                        }),
1232                })
1233            }
1234            _ => None,
1235        }
1236    }
1237
1238    fn consume_children(
1239        &self,
1240        source_range: Range<usize>,
1241        node: &Rc<markup5ever_rcdom::Node>,
1242        elements: &mut Vec<ParsedMarkdownElement>,
1243        context: &ParseHtmlNodeContext,
1244    ) {
1245        for node in node.children.borrow().iter() {
1246            self.parse_html_node(source_range.clone(), node, elements, context);
1247        }
1248    }
1249
1250    fn attr_value(
1251        attrs: &RefCell<Vec<html5ever::Attribute>>,
1252        name: html5ever::LocalName,
1253    ) -> Option<String> {
1254        attrs.borrow().iter().find_map(|attr| {
1255            if attr.name.local == name {
1256                Some(attr.value.to_string())
1257            } else {
1258                None
1259            }
1260        })
1261    }
1262
1263    fn markdown_style_from_html_styles(
1264        styles: HashMap<String, String>,
1265    ) -> Option<MarkdownHighlightStyle> {
1266        let mut markdown_style = MarkdownHighlightStyle::default();
1267
1268        if let Some(text_decoration) = styles.get("text-decoration") {
1269            match text_decoration.to_lowercase().as_str() {
1270                "underline" => {
1271                    markdown_style.underline = true;
1272                }
1273                "line-through" => {
1274                    markdown_style.strikethrough = true;
1275                }
1276                _ => {}
1277            }
1278        }
1279
1280        if let Some(font_style) = styles.get("font-style") {
1281            match font_style.to_lowercase().as_str() {
1282                "italic" => {
1283                    markdown_style.italic = true;
1284                }
1285                "oblique" => {
1286                    markdown_style.oblique = true;
1287                }
1288                _ => {}
1289            }
1290        }
1291
1292        if let Some(font_weight) = styles.get("font-weight") {
1293            match font_weight.to_lowercase().as_str() {
1294                "bold" => {
1295                    markdown_style.weight = FontWeight::BOLD;
1296                }
1297                "lighter" => {
1298                    markdown_style.weight = FontWeight::THIN;
1299                }
1300                _ => {
1301                    if let Some(weight) = font_weight.parse::<f32>().ok() {
1302                        markdown_style.weight = FontWeight(weight);
1303                    }
1304                }
1305            }
1306        }
1307
1308        if markdown_style != MarkdownHighlightStyle::default() {
1309            Some(markdown_style)
1310        } else {
1311            None
1312        }
1313    }
1314
1315    fn extract_styles_from_attributes(
1316        attrs: &RefCell<Vec<html5ever::Attribute>>,
1317    ) -> HashMap<String, String> {
1318        let mut styles = HashMap::new();
1319
1320        if let Some(style) = Self::attr_value(attrs, local_name!("style")) {
1321            for decl in style.split(';') {
1322                let mut parts = decl.splitn(2, ':');
1323                if let Some((key, value)) = parts.next().zip(parts.next()) {
1324                    styles.insert(
1325                        key.trim().to_lowercase().to_string(),
1326                        value.trim().to_string(),
1327                    );
1328                }
1329            }
1330        }
1331
1332        styles
1333    }
1334
1335    fn extract_image(
1336        &self,
1337        source_range: Range<usize>,
1338        attrs: &RefCell<Vec<html5ever::Attribute>>,
1339    ) -> Option<Image> {
1340        let src = Self::attr_value(attrs, local_name!("src"))?;
1341
1342        let mut image = Image::identify(src, source_range, self.file_location_directory.clone())?;
1343
1344        if let Some(alt) = Self::attr_value(attrs, local_name!("alt")) {
1345            image.set_alt_text(alt.into());
1346        }
1347
1348        let styles = Self::extract_styles_from_attributes(attrs);
1349
1350        if let Some(width) = Self::attr_value(attrs, local_name!("width"))
1351            .or_else(|| styles.get("width").cloned())
1352            .and_then(|width| Self::parse_html_element_dimension(&width))
1353        {
1354            image.set_width(width);
1355        }
1356
1357        if let Some(height) = Self::attr_value(attrs, local_name!("height"))
1358            .or_else(|| styles.get("height").cloned())
1359            .and_then(|height| Self::parse_html_element_dimension(&height))
1360        {
1361            image.set_height(height);
1362        }
1363
1364        Some(image)
1365    }
1366
1367    fn extract_html_list(
1368        &self,
1369        node: &Rc<markup5ever_rcdom::Node>,
1370        ordered: bool,
1371        depth: u16,
1372        source_range: Range<usize>,
1373    ) -> Option<Vec<ParsedMarkdownElement>> {
1374        let mut list_items = Vec::with_capacity(node.children.borrow().len());
1375
1376        for (index, node) in node.children.borrow().iter().enumerate() {
1377            match &node.data {
1378                markup5ever_rcdom::NodeData::Element { name, .. } => {
1379                    if local_name!("li") != name.local {
1380                        continue;
1381                    }
1382
1383                    let mut content = Vec::new();
1384                    self.consume_children(
1385                        source_range.clone(),
1386                        node,
1387                        &mut content,
1388                        &ParseHtmlNodeContext {
1389                            list_item_depth: depth + 1,
1390                        },
1391                    );
1392
1393                    if !content.is_empty() {
1394                        list_items.push(ParsedMarkdownElement::ListItem(ParsedMarkdownListItem {
1395                            depth,
1396                            source_range: source_range.clone(),
1397                            item_type: if ordered {
1398                                ParsedMarkdownListItemType::Ordered(index as u64 + 1)
1399                            } else {
1400                                ParsedMarkdownListItemType::Unordered
1401                            },
1402                            content,
1403                            nested: true,
1404                        }));
1405                    }
1406                }
1407                _ => {}
1408            }
1409        }
1410
1411        if list_items.is_empty() {
1412            None
1413        } else {
1414            Some(list_items)
1415        }
1416    }
1417
1418    fn parse_html_element_dimension(value: &str) -> Option<DefiniteLength> {
1419        if value.ends_with("%") {
1420            value
1421                .trim_end_matches("%")
1422                .parse::<f32>()
1423                .ok()
1424                .map(|value| relative(value / 100.))
1425        } else {
1426            value
1427                .trim_end_matches("px")
1428                .parse()
1429                .ok()
1430                .map(|value| px(value).into())
1431        }
1432    }
1433
1434    fn extract_html_blockquote(
1435        &self,
1436        node: &Rc<markup5ever_rcdom::Node>,
1437        source_range: Range<usize>,
1438    ) -> Option<ParsedMarkdownBlockQuote> {
1439        let mut children = Vec::new();
1440        self.consume_children(
1441            source_range.clone(),
1442            node,
1443            &mut children,
1444            &ParseHtmlNodeContext::default(),
1445        );
1446
1447        if children.is_empty() {
1448            None
1449        } else {
1450            Some(ParsedMarkdownBlockQuote {
1451                children,
1452                source_range,
1453            })
1454        }
1455    }
1456
1457    fn extract_html_table(
1458        &self,
1459        node: &Rc<markup5ever_rcdom::Node>,
1460        source_range: Range<usize>,
1461    ) -> Option<ParsedMarkdownTable> {
1462        let mut header_rows = Vec::new();
1463        let mut body_rows = Vec::new();
1464        let mut caption = None;
1465
1466        // node should be a thead, tbody or caption element
1467        for node in node.children.borrow().iter() {
1468            match &node.data {
1469                markup5ever_rcdom::NodeData::Element { name, .. } => {
1470                    if local_name!("caption") == name.local {
1471                        let mut paragraph = MarkdownParagraph::new();
1472                        self.parse_paragraph(
1473                            source_range.clone(),
1474                            node,
1475                            &mut paragraph,
1476                            &mut Vec::new(),
1477                            &mut Vec::new(),
1478                        );
1479                        caption = Some(paragraph);
1480                    }
1481                    if local_name!("thead") == name.local {
1482                        // node should be a tr element
1483                        for node in node.children.borrow().iter() {
1484                            if let Some(row) = self.parse_table_row(source_range.clone(), node) {
1485                                header_rows.push(row);
1486                            }
1487                        }
1488                    } else if local_name!("tbody") == name.local {
1489                        // node should be a tr element
1490                        for node in node.children.borrow().iter() {
1491                            if let Some(row) = self.parse_table_row(source_range.clone(), node) {
1492                                body_rows.push(row);
1493                            }
1494                        }
1495                    }
1496                }
1497                _ => {}
1498            }
1499        }
1500
1501        if !header_rows.is_empty() || !body_rows.is_empty() {
1502            Some(ParsedMarkdownTable {
1503                source_range,
1504                body: body_rows,
1505                header: header_rows,
1506                caption,
1507            })
1508        } else {
1509            None
1510        }
1511    }
1512}
1513
1514#[cfg(test)]
1515mod tests {
1516    use super::*;
1517    use ParsedMarkdownListItemType::*;
1518    use core::panic;
1519    use gpui::{AbsoluteLength, BackgroundExecutor, DefiniteLength};
1520    use language::{HighlightId, LanguageRegistry};
1521    use pretty_assertions::assert_eq;
1522
1523    async fn parse(input: &str) -> ParsedMarkdown {
1524        parse_markdown(input, None, None).await
1525    }
1526
1527    #[gpui::test]
1528    async fn test_headings() {
1529        let parsed = parse("# Heading one\n## Heading two\n### Heading three").await;
1530
1531        assert_eq!(
1532            parsed.children,
1533            vec![
1534                h1(text("Heading one", 2..13), 0..14),
1535                h2(text("Heading two", 17..28), 14..29),
1536                h3(text("Heading three", 33..46), 29..46),
1537            ]
1538        );
1539    }
1540
1541    #[gpui::test]
1542    async fn test_newlines_dont_new_paragraphs() {
1543        let parsed = parse("Some text **that is bolded**\n and *italicized*").await;
1544
1545        assert_eq!(
1546            parsed.children,
1547            vec![p("Some text that is bolded and italicized", 0..46)]
1548        );
1549    }
1550
1551    #[gpui::test]
1552    async fn test_heading_with_paragraph() {
1553        let parsed = parse("# Zed\nThe editor").await;
1554
1555        assert_eq!(
1556            parsed.children,
1557            vec![h1(text("Zed", 2..5), 0..6), p("The editor", 6..16),]
1558        );
1559    }
1560
1561    #[gpui::test]
1562    async fn test_double_newlines_do_new_paragraphs() {
1563        let parsed = parse("Some text **that is bolded**\n\n and *italicized*").await;
1564
1565        assert_eq!(
1566            parsed.children,
1567            vec![
1568                p("Some text that is bolded", 0..29),
1569                p("and italicized", 31..47),
1570            ]
1571        );
1572    }
1573
1574    #[gpui::test]
1575    async fn test_bold_italic_text() {
1576        let parsed = parse("Some text **that is bolded** and *italicized*").await;
1577
1578        assert_eq!(
1579            parsed.children,
1580            vec![p("Some text that is bolded and italicized", 0..45)]
1581        );
1582    }
1583
1584    #[gpui::test]
1585    async fn test_nested_bold_strikethrough_text() {
1586        let parsed = parse("Some **bo~~strikethrough~~ld** text").await;
1587
1588        assert_eq!(parsed.children.len(), 1);
1589        assert_eq!(
1590            parsed.children[0],
1591            ParsedMarkdownElement::Paragraph(vec![MarkdownParagraphChunk::Text(
1592                ParsedMarkdownText {
1593                    source_range: 0..35,
1594                    contents: "Some bostrikethroughld text".into(),
1595                    highlights: Vec::new(),
1596                    regions: Vec::new(),
1597                }
1598            )])
1599        );
1600
1601        let new_text = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
1602            text
1603        } else {
1604            panic!("Expected a paragraph");
1605        };
1606
1607        let paragraph = if let MarkdownParagraphChunk::Text(text) = &new_text[0] {
1608            text
1609        } else {
1610            panic!("Expected a text");
1611        };
1612
1613        assert_eq!(
1614            paragraph.highlights,
1615            vec![
1616                (
1617                    5..7,
1618                    MarkdownHighlight::Style(MarkdownHighlightStyle {
1619                        weight: FontWeight::BOLD,
1620                        ..Default::default()
1621                    }),
1622                ),
1623                (
1624                    7..20,
1625                    MarkdownHighlight::Style(MarkdownHighlightStyle {
1626                        weight: FontWeight::BOLD,
1627                        strikethrough: true,
1628                        ..Default::default()
1629                    }),
1630                ),
1631                (
1632                    20..22,
1633                    MarkdownHighlight::Style(MarkdownHighlightStyle {
1634                        weight: FontWeight::BOLD,
1635                        ..Default::default()
1636                    }),
1637                ),
1638            ]
1639        );
1640    }
1641
1642    #[gpui::test]
1643    async fn test_html_inline_style_elements() {
1644        let parsed =
1645                parse("<p>Some text <strong>strong text</strong> more text <b>bold text</b> more text <i>italic text</i> more text <em>emphasized text</em> more text <del>deleted text</del> more text <ins>inserted text</ins></p>").await;
1646
1647        assert_eq!(1, parsed.children.len());
1648        let chunks = if let ParsedMarkdownElement::Paragraph(chunks) = &parsed.children[0] {
1649            chunks
1650        } else {
1651            panic!("Expected a paragraph");
1652        };
1653
1654        assert_eq!(1, chunks.len());
1655        let text = if let MarkdownParagraphChunk::Text(text) = &chunks[0] {
1656            text
1657        } else {
1658            panic!("Expected a paragraph");
1659        };
1660
1661        assert_eq!(0..205, text.source_range);
1662        assert_eq!(
1663            "Some text strong text more text bold text more text italic text more text emphasized text more text deleted text more text inserted text",
1664            text.contents.as_str(),
1665        );
1666        assert_eq!(
1667            vec![
1668                (
1669                    10..21,
1670                    MarkdownHighlight::Style(MarkdownHighlightStyle {
1671                        weight: FontWeight(700.0),
1672                        ..Default::default()
1673                    },),
1674                ),
1675                (
1676                    32..41,
1677                    MarkdownHighlight::Style(MarkdownHighlightStyle {
1678                        weight: FontWeight(700.0),
1679                        ..Default::default()
1680                    },),
1681                ),
1682                (
1683                    52..63,
1684                    MarkdownHighlight::Style(MarkdownHighlightStyle {
1685                        italic: true,
1686                        weight: FontWeight(400.0),
1687                        ..Default::default()
1688                    },),
1689                ),
1690                (
1691                    74..89,
1692                    MarkdownHighlight::Style(MarkdownHighlightStyle {
1693                        weight: FontWeight(400.0),
1694                        oblique: true,
1695                        ..Default::default()
1696                    },),
1697                ),
1698                (
1699                    100..112,
1700                    MarkdownHighlight::Style(MarkdownHighlightStyle {
1701                        strikethrough: true,
1702                        weight: FontWeight(400.0),
1703                        ..Default::default()
1704                    },),
1705                ),
1706                (
1707                    123..136,
1708                    MarkdownHighlight::Style(MarkdownHighlightStyle {
1709                        underline: true,
1710                        weight: FontWeight(400.0,),
1711                        ..Default::default()
1712                    },),
1713                ),
1714            ],
1715            text.highlights
1716        );
1717    }
1718
1719    #[gpui::test]
1720    async fn test_html_href_element() {
1721        let parsed =
1722            parse("<p>Some text <a href=\"https://example.com\">link</a> more text</p>").await;
1723
1724        assert_eq!(1, parsed.children.len());
1725        let chunks = if let ParsedMarkdownElement::Paragraph(chunks) = &parsed.children[0] {
1726            chunks
1727        } else {
1728            panic!("Expected a paragraph");
1729        };
1730
1731        assert_eq!(1, chunks.len());
1732        let text = if let MarkdownParagraphChunk::Text(text) = &chunks[0] {
1733            text
1734        } else {
1735            panic!("Expected a paragraph");
1736        };
1737
1738        assert_eq!(0..65, text.source_range);
1739        assert_eq!("Some text link more text", text.contents.as_str(),);
1740        assert_eq!(
1741            vec![(
1742                10..14,
1743                MarkdownHighlight::Style(MarkdownHighlightStyle {
1744                    link: true,
1745                    ..Default::default()
1746                },),
1747            )],
1748            text.highlights
1749        );
1750        assert_eq!(
1751            vec![(
1752                10..14,
1753                ParsedRegion {
1754                    code: false,
1755                    link: Some(Link::Web {
1756                        url: "https://example.com".into()
1757                    })
1758                }
1759            )],
1760            text.regions
1761        )
1762    }
1763
1764    #[gpui::test]
1765    async fn test_text_with_inline_html() {
1766        let parsed = parse("This is a paragraph with an inline HTML <sometag>tag</sometag>.").await;
1767
1768        assert_eq!(
1769            parsed.children,
1770            vec![p("This is a paragraph with an inline HTML tag.", 0..63),],
1771        );
1772    }
1773
1774    #[gpui::test]
1775    async fn test_raw_links_detection() {
1776        let parsed = parse("Checkout this https://zed.dev link").await;
1777
1778        assert_eq!(
1779            parsed.children,
1780            vec![p("Checkout this https://zed.dev link", 0..34)]
1781        );
1782    }
1783
1784    #[gpui::test]
1785    async fn test_empty_image() {
1786        let parsed = parse("![]()").await;
1787
1788        let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
1789            text
1790        } else {
1791            panic!("Expected a paragraph");
1792        };
1793        assert_eq!(paragraph.len(), 0);
1794    }
1795
1796    #[gpui::test]
1797    async fn test_image_links_detection() {
1798        let parsed = parse("![test](https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png)").await;
1799
1800        let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
1801            text
1802        } else {
1803            panic!("Expected a paragraph");
1804        };
1805        assert_eq!(
1806                paragraph[0],
1807                MarkdownParagraphChunk::Image(Image {
1808                    source_range: 0..111,
1809                    link: Link::Web {
1810                        url: "https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png".to_string(),
1811                    },
1812                    alt_text: Some("test".into()),
1813                    height: None,
1814                    width: None,
1815                },)
1816            );
1817    }
1818
1819    #[gpui::test]
1820    async fn test_image_alt_text() {
1821        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;
1822
1823        let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
1824            text
1825        } else {
1826            panic!("Expected a paragraph");
1827        };
1828        assert_eq!(
1829                    paragraph[0],
1830                    MarkdownParagraphChunk::Image(Image {
1831                        source_range: 0..142,
1832                        link: Link::Web {
1833                            url: "https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/zed-industries/zed/main/assets/badge/v0.json".to_string(),
1834                        },
1835                        alt_text: Some("Zed".into()),
1836                        height: None,
1837                        width: None,
1838                    },)
1839                );
1840    }
1841
1842    #[gpui::test]
1843    async fn test_image_without_alt_text() {
1844        let parsed = parse("![](http://example.com/foo.png)").await;
1845
1846        let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
1847            text
1848        } else {
1849            panic!("Expected a paragraph");
1850        };
1851        assert_eq!(
1852            paragraph[0],
1853            MarkdownParagraphChunk::Image(Image {
1854                source_range: 0..31,
1855                link: Link::Web {
1856                    url: "http://example.com/foo.png".to_string(),
1857                },
1858                alt_text: None,
1859                height: None,
1860                width: None,
1861            },)
1862        );
1863    }
1864
1865    #[gpui::test]
1866    async fn test_image_with_alt_text_containing_formatting() {
1867        let parsed = parse("![foo *bar* baz](http://example.com/foo.png)").await;
1868
1869        let ParsedMarkdownElement::Paragraph(chunks) = &parsed.children[0] else {
1870            panic!("Expected a paragraph");
1871        };
1872        assert_eq!(
1873            chunks,
1874            &[MarkdownParagraphChunk::Image(Image {
1875                source_range: 0..44,
1876                link: Link::Web {
1877                    url: "http://example.com/foo.png".to_string(),
1878                },
1879                alt_text: Some("foo bar baz".into()),
1880                height: None,
1881                width: None,
1882            }),],
1883        );
1884    }
1885
1886    #[gpui::test]
1887    async fn test_images_with_text_in_between() {
1888        let parsed = parse(
1889            "![foo](http://example.com/foo.png)\nLorem Ipsum\n![bar](http://example.com/bar.png)",
1890        )
1891        .await;
1892
1893        let chunks = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
1894            text
1895        } else {
1896            panic!("Expected a paragraph");
1897        };
1898        assert_eq!(
1899            chunks,
1900            &vec![
1901                MarkdownParagraphChunk::Image(Image {
1902                    source_range: 0..81,
1903                    link: Link::Web {
1904                        url: "http://example.com/foo.png".to_string(),
1905                    },
1906                    alt_text: Some("foo".into()),
1907                    height: None,
1908                    width: None,
1909                }),
1910                MarkdownParagraphChunk::Text(ParsedMarkdownText {
1911                    source_range: 0..81,
1912                    contents: " Lorem Ipsum ".into(),
1913                    highlights: Vec::new(),
1914                    regions: Vec::new(),
1915                }),
1916                MarkdownParagraphChunk::Image(Image {
1917                    source_range: 0..81,
1918                    link: Link::Web {
1919                        url: "http://example.com/bar.png".to_string(),
1920                    },
1921                    alt_text: Some("bar".into()),
1922                    height: None,
1923                    width: None,
1924                })
1925            ]
1926        );
1927    }
1928
1929    #[test]
1930    fn test_parse_html_element_dimension() {
1931        // Test percentage values
1932        assert_eq!(
1933            MarkdownParser::parse_html_element_dimension("50%"),
1934            Some(DefiniteLength::Fraction(0.5))
1935        );
1936        assert_eq!(
1937            MarkdownParser::parse_html_element_dimension("100%"),
1938            Some(DefiniteLength::Fraction(1.0))
1939        );
1940        assert_eq!(
1941            MarkdownParser::parse_html_element_dimension("25%"),
1942            Some(DefiniteLength::Fraction(0.25))
1943        );
1944        assert_eq!(
1945            MarkdownParser::parse_html_element_dimension("0%"),
1946            Some(DefiniteLength::Fraction(0.0))
1947        );
1948
1949        // Test pixel values
1950        assert_eq!(
1951            MarkdownParser::parse_html_element_dimension("100px"),
1952            Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.0))))
1953        );
1954        assert_eq!(
1955            MarkdownParser::parse_html_element_dimension("50px"),
1956            Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(50.0))))
1957        );
1958        assert_eq!(
1959            MarkdownParser::parse_html_element_dimension("0px"),
1960            Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(0.0))))
1961        );
1962
1963        // Test values without units (should be treated as pixels)
1964        assert_eq!(
1965            MarkdownParser::parse_html_element_dimension("100"),
1966            Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.0))))
1967        );
1968        assert_eq!(
1969            MarkdownParser::parse_html_element_dimension("42"),
1970            Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(42.0))))
1971        );
1972
1973        // Test invalid values
1974        assert_eq!(
1975            MarkdownParser::parse_html_element_dimension("invalid"),
1976            None
1977        );
1978        assert_eq!(MarkdownParser::parse_html_element_dimension("px"), None);
1979        assert_eq!(MarkdownParser::parse_html_element_dimension("%"), None);
1980        assert_eq!(MarkdownParser::parse_html_element_dimension(""), None);
1981        assert_eq!(MarkdownParser::parse_html_element_dimension("abc%"), None);
1982        assert_eq!(MarkdownParser::parse_html_element_dimension("abcpx"), None);
1983
1984        // Test decimal values
1985        assert_eq!(
1986            MarkdownParser::parse_html_element_dimension("50.5%"),
1987            Some(DefiniteLength::Fraction(0.505))
1988        );
1989        assert_eq!(
1990            MarkdownParser::parse_html_element_dimension("100.25px"),
1991            Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.25))))
1992        );
1993        assert_eq!(
1994            MarkdownParser::parse_html_element_dimension("42.0"),
1995            Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(42.0))))
1996        );
1997    }
1998
1999    #[gpui::test]
2000    async fn test_html_unordered_list() {
2001        let parsed = parse(
2002            "<ul>
2003              <li>Item 1</li>
2004              <li>Item 2</li>
2005            </ul>",
2006        )
2007        .await;
2008
2009        assert_eq!(
2010            ParsedMarkdown {
2011                children: vec![
2012                    nested_list_item(
2013                        0..82,
2014                        1,
2015                        ParsedMarkdownListItemType::Unordered,
2016                        vec![ParsedMarkdownElement::Paragraph(text("Item 1", 0..82))]
2017                    ),
2018                    nested_list_item(
2019                        0..82,
2020                        1,
2021                        ParsedMarkdownListItemType::Unordered,
2022                        vec![ParsedMarkdownElement::Paragraph(text("Item 2", 0..82))]
2023                    ),
2024                ]
2025            },
2026            parsed
2027        );
2028    }
2029
2030    #[gpui::test]
2031    async fn test_html_ordered_list() {
2032        let parsed = parse(
2033            "<ol>
2034              <li>Item 1</li>
2035              <li>Item 2</li>
2036            </ol>",
2037        )
2038        .await;
2039
2040        assert_eq!(
2041            ParsedMarkdown {
2042                children: vec![
2043                    nested_list_item(
2044                        0..82,
2045                        1,
2046                        ParsedMarkdownListItemType::Ordered(1),
2047                        vec![ParsedMarkdownElement::Paragraph(text("Item 1", 0..82))]
2048                    ),
2049                    nested_list_item(
2050                        0..82,
2051                        1,
2052                        ParsedMarkdownListItemType::Ordered(2),
2053                        vec![ParsedMarkdownElement::Paragraph(text("Item 2", 0..82))]
2054                    ),
2055                ]
2056            },
2057            parsed
2058        );
2059    }
2060
2061    #[gpui::test]
2062    async fn test_html_nested_ordered_list() {
2063        let parsed = parse(
2064            "<ol>
2065              <li>Item 1</li>
2066              <li>Item 2
2067                <ol>
2068                  <li>Sub-Item 1</li>
2069                  <li>Sub-Item 2</li>
2070                </ol>
2071              </li>
2072            </ol>",
2073        )
2074        .await;
2075
2076        assert_eq!(
2077            ParsedMarkdown {
2078                children: vec![
2079                    nested_list_item(
2080                        0..216,
2081                        1,
2082                        ParsedMarkdownListItemType::Ordered(1),
2083                        vec![ParsedMarkdownElement::Paragraph(text("Item 1", 0..216))]
2084                    ),
2085                    nested_list_item(
2086                        0..216,
2087                        1,
2088                        ParsedMarkdownListItemType::Ordered(2),
2089                        vec![
2090                            ParsedMarkdownElement::Paragraph(text("Item 2", 0..216)),
2091                            nested_list_item(
2092                                0..216,
2093                                2,
2094                                ParsedMarkdownListItemType::Ordered(1),
2095                                vec![ParsedMarkdownElement::Paragraph(text("Sub-Item 1", 0..216))]
2096                            ),
2097                            nested_list_item(
2098                                0..216,
2099                                2,
2100                                ParsedMarkdownListItemType::Ordered(2),
2101                                vec![ParsedMarkdownElement::Paragraph(text("Sub-Item 2", 0..216))]
2102                            ),
2103                        ]
2104                    ),
2105                ]
2106            },
2107            parsed
2108        );
2109    }
2110
2111    #[gpui::test]
2112    async fn test_html_nested_unordered_list() {
2113        let parsed = parse(
2114            "<ul>
2115              <li>Item 1</li>
2116              <li>Item 2
2117                <ul>
2118                  <li>Sub-Item 1</li>
2119                  <li>Sub-Item 2</li>
2120                </ul>
2121              </li>
2122            </ul>",
2123        )
2124        .await;
2125
2126        assert_eq!(
2127            ParsedMarkdown {
2128                children: vec![
2129                    nested_list_item(
2130                        0..216,
2131                        1,
2132                        ParsedMarkdownListItemType::Unordered,
2133                        vec![ParsedMarkdownElement::Paragraph(text("Item 1", 0..216))]
2134                    ),
2135                    nested_list_item(
2136                        0..216,
2137                        1,
2138                        ParsedMarkdownListItemType::Unordered,
2139                        vec![
2140                            ParsedMarkdownElement::Paragraph(text("Item 2", 0..216)),
2141                            nested_list_item(
2142                                0..216,
2143                                2,
2144                                ParsedMarkdownListItemType::Unordered,
2145                                vec![ParsedMarkdownElement::Paragraph(text("Sub-Item 1", 0..216))]
2146                            ),
2147                            nested_list_item(
2148                                0..216,
2149                                2,
2150                                ParsedMarkdownListItemType::Unordered,
2151                                vec![ParsedMarkdownElement::Paragraph(text("Sub-Item 2", 0..216))]
2152                            ),
2153                        ]
2154                    ),
2155                ]
2156            },
2157            parsed
2158        );
2159    }
2160
2161    #[gpui::test]
2162    async fn test_inline_html_image_tag() {
2163        let parsed =
2164            parse("<p>Some text<img src=\"http://example.com/foo.png\" /> some more text</p>")
2165                .await;
2166
2167        assert_eq!(
2168            ParsedMarkdown {
2169                children: vec![ParsedMarkdownElement::Paragraph(vec![
2170                    MarkdownParagraphChunk::Text(ParsedMarkdownText {
2171                        source_range: 0..71,
2172                        contents: "Some text".into(),
2173                        highlights: Default::default(),
2174                        regions: Default::default()
2175                    }),
2176                    MarkdownParagraphChunk::Image(Image {
2177                        source_range: 0..71,
2178                        link: Link::Web {
2179                            url: "http://example.com/foo.png".to_string(),
2180                        },
2181                        alt_text: None,
2182                        height: None,
2183                        width: None,
2184                    }),
2185                    MarkdownParagraphChunk::Text(ParsedMarkdownText {
2186                        source_range: 0..71,
2187                        contents: " some more text".into(),
2188                        highlights: Default::default(),
2189                        regions: Default::default()
2190                    }),
2191                ])]
2192            },
2193            parsed
2194        );
2195    }
2196
2197    #[gpui::test]
2198    async fn test_html_block_quote() {
2199        let parsed = parse(
2200            "<blockquote>
2201                <p>some description</p>
2202            </blockquote>",
2203        )
2204        .await;
2205
2206        assert_eq!(
2207            ParsedMarkdown {
2208                children: vec![block_quote(
2209                    vec![ParsedMarkdownElement::Paragraph(text(
2210                        "some description",
2211                        0..78
2212                    ))],
2213                    0..78,
2214                )]
2215            },
2216            parsed
2217        );
2218    }
2219
2220    #[gpui::test]
2221    async fn test_html_nested_block_quote() {
2222        let parsed = parse(
2223            "<blockquote>
2224                <p>some description</p>
2225                <blockquote>
2226                <p>second description</p>
2227                </blockquote>
2228            </blockquote>",
2229        )
2230        .await;
2231
2232        assert_eq!(
2233            ParsedMarkdown {
2234                children: vec![block_quote(
2235                    vec![
2236                        ParsedMarkdownElement::Paragraph(text("some description", 0..179)),
2237                        block_quote(
2238                            vec![ParsedMarkdownElement::Paragraph(text(
2239                                "second description",
2240                                0..179
2241                            ))],
2242                            0..179,
2243                        )
2244                    ],
2245                    0..179,
2246                )]
2247            },
2248            parsed
2249        );
2250    }
2251
2252    #[gpui::test]
2253    async fn test_html_table() {
2254        let parsed = parse(
2255            "<table>
2256          <thead>
2257            <tr>
2258              <th>Id</th>
2259              <th>Name</th>
2260            </tr>
2261          </thead>
2262          <tbody>
2263            <tr>
2264              <td>1</td>
2265              <td>Chris</td>
2266            </tr>
2267            <tr>
2268              <td>2</td>
2269              <td>Dennis</td>
2270            </tr>
2271          </tbody>
2272        </table>",
2273        )
2274        .await;
2275
2276        assert_eq!(
2277            ParsedMarkdown {
2278                children: vec![ParsedMarkdownElement::Table(table(
2279                    0..366,
2280                    None,
2281                    vec![row(vec![
2282                        column(
2283                            1,
2284                            1,
2285                            true,
2286                            text("Id", 0..366),
2287                            ParsedMarkdownTableAlignment::Center
2288                        ),
2289                        column(
2290                            1,
2291                            1,
2292                            true,
2293                            text("Name ", 0..366),
2294                            ParsedMarkdownTableAlignment::Center
2295                        )
2296                    ])],
2297                    vec![
2298                        row(vec![
2299                            column(
2300                                1,
2301                                1,
2302                                false,
2303                                text("1", 0..366),
2304                                ParsedMarkdownTableAlignment::None
2305                            ),
2306                            column(
2307                                1,
2308                                1,
2309                                false,
2310                                text("Chris", 0..366),
2311                                ParsedMarkdownTableAlignment::None
2312                            )
2313                        ]),
2314                        row(vec![
2315                            column(
2316                                1,
2317                                1,
2318                                false,
2319                                text("2", 0..366),
2320                                ParsedMarkdownTableAlignment::None
2321                            ),
2322                            column(
2323                                1,
2324                                1,
2325                                false,
2326                                text("Dennis", 0..366),
2327                                ParsedMarkdownTableAlignment::None
2328                            )
2329                        ]),
2330                    ],
2331                ))],
2332            },
2333            parsed
2334        );
2335    }
2336
2337    #[gpui::test]
2338    async fn test_html_table_with_caption() {
2339        let parsed = parse(
2340            "<table>
2341            <caption>My Table</caption>
2342          <tbody>
2343            <tr>
2344              <td>1</td>
2345              <td>Chris</td>
2346            </tr>
2347            <tr>
2348              <td>2</td>
2349              <td>Dennis</td>
2350            </tr>
2351          </tbody>
2352        </table>",
2353        )
2354        .await;
2355
2356        assert_eq!(
2357            ParsedMarkdown {
2358                children: vec![ParsedMarkdownElement::Table(table(
2359                    0..280,
2360                    Some(vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
2361                        source_range: 0..280,
2362                        contents: "My Table".into(),
2363                        highlights: Default::default(),
2364                        regions: Default::default()
2365                    })]),
2366                    vec![],
2367                    vec![
2368                        row(vec![
2369                            column(
2370                                1,
2371                                1,
2372                                false,
2373                                text("1", 0..280),
2374                                ParsedMarkdownTableAlignment::None
2375                            ),
2376                            column(
2377                                1,
2378                                1,
2379                                false,
2380                                text("Chris", 0..280),
2381                                ParsedMarkdownTableAlignment::None
2382                            )
2383                        ]),
2384                        row(vec![
2385                            column(
2386                                1,
2387                                1,
2388                                false,
2389                                text("2", 0..280),
2390                                ParsedMarkdownTableAlignment::None
2391                            ),
2392                            column(
2393                                1,
2394                                1,
2395                                false,
2396                                text("Dennis", 0..280),
2397                                ParsedMarkdownTableAlignment::None
2398                            )
2399                        ]),
2400                    ],
2401                ))],
2402            },
2403            parsed
2404        );
2405    }
2406
2407    #[gpui::test]
2408    async fn test_html_table_without_headings() {
2409        let parsed = parse(
2410            "<table>
2411          <tbody>
2412            <tr>
2413              <td>1</td>
2414              <td>Chris</td>
2415            </tr>
2416            <tr>
2417              <td>2</td>
2418              <td>Dennis</td>
2419            </tr>
2420          </tbody>
2421        </table>",
2422        )
2423        .await;
2424
2425        assert_eq!(
2426            ParsedMarkdown {
2427                children: vec![ParsedMarkdownElement::Table(table(
2428                    0..240,
2429                    None,
2430                    vec![],
2431                    vec![
2432                        row(vec![
2433                            column(
2434                                1,
2435                                1,
2436                                false,
2437                                text("1", 0..240),
2438                                ParsedMarkdownTableAlignment::None
2439                            ),
2440                            column(
2441                                1,
2442                                1,
2443                                false,
2444                                text("Chris", 0..240),
2445                                ParsedMarkdownTableAlignment::None
2446                            )
2447                        ]),
2448                        row(vec![
2449                            column(
2450                                1,
2451                                1,
2452                                false,
2453                                text("2", 0..240),
2454                                ParsedMarkdownTableAlignment::None
2455                            ),
2456                            column(
2457                                1,
2458                                1,
2459                                false,
2460                                text("Dennis", 0..240),
2461                                ParsedMarkdownTableAlignment::None
2462                            )
2463                        ]),
2464                    ],
2465                ))],
2466            },
2467            parsed
2468        );
2469    }
2470
2471    #[gpui::test]
2472    async fn test_html_table_without_body() {
2473        let parsed = parse(
2474            "<table>
2475          <thead>
2476            <tr>
2477              <th>Id</th>
2478              <th>Name</th>
2479            </tr>
2480          </thead>
2481        </table>",
2482        )
2483        .await;
2484
2485        assert_eq!(
2486            ParsedMarkdown {
2487                children: vec![ParsedMarkdownElement::Table(table(
2488                    0..150,
2489                    None,
2490                    vec![row(vec![
2491                        column(
2492                            1,
2493                            1,
2494                            true,
2495                            text("Id", 0..150),
2496                            ParsedMarkdownTableAlignment::Center
2497                        ),
2498                        column(
2499                            1,
2500                            1,
2501                            true,
2502                            text("Name", 0..150),
2503                            ParsedMarkdownTableAlignment::Center
2504                        )
2505                    ])],
2506                    vec![],
2507                ))],
2508            },
2509            parsed
2510        );
2511    }
2512
2513    #[gpui::test]
2514    async fn test_html_heading_tags() {
2515        let parsed = parse("<h1>Heading</h1><h2>Heading</h2><h3>Heading</h3><h4>Heading</h4><h5>Heading</h5><h6>Heading</h6>").await;
2516
2517        assert_eq!(
2518            ParsedMarkdown {
2519                children: vec![
2520                    ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
2521                        level: HeadingLevel::H1,
2522                        source_range: 0..96,
2523                        contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
2524                            source_range: 0..96,
2525                            contents: "Heading".into(),
2526                            highlights: Vec::default(),
2527                            regions: Vec::default()
2528                        })],
2529                    }),
2530                    ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
2531                        level: HeadingLevel::H2,
2532                        source_range: 0..96,
2533                        contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
2534                            source_range: 0..96,
2535                            contents: "Heading".into(),
2536                            highlights: Vec::default(),
2537                            regions: Vec::default()
2538                        })],
2539                    }),
2540                    ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
2541                        level: HeadingLevel::H3,
2542                        source_range: 0..96,
2543                        contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
2544                            source_range: 0..96,
2545                            contents: "Heading".into(),
2546                            highlights: Vec::default(),
2547                            regions: Vec::default()
2548                        })],
2549                    }),
2550                    ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
2551                        level: HeadingLevel::H4,
2552                        source_range: 0..96,
2553                        contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
2554                            source_range: 0..96,
2555                            contents: "Heading".into(),
2556                            highlights: Vec::default(),
2557                            regions: Vec::default()
2558                        })],
2559                    }),
2560                    ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
2561                        level: HeadingLevel::H5,
2562                        source_range: 0..96,
2563                        contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
2564                            source_range: 0..96,
2565                            contents: "Heading".into(),
2566                            highlights: Vec::default(),
2567                            regions: Vec::default()
2568                        })],
2569                    }),
2570                    ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
2571                        level: HeadingLevel::H6,
2572                        source_range: 0..96,
2573                        contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
2574                            source_range: 0..96,
2575                            contents: "Heading".into(),
2576                            highlights: Vec::default(),
2577                            regions: Vec::default()
2578                        })],
2579                    }),
2580                ],
2581            },
2582            parsed
2583        );
2584    }
2585
2586    #[gpui::test]
2587    async fn test_html_image_tag() {
2588        let parsed = parse("<img src=\"http://example.com/foo.png\" />").await;
2589
2590        assert_eq!(
2591            ParsedMarkdown {
2592                children: vec![ParsedMarkdownElement::Image(Image {
2593                    source_range: 0..40,
2594                    link: Link::Web {
2595                        url: "http://example.com/foo.png".to_string(),
2596                    },
2597                    alt_text: None,
2598                    height: None,
2599                    width: None,
2600                })]
2601            },
2602            parsed
2603        );
2604    }
2605
2606    #[gpui::test]
2607    async fn test_html_image_tag_with_alt_text() {
2608        let parsed = parse("<img src=\"http://example.com/foo.png\" alt=\"Foo\" />").await;
2609
2610        assert_eq!(
2611            ParsedMarkdown {
2612                children: vec![ParsedMarkdownElement::Image(Image {
2613                    source_range: 0..50,
2614                    link: Link::Web {
2615                        url: "http://example.com/foo.png".to_string(),
2616                    },
2617                    alt_text: Some("Foo".into()),
2618                    height: None,
2619                    width: None,
2620                })]
2621            },
2622            parsed
2623        );
2624    }
2625
2626    #[gpui::test]
2627    async fn test_html_image_tag_with_height_and_width() {
2628        let parsed =
2629            parse("<img src=\"http://example.com/foo.png\" height=\"100\" width=\"200\" />").await;
2630
2631        assert_eq!(
2632            ParsedMarkdown {
2633                children: vec![ParsedMarkdownElement::Image(Image {
2634                    source_range: 0..65,
2635                    link: Link::Web {
2636                        url: "http://example.com/foo.png".to_string(),
2637                    },
2638                    alt_text: None,
2639                    height: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.)))),
2640                    width: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(200.)))),
2641                })]
2642            },
2643            parsed
2644        );
2645    }
2646
2647    #[gpui::test]
2648    async fn test_html_image_style_tag_with_height_and_width() {
2649        let parsed = parse(
2650            "<img src=\"http://example.com/foo.png\" style=\"height:100px; width:200px;\" />",
2651        )
2652        .await;
2653
2654        assert_eq!(
2655            ParsedMarkdown {
2656                children: vec![ParsedMarkdownElement::Image(Image {
2657                    source_range: 0..75,
2658                    link: Link::Web {
2659                        url: "http://example.com/foo.png".to_string(),
2660                    },
2661                    alt_text: None,
2662                    height: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.)))),
2663                    width: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(200.)))),
2664                })]
2665            },
2666            parsed
2667        );
2668    }
2669
2670    #[gpui::test]
2671    async fn test_header_only_table() {
2672        let markdown = "\
2673| Header 1 | Header 2 |
2674|----------|----------|
2675
2676Some other content
2677";
2678
2679        let expected_table = table(
2680            0..48,
2681            None,
2682            vec![row(vec![
2683                column(
2684                    1,
2685                    1,
2686                    true,
2687                    text("Header 1", 1..11),
2688                    ParsedMarkdownTableAlignment::None,
2689                ),
2690                column(
2691                    1,
2692                    1,
2693                    true,
2694                    text("Header 2", 12..22),
2695                    ParsedMarkdownTableAlignment::None,
2696                ),
2697            ])],
2698            vec![],
2699        );
2700
2701        assert_eq!(
2702            parse(markdown).await.children[0],
2703            ParsedMarkdownElement::Table(expected_table)
2704        );
2705    }
2706
2707    #[gpui::test]
2708    async fn test_basic_table() {
2709        let markdown = "\
2710| Header 1 | Header 2 |
2711|----------|----------|
2712| Cell 1   | Cell 2   |
2713| Cell 3   | Cell 4   |";
2714
2715        let expected_table = table(
2716            0..95,
2717            None,
2718            vec![row(vec![
2719                column(
2720                    1,
2721                    1,
2722                    true,
2723                    text("Header 1", 1..11),
2724                    ParsedMarkdownTableAlignment::None,
2725                ),
2726                column(
2727                    1,
2728                    1,
2729                    true,
2730                    text("Header 2", 12..22),
2731                    ParsedMarkdownTableAlignment::None,
2732                ),
2733            ])],
2734            vec![
2735                row(vec![
2736                    column(
2737                        1,
2738                        1,
2739                        false,
2740                        text("Cell 1", 49..59),
2741                        ParsedMarkdownTableAlignment::None,
2742                    ),
2743                    column(
2744                        1,
2745                        1,
2746                        false,
2747                        text("Cell 2", 60..70),
2748                        ParsedMarkdownTableAlignment::None,
2749                    ),
2750                ]),
2751                row(vec![
2752                    column(
2753                        1,
2754                        1,
2755                        false,
2756                        text("Cell 3", 73..83),
2757                        ParsedMarkdownTableAlignment::None,
2758                    ),
2759                    column(
2760                        1,
2761                        1,
2762                        false,
2763                        text("Cell 4", 84..94),
2764                        ParsedMarkdownTableAlignment::None,
2765                    ),
2766                ]),
2767            ],
2768        );
2769
2770        assert_eq!(
2771            parse(markdown).await.children[0],
2772            ParsedMarkdownElement::Table(expected_table)
2773        );
2774    }
2775
2776    #[gpui::test]
2777    async fn test_list_basic() {
2778        let parsed = parse(
2779            "\
2780* Item 1
2781* Item 2
2782* Item 3
2783",
2784        )
2785        .await;
2786
2787        assert_eq!(
2788            parsed.children,
2789            vec![
2790                list_item(0..8, 1, Unordered, vec![p("Item 1", 2..8)]),
2791                list_item(9..17, 1, Unordered, vec![p("Item 2", 11..17)]),
2792                list_item(18..26, 1, Unordered, vec![p("Item 3", 20..26)]),
2793            ],
2794        );
2795    }
2796
2797    #[gpui::test]
2798    async fn test_list_with_tasks() {
2799        let parsed = parse(
2800            "\
2801- [ ] TODO
2802- [x] Checked
2803",
2804        )
2805        .await;
2806
2807        assert_eq!(
2808            parsed.children,
2809            vec![
2810                list_item(0..10, 1, Task(false, 2..5), vec![p("TODO", 6..10)]),
2811                list_item(11..24, 1, Task(true, 13..16), vec![p("Checked", 17..24)]),
2812            ],
2813        );
2814    }
2815
2816    #[gpui::test]
2817    async fn test_list_with_indented_task() {
2818        let parsed = parse(
2819            "\
2820- [ ] TODO
2821  - [x] Checked
2822  - Unordered
2823  1. Number 1
2824  1. Number 2
28251. Number A
2826",
2827        )
2828        .await;
2829
2830        assert_eq!(
2831            parsed.children,
2832            vec![
2833                list_item(0..12, 1, Task(false, 2..5), vec![p("TODO", 6..10)]),
2834                list_item(13..26, 2, Task(true, 15..18), vec![p("Checked", 19..26)]),
2835                list_item(29..40, 2, Unordered, vec![p("Unordered", 31..40)]),
2836                list_item(43..54, 2, Ordered(1), vec![p("Number 1", 46..54)]),
2837                list_item(57..68, 2, Ordered(2), vec![p("Number 2", 60..68)]),
2838                list_item(69..80, 1, Ordered(1), vec![p("Number A", 72..80)]),
2839            ],
2840        );
2841    }
2842
2843    #[gpui::test]
2844    async fn test_list_with_linebreak_is_handled_correctly() {
2845        let parsed = parse(
2846            "\
2847- [ ] Task 1
2848
2849- [x] Task 2
2850",
2851        )
2852        .await;
2853
2854        assert_eq!(
2855            parsed.children,
2856            vec![
2857                list_item(0..13, 1, Task(false, 2..5), vec![p("Task 1", 6..12)]),
2858                list_item(14..26, 1, Task(true, 16..19), vec![p("Task 2", 20..26)]),
2859            ],
2860        );
2861    }
2862
2863    #[gpui::test]
2864    async fn test_list_nested() {
2865        let parsed = parse(
2866            "\
2867* Item 1
2868* Item 2
2869* Item 3
2870
28711. Hello
28721. Two
2873   1. Three
28742. Four
28753. Five
2876
2877* First
2878  1. Hello
2879     1. Goodbyte
2880        - Inner
2881        - Inner
2882  2. Goodbyte
2883        - Next item empty
2884        -
2885* Last
2886",
2887        )
2888        .await;
2889
2890        assert_eq!(
2891            parsed.children,
2892            vec![
2893                list_item(0..8, 1, Unordered, vec![p("Item 1", 2..8)]),
2894                list_item(9..17, 1, Unordered, vec![p("Item 2", 11..17)]),
2895                list_item(18..27, 1, Unordered, vec![p("Item 3", 20..26)]),
2896                list_item(28..36, 1, Ordered(1), vec![p("Hello", 31..36)]),
2897                list_item(37..46, 1, Ordered(2), vec![p("Two", 40..43),]),
2898                list_item(47..55, 2, Ordered(1), vec![p("Three", 50..55)]),
2899                list_item(56..63, 1, Ordered(3), vec![p("Four", 59..63)]),
2900                list_item(64..72, 1, Ordered(4), vec![p("Five", 67..71)]),
2901                list_item(73..82, 1, Unordered, vec![p("First", 75..80)]),
2902                list_item(83..96, 2, Ordered(1), vec![p("Hello", 86..91)]),
2903                list_item(97..116, 3, Ordered(1), vec![p("Goodbyte", 100..108)]),
2904                list_item(117..124, 4, Unordered, vec![p("Inner", 119..124)]),
2905                list_item(133..140, 4, Unordered, vec![p("Inner", 135..140)]),
2906                list_item(143..159, 2, Ordered(2), vec![p("Goodbyte", 146..154)]),
2907                list_item(160..180, 3, Unordered, vec![p("Next item empty", 165..180)]),
2908                list_item(186..190, 3, Unordered, vec![]),
2909                list_item(191..197, 1, Unordered, vec![p("Last", 193..197)]),
2910            ]
2911        );
2912    }
2913
2914    #[gpui::test]
2915    async fn test_list_with_nested_content() {
2916        let parsed = parse(
2917            "\
2918*   This is a list item with two paragraphs.
2919
2920    This is the second paragraph in the list item.
2921",
2922        )
2923        .await;
2924
2925        assert_eq!(
2926            parsed.children,
2927            vec![list_item(
2928                0..96,
2929                1,
2930                Unordered,
2931                vec![
2932                    p("This is a list item with two paragraphs.", 4..44),
2933                    p("This is the second paragraph in the list item.", 50..97)
2934                ],
2935            ),],
2936        );
2937    }
2938
2939    #[gpui::test]
2940    async fn test_list_item_with_inline_html() {
2941        let parsed = parse(
2942            "\
2943*   This is a list item with an inline HTML <sometag>tag</sometag>.
2944",
2945        )
2946        .await;
2947
2948        assert_eq!(
2949            parsed.children,
2950            vec![list_item(
2951                0..67,
2952                1,
2953                Unordered,
2954                vec![p("This is a list item with an inline HTML tag.", 4..44),],
2955            ),],
2956        );
2957    }
2958
2959    #[gpui::test]
2960    async fn test_nested_list_with_paragraph_inside() {
2961        let parsed = parse(
2962            "\
29631. a
2964    1. b
2965        1. c
2966
2967    text
2968
2969    1. d
2970",
2971        )
2972        .await;
2973
2974        assert_eq!(
2975            parsed.children,
2976            vec![
2977                list_item(0..7, 1, Ordered(1), vec![p("a", 3..4)],),
2978                list_item(8..20, 2, Ordered(1), vec![p("b", 12..13),],),
2979                list_item(21..27, 3, Ordered(1), vec![p("c", 25..26),],),
2980                p("text", 32..37),
2981                list_item(41..46, 2, Ordered(1), vec![p("d", 45..46),],),
2982            ],
2983        );
2984    }
2985
2986    #[gpui::test]
2987    async fn test_list_with_leading_text() {
2988        let parsed = parse(
2989            "\
2990* `code`
2991* **bold**
2992* [link](https://example.com)
2993",
2994        )
2995        .await;
2996
2997        assert_eq!(
2998            parsed.children,
2999            vec![
3000                list_item(0..8, 1, Unordered, vec![p("code", 2..8)]),
3001                list_item(9..19, 1, Unordered, vec![p("bold", 11..19)]),
3002                list_item(20..49, 1, Unordered, vec![p("link", 22..49)],),
3003            ],
3004        );
3005    }
3006
3007    #[gpui::test]
3008    async fn test_simple_block_quote() {
3009        let parsed = parse("> Simple block quote with **styled text**").await;
3010
3011        assert_eq!(
3012            parsed.children,
3013            vec![block_quote(
3014                vec![p("Simple block quote with styled text", 2..41)],
3015                0..41
3016            )]
3017        );
3018    }
3019
3020    #[gpui::test]
3021    async fn test_simple_block_quote_with_multiple_lines() {
3022        let parsed = parse(
3023            "\
3024> # Heading
3025> More
3026> text
3027>
3028> More text
3029",
3030        )
3031        .await;
3032
3033        assert_eq!(
3034            parsed.children,
3035            vec![block_quote(
3036                vec![
3037                    h1(text("Heading", 4..11), 2..12),
3038                    p("More text", 14..26),
3039                    p("More text", 30..40)
3040                ],
3041                0..40
3042            )]
3043        );
3044    }
3045
3046    #[gpui::test]
3047    async fn test_nested_block_quote() {
3048        let parsed = parse(
3049            "\
3050> A
3051>
3052> > # B
3053>
3054> C
3055
3056More text
3057",
3058        )
3059        .await;
3060
3061        assert_eq!(
3062            parsed.children,
3063            vec![
3064                block_quote(
3065                    vec![
3066                        p("A", 2..4),
3067                        block_quote(vec![h1(text("B", 12..13), 10..14)], 8..14),
3068                        p("C", 18..20)
3069                    ],
3070                    0..20
3071                ),
3072                p("More text", 21..31)
3073            ]
3074        );
3075    }
3076
3077    #[gpui::test]
3078    async fn test_dollar_signs_are_plain_text() {
3079        // Dollar signs should be preserved as plain text, not treated as math delimiters.
3080        // Regression test for https://github.com/zed-industries/zed/issues/50170
3081        let parsed = parse("$100$ per unit").await;
3082        assert_eq!(parsed.children, vec![p("$100$ per unit", 0..14)]);
3083    }
3084
3085    #[gpui::test]
3086    async fn test_dollar_signs_in_list_items() {
3087        let parsed = parse("- $18,000 budget\n- $20,000 budget\n").await;
3088        assert_eq!(
3089            parsed.children,
3090            vec![
3091                list_item(0..16, 1, Unordered, vec![p("$18,000 budget", 2..16)]),
3092                list_item(17..33, 1, Unordered, vec![p("$20,000 budget", 19..33)]),
3093            ]
3094        );
3095    }
3096
3097    #[gpui::test]
3098    async fn test_code_block() {
3099        let parsed = parse(
3100            "\
3101```
3102fn main() {
3103    return 0;
3104}
3105```
3106",
3107        )
3108        .await;
3109
3110        assert_eq!(
3111            parsed.children,
3112            vec![code_block(
3113                None,
3114                "fn main() {\n    return 0;\n}",
3115                0..35,
3116                None
3117            )]
3118        );
3119    }
3120
3121    #[gpui::test]
3122    async fn test_code_block_with_language(executor: BackgroundExecutor) {
3123        let language_registry = Arc::new(LanguageRegistry::test(executor.clone()));
3124        language_registry.add(language::rust_lang());
3125
3126        let parsed = parse_markdown(
3127            "\
3128```rust
3129fn main() {
3130    return 0;
3131}
3132```
3133",
3134            None,
3135            Some(language_registry),
3136        )
3137        .await;
3138
3139        assert_eq!(
3140            parsed.children,
3141            vec![code_block(
3142                Some("rust".to_string()),
3143                "fn main() {\n    return 0;\n}",
3144                0..39,
3145                Some(vec![])
3146            )]
3147        );
3148    }
3149
3150    fn h1(contents: MarkdownParagraph, source_range: Range<usize>) -> ParsedMarkdownElement {
3151        ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
3152            source_range,
3153            level: HeadingLevel::H1,
3154            contents,
3155        })
3156    }
3157
3158    fn h2(contents: MarkdownParagraph, source_range: Range<usize>) -> ParsedMarkdownElement {
3159        ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
3160            source_range,
3161            level: HeadingLevel::H2,
3162            contents,
3163        })
3164    }
3165
3166    fn h3(contents: MarkdownParagraph, source_range: Range<usize>) -> ParsedMarkdownElement {
3167        ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
3168            source_range,
3169            level: HeadingLevel::H3,
3170            contents,
3171        })
3172    }
3173
3174    fn p(contents: &str, source_range: Range<usize>) -> ParsedMarkdownElement {
3175        ParsedMarkdownElement::Paragraph(text(contents, source_range))
3176    }
3177
3178    fn text(contents: &str, source_range: Range<usize>) -> MarkdownParagraph {
3179        vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
3180            highlights: Vec::new(),
3181            regions: Vec::new(),
3182            source_range,
3183            contents: contents.to_string().into(),
3184        })]
3185    }
3186
3187    fn block_quote(
3188        children: Vec<ParsedMarkdownElement>,
3189        source_range: Range<usize>,
3190    ) -> ParsedMarkdownElement {
3191        ParsedMarkdownElement::BlockQuote(ParsedMarkdownBlockQuote {
3192            source_range,
3193            children,
3194        })
3195    }
3196
3197    fn code_block(
3198        language: Option<String>,
3199        code: &str,
3200        source_range: Range<usize>,
3201        highlights: Option<Vec<(Range<usize>, HighlightId)>>,
3202    ) -> ParsedMarkdownElement {
3203        ParsedMarkdownElement::CodeBlock(ParsedMarkdownCodeBlock {
3204            source_range,
3205            language,
3206            contents: code.to_string().into(),
3207            highlights,
3208        })
3209    }
3210
3211    fn list_item(
3212        source_range: Range<usize>,
3213        depth: u16,
3214        item_type: ParsedMarkdownListItemType,
3215        content: Vec<ParsedMarkdownElement>,
3216    ) -> ParsedMarkdownElement {
3217        ParsedMarkdownElement::ListItem(ParsedMarkdownListItem {
3218            source_range,
3219            item_type,
3220            depth,
3221            content,
3222            nested: false,
3223        })
3224    }
3225
3226    fn nested_list_item(
3227        source_range: Range<usize>,
3228        depth: u16,
3229        item_type: ParsedMarkdownListItemType,
3230        content: Vec<ParsedMarkdownElement>,
3231    ) -> ParsedMarkdownElement {
3232        ParsedMarkdownElement::ListItem(ParsedMarkdownListItem {
3233            source_range,
3234            item_type,
3235            depth,
3236            content,
3237            nested: true,
3238        })
3239    }
3240
3241    fn table(
3242        source_range: Range<usize>,
3243        caption: Option<MarkdownParagraph>,
3244        header: Vec<ParsedMarkdownTableRow>,
3245        body: Vec<ParsedMarkdownTableRow>,
3246    ) -> ParsedMarkdownTable {
3247        ParsedMarkdownTable {
3248            source_range,
3249            header,
3250            body,
3251            caption,
3252        }
3253    }
3254
3255    fn row(columns: Vec<ParsedMarkdownTableColumn>) -> ParsedMarkdownTableRow {
3256        ParsedMarkdownTableRow { columns }
3257    }
3258
3259    fn column(
3260        col_span: usize,
3261        row_span: usize,
3262        is_header: bool,
3263        children: MarkdownParagraph,
3264        alignment: ParsedMarkdownTableAlignment,
3265    ) -> ParsedMarkdownTableColumn {
3266        ParsedMarkdownTableColumn {
3267            col_span,
3268            row_span,
3269            is_header,
3270            children,
3271            alignment,
3272        }
3273    }
3274
3275    impl PartialEq for ParsedMarkdownTable {
3276        fn eq(&self, other: &Self) -> bool {
3277            self.source_range == other.source_range
3278                && self.header == other.header
3279                && self.body == other.body
3280        }
3281    }
3282
3283    impl PartialEq for ParsedMarkdownText {
3284        fn eq(&self, other: &Self) -> bool {
3285            self.source_range == other.source_range && self.contents == other.contents
3286        }
3287    }
3288}