markdown_parser.rs

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