1use crate::{
2 markdown_elements::*,
3 markdown_minifier::{Minifier, MinifierOptions},
4};
5use async_recursion::async_recursion;
6use collections::FxHashMap;
7use gpui::{DefiniteLength, FontWeight, px, relative};
8use html5ever::{ParseOpts, local_name, parse_document, tendril::TendrilSink};
9use language::LanguageRegistry;
10use markup5ever_rcdom::RcDom;
11use pulldown_cmark::{Alignment, Event, Options, Parser, Tag, TagEnd};
12use std::{
13 cell::RefCell, collections::HashMap, mem, ops::Range, path::PathBuf, rc::Rc, sync::Arc, vec,
14};
15
16pub async fn parse_markdown(
17 markdown_input: &str,
18 file_location_directory: Option<PathBuf>,
19 language_registry: Option<Arc<LanguageRegistry>>,
20) -> ParsedMarkdown {
21 let mut options = Options::all();
22 options.remove(pulldown_cmark::Options::ENABLE_DEFINITION_LIST);
23
24 let parser = Parser::new_ext(markdown_input, options);
25 let parser = MarkdownParser::new(
26 parser.into_offset_iter().collect(),
27 file_location_directory,
28 language_registry,
29 );
30 let renderer = parser.parse_document().await;
31 ParsedMarkdown {
32 children: renderer.parsed,
33 }
34}
35
36fn cleanup_html(source: &str) -> Vec<u8> {
37 let mut writer = std::io::Cursor::new(Vec::new());
38 let mut reader = std::io::Cursor::new(source);
39 let mut minify = Minifier::new(
40 &mut writer,
41 MinifierOptions {
42 omit_doctype: true,
43 collapse_whitespace: true,
44 ..Default::default()
45 },
46 );
47 if let Ok(()) = minify.minify(&mut reader) {
48 writer.into_inner()
49 } else {
50 source.bytes().collect()
51 }
52}
53
54struct MarkdownParser<'a> {
55 tokens: Vec<(Event<'a>, Range<usize>)>,
56 /// The current index in the tokens array
57 cursor: usize,
58 /// The blocks that we have successfully parsed so far
59 parsed: Vec<ParsedMarkdownElement>,
60 file_location_directory: Option<PathBuf>,
61 language_registry: Option<Arc<LanguageRegistry>>,
62}
63
64struct MarkdownListItem {
65 content: Vec<ParsedMarkdownElement>,
66 item_type: ParsedMarkdownListItemType,
67}
68
69impl Default for MarkdownListItem {
70 fn default() -> Self {
71 Self {
72 content: Vec::new(),
73 item_type: ParsedMarkdownListItemType::Unordered,
74 }
75 }
76}
77
78impl<'a> MarkdownParser<'a> {
79 fn new(
80 tokens: Vec<(Event<'a>, Range<usize>)>,
81 file_location_directory: Option<PathBuf>,
82 language_registry: Option<Arc<LanguageRegistry>>,
83 ) -> Self {
84 Self {
85 tokens,
86 file_location_directory,
87 language_registry,
88 cursor: 0,
89 parsed: vec![],
90 }
91 }
92
93 fn eof(&self) -> bool {
94 if self.tokens.is_empty() {
95 return true;
96 }
97 self.cursor >= self.tokens.len() - 1
98 }
99
100 fn peek(&self, steps: usize) -> Option<&(Event<'_>, Range<usize>)> {
101 if self.eof() || (steps + self.cursor) >= self.tokens.len() {
102 return self.tokens.last();
103 }
104 self.tokens.get(self.cursor + steps)
105 }
106
107 fn previous(&self) -> Option<&(Event<'_>, Range<usize>)> {
108 if self.cursor == 0 || self.cursor > self.tokens.len() {
109 return None;
110 }
111 self.tokens.get(self.cursor - 1)
112 }
113
114 fn current(&self) -> Option<&(Event<'_>, Range<usize>)> {
115 self.peek(0)
116 }
117
118 fn current_event(&self) -> Option<&Event<'_>> {
119 self.current().map(|(event, _)| event)
120 }
121
122 fn is_text_like(event: &Event) -> bool {
123 match event {
124 Event::Text(_)
125 // Represent an inline code block
126 | Event::Code(_)
127 | Event::Html(_)
128 | Event::InlineHtml(_)
129 | Event::FootnoteReference(_)
130 | Event::Start(Tag::Link { .. })
131 | Event::Start(Tag::Emphasis)
132 | Event::Start(Tag::Strong)
133 | Event::Start(Tag::Strikethrough)
134 | Event::Start(Tag::Image { .. }) => {
135 true
136 }
137 _ => false,
138 }
139 }
140
141 async fn parse_document(mut self) -> Self {
142 while !self.eof() {
143 if let Some(block) = self.parse_block().await {
144 self.parsed.extend(block);
145 } else {
146 self.cursor += 1;
147 }
148 }
149 self
150 }
151
152 #[async_recursion]
153 async fn parse_block(&mut self) -> Option<Vec<ParsedMarkdownElement>> {
154 let (current, source_range) = self.current().unwrap();
155 let source_range = source_range.clone();
156 match current {
157 Event::Start(tag) => match tag {
158 Tag::Paragraph => {
159 self.cursor += 1;
160 let text = self.parse_text(false, Some(source_range));
161 Some(vec![ParsedMarkdownElement::Paragraph(text)])
162 }
163 Tag::Heading { level, .. } => {
164 let level = *level;
165 self.cursor += 1;
166 let heading = self.parse_heading(level);
167 Some(vec![ParsedMarkdownElement::Heading(heading)])
168 }
169 Tag::Table(alignment) => {
170 let alignment = alignment.clone();
171 self.cursor += 1;
172 let table = self.parse_table(alignment);
173 Some(vec![ParsedMarkdownElement::Table(table)])
174 }
175 Tag::List(order) => {
176 let order = *order;
177 self.cursor += 1;
178 let list = self.parse_list(order).await;
179 Some(list)
180 }
181 Tag::BlockQuote(_kind) => {
182 self.cursor += 1;
183 let block_quote = self.parse_block_quote().await;
184 Some(vec![ParsedMarkdownElement::BlockQuote(block_quote)])
185 }
186 Tag::CodeBlock(kind) => {
187 let language = match kind {
188 pulldown_cmark::CodeBlockKind::Indented => None,
189 pulldown_cmark::CodeBlockKind::Fenced(language) => {
190 if language.is_empty() {
191 None
192 } else {
193 Some(language.to_string())
194 }
195 }
196 };
197
198 self.cursor += 1;
199
200 let code_block = self.parse_code_block(language).await?;
201 Some(vec![ParsedMarkdownElement::CodeBlock(code_block)])
202 }
203 Tag::HtmlBlock => {
204 self.cursor += 1;
205
206 Some(self.parse_html_block().await)
207 }
208 _ => None,
209 },
210 Event::Rule => {
211 self.cursor += 1;
212 Some(vec![ParsedMarkdownElement::HorizontalRule(source_range)])
213 }
214 _ => None,
215 }
216 }
217
218 fn parse_text(
219 &mut self,
220 should_complete_on_soft_break: bool,
221 source_range: Option<Range<usize>>,
222 ) -> MarkdownParagraph {
223 let source_range = source_range.unwrap_or_else(|| {
224 self.current()
225 .map(|(_, range)| range.clone())
226 .unwrap_or_default()
227 });
228
229 let mut markdown_text_like = Vec::new();
230 let mut text = String::new();
231 let mut bold_depth = 0;
232 let mut italic_depth = 0;
233 let mut strikethrough_depth = 0;
234 let mut link: Option<Link> = None;
235 let mut image: Option<Image> = None;
236 let mut region_ranges: Vec<Range<usize>> = vec![];
237 let mut regions: Vec<ParsedRegion> = vec![];
238 let mut highlights: Vec<(Range<usize>, MarkdownHighlight)> = vec![];
239 let mut link_urls: Vec<String> = vec![];
240 let mut link_ranges: Vec<Range<usize>> = vec![];
241
242 loop {
243 if self.eof() {
244 break;
245 }
246
247 let (current, _) = self.current().unwrap();
248 let prev_len = text.len();
249 match current {
250 Event::SoftBreak => {
251 if should_complete_on_soft_break {
252 break;
253 }
254 text.push(' ');
255 }
256
257 Event::HardBreak => {
258 text.push('\n');
259 }
260
261 // We want to ignore any inline HTML tags in the text but keep
262 // the text between them
263 Event::InlineHtml(_) => {}
264
265 Event::Text(t) => {
266 text.push_str(t.as_ref());
267 let mut style = MarkdownHighlightStyle::default();
268
269 if bold_depth > 0 {
270 style.weight = FontWeight::BOLD;
271 }
272
273 if italic_depth > 0 {
274 style.italic = true;
275 }
276
277 if strikethrough_depth > 0 {
278 style.strikethrough = true;
279 }
280
281 let last_run_len = if let Some(link) = link.clone() {
282 region_ranges.push(prev_len..text.len());
283 regions.push(ParsedRegion {
284 code: false,
285 link: Some(link),
286 });
287 style.link = true;
288 prev_len
289 } else {
290 // Manually scan for links
291 let mut finder = linkify::LinkFinder::new();
292 finder.kinds(&[linkify::LinkKind::Url]);
293 let mut last_link_len = prev_len;
294 for link in finder.links(t) {
295 let start = prev_len + link.start();
296 let end = prev_len + link.end();
297 let range = start..end;
298 link_ranges.push(range.clone());
299 link_urls.push(link.as_str().to_string());
300
301 // If there is a style before we match a link, we have to add this to the highlighted ranges
302 if style != MarkdownHighlightStyle::default() && last_link_len < start {
303 highlights.push((
304 last_link_len..start,
305 MarkdownHighlight::Style(style.clone()),
306 ));
307 }
308
309 highlights.push((
310 range.clone(),
311 MarkdownHighlight::Style(MarkdownHighlightStyle {
312 underline: true,
313 ..style
314 }),
315 ));
316 region_ranges.push(range.clone());
317 regions.push(ParsedRegion {
318 code: false,
319 link: Some(Link::Web {
320 url: link.as_str().to_string(),
321 }),
322 });
323 last_link_len = end;
324 }
325 last_link_len
326 };
327
328 if style != MarkdownHighlightStyle::default() && last_run_len < text.len() {
329 let mut new_highlight = true;
330 if let Some((last_range, last_style)) = highlights.last_mut()
331 && last_range.end == last_run_len
332 && last_style == &MarkdownHighlight::Style(style.clone())
333 {
334 last_range.end = text.len();
335 new_highlight = false;
336 }
337 if new_highlight {
338 highlights.push((
339 last_run_len..text.len(),
340 MarkdownHighlight::Style(style.clone()),
341 ));
342 }
343 }
344 }
345 Event::Code(t) => {
346 text.push_str(t.as_ref());
347 region_ranges.push(prev_len..text.len());
348
349 if link.is_some() {
350 highlights.push((
351 prev_len..text.len(),
352 MarkdownHighlight::Style(MarkdownHighlightStyle {
353 link: true,
354 ..Default::default()
355 }),
356 ));
357 }
358 regions.push(ParsedRegion {
359 code: true,
360 link: link.clone(),
361 });
362 }
363 Event::Start(tag) => match tag {
364 Tag::Emphasis => italic_depth += 1,
365 Tag::Strong => bold_depth += 1,
366 Tag::Strikethrough => strikethrough_depth += 1,
367 Tag::Link { dest_url, .. } => {
368 link = Link::identify(
369 self.file_location_directory.clone(),
370 dest_url.to_string(),
371 );
372 }
373 Tag::Image { dest_url, .. } => {
374 if !text.is_empty() {
375 let parsed_regions = MarkdownParagraphChunk::Text(ParsedMarkdownText {
376 source_range: source_range.clone(),
377 contents: mem::take(&mut text).into(),
378 highlights: mem::take(&mut highlights),
379 region_ranges: mem::take(&mut region_ranges),
380 regions: mem::take(&mut regions),
381 });
382 markdown_text_like.push(parsed_regions);
383 }
384 image = Image::identify(
385 dest_url.to_string(),
386 source_range.clone(),
387 self.file_location_directory.clone(),
388 );
389 }
390 _ => {
391 break;
392 }
393 },
394
395 Event::End(tag) => match tag {
396 TagEnd::Emphasis => italic_depth -= 1,
397 TagEnd::Strong => bold_depth -= 1,
398 TagEnd::Strikethrough => strikethrough_depth -= 1,
399 TagEnd::Link => {
400 link = None;
401 }
402 TagEnd::Image => {
403 if let Some(mut image) = image.take() {
404 if !text.is_empty() {
405 image.set_alt_text(std::mem::take(&mut text).into());
406 mem::take(&mut highlights);
407 mem::take(&mut region_ranges);
408 mem::take(&mut regions);
409 }
410 markdown_text_like.push(MarkdownParagraphChunk::Image(image));
411 }
412 }
413 TagEnd::Paragraph => {
414 self.cursor += 1;
415 break;
416 }
417 _ => {
418 break;
419 }
420 },
421 _ => {
422 break;
423 }
424 }
425
426 self.cursor += 1;
427 }
428 if !text.is_empty() {
429 markdown_text_like.push(MarkdownParagraphChunk::Text(ParsedMarkdownText {
430 source_range,
431 contents: text.into(),
432 highlights,
433 regions,
434 region_ranges,
435 }));
436 }
437 markdown_text_like
438 }
439
440 fn parse_heading(&mut self, level: pulldown_cmark::HeadingLevel) -> ParsedMarkdownHeading {
441 let (_event, source_range) = self.previous().unwrap();
442 let source_range = source_range.clone();
443 let text = self.parse_text(true, None);
444
445 // Advance past the heading end tag
446 self.cursor += 1;
447
448 ParsedMarkdownHeading {
449 source_range,
450 level: match level {
451 pulldown_cmark::HeadingLevel::H1 => HeadingLevel::H1,
452 pulldown_cmark::HeadingLevel::H2 => HeadingLevel::H2,
453 pulldown_cmark::HeadingLevel::H3 => HeadingLevel::H3,
454 pulldown_cmark::HeadingLevel::H4 => HeadingLevel::H4,
455 pulldown_cmark::HeadingLevel::H5 => HeadingLevel::H5,
456 pulldown_cmark::HeadingLevel::H6 => HeadingLevel::H6,
457 },
458 contents: text,
459 }
460 }
461
462 fn parse_table(&mut self, alignment: Vec<Alignment>) -> ParsedMarkdownTable {
463 let (_event, source_range) = self.previous().unwrap();
464 let source_range = source_range.clone();
465 let mut header = vec![];
466 let mut body = vec![];
467 let mut row_columns = vec![];
468 let mut in_header = true;
469 let column_alignments = alignment.iter().map(Self::convert_alignment).collect();
470
471 loop {
472 if self.eof() {
473 break;
474 }
475
476 let (current, source_range) = self.current().unwrap();
477 let source_range = source_range.clone();
478 match current {
479 Event::Start(Tag::TableHead)
480 | Event::Start(Tag::TableRow)
481 | Event::End(TagEnd::TableCell) => {
482 self.cursor += 1;
483 }
484 Event::Start(Tag::TableCell) => {
485 self.cursor += 1;
486 let cell_contents = self.parse_text(false, Some(source_range));
487 row_columns.push(ParsedMarkdownTableColumn {
488 col_span: 1,
489 row_span: 1,
490 is_header: in_header,
491 children: cell_contents,
492 });
493 }
494 Event::End(TagEnd::TableHead) | Event::End(TagEnd::TableRow) => {
495 self.cursor += 1;
496 let columns = std::mem::take(&mut row_columns);
497 if in_header {
498 header.push(ParsedMarkdownTableRow { columns: columns });
499 in_header = false;
500 } else {
501 body.push(ParsedMarkdownTableRow::with_columns(columns));
502 }
503 }
504 Event::End(TagEnd::Table) => {
505 self.cursor += 1;
506 break;
507 }
508 _ => {
509 break;
510 }
511 }
512 }
513
514 ParsedMarkdownTable {
515 source_range,
516 header,
517 body,
518 column_alignments,
519 }
520 }
521
522 fn convert_alignment(alignment: &Alignment) -> ParsedMarkdownTableAlignment {
523 match alignment {
524 Alignment::None => ParsedMarkdownTableAlignment::None,
525 Alignment::Left => ParsedMarkdownTableAlignment::Left,
526 Alignment::Center => ParsedMarkdownTableAlignment::Center,
527 Alignment::Right => ParsedMarkdownTableAlignment::Right,
528 }
529 }
530
531 async fn parse_list(&mut self, order: Option<u64>) -> Vec<ParsedMarkdownElement> {
532 let (_, list_source_range) = self.previous().unwrap();
533
534 let mut items = Vec::new();
535 let mut items_stack = vec![MarkdownListItem::default()];
536 let mut depth = 1;
537 let mut order = order;
538 let mut order_stack = Vec::new();
539
540 let mut insertion_indices = FxHashMap::default();
541 let mut source_ranges = FxHashMap::default();
542 let mut start_item_range = list_source_range.clone();
543
544 while !self.eof() {
545 let (current, source_range) = self.current().unwrap();
546 match current {
547 Event::Start(Tag::List(new_order)) => {
548 if items_stack.last().is_some() && !insertion_indices.contains_key(&depth) {
549 insertion_indices.insert(depth, items.len());
550 }
551
552 // We will use the start of the nested list as the end for the current item's range,
553 // because we don't care about the hierarchy of list items
554 if let collections::hash_map::Entry::Vacant(e) = source_ranges.entry(depth) {
555 e.insert(start_item_range.start..source_range.start);
556 }
557
558 order_stack.push(order);
559 order = *new_order;
560 self.cursor += 1;
561 depth += 1;
562 }
563 Event::End(TagEnd::List(_)) => {
564 order = order_stack.pop().flatten();
565 self.cursor += 1;
566 depth -= 1;
567
568 if depth == 0 {
569 break;
570 }
571 }
572 Event::Start(Tag::Item) => {
573 start_item_range = source_range.clone();
574
575 self.cursor += 1;
576 items_stack.push(MarkdownListItem::default());
577
578 let mut task_list = None;
579 // Check for task list marker (`- [ ]` or `- [x]`)
580 if let Some(event) = self.current_event() {
581 // If there is a linebreak in between two list items the task list marker will actually be the first element of the paragraph
582 if event == &Event::Start(Tag::Paragraph) {
583 self.cursor += 1;
584 }
585
586 if let Some((Event::TaskListMarker(checked), range)) = self.current() {
587 task_list = Some((*checked, range.clone()));
588 self.cursor += 1;
589 }
590 }
591
592 if let Some((event, range)) = self.current() {
593 // This is a plain list item.
594 // For example `- some text` or `1. [Docs](./docs.md)`
595 if MarkdownParser::is_text_like(event) {
596 let text = self.parse_text(false, Some(range.clone()));
597 let block = ParsedMarkdownElement::Paragraph(text);
598 if let Some(content) = items_stack.last_mut() {
599 let item_type = if let Some((checked, range)) = task_list {
600 ParsedMarkdownListItemType::Task(checked, range)
601 } else if let Some(order) = order {
602 ParsedMarkdownListItemType::Ordered(order)
603 } else {
604 ParsedMarkdownListItemType::Unordered
605 };
606 content.item_type = item_type;
607 content.content.push(block);
608 }
609 } else {
610 let block = self.parse_block().await;
611 if let Some(block) = block
612 && let Some(list_item) = items_stack.last_mut()
613 {
614 list_item.content.extend(block);
615 }
616 }
617 }
618
619 // If there is a linebreak in between two list items the task list marker will actually be the first element of the paragraph
620 if self.current_event() == Some(&Event::End(TagEnd::Paragraph)) {
621 self.cursor += 1;
622 }
623 }
624 Event::End(TagEnd::Item) => {
625 self.cursor += 1;
626
627 if let Some(current) = order {
628 order = Some(current + 1);
629 }
630
631 if let Some(list_item) = items_stack.pop() {
632 let source_range = source_ranges
633 .remove(&depth)
634 .unwrap_or(start_item_range.clone());
635
636 // We need to remove the last character of the source range, because it includes the newline character
637 let source_range = source_range.start..source_range.end - 1;
638 let item = ParsedMarkdownElement::ListItem(ParsedMarkdownListItem {
639 source_range,
640 content: list_item.content,
641 depth,
642 item_type: list_item.item_type,
643 });
644
645 if let Some(index) = insertion_indices.get(&depth) {
646 items.insert(*index, item);
647 insertion_indices.remove(&depth);
648 } else {
649 items.push(item);
650 }
651 }
652 }
653 _ => {
654 if depth == 0 {
655 break;
656 }
657 // This can only happen if a list item starts with more then one paragraph,
658 // or the list item contains blocks that should be rendered after the nested list items
659 let block = self.parse_block().await;
660 if let Some(block) = block {
661 if let Some(list_item) = items_stack.last_mut() {
662 // If we did not insert any nested items yet (in this case insertion index is set), we can append the block to the current list item
663 if !insertion_indices.contains_key(&depth) {
664 list_item.content.extend(block);
665 continue;
666 }
667 }
668
669 // Otherwise we need to insert the block after all the nested items
670 // that have been parsed so far
671 items.extend(block);
672 } else {
673 self.cursor += 1;
674 }
675 }
676 }
677 }
678
679 items
680 }
681
682 #[async_recursion]
683 async fn parse_block_quote(&mut self) -> ParsedMarkdownBlockQuote {
684 let (_event, source_range) = self.previous().unwrap();
685 let source_range = source_range.clone();
686 let mut nested_depth = 1;
687
688 let mut children: Vec<ParsedMarkdownElement> = vec![];
689
690 while !self.eof() {
691 let block = self.parse_block().await;
692
693 if let Some(block) = block {
694 children.extend(block);
695 } else {
696 break;
697 }
698
699 if self.eof() {
700 break;
701 }
702
703 let (current, _source_range) = self.current().unwrap();
704 match current {
705 // This is a nested block quote.
706 // Record that we're in a nested block quote and continue parsing.
707 // We don't need to advance the cursor since the next
708 // call to `parse_block` will handle it.
709 Event::Start(Tag::BlockQuote(_kind)) => {
710 nested_depth += 1;
711 }
712 Event::End(TagEnd::BlockQuote(_kind)) => {
713 nested_depth -= 1;
714 if nested_depth == 0 {
715 self.cursor += 1;
716 break;
717 }
718 }
719 _ => {}
720 };
721 }
722
723 ParsedMarkdownBlockQuote {
724 source_range,
725 children,
726 }
727 }
728
729 async fn parse_code_block(
730 &mut self,
731 language: Option<String>,
732 ) -> Option<ParsedMarkdownCodeBlock> {
733 let Some((_event, source_range)) = self.previous() else {
734 return None;
735 };
736
737 let source_range = source_range.clone();
738 let mut code = String::new();
739
740 while !self.eof() {
741 let Some((current, _source_range)) = self.current() else {
742 break;
743 };
744
745 match current {
746 Event::Text(text) => {
747 code.push_str(text);
748 self.cursor += 1;
749 }
750 Event::End(TagEnd::CodeBlock) => {
751 self.cursor += 1;
752 break;
753 }
754 _ => {
755 break;
756 }
757 }
758 }
759
760 code = code.strip_suffix('\n').unwrap_or(&code).to_string();
761
762 let highlights = if let Some(language) = &language {
763 if let Some(registry) = &self.language_registry {
764 let rope: language::Rope = code.as_str().into();
765 registry
766 .language_for_name_or_extension(language)
767 .await
768 .map(|l| l.highlight_text(&rope, 0..code.len()))
769 .ok()
770 } else {
771 None
772 }
773 } else {
774 None
775 };
776
777 Some(ParsedMarkdownCodeBlock {
778 source_range,
779 contents: code.into(),
780 language,
781 highlights,
782 })
783 }
784
785 async fn parse_html_block(&mut self) -> Vec<ParsedMarkdownElement> {
786 let mut elements = Vec::new();
787 let Some((_event, _source_range)) = self.previous() else {
788 return elements;
789 };
790
791 let mut html_source_range_start = None;
792 let mut html_source_range_end = None;
793 let mut html_buffer = String::new();
794
795 while !self.eof() {
796 let Some((current, source_range)) = self.current() else {
797 break;
798 };
799 let source_range = source_range.clone();
800 match current {
801 Event::Html(html) => {
802 html_source_range_start.get_or_insert(source_range.start);
803 html_source_range_end = Some(source_range.end);
804 html_buffer.push_str(html);
805 self.cursor += 1;
806 }
807 Event::End(TagEnd::CodeBlock) => {
808 self.cursor += 1;
809 break;
810 }
811 _ => {
812 break;
813 }
814 }
815 }
816
817 let bytes = cleanup_html(&html_buffer);
818
819 let mut cursor = std::io::Cursor::new(bytes);
820 if let Ok(dom) = parse_document(RcDom::default(), ParseOpts::default())
821 .from_utf8()
822 .read_from(&mut cursor)
823 && let Some((start, end)) = html_source_range_start.zip(html_source_range_end)
824 {
825 self.parse_html_node(start..end, &dom.document, &mut elements);
826 }
827
828 elements
829 }
830
831 fn parse_html_node(
832 &self,
833 source_range: Range<usize>,
834 node: &Rc<markup5ever_rcdom::Node>,
835 elements: &mut Vec<ParsedMarkdownElement>,
836 ) {
837 match &node.data {
838 markup5ever_rcdom::NodeData::Document => {
839 self.consume_children(source_range, node, elements);
840 }
841 markup5ever_rcdom::NodeData::Text { contents } => {
842 elements.push(ParsedMarkdownElement::Paragraph(vec![
843 MarkdownParagraphChunk::Text(ParsedMarkdownText {
844 source_range,
845 regions: Vec::default(),
846 region_ranges: Vec::default(),
847 highlights: Vec::default(),
848 contents: contents.borrow().to_string().into(),
849 }),
850 ]));
851 }
852 markup5ever_rcdom::NodeData::Comment { .. } => {}
853 markup5ever_rcdom::NodeData::Element { name, attrs, .. } => {
854 if local_name!("img") == name.local {
855 if let Some(image) = self.extract_image(source_range, attrs) {
856 elements.push(ParsedMarkdownElement::Image(image));
857 }
858 } else if local_name!("p") == name.local {
859 let mut paragraph = MarkdownParagraph::new();
860 self.parse_paragraph(source_range, node, &mut paragraph);
861
862 if !paragraph.is_empty() {
863 elements.push(ParsedMarkdownElement::Paragraph(paragraph));
864 }
865 } else if matches!(
866 name.local,
867 local_name!("h1")
868 | local_name!("h2")
869 | local_name!("h3")
870 | local_name!("h4")
871 | local_name!("h5")
872 | local_name!("h6")
873 ) {
874 let mut paragraph = MarkdownParagraph::new();
875 self.consume_paragraph(source_range.clone(), node, &mut paragraph);
876
877 if !paragraph.is_empty() {
878 elements.push(ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
879 source_range,
880 level: match name.local {
881 local_name!("h1") => HeadingLevel::H1,
882 local_name!("h2") => HeadingLevel::H2,
883 local_name!("h3") => HeadingLevel::H3,
884 local_name!("h4") => HeadingLevel::H4,
885 local_name!("h5") => HeadingLevel::H5,
886 local_name!("h6") => HeadingLevel::H6,
887 _ => unreachable!(),
888 },
889 contents: paragraph,
890 }));
891 }
892 } else if local_name!("blockquote") == name.local {
893 if let Some(blockquote) = self.extract_html_blockquote(node, source_range) {
894 elements.push(ParsedMarkdownElement::BlockQuote(blockquote));
895 }
896 } else if local_name!("table") == name.local {
897 if let Some(table) = self.extract_html_table(node, source_range) {
898 elements.push(ParsedMarkdownElement::Table(table));
899 }
900 } else {
901 self.consume_children(source_range, node, elements);
902 }
903 }
904 _ => {}
905 }
906 }
907
908 fn parse_paragraph(
909 &self,
910 source_range: Range<usize>,
911 node: &Rc<markup5ever_rcdom::Node>,
912 paragraph: &mut MarkdownParagraph,
913 ) {
914 match &node.data {
915 markup5ever_rcdom::NodeData::Text { contents } => {
916 paragraph.push(MarkdownParagraphChunk::Text(ParsedMarkdownText {
917 source_range,
918 regions: Vec::default(),
919 region_ranges: Vec::default(),
920 highlights: Vec::default(),
921 contents: contents.borrow().to_string().into(),
922 }));
923 }
924 markup5ever_rcdom::NodeData::Element { name, attrs, .. } => {
925 if local_name!("img") == name.local {
926 if let Some(image) = self.extract_image(source_range, attrs) {
927 paragraph.push(MarkdownParagraphChunk::Image(image));
928 }
929 } else {
930 self.consume_paragraph(source_range, node, paragraph);
931 }
932 }
933 _ => {}
934 }
935 }
936
937 fn consume_paragraph(
938 &self,
939 source_range: Range<usize>,
940 node: &Rc<markup5ever_rcdom::Node>,
941 paragraph: &mut MarkdownParagraph,
942 ) {
943 for node in node.children.borrow().iter() {
944 self.parse_paragraph(source_range.clone(), node, paragraph);
945 }
946 }
947
948 fn parse_table_row(
949 &self,
950 source_range: Range<usize>,
951 node: &Rc<markup5ever_rcdom::Node>,
952 ) -> Option<ParsedMarkdownTableRow> {
953 let mut columns = Vec::new();
954
955 match &node.data {
956 markup5ever_rcdom::NodeData::Element { name, .. } => {
957 if local_name!("tr") != name.local {
958 return None;
959 }
960
961 for node in node.children.borrow().iter() {
962 if let Some(column) = self.parse_table_column(source_range.clone(), node) {
963 columns.push(column);
964 }
965 }
966 }
967 _ => {}
968 }
969
970 if columns.is_empty() {
971 None
972 } else {
973 Some(ParsedMarkdownTableRow { columns })
974 }
975 }
976
977 fn parse_table_column(
978 &self,
979 source_range: Range<usize>,
980 node: &Rc<markup5ever_rcdom::Node>,
981 ) -> Option<ParsedMarkdownTableColumn> {
982 match &node.data {
983 markup5ever_rcdom::NodeData::Element { name, attrs, .. } => {
984 if !matches!(name.local, local_name!("th") | local_name!("td")) {
985 return None;
986 }
987
988 let mut children = MarkdownParagraph::new();
989 self.consume_paragraph(source_range, node, &mut children);
990
991 Some(ParsedMarkdownTableColumn {
992 col_span: std::cmp::max(
993 Self::attr_value(attrs, local_name!("colspan"))
994 .and_then(|span| span.parse().ok())
995 .unwrap_or(1),
996 1,
997 ),
998 row_span: std::cmp::max(
999 Self::attr_value(attrs, local_name!("rowspan"))
1000 .and_then(|span| span.parse().ok())
1001 .unwrap_or(1),
1002 1,
1003 ),
1004 is_header: matches!(name.local, local_name!("th")),
1005 children,
1006 })
1007 }
1008 _ => None,
1009 }
1010 }
1011
1012 fn consume_children(
1013 &self,
1014 source_range: Range<usize>,
1015 node: &Rc<markup5ever_rcdom::Node>,
1016 elements: &mut Vec<ParsedMarkdownElement>,
1017 ) {
1018 for node in node.children.borrow().iter() {
1019 self.parse_html_node(source_range.clone(), node, elements);
1020 }
1021 }
1022
1023 fn attr_value(
1024 attrs: &RefCell<Vec<html5ever::Attribute>>,
1025 name: html5ever::LocalName,
1026 ) -> Option<String> {
1027 attrs.borrow().iter().find_map(|attr| {
1028 if attr.name.local == name {
1029 Some(attr.value.to_string())
1030 } else {
1031 None
1032 }
1033 })
1034 }
1035
1036 fn extract_styles_from_attributes(
1037 attrs: &RefCell<Vec<html5ever::Attribute>>,
1038 ) -> HashMap<String, String> {
1039 let mut styles = HashMap::new();
1040
1041 if let Some(style) = Self::attr_value(attrs, local_name!("style")) {
1042 for decl in style.split(';') {
1043 let mut parts = decl.splitn(2, ':');
1044 if let Some((key, value)) = parts.next().zip(parts.next()) {
1045 styles.insert(
1046 key.trim().to_lowercase().to_string(),
1047 value.trim().to_string(),
1048 );
1049 }
1050 }
1051 }
1052
1053 styles
1054 }
1055
1056 fn extract_image(
1057 &self,
1058 source_range: Range<usize>,
1059 attrs: &RefCell<Vec<html5ever::Attribute>>,
1060 ) -> Option<Image> {
1061 let src = Self::attr_value(attrs, local_name!("src"))?;
1062
1063 let mut image = Image::identify(src, source_range, self.file_location_directory.clone())?;
1064
1065 if let Some(alt) = Self::attr_value(attrs, local_name!("alt")) {
1066 image.set_alt_text(alt.into());
1067 }
1068
1069 let styles = Self::extract_styles_from_attributes(attrs);
1070
1071 if let Some(width) = Self::attr_value(attrs, local_name!("width"))
1072 .or_else(|| styles.get("width").cloned())
1073 .and_then(|width| Self::parse_html_element_dimension(&width))
1074 {
1075 image.set_width(width);
1076 }
1077
1078 if let Some(height) = Self::attr_value(attrs, local_name!("height"))
1079 .or_else(|| styles.get("height").cloned())
1080 .and_then(|height| Self::parse_html_element_dimension(&height))
1081 {
1082 image.set_height(height);
1083 }
1084
1085 Some(image)
1086 }
1087
1088 fn parse_html_element_dimension(value: &str) -> Option<DefiniteLength> {
1089 if value.ends_with("%") {
1090 value
1091 .trim_end_matches("%")
1092 .parse::<f32>()
1093 .ok()
1094 .map(|value| relative(value / 100.))
1095 } else {
1096 value
1097 .trim_end_matches("px")
1098 .parse()
1099 .ok()
1100 .map(|value| px(value).into())
1101 }
1102 }
1103
1104 fn extract_html_blockquote(
1105 &self,
1106 node: &Rc<markup5ever_rcdom::Node>,
1107 source_range: Range<usize>,
1108 ) -> Option<ParsedMarkdownBlockQuote> {
1109 let mut children = Vec::new();
1110 self.consume_children(source_range.clone(), node, &mut children);
1111
1112 if children.is_empty() {
1113 None
1114 } else {
1115 Some(ParsedMarkdownBlockQuote {
1116 children,
1117 source_range,
1118 })
1119 }
1120 }
1121
1122 fn extract_html_table(
1123 &self,
1124 node: &Rc<markup5ever_rcdom::Node>,
1125 source_range: Range<usize>,
1126 ) -> Option<ParsedMarkdownTable> {
1127 let mut header_rows = Vec::new();
1128 let mut body_rows = Vec::new();
1129
1130 // node should be a thead or tbody element
1131 for node in node.children.borrow().iter() {
1132 match &node.data {
1133 markup5ever_rcdom::NodeData::Element { name, .. } => {
1134 if local_name!("thead") == name.local {
1135 // node should be a tr element
1136 for node in node.children.borrow().iter() {
1137 if let Some(row) = self.parse_table_row(source_range.clone(), node) {
1138 header_rows.push(row);
1139 }
1140 }
1141 } else if local_name!("tbody") == name.local {
1142 // node should be a tr element
1143 for node in node.children.borrow().iter() {
1144 if let Some(row) = self.parse_table_row(source_range.clone(), node) {
1145 body_rows.push(row);
1146 }
1147 }
1148 }
1149 }
1150 _ => {}
1151 }
1152 }
1153
1154 if !header_rows.is_empty() || !body_rows.is_empty() {
1155 Some(ParsedMarkdownTable {
1156 source_range,
1157 body: body_rows,
1158 column_alignments: Vec::default(),
1159 header: header_rows,
1160 })
1161 } else {
1162 None
1163 }
1164 }
1165}
1166
1167#[cfg(test)]
1168mod tests {
1169 use super::*;
1170 use ParsedMarkdownListItemType::*;
1171 use core::panic;
1172 use gpui::{AbsoluteLength, BackgroundExecutor, DefiniteLength};
1173 use language::{
1174 HighlightId, Language, LanguageConfig, LanguageMatcher, LanguageRegistry, tree_sitter_rust,
1175 };
1176 use pretty_assertions::assert_eq;
1177
1178 async fn parse(input: &str) -> ParsedMarkdown {
1179 parse_markdown(input, None, None).await
1180 }
1181
1182 #[gpui::test]
1183 async fn test_headings() {
1184 let parsed = parse("# Heading one\n## Heading two\n### Heading three").await;
1185
1186 assert_eq!(
1187 parsed.children,
1188 vec![
1189 h1(text("Heading one", 2..13), 0..14),
1190 h2(text("Heading two", 17..28), 14..29),
1191 h3(text("Heading three", 33..46), 29..46),
1192 ]
1193 );
1194 }
1195
1196 #[gpui::test]
1197 async fn test_newlines_dont_new_paragraphs() {
1198 let parsed = parse("Some text **that is bolded**\n and *italicized*").await;
1199
1200 assert_eq!(
1201 parsed.children,
1202 vec![p("Some text that is bolded and italicized", 0..46)]
1203 );
1204 }
1205
1206 #[gpui::test]
1207 async fn test_heading_with_paragraph() {
1208 let parsed = parse("# Zed\nThe editor").await;
1209
1210 assert_eq!(
1211 parsed.children,
1212 vec![h1(text("Zed", 2..5), 0..6), p("The editor", 6..16),]
1213 );
1214 }
1215
1216 #[gpui::test]
1217 async fn test_double_newlines_do_new_paragraphs() {
1218 let parsed = parse("Some text **that is bolded**\n\n and *italicized*").await;
1219
1220 assert_eq!(
1221 parsed.children,
1222 vec![
1223 p("Some text that is bolded", 0..29),
1224 p("and italicized", 31..47),
1225 ]
1226 );
1227 }
1228
1229 #[gpui::test]
1230 async fn test_bold_italic_text() {
1231 let parsed = parse("Some text **that is bolded** and *italicized*").await;
1232
1233 assert_eq!(
1234 parsed.children,
1235 vec![p("Some text that is bolded and italicized", 0..45)]
1236 );
1237 }
1238
1239 #[gpui::test]
1240 async fn test_nested_bold_strikethrough_text() {
1241 let parsed = parse("Some **bo~~strikethrough~~ld** text").await;
1242
1243 assert_eq!(parsed.children.len(), 1);
1244 assert_eq!(
1245 parsed.children[0],
1246 ParsedMarkdownElement::Paragraph(vec![MarkdownParagraphChunk::Text(
1247 ParsedMarkdownText {
1248 source_range: 0..35,
1249 contents: "Some bostrikethroughld text".into(),
1250 highlights: Vec::new(),
1251 region_ranges: Vec::new(),
1252 regions: Vec::new(),
1253 }
1254 )])
1255 );
1256
1257 let new_text = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
1258 text
1259 } else {
1260 panic!("Expected a paragraph");
1261 };
1262
1263 let paragraph = if let MarkdownParagraphChunk::Text(text) = &new_text[0] {
1264 text
1265 } else {
1266 panic!("Expected a text");
1267 };
1268
1269 assert_eq!(
1270 paragraph.highlights,
1271 vec![
1272 (
1273 5..7,
1274 MarkdownHighlight::Style(MarkdownHighlightStyle {
1275 weight: FontWeight::BOLD,
1276 ..Default::default()
1277 }),
1278 ),
1279 (
1280 7..20,
1281 MarkdownHighlight::Style(MarkdownHighlightStyle {
1282 weight: FontWeight::BOLD,
1283 strikethrough: true,
1284 ..Default::default()
1285 }),
1286 ),
1287 (
1288 20..22,
1289 MarkdownHighlight::Style(MarkdownHighlightStyle {
1290 weight: FontWeight::BOLD,
1291 ..Default::default()
1292 }),
1293 ),
1294 ]
1295 );
1296 }
1297
1298 #[gpui::test]
1299 async fn test_text_with_inline_html() {
1300 let parsed = parse("This is a paragraph with an inline HTML <sometag>tag</sometag>.").await;
1301
1302 assert_eq!(
1303 parsed.children,
1304 vec![p("This is a paragraph with an inline HTML tag.", 0..63),],
1305 );
1306 }
1307
1308 #[gpui::test]
1309 async fn test_raw_links_detection() {
1310 let parsed = parse("Checkout this https://zed.dev link").await;
1311
1312 assert_eq!(
1313 parsed.children,
1314 vec![p("Checkout this https://zed.dev link", 0..34)]
1315 );
1316 }
1317
1318 #[gpui::test]
1319 async fn test_empty_image() {
1320 let parsed = parse("![]()").await;
1321
1322 let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
1323 text
1324 } else {
1325 panic!("Expected a paragraph");
1326 };
1327 assert_eq!(paragraph.len(), 0);
1328 }
1329
1330 #[gpui::test]
1331 async fn test_image_links_detection() {
1332 let parsed = parse("").await;
1333
1334 let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
1335 text
1336 } else {
1337 panic!("Expected a paragraph");
1338 };
1339 assert_eq!(
1340 paragraph[0],
1341 MarkdownParagraphChunk::Image(Image {
1342 source_range: 0..111,
1343 link: Link::Web {
1344 url: "https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png".to_string(),
1345 },
1346 alt_text: Some("test".into()),
1347 height: None,
1348 width: None,
1349 },)
1350 );
1351 }
1352
1353 #[gpui::test]
1354 async fn test_image_alt_text() {
1355 let parsed = parse("[](https://zed.dev)\n ").await;
1356
1357 let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
1358 text
1359 } else {
1360 panic!("Expected a paragraph");
1361 };
1362 assert_eq!(
1363 paragraph[0],
1364 MarkdownParagraphChunk::Image(Image {
1365 source_range: 0..142,
1366 link: Link::Web {
1367 url: "https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/zed-industries/zed/main/assets/badge/v0.json".to_string(),
1368 },
1369 alt_text: Some("Zed".into()),
1370 height: None,
1371 width: None,
1372 },)
1373 );
1374 }
1375
1376 #[gpui::test]
1377 async fn test_image_without_alt_text() {
1378 let parsed = parse("").await;
1379
1380 let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
1381 text
1382 } else {
1383 panic!("Expected a paragraph");
1384 };
1385 assert_eq!(
1386 paragraph[0],
1387 MarkdownParagraphChunk::Image(Image {
1388 source_range: 0..31,
1389 link: Link::Web {
1390 url: "http://example.com/foo.png".to_string(),
1391 },
1392 alt_text: None,
1393 height: None,
1394 width: None,
1395 },)
1396 );
1397 }
1398
1399 #[gpui::test]
1400 async fn test_image_with_alt_text_containing_formatting() {
1401 let parsed = parse("").await;
1402
1403 let ParsedMarkdownElement::Paragraph(chunks) = &parsed.children[0] else {
1404 panic!("Expected a paragraph");
1405 };
1406 assert_eq!(
1407 chunks,
1408 &[MarkdownParagraphChunk::Image(Image {
1409 source_range: 0..44,
1410 link: Link::Web {
1411 url: "http://example.com/foo.png".to_string(),
1412 },
1413 alt_text: Some("foo bar baz".into()),
1414 height: None,
1415 width: None,
1416 }),],
1417 );
1418 }
1419
1420 #[gpui::test]
1421 async fn test_images_with_text_in_between() {
1422 let parsed = parse(
1423 "\nLorem Ipsum\n",
1424 )
1425 .await;
1426
1427 let chunks = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
1428 text
1429 } else {
1430 panic!("Expected a paragraph");
1431 };
1432 assert_eq!(
1433 chunks,
1434 &vec![
1435 MarkdownParagraphChunk::Image(Image {
1436 source_range: 0..81,
1437 link: Link::Web {
1438 url: "http://example.com/foo.png".to_string(),
1439 },
1440 alt_text: Some("foo".into()),
1441 height: None,
1442 width: None,
1443 }),
1444 MarkdownParagraphChunk::Text(ParsedMarkdownText {
1445 source_range: 0..81,
1446 contents: " Lorem Ipsum ".into(),
1447 highlights: Vec::new(),
1448 region_ranges: Vec::new(),
1449 regions: Vec::new(),
1450 }),
1451 MarkdownParagraphChunk::Image(Image {
1452 source_range: 0..81,
1453 link: Link::Web {
1454 url: "http://example.com/bar.png".to_string(),
1455 },
1456 alt_text: Some("bar".into()),
1457 height: None,
1458 width: None,
1459 })
1460 ]
1461 );
1462 }
1463
1464 #[test]
1465 fn test_parse_html_element_dimension() {
1466 // Test percentage values
1467 assert_eq!(
1468 MarkdownParser::parse_html_element_dimension("50%"),
1469 Some(DefiniteLength::Fraction(0.5))
1470 );
1471 assert_eq!(
1472 MarkdownParser::parse_html_element_dimension("100%"),
1473 Some(DefiniteLength::Fraction(1.0))
1474 );
1475 assert_eq!(
1476 MarkdownParser::parse_html_element_dimension("25%"),
1477 Some(DefiniteLength::Fraction(0.25))
1478 );
1479 assert_eq!(
1480 MarkdownParser::parse_html_element_dimension("0%"),
1481 Some(DefiniteLength::Fraction(0.0))
1482 );
1483
1484 // Test pixel values
1485 assert_eq!(
1486 MarkdownParser::parse_html_element_dimension("100px"),
1487 Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.0))))
1488 );
1489 assert_eq!(
1490 MarkdownParser::parse_html_element_dimension("50px"),
1491 Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(50.0))))
1492 );
1493 assert_eq!(
1494 MarkdownParser::parse_html_element_dimension("0px"),
1495 Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(0.0))))
1496 );
1497
1498 // Test values without units (should be treated as pixels)
1499 assert_eq!(
1500 MarkdownParser::parse_html_element_dimension("100"),
1501 Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.0))))
1502 );
1503 assert_eq!(
1504 MarkdownParser::parse_html_element_dimension("42"),
1505 Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(42.0))))
1506 );
1507
1508 // Test invalid values
1509 assert_eq!(
1510 MarkdownParser::parse_html_element_dimension("invalid"),
1511 None
1512 );
1513 assert_eq!(MarkdownParser::parse_html_element_dimension("px"), None);
1514 assert_eq!(MarkdownParser::parse_html_element_dimension("%"), None);
1515 assert_eq!(MarkdownParser::parse_html_element_dimension(""), None);
1516 assert_eq!(MarkdownParser::parse_html_element_dimension("abc%"), None);
1517 assert_eq!(MarkdownParser::parse_html_element_dimension("abcpx"), None);
1518
1519 // Test decimal values
1520 assert_eq!(
1521 MarkdownParser::parse_html_element_dimension("50.5%"),
1522 Some(DefiniteLength::Fraction(0.505))
1523 );
1524 assert_eq!(
1525 MarkdownParser::parse_html_element_dimension("100.25px"),
1526 Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.25))))
1527 );
1528 assert_eq!(
1529 MarkdownParser::parse_html_element_dimension("42.0"),
1530 Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(42.0))))
1531 );
1532 }
1533
1534 #[gpui::test]
1535 async fn test_inline_html_image_tag() {
1536 let parsed =
1537 parse("<p>Some text<img src=\"http://example.com/foo.png\" /> some more text</p>")
1538 .await;
1539
1540 assert_eq!(
1541 ParsedMarkdown {
1542 children: vec![ParsedMarkdownElement::Paragraph(vec![
1543 MarkdownParagraphChunk::Text(ParsedMarkdownText {
1544 source_range: 0..71,
1545 contents: "Some text".into(),
1546 highlights: Default::default(),
1547 region_ranges: Default::default(),
1548 regions: Default::default()
1549 }),
1550 MarkdownParagraphChunk::Image(Image {
1551 source_range: 0..71,
1552 link: Link::Web {
1553 url: "http://example.com/foo.png".to_string(),
1554 },
1555 alt_text: None,
1556 height: None,
1557 width: None,
1558 }),
1559 MarkdownParagraphChunk::Text(ParsedMarkdownText {
1560 source_range: 0..71,
1561 contents: " some more text".into(),
1562 highlights: Default::default(),
1563 region_ranges: Default::default(),
1564 regions: Default::default()
1565 }),
1566 ])]
1567 },
1568 parsed
1569 );
1570 }
1571
1572 #[gpui::test]
1573 async fn test_html_block_quote() {
1574 let parsed = parse(
1575 "<blockquote>
1576 <p>some description</p>
1577 </blockquote>",
1578 )
1579 .await;
1580
1581 assert_eq!(
1582 ParsedMarkdown {
1583 children: vec![block_quote(
1584 vec![ParsedMarkdownElement::Paragraph(text(
1585 "some description",
1586 0..76
1587 ))],
1588 0..76,
1589 )]
1590 },
1591 parsed
1592 );
1593 }
1594
1595 #[gpui::test]
1596 async fn test_html_nested_block_quote() {
1597 let parsed = parse(
1598 "<blockquote>
1599 <p>some description</p>
1600 <blockquote>
1601 <p>second description</p>
1602 </blockquote>
1603 </blockquote>",
1604 )
1605 .await;
1606
1607 assert_eq!(
1608 ParsedMarkdown {
1609 children: vec![block_quote(
1610 vec![
1611 ParsedMarkdownElement::Paragraph(text("some description", 0..173)),
1612 block_quote(
1613 vec![ParsedMarkdownElement::Paragraph(text(
1614 "second description",
1615 0..173
1616 ))],
1617 0..173,
1618 )
1619 ],
1620 0..173,
1621 )]
1622 },
1623 parsed
1624 );
1625 }
1626
1627 #[gpui::test]
1628 async fn test_html_table() {
1629 let parsed = parse(
1630 "<table>
1631 <thead>
1632 <tr>
1633 <th>Id</th>
1634 <th>Name</th>
1635 </tr>
1636 </thead>
1637 <tbody>
1638 <tr>
1639 <td>1</td>
1640 <td>Chris</td>
1641 </tr>
1642 <tr>
1643 <td>2</td>
1644 <td>Dennis</td>
1645 </tr>
1646 </tbody>
1647 </table>",
1648 )
1649 .await;
1650
1651 assert_eq!(
1652 ParsedMarkdown {
1653 children: vec![ParsedMarkdownElement::Table(table(
1654 0..366,
1655 vec![row(vec![
1656 column(1, 1, true, text("Id", 0..366)),
1657 column(1, 1, true, text("Name ", 0..366))
1658 ])],
1659 vec![
1660 row(vec![
1661 column(1, 1, false, text("1", 0..366)),
1662 column(1, 1, false, text("Chris", 0..366))
1663 ]),
1664 row(vec![
1665 column(1, 1, false, text("2", 0..366)),
1666 column(1, 1, false, text("Dennis", 0..366))
1667 ]),
1668 ],
1669 ))],
1670 },
1671 parsed
1672 );
1673 }
1674
1675 #[gpui::test]
1676 async fn test_html_table_without_headings() {
1677 let parsed = parse(
1678 "<table>
1679 <tbody>
1680 <tr>
1681 <td>1</td>
1682 <td>Chris</td>
1683 </tr>
1684 <tr>
1685 <td>2</td>
1686 <td>Dennis</td>
1687 </tr>
1688 </tbody>
1689 </table>",
1690 )
1691 .await;
1692
1693 assert_eq!(
1694 ParsedMarkdown {
1695 children: vec![ParsedMarkdownElement::Table(table(
1696 0..240,
1697 vec![],
1698 vec![
1699 row(vec![
1700 column(1, 1, false, text("1", 0..240)),
1701 column(1, 1, false, text("Chris", 0..240))
1702 ]),
1703 row(vec![
1704 column(1, 1, false, text("2", 0..240)),
1705 column(1, 1, false, text("Dennis", 0..240))
1706 ]),
1707 ],
1708 ))],
1709 },
1710 parsed
1711 );
1712 }
1713
1714 #[gpui::test]
1715 async fn test_html_table_without_body() {
1716 let parsed = parse(
1717 "<table>
1718 <thead>
1719 <tr>
1720 <th>Id</th>
1721 <th>Name</th>
1722 </tr>
1723 </thead>
1724 </table>",
1725 )
1726 .await;
1727
1728 assert_eq!(
1729 ParsedMarkdown {
1730 children: vec![ParsedMarkdownElement::Table(table(
1731 0..150,
1732 vec![row(vec![
1733 column(1, 1, true, text("Id", 0..150)),
1734 column(1, 1, true, text("Name", 0..150))
1735 ])],
1736 vec![],
1737 ))],
1738 },
1739 parsed
1740 );
1741 }
1742
1743 #[gpui::test]
1744 async fn test_html_heading_tags() {
1745 let parsed = parse("<h1>Heading</h1><h2>Heading</h2><h3>Heading</h3><h4>Heading</h4><h5>Heading</h5><h6>Heading</h6>").await;
1746
1747 assert_eq!(
1748 ParsedMarkdown {
1749 children: vec![
1750 ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
1751 level: HeadingLevel::H1,
1752 source_range: 0..96,
1753 contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
1754 source_range: 0..96,
1755 contents: "Heading".into(),
1756 highlights: Vec::default(),
1757 region_ranges: Vec::default(),
1758 regions: Vec::default()
1759 })],
1760 }),
1761 ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
1762 level: HeadingLevel::H2,
1763 source_range: 0..96,
1764 contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
1765 source_range: 0..96,
1766 contents: "Heading".into(),
1767 highlights: Vec::default(),
1768 region_ranges: Vec::default(),
1769 regions: Vec::default()
1770 })],
1771 }),
1772 ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
1773 level: HeadingLevel::H3,
1774 source_range: 0..96,
1775 contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
1776 source_range: 0..96,
1777 contents: "Heading".into(),
1778 highlights: Vec::default(),
1779 region_ranges: Vec::default(),
1780 regions: Vec::default()
1781 })],
1782 }),
1783 ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
1784 level: HeadingLevel::H4,
1785 source_range: 0..96,
1786 contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
1787 source_range: 0..96,
1788 contents: "Heading".into(),
1789 highlights: Vec::default(),
1790 region_ranges: Vec::default(),
1791 regions: Vec::default()
1792 })],
1793 }),
1794 ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
1795 level: HeadingLevel::H5,
1796 source_range: 0..96,
1797 contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
1798 source_range: 0..96,
1799 contents: "Heading".into(),
1800 highlights: Vec::default(),
1801 region_ranges: Vec::default(),
1802 regions: Vec::default()
1803 })],
1804 }),
1805 ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
1806 level: HeadingLevel::H6,
1807 source_range: 0..96,
1808 contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
1809 source_range: 0..96,
1810 contents: "Heading".into(),
1811 highlights: Vec::default(),
1812 region_ranges: Vec::default(),
1813 regions: Vec::default()
1814 })],
1815 }),
1816 ],
1817 },
1818 parsed
1819 );
1820 }
1821
1822 #[gpui::test]
1823 async fn test_html_image_tag() {
1824 let parsed = parse("<img src=\"http://example.com/foo.png\" />").await;
1825
1826 assert_eq!(
1827 ParsedMarkdown {
1828 children: vec![ParsedMarkdownElement::Image(Image {
1829 source_range: 0..40,
1830 link: Link::Web {
1831 url: "http://example.com/foo.png".to_string(),
1832 },
1833 alt_text: None,
1834 height: None,
1835 width: None,
1836 })]
1837 },
1838 parsed
1839 );
1840 }
1841
1842 #[gpui::test]
1843 async fn test_html_image_tag_with_alt_text() {
1844 let parsed = parse("<img src=\"http://example.com/foo.png\" alt=\"Foo\" />").await;
1845
1846 assert_eq!(
1847 ParsedMarkdown {
1848 children: vec![ParsedMarkdownElement::Image(Image {
1849 source_range: 0..50,
1850 link: Link::Web {
1851 url: "http://example.com/foo.png".to_string(),
1852 },
1853 alt_text: Some("Foo".into()),
1854 height: None,
1855 width: None,
1856 })]
1857 },
1858 parsed
1859 );
1860 }
1861
1862 #[gpui::test]
1863 async fn test_html_image_tag_with_height_and_width() {
1864 let parsed =
1865 parse("<img src=\"http://example.com/foo.png\" height=\"100\" width=\"200\" />").await;
1866
1867 assert_eq!(
1868 ParsedMarkdown {
1869 children: vec![ParsedMarkdownElement::Image(Image {
1870 source_range: 0..65,
1871 link: Link::Web {
1872 url: "http://example.com/foo.png".to_string(),
1873 },
1874 alt_text: None,
1875 height: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.)))),
1876 width: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(200.)))),
1877 })]
1878 },
1879 parsed
1880 );
1881 }
1882
1883 #[gpui::test]
1884 async fn test_html_image_style_tag_with_height_and_width() {
1885 let parsed = parse(
1886 "<img src=\"http://example.com/foo.png\" style=\"height:100px; width:200px;\" />",
1887 )
1888 .await;
1889
1890 assert_eq!(
1891 ParsedMarkdown {
1892 children: vec![ParsedMarkdownElement::Image(Image {
1893 source_range: 0..75,
1894 link: Link::Web {
1895 url: "http://example.com/foo.png".to_string(),
1896 },
1897 alt_text: None,
1898 height: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.)))),
1899 width: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(200.)))),
1900 })]
1901 },
1902 parsed
1903 );
1904 }
1905
1906 #[gpui::test]
1907 async fn test_header_only_table() {
1908 let markdown = "\
1909| Header 1 | Header 2 |
1910|----------|----------|
1911
1912Some other content
1913";
1914
1915 let expected_table = table(
1916 0..48,
1917 vec![row(vec![
1918 column(1, 1, true, text("Header 1", 1..11)),
1919 column(1, 1, true, text("Header 2", 12..22)),
1920 ])],
1921 vec![],
1922 );
1923
1924 assert_eq!(
1925 parse(markdown).await.children[0],
1926 ParsedMarkdownElement::Table(expected_table)
1927 );
1928 }
1929
1930 #[gpui::test]
1931 async fn test_basic_table() {
1932 let markdown = "\
1933| Header 1 | Header 2 |
1934|----------|----------|
1935| Cell 1 | Cell 2 |
1936| Cell 3 | Cell 4 |";
1937
1938 let expected_table = table(
1939 0..95,
1940 vec![row(vec![
1941 column(1, 1, true, text("Header 1", 1..11)),
1942 column(1, 1, true, text("Header 2", 12..22)),
1943 ])],
1944 vec![
1945 row(vec![
1946 column(1, 1, false, text("Cell 1", 49..59)),
1947 column(1, 1, false, text("Cell 2", 60..70)),
1948 ]),
1949 row(vec![
1950 column(1, 1, false, text("Cell 3", 73..83)),
1951 column(1, 1, false, text("Cell 4", 84..94)),
1952 ]),
1953 ],
1954 );
1955
1956 assert_eq!(
1957 parse(markdown).await.children[0],
1958 ParsedMarkdownElement::Table(expected_table)
1959 );
1960 }
1961
1962 #[gpui::test]
1963 async fn test_list_basic() {
1964 let parsed = parse(
1965 "\
1966* Item 1
1967* Item 2
1968* Item 3
1969",
1970 )
1971 .await;
1972
1973 assert_eq!(
1974 parsed.children,
1975 vec![
1976 list_item(0..8, 1, Unordered, vec![p("Item 1", 2..8)]),
1977 list_item(9..17, 1, Unordered, vec![p("Item 2", 11..17)]),
1978 list_item(18..26, 1, Unordered, vec![p("Item 3", 20..26)]),
1979 ],
1980 );
1981 }
1982
1983 #[gpui::test]
1984 async fn test_list_with_tasks() {
1985 let parsed = parse(
1986 "\
1987- [ ] TODO
1988- [x] Checked
1989",
1990 )
1991 .await;
1992
1993 assert_eq!(
1994 parsed.children,
1995 vec![
1996 list_item(0..10, 1, Task(false, 2..5), vec![p("TODO", 6..10)]),
1997 list_item(11..24, 1, Task(true, 13..16), vec![p("Checked", 17..24)]),
1998 ],
1999 );
2000 }
2001
2002 #[gpui::test]
2003 async fn test_list_with_indented_task() {
2004 let parsed = parse(
2005 "\
2006- [ ] TODO
2007 - [x] Checked
2008 - Unordered
2009 1. Number 1
2010 1. Number 2
20111. Number A
2012",
2013 )
2014 .await;
2015
2016 assert_eq!(
2017 parsed.children,
2018 vec![
2019 list_item(0..12, 1, Task(false, 2..5), vec![p("TODO", 6..10)]),
2020 list_item(13..26, 2, Task(true, 15..18), vec![p("Checked", 19..26)]),
2021 list_item(29..40, 2, Unordered, vec![p("Unordered", 31..40)]),
2022 list_item(43..54, 2, Ordered(1), vec![p("Number 1", 46..54)]),
2023 list_item(57..68, 2, Ordered(2), vec![p("Number 2", 60..68)]),
2024 list_item(69..80, 1, Ordered(1), vec![p("Number A", 72..80)]),
2025 ],
2026 );
2027 }
2028
2029 #[gpui::test]
2030 async fn test_list_with_linebreak_is_handled_correctly() {
2031 let parsed = parse(
2032 "\
2033- [ ] Task 1
2034
2035- [x] Task 2
2036",
2037 )
2038 .await;
2039
2040 assert_eq!(
2041 parsed.children,
2042 vec![
2043 list_item(0..13, 1, Task(false, 2..5), vec![p("Task 1", 6..12)]),
2044 list_item(14..26, 1, Task(true, 16..19), vec![p("Task 2", 20..26)]),
2045 ],
2046 );
2047 }
2048
2049 #[gpui::test]
2050 async fn test_list_nested() {
2051 let parsed = parse(
2052 "\
2053* Item 1
2054* Item 2
2055* Item 3
2056
20571. Hello
20581. Two
2059 1. Three
20602. Four
20613. Five
2062
2063* First
2064 1. Hello
2065 1. Goodbyte
2066 - Inner
2067 - Inner
2068 2. Goodbyte
2069 - Next item empty
2070 -
2071* Last
2072",
2073 )
2074 .await;
2075
2076 assert_eq!(
2077 parsed.children,
2078 vec![
2079 list_item(0..8, 1, Unordered, vec![p("Item 1", 2..8)]),
2080 list_item(9..17, 1, Unordered, vec![p("Item 2", 11..17)]),
2081 list_item(18..27, 1, Unordered, vec![p("Item 3", 20..26)]),
2082 list_item(28..36, 1, Ordered(1), vec![p("Hello", 31..36)]),
2083 list_item(37..46, 1, Ordered(2), vec![p("Two", 40..43),]),
2084 list_item(47..55, 2, Ordered(1), vec![p("Three", 50..55)]),
2085 list_item(56..63, 1, Ordered(3), vec![p("Four", 59..63)]),
2086 list_item(64..72, 1, Ordered(4), vec![p("Five", 67..71)]),
2087 list_item(73..82, 1, Unordered, vec![p("First", 75..80)]),
2088 list_item(83..96, 2, Ordered(1), vec![p("Hello", 86..91)]),
2089 list_item(97..116, 3, Ordered(1), vec![p("Goodbyte", 100..108)]),
2090 list_item(117..124, 4, Unordered, vec![p("Inner", 119..124)]),
2091 list_item(133..140, 4, Unordered, vec![p("Inner", 135..140)]),
2092 list_item(143..159, 2, Ordered(2), vec![p("Goodbyte", 146..154)]),
2093 list_item(160..180, 3, Unordered, vec![p("Next item empty", 165..180)]),
2094 list_item(186..190, 3, Unordered, vec![]),
2095 list_item(191..197, 1, Unordered, vec![p("Last", 193..197)]),
2096 ]
2097 );
2098 }
2099
2100 #[gpui::test]
2101 async fn test_list_with_nested_content() {
2102 let parsed = parse(
2103 "\
2104* This is a list item with two paragraphs.
2105
2106 This is the second paragraph in the list item.
2107",
2108 )
2109 .await;
2110
2111 assert_eq!(
2112 parsed.children,
2113 vec![list_item(
2114 0..96,
2115 1,
2116 Unordered,
2117 vec![
2118 p("This is a list item with two paragraphs.", 4..44),
2119 p("This is the second paragraph in the list item.", 50..97)
2120 ],
2121 ),],
2122 );
2123 }
2124
2125 #[gpui::test]
2126 async fn test_list_item_with_inline_html() {
2127 let parsed = parse(
2128 "\
2129* This is a list item with an inline HTML <sometag>tag</sometag>.
2130",
2131 )
2132 .await;
2133
2134 assert_eq!(
2135 parsed.children,
2136 vec![list_item(
2137 0..67,
2138 1,
2139 Unordered,
2140 vec![p("This is a list item with an inline HTML tag.", 4..44),],
2141 ),],
2142 );
2143 }
2144
2145 #[gpui::test]
2146 async fn test_nested_list_with_paragraph_inside() {
2147 let parsed = parse(
2148 "\
21491. a
2150 1. b
2151 1. c
2152
2153 text
2154
2155 1. d
2156",
2157 )
2158 .await;
2159
2160 assert_eq!(
2161 parsed.children,
2162 vec![
2163 list_item(0..7, 1, Ordered(1), vec![p("a", 3..4)],),
2164 list_item(8..20, 2, Ordered(1), vec![p("b", 12..13),],),
2165 list_item(21..27, 3, Ordered(1), vec![p("c", 25..26),],),
2166 p("text", 32..37),
2167 list_item(41..46, 2, Ordered(1), vec![p("d", 45..46),],),
2168 ],
2169 );
2170 }
2171
2172 #[gpui::test]
2173 async fn test_list_with_leading_text() {
2174 let parsed = parse(
2175 "\
2176* `code`
2177* **bold**
2178* [link](https://example.com)
2179",
2180 )
2181 .await;
2182
2183 assert_eq!(
2184 parsed.children,
2185 vec![
2186 list_item(0..8, 1, Unordered, vec![p("code", 2..8)]),
2187 list_item(9..19, 1, Unordered, vec![p("bold", 11..19)]),
2188 list_item(20..49, 1, Unordered, vec![p("link", 22..49)],),
2189 ],
2190 );
2191 }
2192
2193 #[gpui::test]
2194 async fn test_simple_block_quote() {
2195 let parsed = parse("> Simple block quote with **styled text**").await;
2196
2197 assert_eq!(
2198 parsed.children,
2199 vec![block_quote(
2200 vec![p("Simple block quote with styled text", 2..41)],
2201 0..41
2202 )]
2203 );
2204 }
2205
2206 #[gpui::test]
2207 async fn test_simple_block_quote_with_multiple_lines() {
2208 let parsed = parse(
2209 "\
2210> # Heading
2211> More
2212> text
2213>
2214> More text
2215",
2216 )
2217 .await;
2218
2219 assert_eq!(
2220 parsed.children,
2221 vec![block_quote(
2222 vec![
2223 h1(text("Heading", 4..11), 2..12),
2224 p("More text", 14..26),
2225 p("More text", 30..40)
2226 ],
2227 0..40
2228 )]
2229 );
2230 }
2231
2232 #[gpui::test]
2233 async fn test_nested_block_quote() {
2234 let parsed = parse(
2235 "\
2236> A
2237>
2238> > # B
2239>
2240> C
2241
2242More text
2243",
2244 )
2245 .await;
2246
2247 assert_eq!(
2248 parsed.children,
2249 vec![
2250 block_quote(
2251 vec![
2252 p("A", 2..4),
2253 block_quote(vec![h1(text("B", 12..13), 10..14)], 8..14),
2254 p("C", 18..20)
2255 ],
2256 0..20
2257 ),
2258 p("More text", 21..31)
2259 ]
2260 );
2261 }
2262
2263 #[gpui::test]
2264 async fn test_code_block() {
2265 let parsed = parse(
2266 "\
2267```
2268fn main() {
2269 return 0;
2270}
2271```
2272",
2273 )
2274 .await;
2275
2276 assert_eq!(
2277 parsed.children,
2278 vec![code_block(
2279 None,
2280 "fn main() {\n return 0;\n}",
2281 0..35,
2282 None
2283 )]
2284 );
2285 }
2286
2287 #[gpui::test]
2288 async fn test_code_block_with_language(executor: BackgroundExecutor) {
2289 let language_registry = Arc::new(LanguageRegistry::test(executor.clone()));
2290 language_registry.add(rust_lang());
2291
2292 let parsed = parse_markdown(
2293 "\
2294```rust
2295fn main() {
2296 return 0;
2297}
2298```
2299",
2300 None,
2301 Some(language_registry),
2302 )
2303 .await;
2304
2305 assert_eq!(
2306 parsed.children,
2307 vec![code_block(
2308 Some("rust".to_string()),
2309 "fn main() {\n return 0;\n}",
2310 0..39,
2311 Some(vec![])
2312 )]
2313 );
2314 }
2315
2316 fn rust_lang() -> Arc<Language> {
2317 Arc::new(Language::new(
2318 LanguageConfig {
2319 name: "Rust".into(),
2320 matcher: LanguageMatcher {
2321 path_suffixes: vec!["rs".into()],
2322 ..Default::default()
2323 },
2324 collapsed_placeholder: " /* ... */ ".to_string(),
2325 ..Default::default()
2326 },
2327 Some(tree_sitter_rust::LANGUAGE.into()),
2328 ))
2329 }
2330
2331 fn h1(contents: MarkdownParagraph, source_range: Range<usize>) -> ParsedMarkdownElement {
2332 ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
2333 source_range,
2334 level: HeadingLevel::H1,
2335 contents,
2336 })
2337 }
2338
2339 fn h2(contents: MarkdownParagraph, source_range: Range<usize>) -> ParsedMarkdownElement {
2340 ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
2341 source_range,
2342 level: HeadingLevel::H2,
2343 contents,
2344 })
2345 }
2346
2347 fn h3(contents: MarkdownParagraph, source_range: Range<usize>) -> ParsedMarkdownElement {
2348 ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
2349 source_range,
2350 level: HeadingLevel::H3,
2351 contents,
2352 })
2353 }
2354
2355 fn p(contents: &str, source_range: Range<usize>) -> ParsedMarkdownElement {
2356 ParsedMarkdownElement::Paragraph(text(contents, source_range))
2357 }
2358
2359 fn text(contents: &str, source_range: Range<usize>) -> MarkdownParagraph {
2360 vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
2361 highlights: Vec::new(),
2362 region_ranges: Vec::new(),
2363 regions: Vec::new(),
2364 source_range,
2365 contents: contents.to_string().into(),
2366 })]
2367 }
2368
2369 fn block_quote(
2370 children: Vec<ParsedMarkdownElement>,
2371 source_range: Range<usize>,
2372 ) -> ParsedMarkdownElement {
2373 ParsedMarkdownElement::BlockQuote(ParsedMarkdownBlockQuote {
2374 source_range,
2375 children,
2376 })
2377 }
2378
2379 fn code_block(
2380 language: Option<String>,
2381 code: &str,
2382 source_range: Range<usize>,
2383 highlights: Option<Vec<(Range<usize>, HighlightId)>>,
2384 ) -> ParsedMarkdownElement {
2385 ParsedMarkdownElement::CodeBlock(ParsedMarkdownCodeBlock {
2386 source_range,
2387 language,
2388 contents: code.to_string().into(),
2389 highlights,
2390 })
2391 }
2392
2393 fn list_item(
2394 source_range: Range<usize>,
2395 depth: u16,
2396 item_type: ParsedMarkdownListItemType,
2397 content: Vec<ParsedMarkdownElement>,
2398 ) -> ParsedMarkdownElement {
2399 ParsedMarkdownElement::ListItem(ParsedMarkdownListItem {
2400 source_range,
2401 item_type,
2402 depth,
2403 content,
2404 })
2405 }
2406
2407 fn table(
2408 source_range: Range<usize>,
2409 header: Vec<ParsedMarkdownTableRow>,
2410 body: Vec<ParsedMarkdownTableRow>,
2411 ) -> ParsedMarkdownTable {
2412 ParsedMarkdownTable {
2413 column_alignments: Vec::new(),
2414 source_range,
2415 header,
2416 body,
2417 }
2418 }
2419
2420 fn row(columns: Vec<ParsedMarkdownTableColumn>) -> ParsedMarkdownTableRow {
2421 ParsedMarkdownTableRow { columns }
2422 }
2423
2424 fn column(
2425 col_span: usize,
2426 row_span: usize,
2427 is_header: bool,
2428 children: MarkdownParagraph,
2429 ) -> ParsedMarkdownTableColumn {
2430 ParsedMarkdownTableColumn {
2431 col_span,
2432 row_span,
2433 is_header,
2434 children,
2435 }
2436 }
2437
2438 impl PartialEq for ParsedMarkdownTable {
2439 fn eq(&self, other: &Self) -> bool {
2440 self.source_range == other.source_range
2441 && self.header == other.header
2442 && self.body == other.body
2443 }
2444 }
2445
2446 impl PartialEq for ParsedMarkdownText {
2447 fn eq(&self, other: &Self) -> bool {
2448 self.source_range == other.source_range && self.contents == other.contents
2449 }
2450 }
2451}