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}