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}