markdown_parser.rs

   1use crate::markdown_elements::*;
   2use gpui::FontWeight;
   3use pulldown_cmark::{Alignment, Event, Options, Parser, Tag, TagEnd};
   4use std::{ops::Range, path::PathBuf};
   5
   6pub fn parse_markdown(
   7    markdown_input: &str,
   8    file_location_directory: Option<PathBuf>,
   9) -> ParsedMarkdown {
  10    let options = Options::all();
  11    let parser = Parser::new_ext(markdown_input, options);
  12    let parser = MarkdownParser::new(parser.into_offset_iter().collect(), file_location_directory);
  13    let renderer = parser.parse_document();
  14    ParsedMarkdown {
  15        children: renderer.parsed,
  16    }
  17}
  18
  19struct MarkdownParser<'a> {
  20    tokens: Vec<(Event<'a>, Range<usize>)>,
  21    /// The current index in the tokens array
  22    cursor: usize,
  23    /// The blocks that we have successfully parsed so far
  24    parsed: Vec<ParsedMarkdownElement>,
  25    file_location_directory: Option<PathBuf>,
  26}
  27
  28impl<'a> MarkdownParser<'a> {
  29    fn new(
  30        tokens: Vec<(Event<'a>, Range<usize>)>,
  31        file_location_directory: Option<PathBuf>,
  32    ) -> Self {
  33        Self {
  34            tokens,
  35            file_location_directory,
  36            cursor: 0,
  37            parsed: vec![],
  38        }
  39    }
  40
  41    fn eof(&self) -> bool {
  42        if self.tokens.is_empty() {
  43            return true;
  44        }
  45        self.cursor >= self.tokens.len() - 1
  46    }
  47
  48    fn peek(&self, steps: usize) -> Option<&(Event, Range<usize>)> {
  49        if self.eof() || (steps + self.cursor) >= self.tokens.len() {
  50            return self.tokens.last();
  51        }
  52        return self.tokens.get(self.cursor + steps);
  53    }
  54
  55    fn previous(&self) -> Option<&(Event, Range<usize>)> {
  56        if self.cursor == 0 || self.cursor > self.tokens.len() {
  57            return None;
  58        }
  59        return self.tokens.get(self.cursor - 1);
  60    }
  61
  62    fn current(&self) -> Option<&(Event, Range<usize>)> {
  63        return self.peek(0);
  64    }
  65
  66    fn is_text_like(event: &Event) -> bool {
  67        match event {
  68            Event::Text(_)
  69            // Represent an inline code block
  70            | Event::Code(_)
  71            | Event::Html(_)
  72            | Event::FootnoteReference(_)
  73            | Event::Start(Tag::Link { link_type: _, dest_url: _, title: _, id: _ })
  74            | Event::Start(Tag::Emphasis)
  75            | Event::Start(Tag::Strong)
  76            | Event::Start(Tag::Strikethrough)
  77            | Event::Start(Tag::Image { link_type: _, dest_url: _, title: _, id: _ }) => {
  78                return true;
  79            }
  80            _ => return false,
  81        }
  82    }
  83
  84    fn parse_document(mut self) -> Self {
  85        while !self.eof() {
  86            if let Some(block) = self.parse_block() {
  87                self.parsed.push(block);
  88            }
  89        }
  90        self
  91    }
  92
  93    fn parse_block(&mut self) -> Option<ParsedMarkdownElement> {
  94        let (current, source_range) = self.current().unwrap();
  95        match current {
  96            Event::Start(tag) => match tag {
  97                Tag::Paragraph => {
  98                    self.cursor += 1;
  99                    let text = self.parse_text(false);
 100                    Some(ParsedMarkdownElement::Paragraph(text))
 101                }
 102                Tag::Heading {
 103                    level,
 104                    id: _,
 105                    classes: _,
 106                    attrs: _,
 107                } => {
 108                    let level = *level;
 109                    self.cursor += 1;
 110                    let heading = self.parse_heading(level);
 111                    Some(ParsedMarkdownElement::Heading(heading))
 112                }
 113                Tag::Table(alignment) => {
 114                    let alignment = alignment.clone();
 115                    self.cursor += 1;
 116                    let table = self.parse_table(alignment);
 117                    Some(ParsedMarkdownElement::Table(table))
 118                }
 119                Tag::List(order) => {
 120                    let order = *order;
 121                    self.cursor += 1;
 122                    let list = self.parse_list(1, order);
 123                    Some(ParsedMarkdownElement::List(list))
 124                }
 125                Tag::BlockQuote => {
 126                    self.cursor += 1;
 127                    let block_quote = self.parse_block_quote();
 128                    Some(ParsedMarkdownElement::BlockQuote(block_quote))
 129                }
 130                Tag::CodeBlock(kind) => {
 131                    let language = match kind {
 132                        pulldown_cmark::CodeBlockKind::Indented => None,
 133                        pulldown_cmark::CodeBlockKind::Fenced(language) => {
 134                            if language.is_empty() {
 135                                None
 136                            } else {
 137                                Some(language.to_string())
 138                            }
 139                        }
 140                    };
 141
 142                    self.cursor += 1;
 143
 144                    let code_block = self.parse_code_block(language);
 145                    Some(ParsedMarkdownElement::CodeBlock(code_block))
 146                }
 147                _ => {
 148                    self.cursor += 1;
 149                    None
 150                }
 151            },
 152            Event::Rule => {
 153                let source_range = source_range.clone();
 154                self.cursor += 1;
 155                Some(ParsedMarkdownElement::HorizontalRule(source_range))
 156            }
 157            _ => {
 158                self.cursor += 1;
 159                None
 160            }
 161        }
 162    }
 163
 164    fn parse_text(&mut self, should_complete_on_soft_break: bool) -> ParsedMarkdownText {
 165        let (_current, source_range) = self.previous().unwrap();
 166        let source_range = source_range.clone();
 167
 168        let mut text = String::new();
 169        let mut bold_depth = 0;
 170        let mut italic_depth = 0;
 171        let mut strikethrough_depth = 0;
 172        let mut link: Option<Link> = None;
 173        let mut region_ranges: Vec<Range<usize>> = vec![];
 174        let mut regions: Vec<ParsedRegion> = vec![];
 175        let mut highlights: Vec<(Range<usize>, MarkdownHighlight)> = vec![];
 176
 177        loop {
 178            if self.eof() {
 179                break;
 180            }
 181
 182            let (current, _source_range) = self.current().unwrap();
 183            let prev_len = text.len();
 184            match current {
 185                Event::SoftBreak => {
 186                    if should_complete_on_soft_break {
 187                        break;
 188                    }
 189
 190                    // `Some text\nSome more text` should be treated as a single line.
 191                    text.push(' ');
 192                }
 193
 194                Event::HardBreak => {
 195                    break;
 196                }
 197
 198                Event::Text(t) => {
 199                    text.push_str(t.as_ref());
 200
 201                    let mut style = MarkdownHighlightStyle::default();
 202
 203                    if bold_depth > 0 {
 204                        style.weight = FontWeight::BOLD;
 205                    }
 206
 207                    if italic_depth > 0 {
 208                        style.italic = true;
 209                    }
 210
 211                    if strikethrough_depth > 0 {
 212                        style.strikethrough = true;
 213                    }
 214
 215                    if let Some(link) = link.clone() {
 216                        region_ranges.push(prev_len..text.len());
 217                        regions.push(ParsedRegion {
 218                            code: false,
 219                            link: Some(link),
 220                        });
 221                        style.underline = true;
 222                    }
 223
 224                    if style != MarkdownHighlightStyle::default() {
 225                        let mut new_highlight = true;
 226                        if let Some((last_range, MarkdownHighlight::Style(last_style))) =
 227                            highlights.last_mut()
 228                        {
 229                            if last_range.end == prev_len && last_style == &style {
 230                                last_range.end = text.len();
 231                                new_highlight = false;
 232                            }
 233                        }
 234                        if new_highlight {
 235                            let range = prev_len..text.len();
 236                            highlights.push((range, MarkdownHighlight::Style(style)));
 237                        }
 238                    }
 239                }
 240
 241                // Note: This event means "inline code" and not "code block"
 242                Event::Code(t) => {
 243                    text.push_str(t.as_ref());
 244                    region_ranges.push(prev_len..text.len());
 245
 246                    if link.is_some() {
 247                        highlights.push((
 248                            prev_len..text.len(),
 249                            MarkdownHighlight::Style(MarkdownHighlightStyle {
 250                                underline: true,
 251                                ..Default::default()
 252                            }),
 253                        ));
 254                    }
 255
 256                    regions.push(ParsedRegion {
 257                        code: true,
 258                        link: link.clone(),
 259                    });
 260                }
 261
 262                Event::Start(tag) => match tag {
 263                    Tag::Emphasis => italic_depth += 1,
 264                    Tag::Strong => bold_depth += 1,
 265                    Tag::Strikethrough => strikethrough_depth += 1,
 266                    Tag::Link {
 267                        link_type: _,
 268                        dest_url,
 269                        title: _,
 270                        id: _,
 271                    } => {
 272                        link = Link::identify(
 273                            self.file_location_directory.clone(),
 274                            dest_url.to_string(),
 275                        );
 276                    }
 277                    _ => {
 278                        break;
 279                    }
 280                },
 281
 282                Event::End(tag) => match tag {
 283                    TagEnd::Emphasis => {
 284                        italic_depth -= 1;
 285                    }
 286                    TagEnd::Strong => {
 287                        bold_depth -= 1;
 288                    }
 289                    TagEnd::Strikethrough => {
 290                        strikethrough_depth -= 1;
 291                    }
 292                    TagEnd::Link => {
 293                        link = None;
 294                    }
 295                    TagEnd::Paragraph => {
 296                        self.cursor += 1;
 297                        break;
 298                    }
 299                    _ => {
 300                        break;
 301                    }
 302                },
 303
 304                _ => {
 305                    break;
 306                }
 307            }
 308
 309            self.cursor += 1;
 310        }
 311
 312        ParsedMarkdownText {
 313            source_range,
 314            contents: text,
 315            highlights,
 316            regions,
 317            region_ranges,
 318        }
 319    }
 320
 321    fn parse_heading(&mut self, level: pulldown_cmark::HeadingLevel) -> ParsedMarkdownHeading {
 322        let (_event, source_range) = self.previous().unwrap();
 323        let source_range = source_range.clone();
 324        let text = self.parse_text(true);
 325
 326        // Advance past the heading end tag
 327        self.cursor += 1;
 328
 329        ParsedMarkdownHeading {
 330            source_range: source_range.clone(),
 331            level: match level {
 332                pulldown_cmark::HeadingLevel::H1 => HeadingLevel::H1,
 333                pulldown_cmark::HeadingLevel::H2 => HeadingLevel::H2,
 334                pulldown_cmark::HeadingLevel::H3 => HeadingLevel::H3,
 335                pulldown_cmark::HeadingLevel::H4 => HeadingLevel::H4,
 336                pulldown_cmark::HeadingLevel::H5 => HeadingLevel::H5,
 337                pulldown_cmark::HeadingLevel::H6 => HeadingLevel::H6,
 338            },
 339            contents: text,
 340        }
 341    }
 342
 343    fn parse_table(&mut self, alignment: Vec<Alignment>) -> ParsedMarkdownTable {
 344        let (_event, source_range) = self.previous().unwrap();
 345        let source_range = source_range.clone();
 346        let mut header = ParsedMarkdownTableRow::new();
 347        let mut body = vec![];
 348        let mut current_row = vec![];
 349        let mut in_header = true;
 350        let column_alignments = alignment
 351            .iter()
 352            .map(|a| Self::convert_alignment(a))
 353            .collect();
 354
 355        loop {
 356            if self.eof() {
 357                break;
 358            }
 359
 360            let (current, _source_range) = self.current().unwrap();
 361            match current {
 362                Event::Start(Tag::TableHead)
 363                | Event::Start(Tag::TableRow)
 364                | Event::End(TagEnd::TableCell) => {
 365                    self.cursor += 1;
 366                }
 367                Event::Start(Tag::TableCell) => {
 368                    self.cursor += 1;
 369                    let cell_contents = self.parse_text(false);
 370                    current_row.push(cell_contents);
 371                }
 372                Event::End(TagEnd::TableHead) | Event::End(TagEnd::TableRow) => {
 373                    self.cursor += 1;
 374                    let new_row = std::mem::replace(&mut current_row, vec![]);
 375                    if in_header {
 376                        header.children = new_row;
 377                        in_header = false;
 378                    } else {
 379                        let row = ParsedMarkdownTableRow::with_children(new_row);
 380                        body.push(row);
 381                    }
 382                }
 383                Event::End(TagEnd::Table) => {
 384                    self.cursor += 1;
 385                    break;
 386                }
 387                _ => {
 388                    break;
 389                }
 390            }
 391        }
 392
 393        ParsedMarkdownTable {
 394            source_range,
 395            header,
 396            body,
 397            column_alignments,
 398        }
 399    }
 400
 401    fn convert_alignment(alignment: &Alignment) -> ParsedMarkdownTableAlignment {
 402        match alignment {
 403            Alignment::None => ParsedMarkdownTableAlignment::None,
 404            Alignment::Left => ParsedMarkdownTableAlignment::Left,
 405            Alignment::Center => ParsedMarkdownTableAlignment::Center,
 406            Alignment::Right => ParsedMarkdownTableAlignment::Right,
 407        }
 408    }
 409
 410    fn parse_list(&mut self, depth: u16, order: Option<u64>) -> ParsedMarkdownList {
 411        let (_event, source_range) = self.previous().unwrap();
 412        let source_range = source_range.clone();
 413        let mut children = vec![];
 414        let mut inside_list_item = false;
 415        let mut order = order;
 416        let mut task_item = None;
 417
 418        let mut current_list_items: Vec<Box<ParsedMarkdownElement>> = vec![];
 419
 420        while !self.eof() {
 421            let (current, _source_range) = self.current().unwrap();
 422            match current {
 423                Event::Start(Tag::List(order)) => {
 424                    let order = *order;
 425                    self.cursor += 1;
 426
 427                    let inner_list = self.parse_list(depth + 1, order);
 428                    let block = ParsedMarkdownElement::List(inner_list);
 429                    current_list_items.push(Box::new(block));
 430                }
 431                Event::End(TagEnd::List(_)) => {
 432                    self.cursor += 1;
 433                    break;
 434                }
 435                Event::Start(Tag::Item) => {
 436                    self.cursor += 1;
 437                    inside_list_item = true;
 438
 439                    // Check for task list marker (`- [ ]` or `- [x]`)
 440                    if let Some(next) = self.current() {
 441                        match next.0 {
 442                            Event::TaskListMarker(checked) => {
 443                                task_item = Some(checked);
 444                                self.cursor += 1;
 445                            }
 446                            _ => {}
 447                        }
 448                    }
 449
 450                    if let Some(next) = self.current() {
 451                        // This is a plain list item.
 452                        // For example `- some text` or `1. [Docs](./docs.md)`
 453                        if MarkdownParser::is_text_like(&next.0) {
 454                            let text = self.parse_text(false);
 455                            let block = ParsedMarkdownElement::Paragraph(text);
 456                            current_list_items.push(Box::new(block));
 457                        } else {
 458                            let block = self.parse_block();
 459                            if let Some(block) = block {
 460                                current_list_items.push(Box::new(block));
 461                            }
 462                        }
 463                    }
 464                }
 465                Event::End(TagEnd::Item) => {
 466                    self.cursor += 1;
 467
 468                    let item_type = if let Some(checked) = task_item {
 469                        ParsedMarkdownListItemType::Task(checked)
 470                    } else if let Some(order) = order {
 471                        ParsedMarkdownListItemType::Ordered(order)
 472                    } else {
 473                        ParsedMarkdownListItemType::Unordered
 474                    };
 475
 476                    if let Some(current) = order {
 477                        order = Some(current + 1);
 478                    }
 479
 480                    let contents = std::mem::replace(&mut current_list_items, vec![]);
 481
 482                    children.push(ParsedMarkdownListItem {
 483                        contents,
 484                        depth,
 485                        item_type,
 486                    });
 487
 488                    inside_list_item = false;
 489                    task_item = None;
 490                }
 491                _ => {
 492                    if !inside_list_item {
 493                        break;
 494                    }
 495
 496                    let block = self.parse_block();
 497                    if let Some(block) = block {
 498                        current_list_items.push(Box::new(block));
 499                    }
 500                }
 501            }
 502        }
 503
 504        ParsedMarkdownList {
 505            source_range,
 506            children,
 507        }
 508    }
 509
 510    fn parse_block_quote(&mut self) -> ParsedMarkdownBlockQuote {
 511        let (_event, source_range) = self.previous().unwrap();
 512        let source_range = source_range.clone();
 513        let mut nested_depth = 1;
 514
 515        let mut children: Vec<Box<ParsedMarkdownElement>> = vec![];
 516
 517        while !self.eof() {
 518            let block = self.parse_block();
 519
 520            if let Some(block) = block {
 521                children.push(Box::new(block));
 522            } else {
 523                break;
 524            }
 525
 526            if self.eof() {
 527                break;
 528            }
 529
 530            let (current, _source_range) = self.current().unwrap();
 531            match current {
 532                // This is a nested block quote.
 533                // Record that we're in a nested block quote and continue parsing.
 534                // We don't need to advance the cursor since the next
 535                // call to `parse_block` will handle it.
 536                Event::Start(Tag::BlockQuote) => {
 537                    nested_depth += 1;
 538                }
 539                Event::End(TagEnd::BlockQuote) => {
 540                    nested_depth -= 1;
 541                    if nested_depth == 0 {
 542                        self.cursor += 1;
 543                        break;
 544                    }
 545                }
 546                _ => {}
 547            };
 548        }
 549
 550        ParsedMarkdownBlockQuote {
 551            source_range,
 552            children,
 553        }
 554    }
 555
 556    fn parse_code_block(&mut self, language: Option<String>) -> ParsedMarkdownCodeBlock {
 557        let (_event, source_range) = self.previous().unwrap();
 558        let source_range = source_range.clone();
 559        let mut code = String::new();
 560
 561        while !self.eof() {
 562            let (current, _source_range) = self.current().unwrap();
 563            match current {
 564                Event::Text(text) => {
 565                    code.push_str(&text);
 566                    self.cursor += 1;
 567                }
 568                Event::End(TagEnd::CodeBlock) => {
 569                    self.cursor += 1;
 570                    break;
 571                }
 572                _ => {
 573                    break;
 574                }
 575            }
 576        }
 577
 578        ParsedMarkdownCodeBlock {
 579            source_range,
 580            contents: code.trim().to_string().into(),
 581            language,
 582        }
 583    }
 584}
 585
 586#[cfg(test)]
 587mod tests {
 588    use super::*;
 589
 590    use pretty_assertions::assert_eq;
 591
 592    use ParsedMarkdownElement::*;
 593    use ParsedMarkdownListItemType::*;
 594
 595    fn parse(input: &str) -> ParsedMarkdown {
 596        parse_markdown(input, None)
 597    }
 598
 599    #[test]
 600    fn test_headings() {
 601        let parsed = parse("# Heading one\n## Heading two\n### Heading three");
 602
 603        assert_eq!(
 604            parsed.children,
 605            vec![
 606                h1(text("Heading one", 0..14), 0..14),
 607                h2(text("Heading two", 14..29), 14..29),
 608                h3(text("Heading three", 29..46), 29..46),
 609            ]
 610        );
 611    }
 612
 613    #[test]
 614    fn test_newlines_dont_new_paragraphs() {
 615        let parsed = parse("Some text **that is bolded**\n and *italicized*");
 616
 617        assert_eq!(
 618            parsed.children,
 619            vec![p("Some text that is bolded and italicized", 0..46)]
 620        );
 621    }
 622
 623    #[test]
 624    fn test_heading_with_paragraph() {
 625        let parsed = parse("# Zed\nThe editor");
 626
 627        assert_eq!(
 628            parsed.children,
 629            vec![h1(text("Zed", 0..6), 0..6), p("The editor", 6..16),]
 630        );
 631    }
 632
 633    #[test]
 634    fn test_double_newlines_do_new_paragraphs() {
 635        let parsed = parse("Some text **that is bolded**\n\n and *italicized*");
 636
 637        assert_eq!(
 638            parsed.children,
 639            vec![
 640                p("Some text that is bolded", 0..29),
 641                p("and italicized", 31..47),
 642            ]
 643        );
 644    }
 645
 646    #[test]
 647    fn test_bold_italic_text() {
 648        let parsed = parse("Some text **that is bolded** and *italicized*");
 649
 650        assert_eq!(
 651            parsed.children,
 652            vec![p("Some text that is bolded and italicized", 0..45)]
 653        );
 654    }
 655
 656    #[test]
 657    fn test_nested_bold_strikethrough_text() {
 658        let parsed = parse("Some **bo~~strikethrough~~ld** text");
 659
 660        assert_eq!(parsed.children.len(), 1);
 661        assert_eq!(
 662            parsed.children[0],
 663            ParsedMarkdownElement::Paragraph(ParsedMarkdownText {
 664                source_range: 0..35,
 665                contents: "Some bostrikethroughld text".to_string(),
 666                highlights: Vec::new(),
 667                region_ranges: Vec::new(),
 668                regions: Vec::new(),
 669            })
 670        );
 671
 672        let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
 673            text
 674        } else {
 675            panic!("Expected a paragraph");
 676        };
 677        assert_eq!(
 678            paragraph.highlights,
 679            vec![
 680                (
 681                    5..7,
 682                    MarkdownHighlight::Style(MarkdownHighlightStyle {
 683                        weight: FontWeight::BOLD,
 684                        ..Default::default()
 685                    }),
 686                ),
 687                (
 688                    7..20,
 689                    MarkdownHighlight::Style(MarkdownHighlightStyle {
 690                        weight: FontWeight::BOLD,
 691                        strikethrough: true,
 692                        ..Default::default()
 693                    }),
 694                ),
 695                (
 696                    20..22,
 697                    MarkdownHighlight::Style(MarkdownHighlightStyle {
 698                        weight: FontWeight::BOLD,
 699                        ..Default::default()
 700                    }),
 701                ),
 702            ]
 703        );
 704    }
 705
 706    #[test]
 707    fn test_header_only_table() {
 708        let markdown = "\
 709| Header 1 | Header 2 |
 710|----------|----------|
 711
 712Some other content
 713";
 714
 715        let expected_table = table(
 716            0..48,
 717            row(vec![text("Header 1", 1..11), text("Header 2", 12..22)]),
 718            vec![],
 719        );
 720
 721        assert_eq!(
 722            parse(markdown).children[0],
 723            ParsedMarkdownElement::Table(expected_table)
 724        );
 725    }
 726
 727    #[test]
 728    fn test_basic_table() {
 729        let markdown = "\
 730| Header 1 | Header 2 |
 731|----------|----------|
 732| Cell 1   | Cell 2   |
 733| Cell 3   | Cell 4   |";
 734
 735        let expected_table = table(
 736            0..95,
 737            row(vec![text("Header 1", 1..11), text("Header 2", 12..22)]),
 738            vec![
 739                row(vec![text("Cell 1", 49..59), text("Cell 2", 60..70)]),
 740                row(vec![text("Cell 3", 73..83), text("Cell 4", 84..94)]),
 741            ],
 742        );
 743
 744        assert_eq!(
 745            parse(markdown).children[0],
 746            ParsedMarkdownElement::Table(expected_table)
 747        );
 748    }
 749
 750    #[test]
 751    fn test_list_basic() {
 752        let parsed = parse(
 753            "\
 754* Item 1
 755* Item 2
 756* Item 3
 757",
 758        );
 759
 760        assert_eq!(
 761            parsed.children,
 762            vec![list(
 763                vec![
 764                    list_item(1, Unordered, vec![p("Item 1", 0..9)]),
 765                    list_item(1, Unordered, vec![p("Item 2", 9..18)]),
 766                    list_item(1, Unordered, vec![p("Item 3", 18..27)]),
 767                ],
 768                0..27
 769            ),]
 770        );
 771    }
 772
 773    #[test]
 774    fn test_list_with_tasks() {
 775        let parsed = parse(
 776            "\
 777- [ ] TODO
 778- [x] Checked
 779",
 780        );
 781
 782        assert_eq!(
 783            parsed.children,
 784            vec![list(
 785                vec![
 786                    list_item(1, Task(false), vec![p("TODO", 2..5)]),
 787                    list_item(1, Task(true), vec![p("Checked", 13..16)]),
 788                ],
 789                0..25
 790            ),]
 791        );
 792    }
 793
 794    #[test]
 795    fn test_list_nested() {
 796        let parsed = parse(
 797            "\
 798* Item 1
 799* Item 2
 800* Item 3
 801
 8021. Hello
 8031. Two
 804   1. Three
 8052. Four
 8063. Five
 807
 808* First
 809  1. Hello
 810     1. Goodbyte
 811        - Inner
 812        - Inner
 813  2. Goodbyte
 814* Last
 815",
 816        );
 817
 818        assert_eq!(
 819            parsed.children,
 820            vec![
 821                list(
 822                    vec![
 823                        list_item(1, Unordered, vec![p("Item 1", 0..9)]),
 824                        list_item(1, Unordered, vec![p("Item 2", 9..18)]),
 825                        list_item(1, Unordered, vec![p("Item 3", 18..28)]),
 826                    ],
 827                    0..28
 828                ),
 829                list(
 830                    vec![
 831                        list_item(1, Ordered(1), vec![p("Hello", 28..37)]),
 832                        list_item(
 833                            1,
 834                            Ordered(2),
 835                            vec![
 836                                p("Two", 37..56),
 837                                list(
 838                                    vec![list_item(2, Ordered(1), vec![p("Three", 47..56)]),],
 839                                    47..56
 840                                ),
 841                            ]
 842                        ),
 843                        list_item(1, Ordered(3), vec![p("Four", 56..64)]),
 844                        list_item(1, Ordered(4), vec![p("Five", 64..73)]),
 845                    ],
 846                    28..73
 847                ),
 848                list(
 849                    vec![
 850                        list_item(
 851                            1,
 852                            Unordered,
 853                            vec![
 854                                p("First", 73..155),
 855                                list(
 856                                    vec![
 857                                        list_item(
 858                                            2,
 859                                            Ordered(1),
 860                                            vec![
 861                                                p("Hello", 83..141),
 862                                                list(
 863                                                    vec![list_item(
 864                                                        3,
 865                                                        Ordered(1),
 866                                                        vec![
 867                                                            p("Goodbyte", 97..141),
 868                                                            list(
 869                                                                vec![
 870                                                                    list_item(
 871                                                                        4,
 872                                                                        Unordered,
 873                                                                        vec![p("Inner", 117..125)]
 874                                                                    ),
 875                                                                    list_item(
 876                                                                        4,
 877                                                                        Unordered,
 878                                                                        vec![p("Inner", 133..141)]
 879                                                                    ),
 880                                                                ],
 881                                                                117..141
 882                                                            )
 883                                                        ]
 884                                                    ),],
 885                                                    97..141
 886                                                )
 887                                            ]
 888                                        ),
 889                                        list_item(2, Ordered(2), vec![p("Goodbyte", 143..155)]),
 890                                    ],
 891                                    83..155
 892                                )
 893                            ]
 894                        ),
 895                        list_item(1, Unordered, vec![p("Last", 155..162)]),
 896                    ],
 897                    73..162
 898                ),
 899            ]
 900        );
 901    }
 902
 903    #[test]
 904    fn test_list_with_nested_content() {
 905        let parsed = parse(
 906            "\
 907*   This is a list item with two paragraphs.
 908
 909    This is the second paragraph in the list item.",
 910        );
 911
 912        assert_eq!(
 913            parsed.children,
 914            vec![list(
 915                vec![list_item(
 916                    1,
 917                    Unordered,
 918                    vec![
 919                        p("This is a list item with two paragraphs.", 4..45),
 920                        p("This is the second paragraph in the list item.", 50..96)
 921                    ],
 922                ),],
 923                0..96,
 924            ),]
 925        );
 926    }
 927
 928    #[test]
 929    fn test_list_with_leading_text() {
 930        let parsed = parse(
 931            "\
 932* `code`
 933* **bold**
 934* [link](https://example.com)
 935",
 936        );
 937
 938        assert_eq!(
 939            parsed.children,
 940            vec![list(
 941                vec![
 942                    list_item(1, Unordered, vec![p("code", 0..9)],),
 943                    list_item(1, Unordered, vec![p("bold", 9..20)]),
 944                    list_item(1, Unordered, vec![p("link", 20..50)],)
 945                ],
 946                0..50,
 947            ),]
 948        );
 949    }
 950
 951    #[test]
 952    fn test_simple_block_quote() {
 953        let parsed = parse("> Simple block quote with **styled text**");
 954
 955        assert_eq!(
 956            parsed.children,
 957            vec![block_quote(
 958                vec![p("Simple block quote with styled text", 2..41)],
 959                0..41
 960            )]
 961        );
 962    }
 963
 964    #[test]
 965    fn test_simple_block_quote_with_multiple_lines() {
 966        let parsed = parse(
 967            "\
 968> # Heading
 969> More
 970> text
 971>
 972> More text
 973",
 974        );
 975
 976        assert_eq!(
 977            parsed.children,
 978            vec![block_quote(
 979                vec![
 980                    h1(text("Heading", 2..12), 2..12),
 981                    p("More text", 14..26),
 982                    p("More text", 30..40)
 983                ],
 984                0..40
 985            )]
 986        );
 987    }
 988
 989    #[test]
 990    fn test_nested_block_quote() {
 991        let parsed = parse(
 992            "\
 993> A
 994>
 995> > # B
 996>
 997> C
 998
 999More text
1000",
1001        );
1002
1003        assert_eq!(
1004            parsed.children,
1005            vec![
1006                block_quote(
1007                    vec![
1008                        p("A", 2..4),
1009                        block_quote(vec![h1(text("B", 10..14), 10..14)], 8..14),
1010                        p("C", 18..20)
1011                    ],
1012                    0..20
1013                ),
1014                p("More text", 21..31)
1015            ]
1016        );
1017    }
1018
1019    #[test]
1020    fn test_code_block() {
1021        let parsed = parse(
1022            "\
1023```
1024fn main() {
1025    return 0;
1026}
1027```
1028",
1029        );
1030
1031        assert_eq!(
1032            parsed.children,
1033            vec![code_block(None, "fn main() {\n    return 0;\n}", 0..35)]
1034        );
1035    }
1036
1037    #[test]
1038    fn test_code_block_with_language() {
1039        let parsed = parse(
1040            "\
1041```rust
1042fn main() {
1043    return 0;
1044}
1045```
1046",
1047        );
1048
1049        assert_eq!(
1050            parsed.children,
1051            vec![code_block(
1052                Some("rust".into()),
1053                "fn main() {\n    return 0;\n}",
1054                0..39
1055            )]
1056        );
1057    }
1058
1059    fn h1(contents: ParsedMarkdownText, source_range: Range<usize>) -> ParsedMarkdownElement {
1060        ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
1061            source_range,
1062            level: HeadingLevel::H1,
1063            contents,
1064        })
1065    }
1066
1067    fn h2(contents: ParsedMarkdownText, source_range: Range<usize>) -> ParsedMarkdownElement {
1068        ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
1069            source_range,
1070            level: HeadingLevel::H2,
1071            contents,
1072        })
1073    }
1074
1075    fn h3(contents: ParsedMarkdownText, source_range: Range<usize>) -> ParsedMarkdownElement {
1076        ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
1077            source_range,
1078            level: HeadingLevel::H3,
1079            contents,
1080        })
1081    }
1082
1083    fn p(contents: &str, source_range: Range<usize>) -> ParsedMarkdownElement {
1084        ParsedMarkdownElement::Paragraph(text(contents, source_range))
1085    }
1086
1087    fn text(contents: &str, source_range: Range<usize>) -> ParsedMarkdownText {
1088        ParsedMarkdownText {
1089            highlights: Vec::new(),
1090            region_ranges: Vec::new(),
1091            regions: Vec::new(),
1092            source_range,
1093            contents: contents.to_string(),
1094        }
1095    }
1096
1097    fn block_quote(
1098        children: Vec<ParsedMarkdownElement>,
1099        source_range: Range<usize>,
1100    ) -> ParsedMarkdownElement {
1101        ParsedMarkdownElement::BlockQuote(ParsedMarkdownBlockQuote {
1102            source_range,
1103            children: children.into_iter().map(Box::new).collect(),
1104        })
1105    }
1106
1107    fn code_block(
1108        language: Option<String>,
1109        code: &str,
1110        source_range: Range<usize>,
1111    ) -> ParsedMarkdownElement {
1112        ParsedMarkdownElement::CodeBlock(ParsedMarkdownCodeBlock {
1113            source_range,
1114            language,
1115            contents: code.to_string().into(),
1116        })
1117    }
1118
1119    fn list(
1120        children: Vec<ParsedMarkdownListItem>,
1121        source_range: Range<usize>,
1122    ) -> ParsedMarkdownElement {
1123        List(ParsedMarkdownList {
1124            source_range,
1125            children,
1126        })
1127    }
1128
1129    fn list_item(
1130        depth: u16,
1131        item_type: ParsedMarkdownListItemType,
1132        contents: Vec<ParsedMarkdownElement>,
1133    ) -> ParsedMarkdownListItem {
1134        ParsedMarkdownListItem {
1135            item_type,
1136            depth,
1137            contents: contents.into_iter().map(Box::new).collect(),
1138        }
1139    }
1140
1141    fn table(
1142        source_range: Range<usize>,
1143        header: ParsedMarkdownTableRow,
1144        body: Vec<ParsedMarkdownTableRow>,
1145    ) -> ParsedMarkdownTable {
1146        ParsedMarkdownTable {
1147            column_alignments: Vec::new(),
1148            source_range,
1149            header,
1150            body,
1151        }
1152    }
1153
1154    fn row(children: Vec<ParsedMarkdownText>) -> ParsedMarkdownTableRow {
1155        ParsedMarkdownTableRow { children }
1156    }
1157
1158    impl PartialEq for ParsedMarkdownTable {
1159        fn eq(&self, other: &Self) -> bool {
1160            self.source_range == other.source_range
1161                && self.header == other.header
1162                && self.body == other.body
1163        }
1164    }
1165
1166    impl PartialEq for ParsedMarkdownText {
1167        fn eq(&self, other: &Self) -> bool {
1168            self.source_range == other.source_range && self.contents == other.contents
1169        }
1170    }
1171}