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