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